blob: cf7c170b26150ab6cf8c06b8de21d5b81ea9c5c6 [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 Nisbet99b61942019-07-08 09:43:06 -0700438 labels = self._cleanup_labels(labels)
439 attrs = [{"key": k, "value": v} for k, v in attributes.iteritems()]
440 out_labels = process_labels(labels, platform=stats_map["platform"])
441 skylab_json = {
442 "common": {
443 "attributes": attrs,
Gregory Nisbet23a06b92019-07-11 16:44:43 -0700444 "environment": "ENVIRONMENT_PROD",
Gregory Nisbetce77cef2019-07-29 12:27:21 -0700445 "hostname": hostname,
Gregory Nisbet99b61942019-07-08 09:43:06 -0700446 "id": ID_AUTOGEN_MESSAGE,
447 "labels": out_labels,
Gregory Nisbetcc0941f2019-08-29 13:56:05 -0700448 "serialNumber": attributes.get("serial_number", None),
Gregory Nisbet99b61942019-07-08 09:43:06 -0700449 }
450 }
451 print json.dumps(skylab_json, indent=4, sort_keys=True)
452
453
mblighbe630eb2008-08-01 16:41:48 +0000454class host_jobs(host):
mbligh1494eca2008-08-13 21:24:22 +0000455 """atest host jobs [--max-query] --mlist <file>|<hosts>"""
mblighbe630eb2008-08-01 16:41:48 +0000456 usage_action = 'jobs'
457
mbligh6996fe82008-08-13 00:32:27 +0000458 def __init__(self):
459 super(host_jobs, self).__init__()
460 self.parser.add_option('-q', '--max-query',
461 help='Limits the number of results '
462 '(20 by default)',
463 type='int', default=20)
464
465
466 def parse(self):
467 """Consume the specific options"""
mbligh9deeefa2009-05-01 23:11:08 +0000468 (options, leftover) = super(host_jobs, self).parse()
mbligh6996fe82008-08-13 00:32:27 +0000469 self.max_queries = options.max_query
470 return (options, leftover)
471
472
mblighbe630eb2008-08-01 16:41:48 +0000473 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800474 """Execute 'atest host jobs'."""
mblighbe630eb2008-08-01 16:41:48 +0000475 results = []
476 real_hosts = []
477 for host in self.hosts:
478 if host.endswith('*'):
479 stats = self.execute_rpc('get_hosts',
480 hostname__startswith=host.rstrip('*'))
481 if len(stats) == 0:
482 self.failure('No host matching %s' % host, item=host,
483 what_failed='Failed to stat')
484 [real_hosts.append(stat['hostname']) for stat in stats]
485 else:
486 real_hosts.append(host)
487
488 for host in real_hosts:
489 queue_entries = self.execute_rpc('get_host_queue_entries',
mbligh6996fe82008-08-13 00:32:27 +0000490 host__hostname=host,
mbligh1494eca2008-08-13 21:24:22 +0000491 query_limit=self.max_queries,
showard6958c722009-09-23 20:03:16 +0000492 sort_by=['-job__id'])
mblighbe630eb2008-08-01 16:41:48 +0000493 jobs = []
494 for entry in queue_entries:
495 job = {'job_id': entry['job']['id'],
496 'job_owner': entry['job']['owner'],
497 'job_name': entry['job']['name'],
498 'status': entry['status']}
499 jobs.append(job)
500 results.append((host, jobs))
501 return results
502
503
504 def output(self, results):
xixuand6011f12016-12-08 15:01:58 -0800505 """Print output of 'atest host jobs'.
506
507 @param results: the results to be printed.
508 """
mblighbe630eb2008-08-01 16:41:48 +0000509 for host, jobs in results:
510 print '-'*5
511 print 'Hostname: %s' % host
512 self.print_table(jobs, keys_header=['job_id',
showardbe0d8692009-08-20 23:42:44 +0000513 'job_owner',
514 'job_name',
515 'status'])
mblighbe630eb2008-08-01 16:41:48 +0000516
Justin Giorgi16bba562016-06-22 10:42:13 -0700517class BaseHostModCreate(host):
xixuand6011f12016-12-08 15:01:58 -0800518 """The base class for host_mod and host_create"""
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700519 # Matches one attribute=value pair
520 attribute_regex = r'(?P<attribute>\w+)=(?P<value>.+)?'
mblighbe630eb2008-08-01 16:41:48 +0000521
522 def __init__(self):
Justin Giorgi16bba562016-06-22 10:42:13 -0700523 """Add the options shared between host mod and host create actions."""
mblighbe630eb2008-08-01 16:41:48 +0000524 self.messages = []
Justin Giorgi16bba562016-06-22 10:42:13 -0700525 self.host_ids = {}
526 super(BaseHostModCreate, self).__init__()
mblighbe630eb2008-08-01 16:41:48 +0000527 self.parser.add_option('-l', '--lock',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700528 help='Lock hosts.',
mblighbe630eb2008-08-01 16:41:48 +0000529 action='store_true')
Matthew Sartori68186332015-04-27 17:19:53 -0700530 self.parser.add_option('-r', '--lock_reason',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700531 help='Reason for locking hosts.',
Matthew Sartori68186332015-04-27 17:19:53 -0700532 default='')
Ningning Xiaef35cb52018-05-04 17:58:20 -0700533 self.parser.add_option('-u', '--unlock',
534 help='Unlock hosts.',
535 action='store_true')
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700536
mblighe163b032008-10-18 14:30:27 +0000537 self.parser.add_option('-p', '--protection', type='choice',
mblighaed47e82009-03-17 19:06:18 +0000538 help=('Set the protection level on a host. '
Ningning Xiaef35cb52018-05-04 17:58:20 -0700539 'Must be one of: %s. %s' %
540 (', '.join('"%s"' % p
541 for p in self.protections),
542 skylab_utils.MSG_INVALID_IN_SKYLAB)),
mblighaed47e82009-03-17 19:06:18 +0000543 choices=self.protections)
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700544 self._attributes = []
545 self.parser.add_option('--attribute', '-i',
546 help=('Host attribute to add or change. Format '
547 'is <attribute>=<value>. Multiple '
548 'attributes can be set by passing the '
549 'argument multiple times. Attributes can '
550 'be unset by providing an empty value.'),
551 action='append')
mblighbe630eb2008-08-01 16:41:48 +0000552 self.parser.add_option('-b', '--labels',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700553 help=('Comma separated list of labels. '
554 'When --skylab is provided, a label must '
555 'be in the format of label-key:label-value'
556 ' (e.g., board:lumpy).'))
mblighbe630eb2008-08-01 16:41:48 +0000557 self.parser.add_option('-B', '--blist',
558 help='File listing the labels',
559 type='string',
560 metavar='LABEL_FLIST')
561 self.parser.add_option('-a', '--acls',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700562 help=('Comma separated list of ACLs. %s' %
563 skylab_utils.MSG_INVALID_IN_SKYLAB))
mblighbe630eb2008-08-01 16:41:48 +0000564 self.parser.add_option('-A', '--alist',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700565 help=('File listing the acls. %s' %
566 skylab_utils.MSG_INVALID_IN_SKYLAB),
mblighbe630eb2008-08-01 16:41:48 +0000567 type='string',
568 metavar='ACL_FLIST')
Justin Giorgi16bba562016-06-22 10:42:13 -0700569 self.parser.add_option('-t', '--platform',
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700570 help=('Sets the platform label. %s Please set '
571 'platform in labels (e.g., -b '
572 'platform:platform_name) with --skylab.' %
573 skylab_utils.MSG_INVALID_IN_SKYLAB))
mblighbe630eb2008-08-01 16:41:48 +0000574
575
576 def parse(self):
Justin Giorgi16bba562016-06-22 10:42:13 -0700577 """Consume the options common to host create and host mod.
578 """
mbligh9deeefa2009-05-01 23:11:08 +0000579 label_info = topic_common.item_parse_info(attribute_name='labels',
580 inline_option='labels',
581 filename_option='blist')
582 acl_info = topic_common.item_parse_info(attribute_name='acls',
583 inline_option='acls',
584 filename_option='alist')
585
Justin Giorgi16bba562016-06-22 10:42:13 -0700586 (options, leftover) = super(BaseHostModCreate, self).parse([label_info,
mbligh9deeefa2009-05-01 23:11:08 +0000587 acl_info],
588 req_items='hosts')
mblighbe630eb2008-08-01 16:41:48 +0000589
590 self._parse_lock_options(options)
Justin Giorgi16bba562016-06-22 10:42:13 -0700591
Ningning Xia64ced002018-05-16 17:36:20 -0700592 self.label_map = None
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700593 if self.allow_skylab and self.skylab:
Ningning Xiaef35cb52018-05-04 17:58:20 -0700594 # TODO(nxia): drop these flags when all hosts are migrated to skylab
Ningning Xia64ced002018-05-16 17:36:20 -0700595 if (options.protection or options.acls or options.alist or
596 options.platform):
597 self.invalid_syntax(
598 '--protection, --acls, --alist or --platform is not '
599 'supported with --skylab.')
600
601 if self.labels:
602 self.label_map = device.convert_to_label_map(self.labels)
Ningning Xiaef35cb52018-05-04 17:58:20 -0700603
mblighaed47e82009-03-17 19:06:18 +0000604 if options.protection:
605 self.data['protection'] = options.protection
Justin Giorgi16bba562016-06-22 10:42:13 -0700606 self.messages.append('Protection set to "%s"' % options.protection)
607
608 self.attributes = {}
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700609 if options.attribute:
610 for pair in options.attribute:
611 m = re.match(self.attribute_regex, pair)
612 if not m:
613 raise topic_common.CliError('Attribute must be in key=value '
614 'syntax.')
615 elif m.group('attribute') in self.attributes:
xixuand6011f12016-12-08 15:01:58 -0800616 raise topic_common.CliError(
617 'Multiple values provided for attribute '
618 '%s.' % m.group('attribute'))
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700619 self.attributes[m.group('attribute')] = m.group('value')
Justin Giorgi16bba562016-06-22 10:42:13 -0700620
621 self.platform = options.platform
mblighbe630eb2008-08-01 16:41:48 +0000622 return (options, leftover)
623
624
Justin Giorgi16bba562016-06-22 10:42:13 -0700625 def _set_acls(self, hosts, acls):
626 """Add hosts to acls (and remove from all other acls).
627
628 @param hosts: list of hostnames
629 @param acls: list of acl names
630 """
631 # Remove from all ACLs except 'Everyone' and ACLs in list
632 # Skip hosts that don't exist
633 for host in hosts:
634 if host not in self.host_ids:
635 continue
636 host_id = self.host_ids[host]
637 for a in self.execute_rpc('get_acl_groups', hosts=host_id):
638 if a['name'] not in self.acls and a['id'] != 1:
639 self.execute_rpc('acl_group_remove_hosts', id=a['id'],
640 hosts=self.hosts)
641
642 # Add hosts to the ACLs
643 self.check_and_create_items('get_acl_groups', 'add_acl_group',
644 self.acls)
645 for a in acls:
646 self.execute_rpc('acl_group_add_hosts', id=a, hosts=hosts)
647
648
649 def _remove_labels(self, host, condition):
650 """Remove all labels from host that meet condition(label).
651
652 @param host: hostname
653 @param condition: callable that returns bool when given a label
654 """
655 if host in self.host_ids:
656 host_id = self.host_ids[host]
657 labels_to_remove = []
658 for l in self.execute_rpc('get_labels', host=host_id):
659 if condition(l):
660 labels_to_remove.append(l['id'])
661 if labels_to_remove:
662 self.execute_rpc('host_remove_labels', id=host_id,
663 labels=labels_to_remove)
664
665
666 def _set_labels(self, host, labels):
667 """Apply labels to host (and remove all other labels).
668
669 @param host: hostname
670 @param labels: list of label names
671 """
672 condition = lambda l: l['name'] not in labels and not l['platform']
673 self._remove_labels(host, condition)
674 self.check_and_create_items('get_labels', 'add_label', labels)
675 self.execute_rpc('host_add_labels', id=host, labels=labels)
676
677
678 def _set_platform_label(self, host, platform_label):
679 """Apply the platform label to host (and remove existing).
680
681 @param host: hostname
682 @param platform_label: platform label's name
683 """
684 self._remove_labels(host, lambda l: l['platform'])
685 self.check_and_create_items('get_labels', 'add_label', [platform_label],
686 platform=True)
687 self.execute_rpc('host_add_labels', id=host, labels=[platform_label])
688
689
690 def _set_attributes(self, host, attributes):
691 """Set attributes on host.
692
693 @param host: hostname
694 @param attributes: attribute dictionary
695 """
696 for attr, value in self.attributes.iteritems():
697 self.execute_rpc('set_host_attribute', attribute=attr,
698 value=value, hostname=host)
699
700
701class host_mod(BaseHostModCreate):
702 """atest host mod [--lock|--unlock --force_modify_locking
703 --platform <arch>
704 --labels <labels>|--blist <label_file>
705 --acls <acls>|--alist <acl_file>
706 --protection <protection_type>
707 --attributes <attr>=<value>;<attr>=<value>
708 --mlist <mach_file>] <hosts>"""
709 usage_action = 'mod'
Justin Giorgi16bba562016-06-22 10:42:13 -0700710
711 def __init__(self):
712 """Add the options specific to the mod action"""
713 super(host_mod, self).__init__()
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700714 self.parser.add_option('--unlock-lock-id',
715 help=('Unlock the lock with the lock-id. %s' %
716 skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
717 default=None)
Justin Giorgi16bba562016-06-22 10:42:13 -0700718 self.parser.add_option('-f', '--force_modify_locking',
719 help='Forcefully lock\unlock a host',
720 action='store_true')
721 self.parser.add_option('--remove_acls',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700722 help=('Remove all active acls. %s' %
723 skylab_utils.MSG_INVALID_IN_SKYLAB),
Justin Giorgi16bba562016-06-22 10:42:13 -0700724 action='store_true')
725 self.parser.add_option('--remove_labels',
726 help='Remove all labels.',
727 action='store_true')
728
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700729 self.add_skylab_options()
Ningning Xia1dd82ee2018-06-08 11:41:03 -0700730 self.parser.add_option('--new-env',
731 dest='new_env',
732 choices=['staging', 'prod'],
733 help=('The new environment ("staging" or '
734 '"prod") of the hosts. %s' %
735 skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
736 default=None)
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700737
738
739 def _parse_unlock_options(self, options):
740 """Parse unlock related options."""
741 if self.skylab and options.unlock and options.unlock_lock_id is None:
742 self.invalid_syntax('Must provide --unlock-lock-id with "--skylab '
743 '--unlock".')
744
745 if (not (self.skylab and options.unlock) and
746 options.unlock_lock_id is not None):
747 self.invalid_syntax('--unlock-lock-id is only valid with '
748 '"--skylab --unlock".')
749
750 self.unlock_lock_id = options.unlock_lock_id
751
Justin Giorgi16bba562016-06-22 10:42:13 -0700752
753 def parse(self):
754 """Consume the specific options"""
755 (options, leftover) = super(host_mod, self).parse()
756
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700757 self._parse_unlock_options(options)
758
Justin Giorgi16bba562016-06-22 10:42:13 -0700759 if options.force_modify_locking:
760 self.data['force_modify_locking'] = True
761
Ningning Xiaef35cb52018-05-04 17:58:20 -0700762 if self.skylab and options.remove_acls:
763 # TODO(nxia): drop the flag when all hosts are migrated to skylab
764 self.invalid_syntax('--remove_acls is not supported with --skylab.')
765
Justin Giorgi16bba562016-06-22 10:42:13 -0700766 self.remove_acls = options.remove_acls
767 self.remove_labels = options.remove_labels
Ningning Xia1dd82ee2018-06-08 11:41:03 -0700768 self.new_env = options.new_env
Justin Giorgi16bba562016-06-22 10:42:13 -0700769
770 return (options, leftover)
771
772
Ningning Xiaef35cb52018-05-04 17:58:20 -0700773 def execute_skylab(self):
774 """Execute atest host mod with --skylab.
775
776 @return A list of hostnames which have been successfully modified.
777 """
778 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
779 inventory_repo.initialize()
780 data_dir = inventory_repo.get_data_dir()
781 lab = text_manager.load_lab(data_dir)
782
783 locked_by = None
784 if self.lock:
785 locked_by = inventory_repo.git_repo.config('user.email')
786
787 successes = []
788 for hostname in self.hosts:
789 try:
790 device.modify(
791 lab,
792 'duts',
793 hostname,
794 self.environment,
795 lock=self.lock,
796 locked_by=locked_by,
797 lock_reason = self.lock_reason,
798 unlock=self.unlock,
799 unlock_lock_id=self.unlock_lock_id,
800 attributes=self.attributes,
801 remove_labels=self.remove_labels,
Ningning Xia1dd82ee2018-06-08 11:41:03 -0700802 label_map=self.label_map,
803 new_env=self.new_env)
Ningning Xiaef35cb52018-05-04 17:58:20 -0700804 successes.append(hostname)
805 except device.SkylabDeviceActionError as e:
806 print('Cannot modify host %s: %s' % (hostname, e))
807
808 if successes:
809 text_manager.dump_lab(data_dir, lab)
810
811 status = inventory_repo.git_repo.status()
812 if not status:
813 print('Nothing is changed for hosts %s.' % successes)
814 return []
815
816 message = skylab_utils.construct_commit_message(
817 'Modify %d hosts.\n\n%s' % (len(successes), successes))
818 self.change_number = inventory_repo.upload_change(
819 message, draft=self.draft, dryrun=self.dryrun,
820 submit=self.submit)
821
822 return successes
823
824
Justin Giorgi16bba562016-06-22 10:42:13 -0700825 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800826 """Execute 'atest host mod'."""
Ningning Xiaef35cb52018-05-04 17:58:20 -0700827 if self.skylab:
828 return self.execute_skylab()
829
Justin Giorgi16bba562016-06-22 10:42:13 -0700830 successes = []
831 for host in self.execute_rpc('get_hosts', hostname__in=self.hosts):
832 self.host_ids[host['hostname']] = host['id']
833 for host in self.hosts:
834 if host not in self.host_ids:
835 self.failure('Cannot modify non-existant host %s.' % host)
836 continue
837 host_id = self.host_ids[host]
838
839 try:
840 if self.data:
841 self.execute_rpc('modify_host', item=host,
842 id=host, **self.data)
843
844 if self.attributes:
845 self._set_attributes(host, self.attributes)
846
847 if self.labels or self.remove_labels:
848 self._set_labels(host, self.labels)
849
850 if self.platform:
851 self._set_platform_label(host, self.platform)
852
853 # TODO: Make the AFE return True or False,
854 # especially for lock
855 successes.append(host)
856 except topic_common.CliError, full_error:
857 # Already logged by execute_rpc()
858 pass
859
860 if self.acls or self.remove_acls:
861 self._set_acls(self.hosts, self.acls)
862
863 return successes
864
865
866 def output(self, hosts):
xixuand6011f12016-12-08 15:01:58 -0800867 """Print output of 'atest host mod'.
868
869 @param hosts: the host list to be printed.
870 """
Justin Giorgi16bba562016-06-22 10:42:13 -0700871 for msg in self.messages:
872 self.print_wrapped(msg, hosts)
873
Ningning Xiaef35cb52018-05-04 17:58:20 -0700874 if hosts and self.skylab:
Ningning Xiac8b430d2018-05-17 15:10:25 -0700875 print('Modified hosts: %s.' % ', '.join(hosts))
Ningning Xiaef35cb52018-05-04 17:58:20 -0700876 if self.skylab and not self.dryrun and not self.submit:
Ningning Xiac8b430d2018-05-17 15:10:25 -0700877 print(skylab_utils.get_cl_message(self.change_number))
Ningning Xiaef35cb52018-05-04 17:58:20 -0700878
Justin Giorgi16bba562016-06-22 10:42:13 -0700879
880class HostInfo(object):
881 """Store host information so we don't have to keep looking it up."""
882 def __init__(self, hostname, platform, labels):
883 self.hostname = hostname
884 self.platform = platform
885 self.labels = labels
886
887
888class host_create(BaseHostModCreate):
889 """atest host create [--lock|--unlock --platform <arch>
890 --labels <labels>|--blist <label_file>
891 --acls <acls>|--alist <acl_file>
892 --protection <protection_type>
893 --attributes <attr>=<value>;<attr>=<value>
894 --mlist <mach_file>] <hosts>"""
895 usage_action = 'create'
896
897 def parse(self):
898 """Option logic specific to create action.
899 """
900 (options, leftovers) = super(host_create, self).parse()
901 self.locked = options.lock
902 if 'serials' in self.attributes:
903 if len(self.hosts) > 1:
904 raise topic_common.CliError('Can not specify serials with '
905 'multiple hosts.')
906
907
908 @classmethod
909 def construct_without_parse(
910 cls, web_server, hosts, platform=None,
911 locked=False, lock_reason='', labels=[], acls=[],
912 protection=host_protections.Protection.NO_PROTECTION):
913 """Construct a host_create object and fill in data from args.
914
915 Do not need to call parse after the construction.
916
917 Return an object of site_host_create ready to execute.
918
919 @param web_server: A string specifies the autotest webserver url.
920 It is needed to setup comm to make rpc.
921 @param hosts: A list of hostnames as strings.
922 @param platform: A string or None.
923 @param locked: A boolean.
924 @param lock_reason: A string.
925 @param labels: A list of labels as strings.
926 @param acls: A list of acls as strings.
927 @param protection: An enum defined in host_protections.
928 """
929 obj = cls()
930 obj.web_server = web_server
931 try:
932 # Setup stuff needed for afe comm.
933 obj.afe = rpc.afe_comm(web_server)
934 except rpc.AuthError, s:
935 obj.failure(str(s), fatal=True)
936 obj.hosts = hosts
937 obj.platform = platform
938 obj.locked = locked
939 if locked and lock_reason.strip():
940 obj.data['lock_reason'] = lock_reason.strip()
941 obj.labels = labels
942 obj.acls = acls
943 if protection:
944 obj.data['protection'] = protection
945 obj.attributes = {}
946 return obj
947
948
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700949 def _detect_host_info(self, host):
950 """Detect platform and labels from the host.
951
952 @param host: hostname
953
954 @return: HostInfo object
955 """
956 # Mock an afe_host object so that the host is constructed as if the
957 # data was already in afe
958 data = {'attributes': self.attributes, 'labels': self.labels}
959 afe_host = frontend.Host(None, data)
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700960 store = host_info.InMemoryHostInfoStore(
961 host_info.HostInfo(labels=self.labels,
962 attributes=self.attributes))
963 machine = {
964 'hostname': host,
965 'afe_host': afe_host,
966 'host_info_store': store
967 }
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700968 try:
Allen Li2c32d6b2017-02-03 15:28:10 -0800969 if bin_utils.ping(host, tries=1, deadline=1) == 0:
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700970 serials = self.attributes.get('serials', '').split(',')
Richard Barnette9db80682018-04-26 00:55:15 +0000971 adb_serial = self.attributes.get('serials')
972 host_dut = hosts.create_host(machine,
973 adb_serial=adb_serial)
xixuand6011f12016-12-08 15:01:58 -0800974
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700975 info = HostInfo(host, host_dut.get_platform(),
976 host_dut.get_labels())
xixuand6011f12016-12-08 15:01:58 -0800977 # Clean host to make sure nothing left after calling it,
978 # e.g. tunnels.
979 if hasattr(host_dut, 'close'):
980 host_dut.close()
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700981 else:
982 # Can't ping the host, use default information.
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700983 info = HostInfo(host, None, [])
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700984 except (socket.gaierror, error.AutoservRunError,
985 error.AutoservSSHTimeout):
986 # We may be adding a host that does not exist yet or we can't
987 # reach due to hostname/address issues or if the host is down.
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700988 info = HostInfo(host, None, [])
989 return info
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700990
991
mblighbe630eb2008-08-01 16:41:48 +0000992 def _execute_add_one_host(self, host):
mbligh719e14a2008-12-04 01:17:08 +0000993 # Always add the hosts as locked to avoid the host
Matthew Sartori68186332015-04-27 17:19:53 -0700994 # being picked up by the scheduler before it's ACL'ed.
mbligh719e14a2008-12-04 01:17:08 +0000995 self.data['locked'] = True
Matthew Sartori68186332015-04-27 17:19:53 -0700996 if not self.locked:
997 self.data['lock_reason'] = 'Forced lock on device creation'
Justin Giorgi16bba562016-06-22 10:42:13 -0700998 self.execute_rpc('add_host', hostname=host, status="Ready", **self.data)
mblighbe630eb2008-08-01 16:41:48 +0000999
Justin Giorgi16bba562016-06-22 10:42:13 -07001000 # If there are labels avaliable for host, use them.
Prathmesh Prabhud252d262017-05-31 11:46:34 -07001001 info = self._detect_host_info(host)
Justin Giorgi16bba562016-06-22 10:42:13 -07001002 labels = set(self.labels)
Prathmesh Prabhud252d262017-05-31 11:46:34 -07001003 if info.labels:
1004 labels.update(info.labels)
Justin Giorgi16bba562016-06-22 10:42:13 -07001005
1006 if labels:
1007 self._set_labels(host, list(labels))
1008
1009 # Now add the platform label.
1010 # If a platform was not provided and we were able to retrieve it
1011 # from the host, use the retrieved platform.
Prathmesh Prabhud252d262017-05-31 11:46:34 -07001012 platform = self.platform if self.platform else info.platform
Justin Giorgi16bba562016-06-22 10:42:13 -07001013 if platform:
1014 self._set_platform_label(host, platform)
1015
1016 if self.attributes:
1017 self._set_attributes(host, self.attributes)
mblighbe630eb2008-08-01 16:41:48 +00001018
1019
Justin Giorgi5208eaa2016-07-02 20:12:12 -07001020 def execute(self):
xixuand6011f12016-12-08 15:01:58 -08001021 """Execute 'atest host create'."""
Justin Giorgi16bba562016-06-22 10:42:13 -07001022 successful_hosts = []
1023 for host in self.hosts:
1024 try:
1025 self._execute_add_one_host(host)
1026 successful_hosts.append(host)
1027 except topic_common.CliError:
1028 pass
Jiaxi Luoc342f9f2014-05-19 16:22:03 -07001029
1030 if successful_hosts:
Justin Giorgi16bba562016-06-22 10:42:13 -07001031 self._set_acls(successful_hosts, self.acls)
Jiaxi Luoc342f9f2014-05-19 16:22:03 -07001032
1033 if not self.locked:
1034 for host in successful_hosts:
Matthew Sartori68186332015-04-27 17:19:53 -07001035 self.execute_rpc('modify_host', id=host, locked=False,
1036 lock_reason='')
Jiaxi Luoc342f9f2014-05-19 16:22:03 -07001037 return successful_hosts
1038
1039
mblighbe630eb2008-08-01 16:41:48 +00001040 def output(self, hosts):
xixuand6011f12016-12-08 15:01:58 -08001041 """Print output of 'atest host create'.
1042
1043 @param hosts: the added host list to be printed.
1044 """
mblighbe630eb2008-08-01 16:41:48 +00001045 self.print_wrapped('Added host', hosts)
1046
1047
1048class host_delete(action_common.atest_delete, host):
1049 """atest host delete [--mlist <mach_file>] <hosts>"""
Ningning Xiac8b430d2018-05-17 15:10:25 -07001050
1051 def __init__(self):
1052 super(host_delete, self).__init__()
1053
1054 self.add_skylab_options()
1055
1056
1057 def execute_skylab(self):
1058 """Execute 'atest host delete' with '--skylab'.
1059
1060 @return A list of hostnames which have been successfully deleted.
1061 """
1062 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
1063 inventory_repo.initialize()
1064 data_dir = inventory_repo.get_data_dir()
1065 lab = text_manager.load_lab(data_dir)
1066
1067 successes = []
1068 for hostname in self.hosts:
1069 try:
1070 device.delete(
1071 lab,
1072 'duts',
1073 hostname,
1074 self.environment)
1075 successes.append(hostname)
1076 except device.SkylabDeviceActionError as e:
1077 print('Cannot delete host %s: %s' % (hostname, e))
1078
1079 if successes:
1080 text_manager.dump_lab(data_dir, lab)
1081 message = skylab_utils.construct_commit_message(
1082 'Delete %d hosts.\n\n%s' % (len(successes), successes))
1083 self.change_number = inventory_repo.upload_change(
1084 message, draft=self.draft, dryrun=self.dryrun,
1085 submit=self.submit)
1086
1087 return successes
1088
1089
1090 def execute(self):
1091 """Execute 'atest host delete'.
1092
1093 @return A list of hostnames which have been successfully deleted.
1094 """
1095 if self.skylab:
1096 return self.execute_skylab()
1097
1098 return super(host_delete, self).execute()
Ningning Xiac46bdd12018-05-29 11:24:14 -07001099
1100
1101class InvalidHostnameError(Exception):
1102 """Cannot perform actions on the host because of invalid hostname."""
1103
1104
1105def _add_hostname_suffix(hostname, suffix):
1106 """Add the suffix to the hostname."""
1107 if hostname.endswith(suffix):
1108 raise InvalidHostnameError(
1109 'Cannot add "%s" as it already contains the suffix.' % suffix)
1110
1111 return hostname + suffix
1112
1113
Gregory Nisbet48f1eea2019-08-05 16:18:56 -07001114def _remove_hostname_suffix_if_present(hostname, suffix):
Ningning Xiac46bdd12018-05-29 11:24:14 -07001115 """Remove the suffix from the hostname."""
Gregory Nisbet48f1eea2019-08-05 16:18:56 -07001116 if hostname.endswith(suffix):
1117 return hostname[:len(hostname) - len(suffix)]
1118 else:
1119 return hostname
Ningning Xiac46bdd12018-05-29 11:24:14 -07001120
1121
1122class host_rename(host):
1123 """Host rename is only for migrating hosts between skylab and AFE DB."""
1124
1125 usage_action = 'rename'
1126
1127 def __init__(self):
1128 """Add the options specific to the rename action."""
1129 super(host_rename, self).__init__()
1130
1131 self.parser.add_option('--for-migration',
1132 help=('Rename hostnames for migration. Rename '
1133 'each "hostname" to "hostname%s". '
1134 'The original "hostname" must not contain '
1135 'suffix.' % MIGRATED_HOST_SUFFIX),
1136 action='store_true',
1137 default=False)
1138 self.parser.add_option('--for-rollback',
1139 help=('Rename hostnames for migration rollback. '
1140 'Rename each "hostname%s" to its original '
1141 '"hostname".' % MIGRATED_HOST_SUFFIX),
1142 action='store_true',
1143 default=False)
1144 self.parser.add_option('--dryrun',
1145 help='Execute the action as a dryrun.',
1146 action='store_true',
1147 default=False)
Gregory Nisbetee457f62019-07-08 15:47:26 -07001148 self.parser.add_option('--non-interactive',
1149 help='run non-interactively',
Gregory Nisbet9744ea92019-07-02 12:36:59 -07001150 action='store_true',
1151 default=False)
Ningning Xiac46bdd12018-05-29 11:24:14 -07001152
1153
1154 def parse(self):
1155 """Consume the options common to host rename."""
1156 (options, leftovers) = super(host_rename, self).parse()
1157 self.for_migration = options.for_migration
1158 self.for_rollback = options.for_rollback
1159 self.dryrun = options.dryrun
Gregory Nisbetee457f62019-07-08 15:47:26 -07001160 self.interactive = not options.non_interactive
Ningning Xiac46bdd12018-05-29 11:24:14 -07001161 self.host_ids = {}
1162
1163 if not (self.for_migration ^ self.for_rollback):
1164 self.invalid_syntax('--for-migration and --for-rollback are '
1165 'exclusive, and one of them must be enabled.')
1166
1167 if not self.hosts:
1168 self.invalid_syntax('Must provide hostname(s).')
1169
1170 if self.dryrun:
1171 print('This will be a dryrun and will not rename hostnames.')
1172
1173 return (options, leftovers)
1174
1175
1176 def execute(self):
1177 """Execute 'atest host rename'."""
Gregory Nisbetee457f62019-07-08 15:47:26 -07001178 if self.interactive:
1179 if self.prompt_confirmation():
1180 pass
1181 else:
1182 return
Ningning Xiac46bdd12018-05-29 11:24:14 -07001183
1184 successes = []
1185 for host in self.execute_rpc('get_hosts', hostname__in=self.hosts):
1186 self.host_ids[host['hostname']] = host['id']
1187 for host in self.hosts:
1188 if host not in self.host_ids:
1189 self.failure('Cannot rename non-existant host %s.' % host,
1190 item=host, what_failed='Failed to rename')
1191 continue
1192 try:
Ningning Xiab261b162018-06-07 17:24:59 -07001193 host_id = self.host_ids[host]
Ningning Xiac46bdd12018-05-29 11:24:14 -07001194 if self.for_migration:
1195 new_hostname = _add_hostname_suffix(
1196 host, MIGRATED_HOST_SUFFIX)
1197 else:
1198 #for_rollback
1199 new_hostname = _remove_hostname_suffix(
1200 host, MIGRATED_HOST_SUFFIX)
1201
1202 if not self.dryrun:
Ningning Xia423c7962018-06-07 15:27:18 -07001203 # TODO(crbug.com/850737): delete and abort HQE.
Ningning Xiac46bdd12018-05-29 11:24:14 -07001204 data = {'hostname': new_hostname}
Ningning Xiab261b162018-06-07 17:24:59 -07001205 self.execute_rpc('modify_host', item=host, id=host_id,
1206 **data)
Ningning Xiac46bdd12018-05-29 11:24:14 -07001207 successes.append((host, new_hostname))
1208 except InvalidHostnameError as e:
1209 self.failure('Cannot rename host %s: %s' % (host, e), item=host,
1210 what_failed='Failed to rename')
1211 except topic_common.CliError, full_error:
1212 # Already logged by execute_rpc()
1213 pass
1214
1215 return successes
1216
1217
1218 def output(self, results):
1219 """Print output of 'atest host rename'."""
1220 if results:
1221 print('Successfully renamed:')
1222 for old_hostname, new_hostname in results:
1223 print('%s to %s' % (old_hostname, new_hostname))
Ningning Xia9df3d152018-05-23 17:15:14 -07001224
1225
1226class host_migrate(action_common.atest_list, host):
1227 """'atest host migrate' to migrate or rollback hosts."""
1228
1229 usage_action = 'migrate'
1230
1231 def __init__(self):
1232 super(host_migrate, self).__init__()
1233
1234 self.parser.add_option('--migration',
1235 dest='migration',
1236 help='Migrate the hosts to skylab.',
1237 action='store_true',
1238 default=False)
1239 self.parser.add_option('--rollback',
1240 dest='rollback',
1241 help='Rollback the hosts migrated to skylab.',
1242 action='store_true',
1243 default=False)
1244 self.parser.add_option('--model',
1245 help='Model of the hosts to migrate.',
1246 dest='model',
1247 default=None)
Xixuan Wu84621e52018-08-28 14:29:53 -07001248 self.parser.add_option('--board',
1249 help='Board of the hosts to migrate.',
1250 dest='board',
1251 default=None)
Ningning Xia9df3d152018-05-23 17:15:14 -07001252 self.parser.add_option('--pool',
1253 help=('Pool of the hosts to migrate. Must '
1254 'specify --model for the pool.'),
1255 dest='pool',
1256 default=None)
1257
1258 self.add_skylab_options(enforce_skylab=True)
1259
1260
1261 def parse(self):
1262 """Consume the specific options"""
1263 (options, leftover) = super(host_migrate, self).parse()
1264
1265 self.migration = options.migration
1266 self.rollback = options.rollback
1267 self.model = options.model
1268 self.pool = options.pool
Xixuan Wu84621e52018-08-28 14:29:53 -07001269 self.board = options.board
Ningning Xia9df3d152018-05-23 17:15:14 -07001270 self.host_ids = {}
1271
1272 if not (self.migration ^ self.rollback):
1273 self.invalid_syntax('--migration and --rollback are exclusive, '
1274 'and one of them must be enabled.')
1275
Xixuan Wu84621e52018-08-28 14:29:53 -07001276 if self.pool is not None and (self.model is None and
1277 self.board is None):
1278 self.invalid_syntax('Must provide --model or --board with --pool.')
Ningning Xia9df3d152018-05-23 17:15:14 -07001279
Xixuan Wu84621e52018-08-28 14:29:53 -07001280 if not self.hosts and not (self.model or self.board):
1281 self.invalid_syntax('Must provide hosts or --model or --board.')
Ningning Xia9df3d152018-05-23 17:15:14 -07001282
1283 return (options, leftover)
1284
1285
Ningning Xia56d68432018-06-06 17:28:20 -07001286 def _remove_invalid_hostnames(self, hostnames, log_failure=False):
1287 """Remove hostnames with MIGRATED_HOST_SUFFIX.
1288
1289 @param hostnames: A list of hostnames.
1290 @param log_failure: Bool indicating whether to log invalid hostsnames.
1291
1292 @return A list of valid hostnames.
1293 """
1294 invalid_hostnames = set()
Ningning Xia9df3d152018-05-23 17:15:14 -07001295 for hostname in hostnames:
1296 if hostname.endswith(MIGRATED_HOST_SUFFIX):
Ningning Xia56d68432018-06-06 17:28:20 -07001297 if log_failure:
1298 self.failure('Cannot migrate host with suffix "%s" %s.' %
1299 (MIGRATED_HOST_SUFFIX, hostname),
1300 item=hostname, what_failed='Failed to rename')
1301 invalid_hostnames.add(hostname)
1302
1303 hostnames = list(set(hostnames) - invalid_hostnames)
1304
1305 return hostnames
1306
1307
1308 def execute(self):
1309 """Execute 'atest host migrate'."""
1310 hostnames = self._remove_invalid_hostnames(self.hosts, log_failure=True)
Ningning Xia9df3d152018-05-23 17:15:14 -07001311
1312 filters = {}
1313 check_results = {}
1314 if hostnames:
1315 check_results['hostname__in'] = 'hostname'
1316 if self.migration:
1317 filters['hostname__in'] = hostnames
1318 else:
1319 # rollback
1320 hostnames_with_suffix = [
1321 _add_hostname_suffix(h, MIGRATED_HOST_SUFFIX)
1322 for h in hostnames]
1323 filters['hostname__in'] = hostnames_with_suffix
Ningning Xia56d68432018-06-06 17:28:20 -07001324 else:
1325 # TODO(nxia): add exclude_filter {'hostname__endswith':
1326 # MIGRATED_HOST_SUFFIX} for --migration
1327 if self.rollback:
1328 filters['hostname__endswith'] = MIGRATED_HOST_SUFFIX
Ningning Xia9df3d152018-05-23 17:15:14 -07001329
1330 labels = []
1331 if self.model:
1332 labels.append('model:%s' % self.model)
1333 if self.pool:
1334 labels.append('pool:%s' % self.pool)
Xixuan Wu84621e52018-08-28 14:29:53 -07001335 if self.board:
1336 labels.append('board:%s' % self.board)
Ningning Xia9df3d152018-05-23 17:15:14 -07001337
1338 if labels:
1339 if len(labels) == 1:
1340 filters['labels__name__in'] = labels
1341 check_results['labels__name__in'] = None
1342 else:
1343 filters['multiple_labels'] = labels
1344 check_results['multiple_labels'] = None
1345
1346 results = super(host_migrate, self).execute(
1347 op='get_hosts', filters=filters, check_results=check_results)
1348 hostnames = [h['hostname'] for h in results]
1349
Ningning Xia56d68432018-06-06 17:28:20 -07001350 if self.migration:
1351 hostnames = self._remove_invalid_hostnames(hostnames)
1352 else:
1353 # rollback
1354 hostnames = [_remove_hostname_suffix(h, MIGRATED_HOST_SUFFIX)
1355 for h in hostnames]
1356
Ningning Xia9df3d152018-05-23 17:15:14 -07001357 return self.execute_skylab_migration(hostnames)
1358
1359
Xixuan Wua8d93c82018-08-03 16:23:26 -07001360 def assign_duts_to_drone(self, infra, devices, environment):
Ningning Xia877817f2018-06-08 12:23:48 -07001361 """Assign uids of the devices to a random skylab drone.
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001362
1363 @param infra: An instance of lab_pb2.Infrastructure.
Xixuan Wua8d93c82018-08-03 16:23:26 -07001364 @param devices: A list of device_pb2.Device to be assigned to the drone.
1365 @param environment: 'staging' or 'prod'.
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001366 """
1367 skylab_drones = skylab_server.get_servers(
Xixuan Wua8d93c82018-08-03 16:23:26 -07001368 infra, environment, role='skylab_drone', status='primary')
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001369
1370 if len(skylab_drones) == 0:
1371 raise device.SkylabDeviceActionError(
1372 'No skylab drone is found in primary status and staging '
1373 'environment. Please confirm there is at least one valid skylab'
1374 ' drone added in skylab inventory.')
1375
Xixuan Wudfc2de22018-08-06 09:29:01 -07001376 for device in devices:
1377 # Randomly distribute each device to a skylab_drone.
1378 skylab_drone = random.choice(skylab_drones)
1379 skylab_server.add_dut_uids(skylab_drone, [device])
Ningning Xia877817f2018-06-08 12:23:48 -07001380
1381
1382 def remove_duts_from_drone(self, infra, devices):
1383 """Remove uids of the devices from their skylab drones.
1384
1385 @param infra: An instance of lab_pb2.Infrastructure.
1386 @devices: A list of device_pb2.Device to be remove from the drone.
1387 """
1388 skylab_drones = skylab_server.get_servers(
1389 infra, 'staging', role='skylab_drone', status='primary')
1390
1391 for skylab_drone in skylab_drones:
1392 skylab_server.remove_dut_uids(skylab_drone, devices)
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001393
1394
Ningning Xia9df3d152018-05-23 17:15:14 -07001395 def execute_skylab_migration(self, hostnames):
1396 """Execute migration in skylab_inventory.
1397
1398 @param hostnames: A list of hostnames to migrate.
1399 @return If there're hosts to migrate, return a list of the hostnames and
1400 a message instructing actions after the migration; else return
1401 None.
1402 """
1403 if not hostnames:
1404 return
1405
1406 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
1407 inventory_repo.initialize()
1408
1409 subdirs = ['skylab', 'prod', 'staging']
1410 data_dirs = skylab_data_dir, prod_data_dir, staging_data_dir = [
1411 inventory_repo.get_data_dir(data_subdir=d) for d in subdirs]
1412 skylab_lab, prod_lab, staging_lab = [
1413 text_manager.load_lab(d) for d in data_dirs]
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001414 infra = text_manager.load_infrastructure(skylab_data_dir)
Ningning Xia9df3d152018-05-23 17:15:14 -07001415
1416 label_map = None
1417 labels = []
Xixuan Wu46f8e202018-12-13 14:40:58 -08001418 if self.board:
1419 labels.append('board:%s' % self.board)
Ningning Xia9df3d152018-05-23 17:15:14 -07001420 if self.model:
Xixuan Wu46f8e202018-12-13 14:40:58 -08001421 labels.append('model:%s' % self.model)
Ningning Xia9df3d152018-05-23 17:15:14 -07001422 if self.pool:
1423 labels.append('critical_pool:%s' % self.pool)
1424 if labels:
1425 label_map = device.convert_to_label_map(labels)
1426
1427 if self.migration:
1428 prod_devices = device.move_devices(
1429 prod_lab, skylab_lab, 'duts', label_map=label_map,
1430 hostnames=hostnames)
1431 staging_devices = device.move_devices(
1432 staging_lab, skylab_lab, 'duts', label_map=label_map,
1433 hostnames=hostnames)
1434
1435 all_devices = prod_devices + staging_devices
1436 # Hostnames in afe_hosts tabel.
1437 device_hostnames = [str(d.common.hostname) for d in all_devices]
1438 message = (
1439 'Migration: move %s hosts into skylab_inventory.\n\n'
Ningning Xia423c7962018-06-07 15:27:18 -07001440 'Please run this command after the CL is submitted:\n'
Ningning Xia9df3d152018-05-23 17:15:14 -07001441 'atest host rename --for-migration %s' %
1442 (len(all_devices), ' '.join(device_hostnames)))
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001443
Xixuan Wua8d93c82018-08-03 16:23:26 -07001444 self.assign_duts_to_drone(infra, prod_devices, 'prod')
1445 self.assign_duts_to_drone(infra, staging_devices, 'staging')
Ningning Xia9df3d152018-05-23 17:15:14 -07001446 else:
1447 # rollback
1448 prod_devices = device.move_devices(
1449 skylab_lab, prod_lab, 'duts', environment='prod',
Ningning Xia56d68432018-06-06 17:28:20 -07001450 label_map=label_map, hostnames=hostnames)
Ningning Xia9df3d152018-05-23 17:15:14 -07001451 staging_devices = device.move_devices(
Ningning Xia877817f2018-06-08 12:23:48 -07001452 skylab_lab, staging_lab, 'duts', environment='staging',
Ningning Xia56d68432018-06-06 17:28:20 -07001453 label_map=label_map, hostnames=hostnames)
Ningning Xia9df3d152018-05-23 17:15:14 -07001454
1455 all_devices = prod_devices + staging_devices
1456 # Hostnames in afe_hosts tabel.
1457 device_hostnames = [_add_hostname_suffix(str(d.common.hostname),
1458 MIGRATED_HOST_SUFFIX)
1459 for d in all_devices]
1460 message = (
1461 'Rollback: remove %s hosts from skylab_inventory.\n\n'
Ningning Xia423c7962018-06-07 15:27:18 -07001462 'Please run this command after the CL is submitted:\n'
Ningning Xia9df3d152018-05-23 17:15:14 -07001463 'atest host rename --for-rollback %s' %
1464 (len(all_devices), ' '.join(device_hostnames)))
1465
Ningning Xia877817f2018-06-08 12:23:48 -07001466 self.remove_duts_from_drone(infra, all_devices)
1467
Ningning Xia9df3d152018-05-23 17:15:14 -07001468 if all_devices:
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001469 text_manager.dump_infrastructure(skylab_data_dir, infra)
1470
Ningning Xia9df3d152018-05-23 17:15:14 -07001471 if prod_devices:
1472 text_manager.dump_lab(prod_data_dir, prod_lab)
1473
1474 if staging_devices:
1475 text_manager.dump_lab(staging_data_dir, staging_lab)
1476
1477 text_manager.dump_lab(skylab_data_dir, skylab_lab)
1478
1479 self.change_number = inventory_repo.upload_change(
1480 message, draft=self.draft, dryrun=self.dryrun,
1481 submit=self.submit)
1482
1483 return all_devices, message
1484
1485
1486 def output(self, result):
1487 """Print output of 'atest host list'.
1488
1489 @param result: the result to be printed.
1490 """
1491 if result:
1492 devices, message = result
1493
1494 if devices:
1495 hostnames = [h.common.hostname for h in devices]
1496 if self.migration:
1497 print('Migrating hosts: %s' % ','.join(hostnames))
1498 else:
1499 # rollback
1500 print('Rolling back hosts: %s' % ','.join(hostnames))
1501
1502 if not self.dryrun:
1503 if not self.submit:
1504 print(skylab_utils.get_cl_message(self.change_number))
1505 else:
1506 # Print the instruction command for renaming hosts.
1507 print('%s' % message)
1508 else:
1509 print('No hosts were migrated.')
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001510
1511
1512class host_skylab_migrate(action_common.atest_list, host):
1513 usage_action = 'skylab_migrate'
1514
1515 def __init__(self):
1516 super(host_skylab_migrate, self).__init__()
1517 self.parser.add_option('--dry-run',
1518 help='Dry run. Show only candidate hosts.',
1519 action='store_true',
1520 dest='dry_run')
1521 self.parser.add_option('--ratio',
1522 help='ratio of hosts to migrate as number from 0 to 1.',
1523 type=float,
1524 dest='ratio',
1525 default=1)
1526 self.parser.add_option('--bug-number',
1527 help='bug number for tracking purposes.',
1528 dest='bug_number',
1529 default=None)
1530 self.parser.add_option('--board',
1531 help='Board of the hosts to migrate',
1532 dest='board',
1533 default=None)
1534 self.parser.add_option('--model',
1535 help='Model of the hosts to migrate',
1536 dest='model',
1537 default=None)
1538 self.parser.add_option('--pool',
1539 help='Pool of the hosts to migrate',
1540 dest='pool',
1541 default=None)
1542
1543 def parse(self):
1544 (options, leftover) = super(host_skylab_migrate, self).parse()
1545 self.dry_run = options.dry_run
1546 self.ratio = options.ratio
1547 self.bug_number = options.bug_number
1548 self.model = options.model
1549 self.pool = options.pool
1550 self.board = options.board
1551 self._reason = "migration to skylab: %s" % self.bug_number
1552 return (options, leftover)
1553
1554
1555 def _host_skylab_migrate_get_hostnames(self, model=None, pool=None, board=None):
1556 """
1557 @params : in 'model', 'pool', 'board'
1558
1559 """
1560 # TODO(gregorynisbet)
1561 # this just gets all the hostnames, it doesn't filter by
1562 # presence or absence of migrated-do-not-use.
1563 labels = []
1564 for key, value in {'model': model, 'board': board, 'pool': pool}:
1565 if value:
1566 labels.append(key + ":" + value)
1567 filters = {}
1568 check_results = {}
1569 # Copy the filter and check_results initialization logic from
1570 # the 'execute' method of the class 'host_migrate'.
1571 if not labels:
1572 return []
1573 elif len(labels) == 1:
1574 filters['labels__name__in'] = labels
1575 check_results['labels__name__in'] = None
1576 elif len(labels) > 1:
1577 filters['multiple_labels'] = labels
1578 check_results['multiple_labels'] = None
1579 else:
1580 assert False
1581
1582 results = super(host_skylab_migrate, self).execute(
1583 op='get_hosts', filters=filters, check_results=check_results)
1584 return [result['hostname'] for result in results]
1585
1586
1587 def _validate_one_hostname_source(self):
1588 """Validate that hostname source is explicit hostnames or valid query.
1589
1590 Hostnames must either be provided explicitly or be the result of a
1591 query defined by 'model', 'board', and 'pool'.
1592
1593 @returns : whether the hostnames come from exactly one valid source.
1594 """
1595 has_criteria = any([(self.model and self.board), self.board, self.pool])
1596 has_command_line_hosts = bool(self.hosts)
1597 if has_criteria != has_command_line_hosts:
1598 # all good, one data source
1599 return True
1600 if has_criteria and has_command_line_hosts:
1601 self.failure(
1602 '--model/host/board and explicit hostnames are alternatives. Provide exactly one.',
1603 item='cli',
1604 what_failed='user')
1605 return False
1606 self.failure(
1607 'no explicit hosts and no criteria provided.',
1608 item='cli',
1609 what_failed='user')
1610 return False
1611
1612
1613 def execute(self):
1614 if not self._validate_one_hostname_source():
1615 return None
1616 if self.hosts:
1617 hostnames = self.hosts
1618 else:
1619 hostnames = self.__get_hostnames(
1620 model=self.model,
1621 board=self.board,
1622 pool=self.pool,
1623 )
1624 if self.dry_run:
1625 return hostnames
1626 if not hostnames:
1627 return {'error': 'no hosts to migrate'}
1628 res = skylab_migration.migrate(
1629 ratio=self.ratio,
1630 reason=self._reason,
1631 hostnames=hostnames,
1632 max_duration=10 * 60,
1633 interval_len=2,
1634 min_ready_intervals=10,
1635 immediately=True,
1636 )
1637 return res
1638
1639
1640 def output(self, result):
1641 if result is not None:
1642 print json.dumps(result, indent=4, sort_keys=True)