blob: ce26585ce9c68921dfd58daf79596868027f19ba [file] [log] [blame]
mblighbe630eb2008-08-01 16:41:48 +00001# Copyright 2008 Google Inc. All Rights Reserved.
2
3"""
4The host module contains the objects and method used to
5manage a host in Autotest.
6
7The valid actions are:
8create: adds host(s)
9delete: deletes host(s)
10list: lists host(s)
11stat: displays host(s) information
12mod: modifies host(s)
13jobs: lists all jobs that ran on host(s)
14
15The common options are:
16-M|--mlist: file containing a list of machines
17
18
mblighbe630eb2008-08-01 16:41:48 +000019See topic_common.py for a High Level Design and Algorithm.
20
21"""
Justin Giorgi16bba562016-06-22 10:42:13 -070022import common
Gregory Nisbet99b61942019-07-08 09:43:06 -070023import json
Ningning Xiaffb3c1f2018-06-07 18:42:32 -070024import random
Simran Basi0739d682015-02-25 16:22:56 -080025import re
Justin Giorgi16bba562016-06-22 10:42:13 -070026import socket
Gregory Nisbet93fa5b52019-07-19 14:56:35 -070027import time
mblighbe630eb2008-08-01 16:41:48 +000028
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -070029from autotest_lib.cli import action_common, rpc, topic_common, skylab_utils, skylab_migration
Gregory Nisbet93fa5b52019-07-19 14:56:35 -070030from autotest_lib.cli import fair_partition
Xixuan Wu06f7e232019-07-12 23:33:28 +000031from autotest_lib.client.bin import utils as bin_utils
Gregory Nisbet93fa5b52019-07-19 14:56:35 -070032from autotest_lib.cli.skylab_json_utils import process_labels
Justin Giorgi16bba562016-06-22 10:42:13 -070033from autotest_lib.client.common_lib import error, host_protections
Justin Giorgi5208eaa2016-07-02 20:12:12 -070034from autotest_lib.server import frontend, hosts
Prathmesh Prabhud252d262017-05-31 11:46:34 -070035from autotest_lib.server.hosts import host_info
Gregory Nisbet93fa5b52019-07-19 14:56:35 -070036from autotest_lib.server.lib.status_history import HostJobHistory
37from autotest_lib.server.lib.status_history import UNUSED, WORKING
38from autotest_lib.server.lib.status_history import BROKEN, UNKNOWN
mblighbe630eb2008-08-01 16:41:48 +000039
40
Ningning Xiaea02ab12018-05-11 15:08:57 -070041try:
42 from skylab_inventory import text_manager
43 from skylab_inventory.lib import device
Ningning Xiaffb3c1f2018-06-07 18:42:32 -070044 from skylab_inventory.lib import server as skylab_server
Ningning Xiaea02ab12018-05-11 15:08:57 -070045except ImportError:
46 pass
47
48
Ningning Xiac46bdd12018-05-29 11:24:14 -070049MIGRATED_HOST_SUFFIX = '-migrated-do-not-use'
50
51
Gregory Nisbet99b61942019-07-08 09:43:06 -070052ID_AUTOGEN_MESSAGE = ("[IGNORED]. Do not edit (crbug.com/950553). ID is "
53 "auto-generated.")
54
55
56
mblighbe630eb2008-08-01 16:41:48 +000057class host(topic_common.atest):
58 """Host class
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -070059 atest host [create|delete|list|stat|mod|jobs|rename|migrate|skylab_migrate|statjson] <options>"""
60 usage_action = '[create|delete|list|stat|mod|jobs|rename|migrate|skylab_migrate|statjson]'
mblighbe630eb2008-08-01 16:41:48 +000061 topic = msg_topic = 'host'
62 msg_items = '<hosts>'
63
mblighaed47e82009-03-17 19:06:18 +000064 protections = host_protections.Protection.names
65
mblighbe630eb2008-08-01 16:41:48 +000066
67 def __init__(self):
68 """Add to the parser the options common to all the
69 host actions"""
70 super(host, self).__init__()
71
72 self.parser.add_option('-M', '--mlist',
73 help='File listing the machines',
74 type='string',
75 default=None,
76 metavar='MACHINE_FLIST')
77
mbligh9deeefa2009-05-01 23:11:08 +000078 self.topic_parse_info = topic_common.item_parse_info(
79 attribute_name='hosts',
80 filename_option='mlist',
81 use_leftover=True)
mblighbe630eb2008-08-01 16:41:48 +000082
83
84 def _parse_lock_options(self, options):
85 if options.lock and options.unlock:
86 self.invalid_syntax('Only specify one of '
87 '--lock and --unlock.')
88
Ningning Xiaef35cb52018-05-04 17:58:20 -070089 self.lock = options.lock
90 self.unlock = options.unlock
91 self.lock_reason = options.lock_reason
Ningning Xiaef35cb52018-05-04 17:58:20 -070092
mblighbe630eb2008-08-01 16:41:48 +000093 if options.lock:
94 self.data['locked'] = True
95 self.messages.append('Locked host')
96 elif options.unlock:
97 self.data['locked'] = False
Matthew Sartori68186332015-04-27 17:19:53 -070098 self.data['lock_reason'] = ''
mblighbe630eb2008-08-01 16:41:48 +000099 self.messages.append('Unlocked host')
100
Matthew Sartori68186332015-04-27 17:19:53 -0700101 if options.lock and options.lock_reason:
102 self.data['lock_reason'] = options.lock_reason
103
mblighbe630eb2008-08-01 16:41:48 +0000104
105 def _cleanup_labels(self, labels, platform=None):
106 """Removes the platform label from the overall labels"""
107 if platform:
108 return [label for label in labels
109 if label != platform]
110 else:
111 try:
112 return [label for label in labels
113 if not label['platform']]
114 except TypeError:
115 # This is a hack - the server will soon
116 # do this, so all this code should be removed.
117 return labels
118
119
120 def get_items(self):
121 return self.hosts
122
123
124class host_help(host):
125 """Just here to get the atest logic working.
126 Usage is set by its parent"""
127 pass
128
129
130class host_list(action_common.atest_list, host):
131 """atest host list [--mlist <file>|<hosts>] [--label <label>]
mbligh536a5242008-10-18 14:35:54 +0000132 [--status <status1,status2>] [--acl <ACL>] [--user <user>]"""
mblighbe630eb2008-08-01 16:41:48 +0000133
134 def __init__(self):
135 super(host_list, self).__init__()
136
137 self.parser.add_option('-b', '--label',
mbligh6444c6b2008-10-27 20:55:13 +0000138 default='',
139 help='Only list hosts with all these labels '
Ningning Xiaea02ab12018-05-11 15:08:57 -0700140 '(comma separated). When --skylab is provided, '
141 'a label must be in the format of '
142 'label-key:label-value (e.g., board:lumpy).')
mblighbe630eb2008-08-01 16:41:48 +0000143 self.parser.add_option('-s', '--status',
mbligh6444c6b2008-10-27 20:55:13 +0000144 default='',
145 help='Only list hosts with any of these '
146 'statuses (comma separated)')
mbligh536a5242008-10-18 14:35:54 +0000147 self.parser.add_option('-a', '--acl',
mbligh6444c6b2008-10-27 20:55:13 +0000148 default='',
Ningning Xiaea02ab12018-05-11 15:08:57 -0700149 help=('Only list hosts within this ACL. %s' %
150 skylab_utils.MSG_INVALID_IN_SKYLAB))
mbligh536a5242008-10-18 14:35:54 +0000151 self.parser.add_option('-u', '--user',
mbligh6444c6b2008-10-27 20:55:13 +0000152 default='',
Ningning Xiaea02ab12018-05-11 15:08:57 -0700153 help=('Only list hosts available to this user. '
154 '%s' % skylab_utils.MSG_INVALID_IN_SKYLAB))
mbligh70b9bf42008-11-18 15:00:46 +0000155 self.parser.add_option('-N', '--hostnames-only', help='Only return '
156 'hostnames for the machines queried.',
157 action='store_true')
mbligh91e0efd2009-02-26 01:02:16 +0000158 self.parser.add_option('--locked',
159 default=False,
160 help='Only list locked hosts',
161 action='store_true')
162 self.parser.add_option('--unlocked',
163 default=False,
164 help='Only list unlocked hosts',
165 action='store_true')
Ningning Xiaea02ab12018-05-11 15:08:57 -0700166 self.parser.add_option('--full-output',
167 default=False,
168 help=('Print out the full content of the hosts. '
169 'Only supported with --skylab.'),
170 action='store_true',
171 dest='full_output')
mbligh91e0efd2009-02-26 01:02:16 +0000172
Ningning Xiaea02ab12018-05-11 15:08:57 -0700173 self.add_skylab_options()
mblighbe630eb2008-08-01 16:41:48 +0000174
175
176 def parse(self):
177 """Consume the specific options"""
jamesrenc2863162010-07-12 21:20:51 +0000178 label_info = topic_common.item_parse_info(attribute_name='labels',
179 inline_option='label')
180
181 (options, leftover) = super(host_list, self).parse([label_info])
182
mblighbe630eb2008-08-01 16:41:48 +0000183 self.status = options.status
mbligh536a5242008-10-18 14:35:54 +0000184 self.acl = options.acl
185 self.user = options.user
mbligh70b9bf42008-11-18 15:00:46 +0000186 self.hostnames_only = options.hostnames_only
mbligh91e0efd2009-02-26 01:02:16 +0000187
188 if options.locked and options.unlocked:
189 self.invalid_syntax('--locked and --unlocked are '
190 'mutually exclusive')
Ningning Xiaea02ab12018-05-11 15:08:57 -0700191
mbligh91e0efd2009-02-26 01:02:16 +0000192 self.locked = options.locked
193 self.unlocked = options.unlocked
Ningning Xia64ced002018-05-16 17:36:20 -0700194 self.label_map = None
Ningning Xiaea02ab12018-05-11 15:08:57 -0700195
196 if self.skylab:
197 if options.user or options.acl or options.status:
198 self.invalid_syntax('--user, --acl or --status is not '
199 'supported with --skylab.')
200 self.full_output = options.full_output
201 if self.full_output and self.hostnames_only:
202 self.invalid_syntax('--full-output is conflicted with '
203 '--hostnames-only.')
Ningning Xia64ced002018-05-16 17:36:20 -0700204
205 if self.labels:
206 self.label_map = device.convert_to_label_map(self.labels)
Ningning Xiaea02ab12018-05-11 15:08:57 -0700207 else:
208 if options.full_output:
209 self.invalid_syntax('--full_output is only supported with '
210 '--skylab.')
211
mblighbe630eb2008-08-01 16:41:48 +0000212 return (options, leftover)
213
214
Ningning Xiaea02ab12018-05-11 15:08:57 -0700215 def execute_skylab(self):
216 """Execute 'atest host list' with --skylab."""
217 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
218 inventory_repo.initialize()
219 lab = text_manager.load_lab(inventory_repo.get_data_dir())
220
221 # TODO(nxia): support filtering on run-time labels and status.
222 return device.get_devices(
223 lab,
224 'duts',
225 self.environment,
Ningning Xia64ced002018-05-16 17:36:20 -0700226 label_map=self.label_map,
Ningning Xiaea02ab12018-05-11 15:08:57 -0700227 hostnames=self.hosts,
228 locked=self.locked,
229 unlocked=self.unlocked)
230
231
mblighbe630eb2008-08-01 16:41:48 +0000232 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800233 """Execute 'atest host list'."""
Ningning Xiaea02ab12018-05-11 15:08:57 -0700234 if self.skylab:
235 return self.execute_skylab()
236
mblighbe630eb2008-08-01 16:41:48 +0000237 filters = {}
238 check_results = {}
239 if self.hosts:
240 filters['hostname__in'] = self.hosts
241 check_results['hostname__in'] = 'hostname'
mbligh6444c6b2008-10-27 20:55:13 +0000242
243 if self.labels:
jamesrenc2863162010-07-12 21:20:51 +0000244 if len(self.labels) == 1:
mblighf703fb42009-01-30 00:35:05 +0000245 # This is needed for labels with wildcards (x86*)
jamesrenc2863162010-07-12 21:20:51 +0000246 filters['labels__name__in'] = self.labels
mblighf703fb42009-01-30 00:35:05 +0000247 check_results['labels__name__in'] = None
248 else:
jamesrenc2863162010-07-12 21:20:51 +0000249 filters['multiple_labels'] = self.labels
mblighf703fb42009-01-30 00:35:05 +0000250 check_results['multiple_labels'] = None
mbligh6444c6b2008-10-27 20:55:13 +0000251
mblighbe630eb2008-08-01 16:41:48 +0000252 if self.status:
mbligh6444c6b2008-10-27 20:55:13 +0000253 statuses = self.status.split(',')
254 statuses = [status.strip() for status in statuses
255 if status.strip()]
256
257 filters['status__in'] = statuses
mblighcd8eb972008-08-25 19:20:39 +0000258 check_results['status__in'] = None
mbligh6444c6b2008-10-27 20:55:13 +0000259
mbligh536a5242008-10-18 14:35:54 +0000260 if self.acl:
showardd9ac4452009-02-07 02:04:37 +0000261 filters['aclgroup__name'] = self.acl
262 check_results['aclgroup__name'] = None
mbligh536a5242008-10-18 14:35:54 +0000263 if self.user:
showardd9ac4452009-02-07 02:04:37 +0000264 filters['aclgroup__users__login'] = self.user
265 check_results['aclgroup__users__login'] = None
mbligh91e0efd2009-02-26 01:02:16 +0000266
267 if self.locked or self.unlocked:
268 filters['locked'] = self.locked
269 check_results['locked'] = None
270
mblighbe630eb2008-08-01 16:41:48 +0000271 return super(host_list, self).execute(op='get_hosts',
272 filters=filters,
273 check_results=check_results)
274
275
276 def output(self, results):
xixuand6011f12016-12-08 15:01:58 -0800277 """Print output of 'atest host list'.
278
279 @param results: the results to be printed.
280 """
Ningning Xiaea02ab12018-05-11 15:08:57 -0700281 if results and not self.skylab:
mblighbe630eb2008-08-01 16:41:48 +0000282 # Remove the platform from the labels.
283 for result in results:
284 result['labels'] = self._cleanup_labels(result['labels'],
285 result['platform'])
Ningning Xiaea02ab12018-05-11 15:08:57 -0700286 if self.skylab and self.full_output:
287 print results
288 return
289
290 if self.skylab:
291 results = device.convert_to_autotest_hosts(results)
292
mbligh70b9bf42008-11-18 15:00:46 +0000293 if self.hostnames_only:
mblighdf75f8b2008-11-18 19:07:42 +0000294 self.print_list(results, key='hostname')
mbligh70b9bf42008-11-18 15:00:46 +0000295 else:
Ningning Xiaea02ab12018-05-11 15:08:57 -0700296 keys = ['hostname', 'status', 'shard', 'locked', 'lock_reason',
297 'locked_by', 'platform', 'labels']
Prashanth Balasubramaniana5048562014-12-12 10:14:11 -0800298 super(host_list, self).output(results, keys=keys)
mblighbe630eb2008-08-01 16:41:48 +0000299
300
301class host_stat(host):
302 """atest host stat --mlist <file>|<hosts>"""
303 usage_action = 'stat'
304
305 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800306 """Execute 'atest host stat'."""
mblighbe630eb2008-08-01 16:41:48 +0000307 results = []
308 # Convert wildcards into real host stats.
309 existing_hosts = []
310 for host in self.hosts:
311 if host.endswith('*'):
312 stats = self.execute_rpc('get_hosts',
313 hostname__startswith=host.rstrip('*'))
314 if len(stats) == 0:
315 self.failure('No hosts matching %s' % host, item=host,
316 what_failed='Failed to stat')
317 continue
318 else:
319 stats = self.execute_rpc('get_hosts', hostname=host)
320 if len(stats) == 0:
321 self.failure('Unknown host %s' % host, item=host,
322 what_failed='Failed to stat')
323 continue
324 existing_hosts.extend(stats)
325
326 for stat in existing_hosts:
327 host = stat['hostname']
328 # The host exists, these should succeed
329 acls = self.execute_rpc('get_acl_groups', hosts__hostname=host)
330
331 labels = self.execute_rpc('get_labels', host__hostname=host)
Simran Basi0739d682015-02-25 16:22:56 -0800332 results.append([[stat], acls, labels, stat['attributes']])
mblighbe630eb2008-08-01 16:41:48 +0000333 return results
334
335
336 def output(self, results):
xixuand6011f12016-12-08 15:01:58 -0800337 """Print output of 'atest host stat'.
338
339 @param results: the results to be printed.
340 """
Simran Basi0739d682015-02-25 16:22:56 -0800341 for stats, acls, labels, attributes in results:
mblighbe630eb2008-08-01 16:41:48 +0000342 print '-'*5
343 self.print_fields(stats,
Aviv Kesheta40d1272017-11-16 00:56:35 -0800344 keys=['hostname', 'id', 'platform',
mblighe163b032008-10-18 14:30:27 +0000345 'status', 'locked', 'locked_by',
Matthew Sartori68186332015-04-27 17:19:53 -0700346 'lock_time', 'lock_reason', 'protection',])
mblighbe630eb2008-08-01 16:41:48 +0000347 self.print_by_ids(acls, 'ACLs', line_before=True)
348 labels = self._cleanup_labels(labels)
349 self.print_by_ids(labels, 'Labels', line_before=True)
Simran Basi0739d682015-02-25 16:22:56 -0800350 self.print_dict(attributes, 'Host Attributes', line_before=True)
mblighbe630eb2008-08-01 16:41:48 +0000351
352
Gregory Nisbet93fa5b52019-07-19 14:56:35 -0700353class host_get_migration_plan(host_stat):
354 """atest host get_migration_plan --mlist <file>|<hosts>"""
355 usage_action = "get_migration_plan"
356
357 def __init__(self):
358 super(host_get_migration_plan, self).__init__()
359 self.parser.add_option("--ratio", default=0.5, type=float, dest="ratio")
360 self.add_skylab_options()
361
362 def parse(self):
363 (options, leftover) = super(host_get_migration_plan, self).parse()
364 self.ratio = options.ratio
365 return (options, leftover)
366
367 def execute(self):
368 afe = frontend.AFE()
369 results = super(host_get_migration_plan, self).execute()
370 working = []
371 non_working = []
372 for stats, _, _, _ in results:
373 assert len(stats) == 1
374 stats = stats[0]
375 hostname = stats["hostname"]
376 now = time.time()
377 history = HostJobHistory.get_host_history(
378 afe=afe,
379 hostname=hostname,
380 start_time=now,
381 end_time=now - 24 * 60 * 60,
382 )
383 dut_status, _ = history.last_diagnosis()
384 if dut_status in [UNUSED, WORKING]:
385 working.append(hostname)
386 elif dut_status == BROKEN:
387 non_working.append(hostname)
388 elif dut_status == UNKNOWN:
389 # if it's unknown, randomly assign it to working or
390 # nonworking, since we don't know.
391 # The two choices aren't actually equiprobable, but it
392 # should be fine.
393 random.choice([working, non_working]).append(hostname)
394 else:
395 raise ValueError("unknown status %s" % dut_status)
396 working_transfer, working_retain = fair_partition.partition(working, self.ratio)
397 non_working_transfer, non_working_retain = \
398 fair_partition.partition(non_working, self.ratio)
399 return {
400 "transfer": working_transfer + non_working_transfer,
401 "retain": working_retain + non_working_retain,
402 }
403
404 def output(self, results):
405 print json.dumps(results, indent=4, sort_keys=True)
406
Gregory Nisbet99b61942019-07-08 09:43:06 -0700407
408class host_statjson(host_stat):
409 """atest host statjson --mlist <file>|<hosts>
410
411 exposes the same information that 'atest host stat' does, but in the json
412 format that 'skylab add-dut' expects
413 """
414
415 usage_action = "statjson"
416
417
418 def output(self, results):
419 """Print output of 'atest host stat-skylab-json'"""
420 for row in results:
421 stats, acls, labels, attributes = row
422 # TODO(gregorynisbet): under what circumstances is stats
423 # not a list of length 1?
424 assert len(stats) == 1
425 stats_map = stats[0]
Gregory Nisbetce77cef2019-07-29 12:27:21 -0700426
427 # Stripping the MIGRATED_HOST_SUFFIX makes it possible to
428 # migrate a DUT from autotest to skylab even after its hostname
429 # has been changed.
430 # This enables the steps (renaming the host,
431 # copying the inventory information to skylab) to be doable in
432 # either order.
Gregory Nisbet48f1eea2019-08-05 16:18:56 -0700433 hostname = _remove_hostname_suffix_if_present(
434 stats_map["hostname"],
435 MIGRATED_HOST_SUFFIX
436 )
437
Gregory Nisbet932f5a92019-09-05 14:02:31 -0700438 # TODO(gregorynisbet): clean up servo information
439 if "servo_host" not in attributes:
440 attributes["servo_host"] = "dummy_host"
441 if "servo_port" not in attributes:
442 attributes["servo_port"] = "dummy_port"
443
Gregory Nisbet99b61942019-07-08 09:43:06 -0700444 labels = self._cleanup_labels(labels)
445 attrs = [{"key": k, "value": v} for k, v in attributes.iteritems()]
446 out_labels = process_labels(labels, platform=stats_map["platform"])
447 skylab_json = {
448 "common": {
449 "attributes": attrs,
Gregory Nisbet23a06b92019-07-11 16:44:43 -0700450 "environment": "ENVIRONMENT_PROD",
Gregory Nisbetce77cef2019-07-29 12:27:21 -0700451 "hostname": hostname,
Gregory Nisbet99b61942019-07-08 09:43:06 -0700452 "id": ID_AUTOGEN_MESSAGE,
453 "labels": out_labels,
Gregory Nisbetcc0941f2019-08-29 13:56:05 -0700454 "serialNumber": attributes.get("serial_number", None),
Gregory Nisbet99b61942019-07-08 09:43:06 -0700455 }
456 }
457 print json.dumps(skylab_json, indent=4, sort_keys=True)
458
459
mblighbe630eb2008-08-01 16:41:48 +0000460class host_jobs(host):
mbligh1494eca2008-08-13 21:24:22 +0000461 """atest host jobs [--max-query] --mlist <file>|<hosts>"""
mblighbe630eb2008-08-01 16:41:48 +0000462 usage_action = 'jobs'
463
mbligh6996fe82008-08-13 00:32:27 +0000464 def __init__(self):
465 super(host_jobs, self).__init__()
466 self.parser.add_option('-q', '--max-query',
467 help='Limits the number of results '
468 '(20 by default)',
469 type='int', default=20)
470
471
472 def parse(self):
473 """Consume the specific options"""
mbligh9deeefa2009-05-01 23:11:08 +0000474 (options, leftover) = super(host_jobs, self).parse()
mbligh6996fe82008-08-13 00:32:27 +0000475 self.max_queries = options.max_query
476 return (options, leftover)
477
478
mblighbe630eb2008-08-01 16:41:48 +0000479 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800480 """Execute 'atest host jobs'."""
mblighbe630eb2008-08-01 16:41:48 +0000481 results = []
482 real_hosts = []
483 for host in self.hosts:
484 if host.endswith('*'):
485 stats = self.execute_rpc('get_hosts',
486 hostname__startswith=host.rstrip('*'))
487 if len(stats) == 0:
488 self.failure('No host matching %s' % host, item=host,
489 what_failed='Failed to stat')
490 [real_hosts.append(stat['hostname']) for stat in stats]
491 else:
492 real_hosts.append(host)
493
494 for host in real_hosts:
495 queue_entries = self.execute_rpc('get_host_queue_entries',
mbligh6996fe82008-08-13 00:32:27 +0000496 host__hostname=host,
mbligh1494eca2008-08-13 21:24:22 +0000497 query_limit=self.max_queries,
showard6958c722009-09-23 20:03:16 +0000498 sort_by=['-job__id'])
mblighbe630eb2008-08-01 16:41:48 +0000499 jobs = []
500 for entry in queue_entries:
501 job = {'job_id': entry['job']['id'],
502 'job_owner': entry['job']['owner'],
503 'job_name': entry['job']['name'],
504 'status': entry['status']}
505 jobs.append(job)
506 results.append((host, jobs))
507 return results
508
509
510 def output(self, results):
xixuand6011f12016-12-08 15:01:58 -0800511 """Print output of 'atest host jobs'.
512
513 @param results: the results to be printed.
514 """
mblighbe630eb2008-08-01 16:41:48 +0000515 for host, jobs in results:
516 print '-'*5
517 print 'Hostname: %s' % host
518 self.print_table(jobs, keys_header=['job_id',
showardbe0d8692009-08-20 23:42:44 +0000519 'job_owner',
520 'job_name',
521 'status'])
mblighbe630eb2008-08-01 16:41:48 +0000522
Justin Giorgi16bba562016-06-22 10:42:13 -0700523class BaseHostModCreate(host):
xixuand6011f12016-12-08 15:01:58 -0800524 """The base class for host_mod and host_create"""
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700525 # Matches one attribute=value pair
526 attribute_regex = r'(?P<attribute>\w+)=(?P<value>.+)?'
mblighbe630eb2008-08-01 16:41:48 +0000527
528 def __init__(self):
Justin Giorgi16bba562016-06-22 10:42:13 -0700529 """Add the options shared between host mod and host create actions."""
mblighbe630eb2008-08-01 16:41:48 +0000530 self.messages = []
Justin Giorgi16bba562016-06-22 10:42:13 -0700531 self.host_ids = {}
532 super(BaseHostModCreate, self).__init__()
mblighbe630eb2008-08-01 16:41:48 +0000533 self.parser.add_option('-l', '--lock',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700534 help='Lock hosts.',
mblighbe630eb2008-08-01 16:41:48 +0000535 action='store_true')
Matthew Sartori68186332015-04-27 17:19:53 -0700536 self.parser.add_option('-r', '--lock_reason',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700537 help='Reason for locking hosts.',
Matthew Sartori68186332015-04-27 17:19:53 -0700538 default='')
Ningning Xiaef35cb52018-05-04 17:58:20 -0700539 self.parser.add_option('-u', '--unlock',
540 help='Unlock hosts.',
541 action='store_true')
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700542
mblighe163b032008-10-18 14:30:27 +0000543 self.parser.add_option('-p', '--protection', type='choice',
mblighaed47e82009-03-17 19:06:18 +0000544 help=('Set the protection level on a host. '
Ningning Xiaef35cb52018-05-04 17:58:20 -0700545 'Must be one of: %s. %s' %
546 (', '.join('"%s"' % p
547 for p in self.protections),
548 skylab_utils.MSG_INVALID_IN_SKYLAB)),
mblighaed47e82009-03-17 19:06:18 +0000549 choices=self.protections)
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700550 self._attributes = []
551 self.parser.add_option('--attribute', '-i',
552 help=('Host attribute to add or change. Format '
553 'is <attribute>=<value>. Multiple '
554 'attributes can be set by passing the '
555 'argument multiple times. Attributes can '
556 'be unset by providing an empty value.'),
557 action='append')
mblighbe630eb2008-08-01 16:41:48 +0000558 self.parser.add_option('-b', '--labels',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700559 help=('Comma separated list of labels. '
560 'When --skylab is provided, a label must '
561 'be in the format of label-key:label-value'
562 ' (e.g., board:lumpy).'))
mblighbe630eb2008-08-01 16:41:48 +0000563 self.parser.add_option('-B', '--blist',
564 help='File listing the labels',
565 type='string',
566 metavar='LABEL_FLIST')
567 self.parser.add_option('-a', '--acls',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700568 help=('Comma separated list of ACLs. %s' %
569 skylab_utils.MSG_INVALID_IN_SKYLAB))
mblighbe630eb2008-08-01 16:41:48 +0000570 self.parser.add_option('-A', '--alist',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700571 help=('File listing the acls. %s' %
572 skylab_utils.MSG_INVALID_IN_SKYLAB),
mblighbe630eb2008-08-01 16:41:48 +0000573 type='string',
574 metavar='ACL_FLIST')
Justin Giorgi16bba562016-06-22 10:42:13 -0700575 self.parser.add_option('-t', '--platform',
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700576 help=('Sets the platform label. %s Please set '
577 'platform in labels (e.g., -b '
578 'platform:platform_name) with --skylab.' %
579 skylab_utils.MSG_INVALID_IN_SKYLAB))
mblighbe630eb2008-08-01 16:41:48 +0000580
581
582 def parse(self):
Justin Giorgi16bba562016-06-22 10:42:13 -0700583 """Consume the options common to host create and host mod.
584 """
mbligh9deeefa2009-05-01 23:11:08 +0000585 label_info = topic_common.item_parse_info(attribute_name='labels',
586 inline_option='labels',
587 filename_option='blist')
588 acl_info = topic_common.item_parse_info(attribute_name='acls',
589 inline_option='acls',
590 filename_option='alist')
591
Justin Giorgi16bba562016-06-22 10:42:13 -0700592 (options, leftover) = super(BaseHostModCreate, self).parse([label_info,
mbligh9deeefa2009-05-01 23:11:08 +0000593 acl_info],
594 req_items='hosts')
mblighbe630eb2008-08-01 16:41:48 +0000595
596 self._parse_lock_options(options)
Justin Giorgi16bba562016-06-22 10:42:13 -0700597
Ningning Xia64ced002018-05-16 17:36:20 -0700598 self.label_map = None
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700599 if self.allow_skylab and self.skylab:
Ningning Xiaef35cb52018-05-04 17:58:20 -0700600 # TODO(nxia): drop these flags when all hosts are migrated to skylab
Ningning Xia64ced002018-05-16 17:36:20 -0700601 if (options.protection or options.acls or options.alist or
602 options.platform):
603 self.invalid_syntax(
604 '--protection, --acls, --alist or --platform is not '
605 'supported with --skylab.')
606
607 if self.labels:
608 self.label_map = device.convert_to_label_map(self.labels)
Ningning Xiaef35cb52018-05-04 17:58:20 -0700609
mblighaed47e82009-03-17 19:06:18 +0000610 if options.protection:
611 self.data['protection'] = options.protection
Justin Giorgi16bba562016-06-22 10:42:13 -0700612 self.messages.append('Protection set to "%s"' % options.protection)
613
614 self.attributes = {}
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700615 if options.attribute:
616 for pair in options.attribute:
617 m = re.match(self.attribute_regex, pair)
618 if not m:
619 raise topic_common.CliError('Attribute must be in key=value '
620 'syntax.')
621 elif m.group('attribute') in self.attributes:
xixuand6011f12016-12-08 15:01:58 -0800622 raise topic_common.CliError(
623 'Multiple values provided for attribute '
624 '%s.' % m.group('attribute'))
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700625 self.attributes[m.group('attribute')] = m.group('value')
Justin Giorgi16bba562016-06-22 10:42:13 -0700626
627 self.platform = options.platform
mblighbe630eb2008-08-01 16:41:48 +0000628 return (options, leftover)
629
630
Justin Giorgi16bba562016-06-22 10:42:13 -0700631 def _set_acls(self, hosts, acls):
632 """Add hosts to acls (and remove from all other acls).
633
634 @param hosts: list of hostnames
635 @param acls: list of acl names
636 """
637 # Remove from all ACLs except 'Everyone' and ACLs in list
638 # Skip hosts that don't exist
639 for host in hosts:
640 if host not in self.host_ids:
641 continue
642 host_id = self.host_ids[host]
643 for a in self.execute_rpc('get_acl_groups', hosts=host_id):
644 if a['name'] not in self.acls and a['id'] != 1:
645 self.execute_rpc('acl_group_remove_hosts', id=a['id'],
646 hosts=self.hosts)
647
648 # Add hosts to the ACLs
649 self.check_and_create_items('get_acl_groups', 'add_acl_group',
650 self.acls)
651 for a in acls:
652 self.execute_rpc('acl_group_add_hosts', id=a, hosts=hosts)
653
654
655 def _remove_labels(self, host, condition):
656 """Remove all labels from host that meet condition(label).
657
658 @param host: hostname
659 @param condition: callable that returns bool when given a label
660 """
661 if host in self.host_ids:
662 host_id = self.host_ids[host]
663 labels_to_remove = []
664 for l in self.execute_rpc('get_labels', host=host_id):
665 if condition(l):
666 labels_to_remove.append(l['id'])
667 if labels_to_remove:
668 self.execute_rpc('host_remove_labels', id=host_id,
669 labels=labels_to_remove)
670
671
672 def _set_labels(self, host, labels):
673 """Apply labels to host (and remove all other labels).
674
675 @param host: hostname
676 @param labels: list of label names
677 """
678 condition = lambda l: l['name'] not in labels and not l['platform']
679 self._remove_labels(host, condition)
680 self.check_and_create_items('get_labels', 'add_label', labels)
681 self.execute_rpc('host_add_labels', id=host, labels=labels)
682
683
684 def _set_platform_label(self, host, platform_label):
685 """Apply the platform label to host (and remove existing).
686
687 @param host: hostname
688 @param platform_label: platform label's name
689 """
690 self._remove_labels(host, lambda l: l['platform'])
691 self.check_and_create_items('get_labels', 'add_label', [platform_label],
692 platform=True)
693 self.execute_rpc('host_add_labels', id=host, labels=[platform_label])
694
695
696 def _set_attributes(self, host, attributes):
697 """Set attributes on host.
698
699 @param host: hostname
700 @param attributes: attribute dictionary
701 """
702 for attr, value in self.attributes.iteritems():
703 self.execute_rpc('set_host_attribute', attribute=attr,
704 value=value, hostname=host)
705
706
707class host_mod(BaseHostModCreate):
708 """atest host mod [--lock|--unlock --force_modify_locking
709 --platform <arch>
710 --labels <labels>|--blist <label_file>
711 --acls <acls>|--alist <acl_file>
712 --protection <protection_type>
713 --attributes <attr>=<value>;<attr>=<value>
714 --mlist <mach_file>] <hosts>"""
715 usage_action = 'mod'
Justin Giorgi16bba562016-06-22 10:42:13 -0700716
717 def __init__(self):
718 """Add the options specific to the mod action"""
719 super(host_mod, self).__init__()
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700720 self.parser.add_option('--unlock-lock-id',
721 help=('Unlock the lock with the lock-id. %s' %
722 skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
723 default=None)
Justin Giorgi16bba562016-06-22 10:42:13 -0700724 self.parser.add_option('-f', '--force_modify_locking',
725 help='Forcefully lock\unlock a host',
726 action='store_true')
727 self.parser.add_option('--remove_acls',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700728 help=('Remove all active acls. %s' %
729 skylab_utils.MSG_INVALID_IN_SKYLAB),
Justin Giorgi16bba562016-06-22 10:42:13 -0700730 action='store_true')
731 self.parser.add_option('--remove_labels',
732 help='Remove all labels.',
733 action='store_true')
734
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700735 self.add_skylab_options()
Ningning Xia1dd82ee2018-06-08 11:41:03 -0700736 self.parser.add_option('--new-env',
737 dest='new_env',
738 choices=['staging', 'prod'],
739 help=('The new environment ("staging" or '
740 '"prod") of the hosts. %s' %
741 skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
742 default=None)
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700743
744
745 def _parse_unlock_options(self, options):
746 """Parse unlock related options."""
747 if self.skylab and options.unlock and options.unlock_lock_id is None:
748 self.invalid_syntax('Must provide --unlock-lock-id with "--skylab '
749 '--unlock".')
750
751 if (not (self.skylab and options.unlock) and
752 options.unlock_lock_id is not None):
753 self.invalid_syntax('--unlock-lock-id is only valid with '
754 '"--skylab --unlock".')
755
756 self.unlock_lock_id = options.unlock_lock_id
757
Justin Giorgi16bba562016-06-22 10:42:13 -0700758
759 def parse(self):
760 """Consume the specific options"""
761 (options, leftover) = super(host_mod, self).parse()
762
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700763 self._parse_unlock_options(options)
764
Justin Giorgi16bba562016-06-22 10:42:13 -0700765 if options.force_modify_locking:
766 self.data['force_modify_locking'] = True
767
Ningning Xiaef35cb52018-05-04 17:58:20 -0700768 if self.skylab and options.remove_acls:
769 # TODO(nxia): drop the flag when all hosts are migrated to skylab
770 self.invalid_syntax('--remove_acls is not supported with --skylab.')
771
Justin Giorgi16bba562016-06-22 10:42:13 -0700772 self.remove_acls = options.remove_acls
773 self.remove_labels = options.remove_labels
Ningning Xia1dd82ee2018-06-08 11:41:03 -0700774 self.new_env = options.new_env
Justin Giorgi16bba562016-06-22 10:42:13 -0700775
776 return (options, leftover)
777
778
Ningning Xiaef35cb52018-05-04 17:58:20 -0700779 def execute_skylab(self):
780 """Execute atest host mod with --skylab.
781
782 @return A list of hostnames which have been successfully modified.
783 """
784 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
785 inventory_repo.initialize()
786 data_dir = inventory_repo.get_data_dir()
787 lab = text_manager.load_lab(data_dir)
788
789 locked_by = None
790 if self.lock:
791 locked_by = inventory_repo.git_repo.config('user.email')
792
793 successes = []
794 for hostname in self.hosts:
795 try:
796 device.modify(
797 lab,
798 'duts',
799 hostname,
800 self.environment,
801 lock=self.lock,
802 locked_by=locked_by,
803 lock_reason = self.lock_reason,
804 unlock=self.unlock,
805 unlock_lock_id=self.unlock_lock_id,
806 attributes=self.attributes,
807 remove_labels=self.remove_labels,
Ningning Xia1dd82ee2018-06-08 11:41:03 -0700808 label_map=self.label_map,
809 new_env=self.new_env)
Ningning Xiaef35cb52018-05-04 17:58:20 -0700810 successes.append(hostname)
811 except device.SkylabDeviceActionError as e:
812 print('Cannot modify host %s: %s' % (hostname, e))
813
814 if successes:
815 text_manager.dump_lab(data_dir, lab)
816
817 status = inventory_repo.git_repo.status()
818 if not status:
819 print('Nothing is changed for hosts %s.' % successes)
820 return []
821
822 message = skylab_utils.construct_commit_message(
823 'Modify %d hosts.\n\n%s' % (len(successes), successes))
824 self.change_number = inventory_repo.upload_change(
825 message, draft=self.draft, dryrun=self.dryrun,
826 submit=self.submit)
827
828 return successes
829
830
Justin Giorgi16bba562016-06-22 10:42:13 -0700831 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800832 """Execute 'atest host mod'."""
Ningning Xiaef35cb52018-05-04 17:58:20 -0700833 if self.skylab:
834 return self.execute_skylab()
835
Justin Giorgi16bba562016-06-22 10:42:13 -0700836 successes = []
837 for host in self.execute_rpc('get_hosts', hostname__in=self.hosts):
838 self.host_ids[host['hostname']] = host['id']
839 for host in self.hosts:
840 if host not in self.host_ids:
841 self.failure('Cannot modify non-existant host %s.' % host)
842 continue
843 host_id = self.host_ids[host]
844
845 try:
846 if self.data:
847 self.execute_rpc('modify_host', item=host,
848 id=host, **self.data)
849
850 if self.attributes:
851 self._set_attributes(host, self.attributes)
852
853 if self.labels or self.remove_labels:
854 self._set_labels(host, self.labels)
855
856 if self.platform:
857 self._set_platform_label(host, self.platform)
858
859 # TODO: Make the AFE return True or False,
860 # especially for lock
861 successes.append(host)
862 except topic_common.CliError, full_error:
863 # Already logged by execute_rpc()
864 pass
865
866 if self.acls or self.remove_acls:
867 self._set_acls(self.hosts, self.acls)
868
869 return successes
870
871
872 def output(self, hosts):
xixuand6011f12016-12-08 15:01:58 -0800873 """Print output of 'atest host mod'.
874
875 @param hosts: the host list to be printed.
876 """
Justin Giorgi16bba562016-06-22 10:42:13 -0700877 for msg in self.messages:
878 self.print_wrapped(msg, hosts)
879
Ningning Xiaef35cb52018-05-04 17:58:20 -0700880 if hosts and self.skylab:
Ningning Xiac8b430d2018-05-17 15:10:25 -0700881 print('Modified hosts: %s.' % ', '.join(hosts))
Ningning Xiaef35cb52018-05-04 17:58:20 -0700882 if self.skylab and not self.dryrun and not self.submit:
Ningning Xiac8b430d2018-05-17 15:10:25 -0700883 print(skylab_utils.get_cl_message(self.change_number))
Ningning Xiaef35cb52018-05-04 17:58:20 -0700884
Justin Giorgi16bba562016-06-22 10:42:13 -0700885
886class HostInfo(object):
887 """Store host information so we don't have to keep looking it up."""
888 def __init__(self, hostname, platform, labels):
889 self.hostname = hostname
890 self.platform = platform
891 self.labels = labels
892
893
894class host_create(BaseHostModCreate):
895 """atest host create [--lock|--unlock --platform <arch>
896 --labels <labels>|--blist <label_file>
897 --acls <acls>|--alist <acl_file>
898 --protection <protection_type>
899 --attributes <attr>=<value>;<attr>=<value>
900 --mlist <mach_file>] <hosts>"""
901 usage_action = 'create'
902
903 def parse(self):
904 """Option logic specific to create action.
905 """
906 (options, leftovers) = super(host_create, self).parse()
907 self.locked = options.lock
908 if 'serials' in self.attributes:
909 if len(self.hosts) > 1:
910 raise topic_common.CliError('Can not specify serials with '
911 'multiple hosts.')
912
913
914 @classmethod
915 def construct_without_parse(
916 cls, web_server, hosts, platform=None,
917 locked=False, lock_reason='', labels=[], acls=[],
918 protection=host_protections.Protection.NO_PROTECTION):
919 """Construct a host_create object and fill in data from args.
920
921 Do not need to call parse after the construction.
922
923 Return an object of site_host_create ready to execute.
924
925 @param web_server: A string specifies the autotest webserver url.
926 It is needed to setup comm to make rpc.
927 @param hosts: A list of hostnames as strings.
928 @param platform: A string or None.
929 @param locked: A boolean.
930 @param lock_reason: A string.
931 @param labels: A list of labels as strings.
932 @param acls: A list of acls as strings.
933 @param protection: An enum defined in host_protections.
934 """
935 obj = cls()
936 obj.web_server = web_server
937 try:
938 # Setup stuff needed for afe comm.
939 obj.afe = rpc.afe_comm(web_server)
940 except rpc.AuthError, s:
941 obj.failure(str(s), fatal=True)
942 obj.hosts = hosts
943 obj.platform = platform
944 obj.locked = locked
945 if locked and lock_reason.strip():
946 obj.data['lock_reason'] = lock_reason.strip()
947 obj.labels = labels
948 obj.acls = acls
949 if protection:
950 obj.data['protection'] = protection
951 obj.attributes = {}
952 return obj
953
954
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700955 def _detect_host_info(self, host):
956 """Detect platform and labels from the host.
957
958 @param host: hostname
959
960 @return: HostInfo object
961 """
962 # Mock an afe_host object so that the host is constructed as if the
963 # data was already in afe
964 data = {'attributes': self.attributes, 'labels': self.labels}
965 afe_host = frontend.Host(None, data)
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700966 store = host_info.InMemoryHostInfoStore(
967 host_info.HostInfo(labels=self.labels,
968 attributes=self.attributes))
969 machine = {
970 'hostname': host,
971 'afe_host': afe_host,
972 'host_info_store': store
973 }
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700974 try:
Allen Li2c32d6b2017-02-03 15:28:10 -0800975 if bin_utils.ping(host, tries=1, deadline=1) == 0:
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700976 serials = self.attributes.get('serials', '').split(',')
Richard Barnette9db80682018-04-26 00:55:15 +0000977 adb_serial = self.attributes.get('serials')
978 host_dut = hosts.create_host(machine,
979 adb_serial=adb_serial)
xixuand6011f12016-12-08 15:01:58 -0800980
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700981 info = HostInfo(host, host_dut.get_platform(),
982 host_dut.get_labels())
xixuand6011f12016-12-08 15:01:58 -0800983 # Clean host to make sure nothing left after calling it,
984 # e.g. tunnels.
985 if hasattr(host_dut, 'close'):
986 host_dut.close()
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700987 else:
988 # Can't ping the host, use default information.
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700989 info = HostInfo(host, None, [])
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700990 except (socket.gaierror, error.AutoservRunError,
991 error.AutoservSSHTimeout):
992 # We may be adding a host that does not exist yet or we can't
993 # reach due to hostname/address issues or if the host is down.
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700994 info = HostInfo(host, None, [])
995 return info
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700996
997
mblighbe630eb2008-08-01 16:41:48 +0000998 def _execute_add_one_host(self, host):
mbligh719e14a2008-12-04 01:17:08 +0000999 # Always add the hosts as locked to avoid the host
Matthew Sartori68186332015-04-27 17:19:53 -07001000 # being picked up by the scheduler before it's ACL'ed.
mbligh719e14a2008-12-04 01:17:08 +00001001 self.data['locked'] = True
Matthew Sartori68186332015-04-27 17:19:53 -07001002 if not self.locked:
1003 self.data['lock_reason'] = 'Forced lock on device creation'
Justin Giorgi16bba562016-06-22 10:42:13 -07001004 self.execute_rpc('add_host', hostname=host, status="Ready", **self.data)
mblighbe630eb2008-08-01 16:41:48 +00001005
Justin Giorgi16bba562016-06-22 10:42:13 -07001006 # If there are labels avaliable for host, use them.
Prathmesh Prabhud252d262017-05-31 11:46:34 -07001007 info = self._detect_host_info(host)
Justin Giorgi16bba562016-06-22 10:42:13 -07001008 labels = set(self.labels)
Prathmesh Prabhud252d262017-05-31 11:46:34 -07001009 if info.labels:
1010 labels.update(info.labels)
Justin Giorgi16bba562016-06-22 10:42:13 -07001011
1012 if labels:
1013 self._set_labels(host, list(labels))
1014
1015 # Now add the platform label.
1016 # If a platform was not provided and we were able to retrieve it
1017 # from the host, use the retrieved platform.
Prathmesh Prabhud252d262017-05-31 11:46:34 -07001018 platform = self.platform if self.platform else info.platform
Justin Giorgi16bba562016-06-22 10:42:13 -07001019 if platform:
1020 self._set_platform_label(host, platform)
1021
1022 if self.attributes:
1023 self._set_attributes(host, self.attributes)
mblighbe630eb2008-08-01 16:41:48 +00001024
1025
Justin Giorgi5208eaa2016-07-02 20:12:12 -07001026 def execute(self):
xixuand6011f12016-12-08 15:01:58 -08001027 """Execute 'atest host create'."""
Justin Giorgi16bba562016-06-22 10:42:13 -07001028 successful_hosts = []
1029 for host in self.hosts:
1030 try:
1031 self._execute_add_one_host(host)
1032 successful_hosts.append(host)
1033 except topic_common.CliError:
1034 pass
Jiaxi Luoc342f9f2014-05-19 16:22:03 -07001035
1036 if successful_hosts:
Justin Giorgi16bba562016-06-22 10:42:13 -07001037 self._set_acls(successful_hosts, self.acls)
Jiaxi Luoc342f9f2014-05-19 16:22:03 -07001038
1039 if not self.locked:
1040 for host in successful_hosts:
Matthew Sartori68186332015-04-27 17:19:53 -07001041 self.execute_rpc('modify_host', id=host, locked=False,
1042 lock_reason='')
Jiaxi Luoc342f9f2014-05-19 16:22:03 -07001043 return successful_hosts
1044
1045
mblighbe630eb2008-08-01 16:41:48 +00001046 def output(self, hosts):
xixuand6011f12016-12-08 15:01:58 -08001047 """Print output of 'atest host create'.
1048
1049 @param hosts: the added host list to be printed.
1050 """
mblighbe630eb2008-08-01 16:41:48 +00001051 self.print_wrapped('Added host', hosts)
1052
1053
1054class host_delete(action_common.atest_delete, host):
1055 """atest host delete [--mlist <mach_file>] <hosts>"""
Ningning Xiac8b430d2018-05-17 15:10:25 -07001056
1057 def __init__(self):
1058 super(host_delete, self).__init__()
1059
1060 self.add_skylab_options()
1061
1062
1063 def execute_skylab(self):
1064 """Execute 'atest host delete' with '--skylab'.
1065
1066 @return A list of hostnames which have been successfully deleted.
1067 """
1068 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
1069 inventory_repo.initialize()
1070 data_dir = inventory_repo.get_data_dir()
1071 lab = text_manager.load_lab(data_dir)
1072
1073 successes = []
1074 for hostname in self.hosts:
1075 try:
1076 device.delete(
1077 lab,
1078 'duts',
1079 hostname,
1080 self.environment)
1081 successes.append(hostname)
1082 except device.SkylabDeviceActionError as e:
1083 print('Cannot delete host %s: %s' % (hostname, e))
1084
1085 if successes:
1086 text_manager.dump_lab(data_dir, lab)
1087 message = skylab_utils.construct_commit_message(
1088 'Delete %d hosts.\n\n%s' % (len(successes), successes))
1089 self.change_number = inventory_repo.upload_change(
1090 message, draft=self.draft, dryrun=self.dryrun,
1091 submit=self.submit)
1092
1093 return successes
1094
1095
1096 def execute(self):
1097 """Execute 'atest host delete'.
1098
1099 @return A list of hostnames which have been successfully deleted.
1100 """
1101 if self.skylab:
1102 return self.execute_skylab()
1103
1104 return super(host_delete, self).execute()
Ningning Xiac46bdd12018-05-29 11:24:14 -07001105
1106
1107class InvalidHostnameError(Exception):
1108 """Cannot perform actions on the host because of invalid hostname."""
1109
1110
1111def _add_hostname_suffix(hostname, suffix):
1112 """Add the suffix to the hostname."""
1113 if hostname.endswith(suffix):
1114 raise InvalidHostnameError(
1115 'Cannot add "%s" as it already contains the suffix.' % suffix)
1116
1117 return hostname + suffix
1118
1119
Gregory Nisbet48f1eea2019-08-05 16:18:56 -07001120def _remove_hostname_suffix_if_present(hostname, suffix):
Ningning Xiac46bdd12018-05-29 11:24:14 -07001121 """Remove the suffix from the hostname."""
Gregory Nisbet48f1eea2019-08-05 16:18:56 -07001122 if hostname.endswith(suffix):
1123 return hostname[:len(hostname) - len(suffix)]
1124 else:
1125 return hostname
Ningning Xiac46bdd12018-05-29 11:24:14 -07001126
1127
1128class host_rename(host):
1129 """Host rename is only for migrating hosts between skylab and AFE DB."""
1130
1131 usage_action = 'rename'
1132
1133 def __init__(self):
1134 """Add the options specific to the rename action."""
1135 super(host_rename, self).__init__()
1136
1137 self.parser.add_option('--for-migration',
1138 help=('Rename hostnames for migration. Rename '
1139 'each "hostname" to "hostname%s". '
1140 'The original "hostname" must not contain '
1141 'suffix.' % MIGRATED_HOST_SUFFIX),
1142 action='store_true',
1143 default=False)
1144 self.parser.add_option('--for-rollback',
1145 help=('Rename hostnames for migration rollback. '
1146 'Rename each "hostname%s" to its original '
1147 '"hostname".' % MIGRATED_HOST_SUFFIX),
1148 action='store_true',
1149 default=False)
1150 self.parser.add_option('--dryrun',
1151 help='Execute the action as a dryrun.',
1152 action='store_true',
1153 default=False)
Gregory Nisbetee457f62019-07-08 15:47:26 -07001154 self.parser.add_option('--non-interactive',
1155 help='run non-interactively',
Gregory Nisbet9744ea92019-07-02 12:36:59 -07001156 action='store_true',
1157 default=False)
Ningning Xiac46bdd12018-05-29 11:24:14 -07001158
1159
1160 def parse(self):
1161 """Consume the options common to host rename."""
1162 (options, leftovers) = super(host_rename, self).parse()
1163 self.for_migration = options.for_migration
1164 self.for_rollback = options.for_rollback
1165 self.dryrun = options.dryrun
Gregory Nisbetee457f62019-07-08 15:47:26 -07001166 self.interactive = not options.non_interactive
Ningning Xiac46bdd12018-05-29 11:24:14 -07001167 self.host_ids = {}
1168
1169 if not (self.for_migration ^ self.for_rollback):
1170 self.invalid_syntax('--for-migration and --for-rollback are '
1171 'exclusive, and one of them must be enabled.')
1172
1173 if not self.hosts:
1174 self.invalid_syntax('Must provide hostname(s).')
1175
1176 if self.dryrun:
1177 print('This will be a dryrun and will not rename hostnames.')
1178
1179 return (options, leftovers)
1180
1181
1182 def execute(self):
1183 """Execute 'atest host rename'."""
Gregory Nisbetee457f62019-07-08 15:47:26 -07001184 if self.interactive:
1185 if self.prompt_confirmation():
1186 pass
1187 else:
1188 return
Ningning Xiac46bdd12018-05-29 11:24:14 -07001189
1190 successes = []
1191 for host in self.execute_rpc('get_hosts', hostname__in=self.hosts):
1192 self.host_ids[host['hostname']] = host['id']
1193 for host in self.hosts:
1194 if host not in self.host_ids:
1195 self.failure('Cannot rename non-existant host %s.' % host,
1196 item=host, what_failed='Failed to rename')
1197 continue
1198 try:
Ningning Xiab261b162018-06-07 17:24:59 -07001199 host_id = self.host_ids[host]
Ningning Xiac46bdd12018-05-29 11:24:14 -07001200 if self.for_migration:
1201 new_hostname = _add_hostname_suffix(
1202 host, MIGRATED_HOST_SUFFIX)
1203 else:
1204 #for_rollback
Gregory Nisbet917ee332019-09-05 11:41:23 -07001205 new_hostname = _remove_hostname_suffix_if_present(
Ningning Xiac46bdd12018-05-29 11:24:14 -07001206 host, MIGRATED_HOST_SUFFIX)
1207
1208 if not self.dryrun:
Ningning Xia423c7962018-06-07 15:27:18 -07001209 # TODO(crbug.com/850737): delete and abort HQE.
Ningning Xiac46bdd12018-05-29 11:24:14 -07001210 data = {'hostname': new_hostname}
Ningning Xiab261b162018-06-07 17:24:59 -07001211 self.execute_rpc('modify_host', item=host, id=host_id,
1212 **data)
Ningning Xiac46bdd12018-05-29 11:24:14 -07001213 successes.append((host, new_hostname))
1214 except InvalidHostnameError as e:
1215 self.failure('Cannot rename host %s: %s' % (host, e), item=host,
1216 what_failed='Failed to rename')
1217 except topic_common.CliError, full_error:
1218 # Already logged by execute_rpc()
1219 pass
1220
1221 return successes
1222
1223
1224 def output(self, results):
1225 """Print output of 'atest host rename'."""
1226 if results:
1227 print('Successfully renamed:')
1228 for old_hostname, new_hostname in results:
1229 print('%s to %s' % (old_hostname, new_hostname))
Ningning Xia9df3d152018-05-23 17:15:14 -07001230
1231
1232class host_migrate(action_common.atest_list, host):
1233 """'atest host migrate' to migrate or rollback hosts."""
1234
1235 usage_action = 'migrate'
1236
1237 def __init__(self):
1238 super(host_migrate, self).__init__()
1239
1240 self.parser.add_option('--migration',
1241 dest='migration',
1242 help='Migrate the hosts to skylab.',
1243 action='store_true',
1244 default=False)
1245 self.parser.add_option('--rollback',
1246 dest='rollback',
1247 help='Rollback the hosts migrated to skylab.',
1248 action='store_true',
1249 default=False)
1250 self.parser.add_option('--model',
1251 help='Model of the hosts to migrate.',
1252 dest='model',
1253 default=None)
Xixuan Wu84621e52018-08-28 14:29:53 -07001254 self.parser.add_option('--board',
1255 help='Board of the hosts to migrate.',
1256 dest='board',
1257 default=None)
Ningning Xia9df3d152018-05-23 17:15:14 -07001258 self.parser.add_option('--pool',
1259 help=('Pool of the hosts to migrate. Must '
1260 'specify --model for the pool.'),
1261 dest='pool',
1262 default=None)
1263
1264 self.add_skylab_options(enforce_skylab=True)
1265
1266
1267 def parse(self):
1268 """Consume the specific options"""
1269 (options, leftover) = super(host_migrate, self).parse()
1270
1271 self.migration = options.migration
1272 self.rollback = options.rollback
1273 self.model = options.model
1274 self.pool = options.pool
Xixuan Wu84621e52018-08-28 14:29:53 -07001275 self.board = options.board
Ningning Xia9df3d152018-05-23 17:15:14 -07001276 self.host_ids = {}
1277
1278 if not (self.migration ^ self.rollback):
1279 self.invalid_syntax('--migration and --rollback are exclusive, '
1280 'and one of them must be enabled.')
1281
Xixuan Wu84621e52018-08-28 14:29:53 -07001282 if self.pool is not None and (self.model is None and
1283 self.board is None):
1284 self.invalid_syntax('Must provide --model or --board with --pool.')
Ningning Xia9df3d152018-05-23 17:15:14 -07001285
Xixuan Wu84621e52018-08-28 14:29:53 -07001286 if not self.hosts and not (self.model or self.board):
1287 self.invalid_syntax('Must provide hosts or --model or --board.')
Ningning Xia9df3d152018-05-23 17:15:14 -07001288
1289 return (options, leftover)
1290
1291
Ningning Xia56d68432018-06-06 17:28:20 -07001292 def _remove_invalid_hostnames(self, hostnames, log_failure=False):
1293 """Remove hostnames with MIGRATED_HOST_SUFFIX.
1294
1295 @param hostnames: A list of hostnames.
1296 @param log_failure: Bool indicating whether to log invalid hostsnames.
1297
1298 @return A list of valid hostnames.
1299 """
1300 invalid_hostnames = set()
Ningning Xia9df3d152018-05-23 17:15:14 -07001301 for hostname in hostnames:
1302 if hostname.endswith(MIGRATED_HOST_SUFFIX):
Ningning Xia56d68432018-06-06 17:28:20 -07001303 if log_failure:
1304 self.failure('Cannot migrate host with suffix "%s" %s.' %
1305 (MIGRATED_HOST_SUFFIX, hostname),
1306 item=hostname, what_failed='Failed to rename')
1307 invalid_hostnames.add(hostname)
1308
1309 hostnames = list(set(hostnames) - invalid_hostnames)
1310
1311 return hostnames
1312
1313
1314 def execute(self):
1315 """Execute 'atest host migrate'."""
1316 hostnames = self._remove_invalid_hostnames(self.hosts, log_failure=True)
Ningning Xia9df3d152018-05-23 17:15:14 -07001317
1318 filters = {}
1319 check_results = {}
1320 if hostnames:
1321 check_results['hostname__in'] = 'hostname'
1322 if self.migration:
1323 filters['hostname__in'] = hostnames
1324 else:
1325 # rollback
1326 hostnames_with_suffix = [
1327 _add_hostname_suffix(h, MIGRATED_HOST_SUFFIX)
1328 for h in hostnames]
1329 filters['hostname__in'] = hostnames_with_suffix
Ningning Xia56d68432018-06-06 17:28:20 -07001330 else:
1331 # TODO(nxia): add exclude_filter {'hostname__endswith':
1332 # MIGRATED_HOST_SUFFIX} for --migration
1333 if self.rollback:
1334 filters['hostname__endswith'] = MIGRATED_HOST_SUFFIX
Ningning Xia9df3d152018-05-23 17:15:14 -07001335
1336 labels = []
1337 if self.model:
1338 labels.append('model:%s' % self.model)
1339 if self.pool:
1340 labels.append('pool:%s' % self.pool)
Xixuan Wu84621e52018-08-28 14:29:53 -07001341 if self.board:
1342 labels.append('board:%s' % self.board)
Ningning Xia9df3d152018-05-23 17:15:14 -07001343
1344 if labels:
1345 if len(labels) == 1:
1346 filters['labels__name__in'] = labels
1347 check_results['labels__name__in'] = None
1348 else:
1349 filters['multiple_labels'] = labels
1350 check_results['multiple_labels'] = None
1351
1352 results = super(host_migrate, self).execute(
1353 op='get_hosts', filters=filters, check_results=check_results)
1354 hostnames = [h['hostname'] for h in results]
1355
Ningning Xia56d68432018-06-06 17:28:20 -07001356 if self.migration:
1357 hostnames = self._remove_invalid_hostnames(hostnames)
1358 else:
1359 # rollback
1360 hostnames = [_remove_hostname_suffix(h, MIGRATED_HOST_SUFFIX)
1361 for h in hostnames]
1362
Ningning Xia9df3d152018-05-23 17:15:14 -07001363 return self.execute_skylab_migration(hostnames)
1364
1365
Xixuan Wua8d93c82018-08-03 16:23:26 -07001366 def assign_duts_to_drone(self, infra, devices, environment):
Ningning Xia877817f2018-06-08 12:23:48 -07001367 """Assign uids of the devices to a random skylab drone.
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001368
1369 @param infra: An instance of lab_pb2.Infrastructure.
Xixuan Wua8d93c82018-08-03 16:23:26 -07001370 @param devices: A list of device_pb2.Device to be assigned to the drone.
1371 @param environment: 'staging' or 'prod'.
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001372 """
1373 skylab_drones = skylab_server.get_servers(
Xixuan Wua8d93c82018-08-03 16:23:26 -07001374 infra, environment, role='skylab_drone', status='primary')
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001375
1376 if len(skylab_drones) == 0:
1377 raise device.SkylabDeviceActionError(
1378 'No skylab drone is found in primary status and staging '
1379 'environment. Please confirm there is at least one valid skylab'
1380 ' drone added in skylab inventory.')
1381
Xixuan Wudfc2de22018-08-06 09:29:01 -07001382 for device in devices:
1383 # Randomly distribute each device to a skylab_drone.
1384 skylab_drone = random.choice(skylab_drones)
1385 skylab_server.add_dut_uids(skylab_drone, [device])
Ningning Xia877817f2018-06-08 12:23:48 -07001386
1387
1388 def remove_duts_from_drone(self, infra, devices):
1389 """Remove uids of the devices from their skylab drones.
1390
1391 @param infra: An instance of lab_pb2.Infrastructure.
1392 @devices: A list of device_pb2.Device to be remove from the drone.
1393 """
1394 skylab_drones = skylab_server.get_servers(
1395 infra, 'staging', role='skylab_drone', status='primary')
1396
1397 for skylab_drone in skylab_drones:
1398 skylab_server.remove_dut_uids(skylab_drone, devices)
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001399
1400
Ningning Xia9df3d152018-05-23 17:15:14 -07001401 def execute_skylab_migration(self, hostnames):
1402 """Execute migration in skylab_inventory.
1403
1404 @param hostnames: A list of hostnames to migrate.
1405 @return If there're hosts to migrate, return a list of the hostnames and
1406 a message instructing actions after the migration; else return
1407 None.
1408 """
1409 if not hostnames:
1410 return
1411
1412 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
1413 inventory_repo.initialize()
1414
1415 subdirs = ['skylab', 'prod', 'staging']
1416 data_dirs = skylab_data_dir, prod_data_dir, staging_data_dir = [
1417 inventory_repo.get_data_dir(data_subdir=d) for d in subdirs]
1418 skylab_lab, prod_lab, staging_lab = [
1419 text_manager.load_lab(d) for d in data_dirs]
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001420 infra = text_manager.load_infrastructure(skylab_data_dir)
Ningning Xia9df3d152018-05-23 17:15:14 -07001421
1422 label_map = None
1423 labels = []
Xixuan Wu46f8e202018-12-13 14:40:58 -08001424 if self.board:
1425 labels.append('board:%s' % self.board)
Ningning Xia9df3d152018-05-23 17:15:14 -07001426 if self.model:
Xixuan Wu46f8e202018-12-13 14:40:58 -08001427 labels.append('model:%s' % self.model)
Ningning Xia9df3d152018-05-23 17:15:14 -07001428 if self.pool:
1429 labels.append('critical_pool:%s' % self.pool)
1430 if labels:
1431 label_map = device.convert_to_label_map(labels)
1432
1433 if self.migration:
1434 prod_devices = device.move_devices(
1435 prod_lab, skylab_lab, 'duts', label_map=label_map,
1436 hostnames=hostnames)
1437 staging_devices = device.move_devices(
1438 staging_lab, skylab_lab, 'duts', label_map=label_map,
1439 hostnames=hostnames)
1440
1441 all_devices = prod_devices + staging_devices
1442 # Hostnames in afe_hosts tabel.
1443 device_hostnames = [str(d.common.hostname) for d in all_devices]
1444 message = (
1445 'Migration: move %s hosts into skylab_inventory.\n\n'
Ningning Xia423c7962018-06-07 15:27:18 -07001446 'Please run this command after the CL is submitted:\n'
Ningning Xia9df3d152018-05-23 17:15:14 -07001447 'atest host rename --for-migration %s' %
1448 (len(all_devices), ' '.join(device_hostnames)))
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001449
Xixuan Wua8d93c82018-08-03 16:23:26 -07001450 self.assign_duts_to_drone(infra, prod_devices, 'prod')
1451 self.assign_duts_to_drone(infra, staging_devices, 'staging')
Ningning Xia9df3d152018-05-23 17:15:14 -07001452 else:
1453 # rollback
1454 prod_devices = device.move_devices(
1455 skylab_lab, prod_lab, 'duts', environment='prod',
Ningning Xia56d68432018-06-06 17:28:20 -07001456 label_map=label_map, hostnames=hostnames)
Ningning Xia9df3d152018-05-23 17:15:14 -07001457 staging_devices = device.move_devices(
Ningning Xia877817f2018-06-08 12:23:48 -07001458 skylab_lab, staging_lab, 'duts', environment='staging',
Ningning Xia56d68432018-06-06 17:28:20 -07001459 label_map=label_map, hostnames=hostnames)
Ningning Xia9df3d152018-05-23 17:15:14 -07001460
1461 all_devices = prod_devices + staging_devices
1462 # Hostnames in afe_hosts tabel.
1463 device_hostnames = [_add_hostname_suffix(str(d.common.hostname),
1464 MIGRATED_HOST_SUFFIX)
1465 for d in all_devices]
1466 message = (
1467 'Rollback: remove %s hosts from skylab_inventory.\n\n'
Ningning Xia423c7962018-06-07 15:27:18 -07001468 'Please run this command after the CL is submitted:\n'
Ningning Xia9df3d152018-05-23 17:15:14 -07001469 'atest host rename --for-rollback %s' %
1470 (len(all_devices), ' '.join(device_hostnames)))
1471
Ningning Xia877817f2018-06-08 12:23:48 -07001472 self.remove_duts_from_drone(infra, all_devices)
1473
Ningning Xia9df3d152018-05-23 17:15:14 -07001474 if all_devices:
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001475 text_manager.dump_infrastructure(skylab_data_dir, infra)
1476
Ningning Xia9df3d152018-05-23 17:15:14 -07001477 if prod_devices:
1478 text_manager.dump_lab(prod_data_dir, prod_lab)
1479
1480 if staging_devices:
1481 text_manager.dump_lab(staging_data_dir, staging_lab)
1482
1483 text_manager.dump_lab(skylab_data_dir, skylab_lab)
1484
1485 self.change_number = inventory_repo.upload_change(
1486 message, draft=self.draft, dryrun=self.dryrun,
1487 submit=self.submit)
1488
1489 return all_devices, message
1490
1491
1492 def output(self, result):
1493 """Print output of 'atest host list'.
1494
1495 @param result: the result to be printed.
1496 """
1497 if result:
1498 devices, message = result
1499
1500 if devices:
1501 hostnames = [h.common.hostname for h in devices]
1502 if self.migration:
1503 print('Migrating hosts: %s' % ','.join(hostnames))
1504 else:
1505 # rollback
1506 print('Rolling back hosts: %s' % ','.join(hostnames))
1507
1508 if not self.dryrun:
1509 if not self.submit:
1510 print(skylab_utils.get_cl_message(self.change_number))
1511 else:
1512 # Print the instruction command for renaming hosts.
1513 print('%s' % message)
1514 else:
1515 print('No hosts were migrated.')
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001516
1517
1518class host_skylab_migrate(action_common.atest_list, host):
1519 usage_action = 'skylab_migrate'
1520
1521 def __init__(self):
1522 super(host_skylab_migrate, self).__init__()
1523 self.parser.add_option('--dry-run',
1524 help='Dry run. Show only candidate hosts.',
1525 action='store_true',
1526 dest='dry_run')
1527 self.parser.add_option('--ratio',
1528 help='ratio of hosts to migrate as number from 0 to 1.',
1529 type=float,
1530 dest='ratio',
1531 default=1)
1532 self.parser.add_option('--bug-number',
1533 help='bug number for tracking purposes.',
1534 dest='bug_number',
1535 default=None)
1536 self.parser.add_option('--board',
1537 help='Board of the hosts to migrate',
1538 dest='board',
1539 default=None)
1540 self.parser.add_option('--model',
1541 help='Model of the hosts to migrate',
1542 dest='model',
1543 default=None)
1544 self.parser.add_option('--pool',
1545 help='Pool of the hosts to migrate',
1546 dest='pool',
1547 default=None)
Gregory Nisbet917ee332019-09-05 11:41:23 -07001548 # TODO(gregorynisbet): remove this flag and make quick-add-duts default.
1549 self.parser.add_option('-q',
1550 '--use-quick-add',
1551 help='whether to use "skylab quick-add-duts"',
1552 dest='use_quick_add',
1553 action='store_true')
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001554 def parse(self):
1555 (options, leftover) = super(host_skylab_migrate, self).parse()
1556 self.dry_run = options.dry_run
1557 self.ratio = options.ratio
1558 self.bug_number = options.bug_number
1559 self.model = options.model
1560 self.pool = options.pool
1561 self.board = options.board
1562 self._reason = "migration to skylab: %s" % self.bug_number
Gregory Nisbet917ee332019-09-05 11:41:23 -07001563 self.use_quick_add = options.use_quick_add
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001564 return (options, leftover)
1565
1566
1567 def _host_skylab_migrate_get_hostnames(self, model=None, pool=None, board=None):
1568 """
1569 @params : in 'model', 'pool', 'board'
1570
1571 """
1572 # TODO(gregorynisbet)
1573 # this just gets all the hostnames, it doesn't filter by
1574 # presence or absence of migrated-do-not-use.
1575 labels = []
Gregory Nisbetc406c812019-09-05 10:48:37 -07001576 for key, value in ({'model': model, 'board': board, 'pool': pool}).items():
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001577 if value:
1578 labels.append(key + ":" + value)
1579 filters = {}
1580 check_results = {}
1581 # Copy the filter and check_results initialization logic from
1582 # the 'execute' method of the class 'host_migrate'.
1583 if not labels:
1584 return []
1585 elif len(labels) == 1:
1586 filters['labels__name__in'] = labels
1587 check_results['labels__name__in'] = None
1588 elif len(labels) > 1:
1589 filters['multiple_labels'] = labels
1590 check_results['multiple_labels'] = None
1591 else:
1592 assert False
1593
1594 results = super(host_skylab_migrate, self).execute(
1595 op='get_hosts', filters=filters, check_results=check_results)
1596 return [result['hostname'] for result in results]
1597
1598
1599 def _validate_one_hostname_source(self):
1600 """Validate that hostname source is explicit hostnames or valid query.
1601
1602 Hostnames must either be provided explicitly or be the result of a
1603 query defined by 'model', 'board', and 'pool'.
1604
1605 @returns : whether the hostnames come from exactly one valid source.
1606 """
1607 has_criteria = any([(self.model and self.board), self.board, self.pool])
1608 has_command_line_hosts = bool(self.hosts)
1609 if has_criteria != has_command_line_hosts:
1610 # all good, one data source
1611 return True
1612 if has_criteria and has_command_line_hosts:
1613 self.failure(
1614 '--model/host/board and explicit hostnames are alternatives. Provide exactly one.',
1615 item='cli',
1616 what_failed='user')
1617 return False
1618 self.failure(
1619 'no explicit hosts and no criteria provided.',
1620 item='cli',
1621 what_failed='user')
1622 return False
1623
1624
1625 def execute(self):
1626 if not self._validate_one_hostname_source():
1627 return None
1628 if self.hosts:
1629 hostnames = self.hosts
1630 else:
Gregory Nisbet5635ddc2019-09-05 10:38:05 -07001631 hostnames = self._host_skylab_migrate_get_hostnames(
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001632 model=self.model,
1633 board=self.board,
1634 pool=self.pool,
1635 )
1636 if self.dry_run:
1637 return hostnames
1638 if not hostnames:
1639 return {'error': 'no hosts to migrate'}
1640 res = skylab_migration.migrate(
1641 ratio=self.ratio,
1642 reason=self._reason,
1643 hostnames=hostnames,
1644 max_duration=10 * 60,
1645 interval_len=2,
1646 min_ready_intervals=10,
1647 immediately=True,
Gregory Nisbet917ee332019-09-05 11:41:23 -07001648 use_quick_add=self.use_quick_add,
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001649 )
1650 return res
1651
1652
1653 def output(self, result):
1654 if result is not None:
1655 print json.dumps(result, indent=4, sort_keys=True)