blob: e451ab5bb0d17ec3203428d2c5ec8c850d77ce31 [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
Gregory Nisbetac7f7f72019-09-10 16:25:20 -070033from autotest_lib.cli import skylab_rollback
Gregory Nisbetf37da462019-09-11 10:19:03 -070034from autotest_lib.cli.skylab_json_utils import process_labels, validate_required_fields_for_skylab
Justin Giorgi16bba562016-06-22 10:42:13 -070035from autotest_lib.client.common_lib import error, host_protections
Justin Giorgi5208eaa2016-07-02 20:12:12 -070036from autotest_lib.server import frontend, hosts
Prathmesh Prabhud252d262017-05-31 11:46:34 -070037from autotest_lib.server.hosts import host_info
Gregory Nisbet93fa5b52019-07-19 14:56:35 -070038from autotest_lib.server.lib.status_history import HostJobHistory
39from autotest_lib.server.lib.status_history import UNUSED, WORKING
40from autotest_lib.server.lib.status_history import BROKEN, UNKNOWN
mblighbe630eb2008-08-01 16:41:48 +000041
42
Ningning Xiaea02ab12018-05-11 15:08:57 -070043try:
44 from skylab_inventory import text_manager
45 from skylab_inventory.lib import device
Ningning Xiaffb3c1f2018-06-07 18:42:32 -070046 from skylab_inventory.lib import server as skylab_server
Ningning Xiaea02ab12018-05-11 15:08:57 -070047except ImportError:
48 pass
49
50
Ningning Xiac46bdd12018-05-29 11:24:14 -070051MIGRATED_HOST_SUFFIX = '-migrated-do-not-use'
52
53
Gregory Nisbet99b61942019-07-08 09:43:06 -070054ID_AUTOGEN_MESSAGE = ("[IGNORED]. Do not edit (crbug.com/950553). ID is "
55 "auto-generated.")
56
57
58
mblighbe630eb2008-08-01 16:41:48 +000059class host(topic_common.atest):
60 """Host class
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -070061 atest host [create|delete|list|stat|mod|jobs|rename|migrate|skylab_migrate|statjson] <options>"""
62 usage_action = '[create|delete|list|stat|mod|jobs|rename|migrate|skylab_migrate|statjson]'
mblighbe630eb2008-08-01 16:41:48 +000063 topic = msg_topic = 'host'
64 msg_items = '<hosts>'
65
mblighaed47e82009-03-17 19:06:18 +000066 protections = host_protections.Protection.names
67
mblighbe630eb2008-08-01 16:41:48 +000068
69 def __init__(self):
70 """Add to the parser the options common to all the
71 host actions"""
72 super(host, self).__init__()
73
74 self.parser.add_option('-M', '--mlist',
75 help='File listing the machines',
76 type='string',
77 default=None,
78 metavar='MACHINE_FLIST')
79
mbligh9deeefa2009-05-01 23:11:08 +000080 self.topic_parse_info = topic_common.item_parse_info(
81 attribute_name='hosts',
82 filename_option='mlist',
83 use_leftover=True)
mblighbe630eb2008-08-01 16:41:48 +000084
85
86 def _parse_lock_options(self, options):
87 if options.lock and options.unlock:
88 self.invalid_syntax('Only specify one of '
89 '--lock and --unlock.')
90
Ningning Xiaef35cb52018-05-04 17:58:20 -070091 self.lock = options.lock
92 self.unlock = options.unlock
93 self.lock_reason = options.lock_reason
Ningning Xiaef35cb52018-05-04 17:58:20 -070094
mblighbe630eb2008-08-01 16:41:48 +000095 if options.lock:
96 self.data['locked'] = True
97 self.messages.append('Locked host')
98 elif options.unlock:
99 self.data['locked'] = False
Matthew Sartori68186332015-04-27 17:19:53 -0700100 self.data['lock_reason'] = ''
mblighbe630eb2008-08-01 16:41:48 +0000101 self.messages.append('Unlocked host')
102
Matthew Sartori68186332015-04-27 17:19:53 -0700103 if options.lock and options.lock_reason:
104 self.data['lock_reason'] = options.lock_reason
105
mblighbe630eb2008-08-01 16:41:48 +0000106
107 def _cleanup_labels(self, labels, platform=None):
108 """Removes the platform label from the overall labels"""
109 if platform:
110 return [label for label in labels
111 if label != platform]
112 else:
113 try:
114 return [label for label in labels
115 if not label['platform']]
116 except TypeError:
117 # This is a hack - the server will soon
118 # do this, so all this code should be removed.
119 return labels
120
121
122 def get_items(self):
123 return self.hosts
124
125
126class host_help(host):
127 """Just here to get the atest logic working.
128 Usage is set by its parent"""
129 pass
130
131
132class host_list(action_common.atest_list, host):
133 """atest host list [--mlist <file>|<hosts>] [--label <label>]
mbligh536a5242008-10-18 14:35:54 +0000134 [--status <status1,status2>] [--acl <ACL>] [--user <user>]"""
mblighbe630eb2008-08-01 16:41:48 +0000135
136 def __init__(self):
137 super(host_list, self).__init__()
138
139 self.parser.add_option('-b', '--label',
mbligh6444c6b2008-10-27 20:55:13 +0000140 default='',
141 help='Only list hosts with all these labels '
Ningning Xiaea02ab12018-05-11 15:08:57 -0700142 '(comma separated). When --skylab is provided, '
143 'a label must be in the format of '
144 'label-key:label-value (e.g., board:lumpy).')
mblighbe630eb2008-08-01 16:41:48 +0000145 self.parser.add_option('-s', '--status',
mbligh6444c6b2008-10-27 20:55:13 +0000146 default='',
147 help='Only list hosts with any of these '
148 'statuses (comma separated)')
mbligh536a5242008-10-18 14:35:54 +0000149 self.parser.add_option('-a', '--acl',
mbligh6444c6b2008-10-27 20:55:13 +0000150 default='',
Ningning Xiaea02ab12018-05-11 15:08:57 -0700151 help=('Only list hosts within this ACL. %s' %
152 skylab_utils.MSG_INVALID_IN_SKYLAB))
mbligh536a5242008-10-18 14:35:54 +0000153 self.parser.add_option('-u', '--user',
mbligh6444c6b2008-10-27 20:55:13 +0000154 default='',
Ningning Xiaea02ab12018-05-11 15:08:57 -0700155 help=('Only list hosts available to this user. '
156 '%s' % skylab_utils.MSG_INVALID_IN_SKYLAB))
mbligh70b9bf42008-11-18 15:00:46 +0000157 self.parser.add_option('-N', '--hostnames-only', help='Only return '
158 'hostnames for the machines queried.',
159 action='store_true')
mbligh91e0efd2009-02-26 01:02:16 +0000160 self.parser.add_option('--locked',
161 default=False,
162 help='Only list locked hosts',
163 action='store_true')
164 self.parser.add_option('--unlocked',
165 default=False,
166 help='Only list unlocked hosts',
167 action='store_true')
Ningning Xiaea02ab12018-05-11 15:08:57 -0700168 self.parser.add_option('--full-output',
169 default=False,
170 help=('Print out the full content of the hosts. '
171 'Only supported with --skylab.'),
172 action='store_true',
173 dest='full_output')
mbligh91e0efd2009-02-26 01:02:16 +0000174
Ningning Xiaea02ab12018-05-11 15:08:57 -0700175 self.add_skylab_options()
mblighbe630eb2008-08-01 16:41:48 +0000176
177
178 def parse(self):
179 """Consume the specific options"""
jamesrenc2863162010-07-12 21:20:51 +0000180 label_info = topic_common.item_parse_info(attribute_name='labels',
181 inline_option='label')
182
183 (options, leftover) = super(host_list, self).parse([label_info])
184
mblighbe630eb2008-08-01 16:41:48 +0000185 self.status = options.status
mbligh536a5242008-10-18 14:35:54 +0000186 self.acl = options.acl
187 self.user = options.user
mbligh70b9bf42008-11-18 15:00:46 +0000188 self.hostnames_only = options.hostnames_only
mbligh91e0efd2009-02-26 01:02:16 +0000189
190 if options.locked and options.unlocked:
191 self.invalid_syntax('--locked and --unlocked are '
192 'mutually exclusive')
Ningning Xiaea02ab12018-05-11 15:08:57 -0700193
mbligh91e0efd2009-02-26 01:02:16 +0000194 self.locked = options.locked
195 self.unlocked = options.unlocked
Ningning Xia64ced002018-05-16 17:36:20 -0700196 self.label_map = None
Ningning Xiaea02ab12018-05-11 15:08:57 -0700197
198 if self.skylab:
199 if options.user or options.acl or options.status:
200 self.invalid_syntax('--user, --acl or --status is not '
201 'supported with --skylab.')
202 self.full_output = options.full_output
203 if self.full_output and self.hostnames_only:
204 self.invalid_syntax('--full-output is conflicted with '
205 '--hostnames-only.')
Ningning Xia64ced002018-05-16 17:36:20 -0700206
207 if self.labels:
208 self.label_map = device.convert_to_label_map(self.labels)
Ningning Xiaea02ab12018-05-11 15:08:57 -0700209 else:
210 if options.full_output:
211 self.invalid_syntax('--full_output is only supported with '
212 '--skylab.')
213
mblighbe630eb2008-08-01 16:41:48 +0000214 return (options, leftover)
215
216
Ningning Xiaea02ab12018-05-11 15:08:57 -0700217 def execute_skylab(self):
218 """Execute 'atest host list' with --skylab."""
219 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
220 inventory_repo.initialize()
221 lab = text_manager.load_lab(inventory_repo.get_data_dir())
222
223 # TODO(nxia): support filtering on run-time labels and status.
224 return device.get_devices(
225 lab,
226 'duts',
227 self.environment,
Ningning Xia64ced002018-05-16 17:36:20 -0700228 label_map=self.label_map,
Ningning Xiaea02ab12018-05-11 15:08:57 -0700229 hostnames=self.hosts,
230 locked=self.locked,
231 unlocked=self.unlocked)
232
233
mblighbe630eb2008-08-01 16:41:48 +0000234 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800235 """Execute 'atest host list'."""
Ningning Xiaea02ab12018-05-11 15:08:57 -0700236 if self.skylab:
237 return self.execute_skylab()
238
mblighbe630eb2008-08-01 16:41:48 +0000239 filters = {}
240 check_results = {}
241 if self.hosts:
242 filters['hostname__in'] = self.hosts
243 check_results['hostname__in'] = 'hostname'
mbligh6444c6b2008-10-27 20:55:13 +0000244
245 if self.labels:
jamesrenc2863162010-07-12 21:20:51 +0000246 if len(self.labels) == 1:
mblighf703fb42009-01-30 00:35:05 +0000247 # This is needed for labels with wildcards (x86*)
jamesrenc2863162010-07-12 21:20:51 +0000248 filters['labels__name__in'] = self.labels
mblighf703fb42009-01-30 00:35:05 +0000249 check_results['labels__name__in'] = None
250 else:
jamesrenc2863162010-07-12 21:20:51 +0000251 filters['multiple_labels'] = self.labels
mblighf703fb42009-01-30 00:35:05 +0000252 check_results['multiple_labels'] = None
mbligh6444c6b2008-10-27 20:55:13 +0000253
mblighbe630eb2008-08-01 16:41:48 +0000254 if self.status:
mbligh6444c6b2008-10-27 20:55:13 +0000255 statuses = self.status.split(',')
256 statuses = [status.strip() for status in statuses
257 if status.strip()]
258
259 filters['status__in'] = statuses
mblighcd8eb972008-08-25 19:20:39 +0000260 check_results['status__in'] = None
mbligh6444c6b2008-10-27 20:55:13 +0000261
mbligh536a5242008-10-18 14:35:54 +0000262 if self.acl:
showardd9ac4452009-02-07 02:04:37 +0000263 filters['aclgroup__name'] = self.acl
264 check_results['aclgroup__name'] = None
mbligh536a5242008-10-18 14:35:54 +0000265 if self.user:
showardd9ac4452009-02-07 02:04:37 +0000266 filters['aclgroup__users__login'] = self.user
267 check_results['aclgroup__users__login'] = None
mbligh91e0efd2009-02-26 01:02:16 +0000268
269 if self.locked or self.unlocked:
270 filters['locked'] = self.locked
271 check_results['locked'] = None
272
mblighbe630eb2008-08-01 16:41:48 +0000273 return super(host_list, self).execute(op='get_hosts',
274 filters=filters,
275 check_results=check_results)
276
277
278 def output(self, results):
xixuand6011f12016-12-08 15:01:58 -0800279 """Print output of 'atest host list'.
280
281 @param results: the results to be printed.
282 """
Ningning Xiaea02ab12018-05-11 15:08:57 -0700283 if results and not self.skylab:
mblighbe630eb2008-08-01 16:41:48 +0000284 # Remove the platform from the labels.
285 for result in results:
286 result['labels'] = self._cleanup_labels(result['labels'],
287 result['platform'])
Ningning Xiaea02ab12018-05-11 15:08:57 -0700288 if self.skylab and self.full_output:
289 print results
290 return
291
292 if self.skylab:
293 results = device.convert_to_autotest_hosts(results)
294
mbligh70b9bf42008-11-18 15:00:46 +0000295 if self.hostnames_only:
mblighdf75f8b2008-11-18 19:07:42 +0000296 self.print_list(results, key='hostname')
mbligh70b9bf42008-11-18 15:00:46 +0000297 else:
Ningning Xiaea02ab12018-05-11 15:08:57 -0700298 keys = ['hostname', 'status', 'shard', 'locked', 'lock_reason',
299 'locked_by', 'platform', 'labels']
Prashanth Balasubramaniana5048562014-12-12 10:14:11 -0800300 super(host_list, self).output(results, keys=keys)
mblighbe630eb2008-08-01 16:41:48 +0000301
302
303class host_stat(host):
304 """atest host stat --mlist <file>|<hosts>"""
305 usage_action = 'stat'
306
307 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800308 """Execute 'atest host stat'."""
mblighbe630eb2008-08-01 16:41:48 +0000309 results = []
310 # Convert wildcards into real host stats.
311 existing_hosts = []
312 for host in self.hosts:
313 if host.endswith('*'):
314 stats = self.execute_rpc('get_hosts',
315 hostname__startswith=host.rstrip('*'))
316 if len(stats) == 0:
317 self.failure('No hosts matching %s' % host, item=host,
318 what_failed='Failed to stat')
319 continue
320 else:
321 stats = self.execute_rpc('get_hosts', hostname=host)
322 if len(stats) == 0:
323 self.failure('Unknown host %s' % host, item=host,
324 what_failed='Failed to stat')
325 continue
326 existing_hosts.extend(stats)
327
328 for stat in existing_hosts:
329 host = stat['hostname']
330 # The host exists, these should succeed
331 acls = self.execute_rpc('get_acl_groups', hosts__hostname=host)
332
333 labels = self.execute_rpc('get_labels', host__hostname=host)
Simran Basi0739d682015-02-25 16:22:56 -0800334 results.append([[stat], acls, labels, stat['attributes']])
mblighbe630eb2008-08-01 16:41:48 +0000335 return results
336
337
338 def output(self, results):
xixuand6011f12016-12-08 15:01:58 -0800339 """Print output of 'atest host stat'.
340
341 @param results: the results to be printed.
342 """
Simran Basi0739d682015-02-25 16:22:56 -0800343 for stats, acls, labels, attributes in results:
mblighbe630eb2008-08-01 16:41:48 +0000344 print '-'*5
345 self.print_fields(stats,
Aviv Kesheta40d1272017-11-16 00:56:35 -0800346 keys=['hostname', 'id', 'platform',
mblighe163b032008-10-18 14:30:27 +0000347 'status', 'locked', 'locked_by',
Matthew Sartori68186332015-04-27 17:19:53 -0700348 'lock_time', 'lock_reason', 'protection',])
mblighbe630eb2008-08-01 16:41:48 +0000349 self.print_by_ids(acls, 'ACLs', line_before=True)
350 labels = self._cleanup_labels(labels)
351 self.print_by_ids(labels, 'Labels', line_before=True)
Simran Basi0739d682015-02-25 16:22:56 -0800352 self.print_dict(attributes, 'Host Attributes', line_before=True)
mblighbe630eb2008-08-01 16:41:48 +0000353
354
Gregory Nisbet93fa5b52019-07-19 14:56:35 -0700355class host_get_migration_plan(host_stat):
356 """atest host get_migration_plan --mlist <file>|<hosts>"""
357 usage_action = "get_migration_plan"
358
359 def __init__(self):
360 super(host_get_migration_plan, self).__init__()
361 self.parser.add_option("--ratio", default=0.5, type=float, dest="ratio")
362 self.add_skylab_options()
363
364 def parse(self):
365 (options, leftover) = super(host_get_migration_plan, self).parse()
366 self.ratio = options.ratio
367 return (options, leftover)
368
369 def execute(self):
370 afe = frontend.AFE()
371 results = super(host_get_migration_plan, self).execute()
372 working = []
373 non_working = []
374 for stats, _, _, _ in results:
375 assert len(stats) == 1
376 stats = stats[0]
377 hostname = stats["hostname"]
378 now = time.time()
379 history = HostJobHistory.get_host_history(
380 afe=afe,
381 hostname=hostname,
382 start_time=now,
383 end_time=now - 24 * 60 * 60,
384 )
385 dut_status, _ = history.last_diagnosis()
386 if dut_status in [UNUSED, WORKING]:
387 working.append(hostname)
388 elif dut_status == BROKEN:
389 non_working.append(hostname)
390 elif dut_status == UNKNOWN:
391 # if it's unknown, randomly assign it to working or
392 # nonworking, since we don't know.
393 # The two choices aren't actually equiprobable, but it
394 # should be fine.
395 random.choice([working, non_working]).append(hostname)
396 else:
397 raise ValueError("unknown status %s" % dut_status)
398 working_transfer, working_retain = fair_partition.partition(working, self.ratio)
399 non_working_transfer, non_working_retain = \
400 fair_partition.partition(non_working, self.ratio)
401 return {
402 "transfer": working_transfer + non_working_transfer,
403 "retain": working_retain + non_working_retain,
404 }
405
406 def output(self, results):
407 print json.dumps(results, indent=4, sort_keys=True)
408
Gregory Nisbet99b61942019-07-08 09:43:06 -0700409
410class host_statjson(host_stat):
411 """atest host statjson --mlist <file>|<hosts>
412
413 exposes the same information that 'atest host stat' does, but in the json
414 format that 'skylab add-dut' expects
415 """
416
417 usage_action = "statjson"
418
Gregory Nisbetf37da462019-09-11 10:19:03 -0700419 def __init__(self):
420 super(host_statjson, self).__init__()
421 self.parser.add_option('--verify',
422 default=False,
423 help='Verify that required fields are provided',
424 action='store_true',
425 dest='verify')
426
427 def parse(self):
428 (options, leftover) = super(host_statjson, self).parse()
429 self.verify = options.verify
430 return (options, leftover)
Gregory Nisbet99b61942019-07-08 09:43:06 -0700431
432 def output(self, results):
Gregory Nisbetf37da462019-09-11 10:19:03 -0700433 """Print output of 'atest host statjson <...>'"""
Gregory Nisbet99b61942019-07-08 09:43:06 -0700434 for row in results:
435 stats, acls, labels, attributes = row
436 # TODO(gregorynisbet): under what circumstances is stats
437 # not a list of length 1?
438 assert len(stats) == 1
439 stats_map = stats[0]
Gregory Nisbetce77cef2019-07-29 12:27:21 -0700440
441 # Stripping the MIGRATED_HOST_SUFFIX makes it possible to
442 # migrate a DUT from autotest to skylab even after its hostname
443 # has been changed.
444 # This enables the steps (renaming the host,
445 # copying the inventory information to skylab) to be doable in
446 # either order.
Gregory Nisbet48f1eea2019-08-05 16:18:56 -0700447 hostname = _remove_hostname_suffix_if_present(
448 stats_map["hostname"],
449 MIGRATED_HOST_SUFFIX
450 )
451
Gregory Nisbet932f5a92019-09-05 14:02:31 -0700452 # TODO(gregorynisbet): clean up servo information
453 if "servo_host" not in attributes:
454 attributes["servo_host"] = "dummy_host"
455 if "servo_port" not in attributes:
456 attributes["servo_port"] = "dummy_port"
457
Gregory Nisbet99b61942019-07-08 09:43:06 -0700458 labels = self._cleanup_labels(labels)
459 attrs = [{"key": k, "value": v} for k, v in attributes.iteritems()]
460 out_labels = process_labels(labels, platform=stats_map["platform"])
461 skylab_json = {
462 "common": {
463 "attributes": attrs,
Gregory Nisbet23a06b92019-07-11 16:44:43 -0700464 "environment": "ENVIRONMENT_PROD",
Gregory Nisbetce77cef2019-07-29 12:27:21 -0700465 "hostname": hostname,
Gregory Nisbet99b61942019-07-08 09:43:06 -0700466 "id": ID_AUTOGEN_MESSAGE,
467 "labels": out_labels,
Gregory Nisbetcc0941f2019-08-29 13:56:05 -0700468 "serialNumber": attributes.get("serial_number", None),
Gregory Nisbet99b61942019-07-08 09:43:06 -0700469 }
470 }
Gregory Nisbetf37da462019-09-11 10:19:03 -0700471 # if the validate flag is provided, check that a given json blob
472 # has all the required fields for skylab.
473 if self.verify:
474 validate_required_fields_for_skylab(skylab_json)
Gregory Nisbet99b61942019-07-08 09:43:06 -0700475 print json.dumps(skylab_json, indent=4, sort_keys=True)
476
477
mblighbe630eb2008-08-01 16:41:48 +0000478class host_jobs(host):
mbligh1494eca2008-08-13 21:24:22 +0000479 """atest host jobs [--max-query] --mlist <file>|<hosts>"""
mblighbe630eb2008-08-01 16:41:48 +0000480 usage_action = 'jobs'
481
mbligh6996fe82008-08-13 00:32:27 +0000482 def __init__(self):
483 super(host_jobs, self).__init__()
484 self.parser.add_option('-q', '--max-query',
485 help='Limits the number of results '
486 '(20 by default)',
487 type='int', default=20)
488
489
490 def parse(self):
491 """Consume the specific options"""
mbligh9deeefa2009-05-01 23:11:08 +0000492 (options, leftover) = super(host_jobs, self).parse()
mbligh6996fe82008-08-13 00:32:27 +0000493 self.max_queries = options.max_query
494 return (options, leftover)
495
496
mblighbe630eb2008-08-01 16:41:48 +0000497 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800498 """Execute 'atest host jobs'."""
mblighbe630eb2008-08-01 16:41:48 +0000499 results = []
500 real_hosts = []
501 for host in self.hosts:
502 if host.endswith('*'):
503 stats = self.execute_rpc('get_hosts',
504 hostname__startswith=host.rstrip('*'))
505 if len(stats) == 0:
506 self.failure('No host matching %s' % host, item=host,
507 what_failed='Failed to stat')
508 [real_hosts.append(stat['hostname']) for stat in stats]
509 else:
510 real_hosts.append(host)
511
512 for host in real_hosts:
513 queue_entries = self.execute_rpc('get_host_queue_entries',
mbligh6996fe82008-08-13 00:32:27 +0000514 host__hostname=host,
mbligh1494eca2008-08-13 21:24:22 +0000515 query_limit=self.max_queries,
showard6958c722009-09-23 20:03:16 +0000516 sort_by=['-job__id'])
mblighbe630eb2008-08-01 16:41:48 +0000517 jobs = []
518 for entry in queue_entries:
519 job = {'job_id': entry['job']['id'],
520 'job_owner': entry['job']['owner'],
521 'job_name': entry['job']['name'],
522 'status': entry['status']}
523 jobs.append(job)
524 results.append((host, jobs))
525 return results
526
527
528 def output(self, results):
xixuand6011f12016-12-08 15:01:58 -0800529 """Print output of 'atest host jobs'.
530
531 @param results: the results to be printed.
532 """
mblighbe630eb2008-08-01 16:41:48 +0000533 for host, jobs in results:
534 print '-'*5
535 print 'Hostname: %s' % host
536 self.print_table(jobs, keys_header=['job_id',
showardbe0d8692009-08-20 23:42:44 +0000537 'job_owner',
538 'job_name',
539 'status'])
mblighbe630eb2008-08-01 16:41:48 +0000540
Justin Giorgi16bba562016-06-22 10:42:13 -0700541class BaseHostModCreate(host):
xixuand6011f12016-12-08 15:01:58 -0800542 """The base class for host_mod and host_create"""
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700543 # Matches one attribute=value pair
544 attribute_regex = r'(?P<attribute>\w+)=(?P<value>.+)?'
mblighbe630eb2008-08-01 16:41:48 +0000545
546 def __init__(self):
Justin Giorgi16bba562016-06-22 10:42:13 -0700547 """Add the options shared between host mod and host create actions."""
mblighbe630eb2008-08-01 16:41:48 +0000548 self.messages = []
Justin Giorgi16bba562016-06-22 10:42:13 -0700549 self.host_ids = {}
550 super(BaseHostModCreate, self).__init__()
mblighbe630eb2008-08-01 16:41:48 +0000551 self.parser.add_option('-l', '--lock',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700552 help='Lock hosts.',
mblighbe630eb2008-08-01 16:41:48 +0000553 action='store_true')
Matthew Sartori68186332015-04-27 17:19:53 -0700554 self.parser.add_option('-r', '--lock_reason',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700555 help='Reason for locking hosts.',
Matthew Sartori68186332015-04-27 17:19:53 -0700556 default='')
Ningning Xiaef35cb52018-05-04 17:58:20 -0700557 self.parser.add_option('-u', '--unlock',
558 help='Unlock hosts.',
559 action='store_true')
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700560
mblighe163b032008-10-18 14:30:27 +0000561 self.parser.add_option('-p', '--protection', type='choice',
mblighaed47e82009-03-17 19:06:18 +0000562 help=('Set the protection level on a host. '
Ningning Xiaef35cb52018-05-04 17:58:20 -0700563 'Must be one of: %s. %s' %
564 (', '.join('"%s"' % p
565 for p in self.protections),
566 skylab_utils.MSG_INVALID_IN_SKYLAB)),
mblighaed47e82009-03-17 19:06:18 +0000567 choices=self.protections)
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700568 self._attributes = []
569 self.parser.add_option('--attribute', '-i',
570 help=('Host attribute to add or change. Format '
571 'is <attribute>=<value>. Multiple '
572 'attributes can be set by passing the '
573 'argument multiple times. Attributes can '
574 'be unset by providing an empty value.'),
575 action='append')
mblighbe630eb2008-08-01 16:41:48 +0000576 self.parser.add_option('-b', '--labels',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700577 help=('Comma separated list of labels. '
578 'When --skylab is provided, a label must '
579 'be in the format of label-key:label-value'
580 ' (e.g., board:lumpy).'))
mblighbe630eb2008-08-01 16:41:48 +0000581 self.parser.add_option('-B', '--blist',
582 help='File listing the labels',
583 type='string',
584 metavar='LABEL_FLIST')
585 self.parser.add_option('-a', '--acls',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700586 help=('Comma separated list of ACLs. %s' %
587 skylab_utils.MSG_INVALID_IN_SKYLAB))
mblighbe630eb2008-08-01 16:41:48 +0000588 self.parser.add_option('-A', '--alist',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700589 help=('File listing the acls. %s' %
590 skylab_utils.MSG_INVALID_IN_SKYLAB),
mblighbe630eb2008-08-01 16:41:48 +0000591 type='string',
592 metavar='ACL_FLIST')
Justin Giorgi16bba562016-06-22 10:42:13 -0700593 self.parser.add_option('-t', '--platform',
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700594 help=('Sets the platform label. %s Please set '
595 'platform in labels (e.g., -b '
596 'platform:platform_name) with --skylab.' %
597 skylab_utils.MSG_INVALID_IN_SKYLAB))
mblighbe630eb2008-08-01 16:41:48 +0000598
599
600 def parse(self):
Justin Giorgi16bba562016-06-22 10:42:13 -0700601 """Consume the options common to host create and host mod.
602 """
mbligh9deeefa2009-05-01 23:11:08 +0000603 label_info = topic_common.item_parse_info(attribute_name='labels',
604 inline_option='labels',
605 filename_option='blist')
606 acl_info = topic_common.item_parse_info(attribute_name='acls',
607 inline_option='acls',
608 filename_option='alist')
609
Justin Giorgi16bba562016-06-22 10:42:13 -0700610 (options, leftover) = super(BaseHostModCreate, self).parse([label_info,
mbligh9deeefa2009-05-01 23:11:08 +0000611 acl_info],
612 req_items='hosts')
mblighbe630eb2008-08-01 16:41:48 +0000613
614 self._parse_lock_options(options)
Justin Giorgi16bba562016-06-22 10:42:13 -0700615
Ningning Xia64ced002018-05-16 17:36:20 -0700616 self.label_map = None
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700617 if self.allow_skylab and self.skylab:
Ningning Xiaef35cb52018-05-04 17:58:20 -0700618 # TODO(nxia): drop these flags when all hosts are migrated to skylab
Ningning Xia64ced002018-05-16 17:36:20 -0700619 if (options.protection or options.acls or options.alist or
620 options.platform):
621 self.invalid_syntax(
622 '--protection, --acls, --alist or --platform is not '
623 'supported with --skylab.')
624
625 if self.labels:
626 self.label_map = device.convert_to_label_map(self.labels)
Ningning Xiaef35cb52018-05-04 17:58:20 -0700627
mblighaed47e82009-03-17 19:06:18 +0000628 if options.protection:
629 self.data['protection'] = options.protection
Justin Giorgi16bba562016-06-22 10:42:13 -0700630 self.messages.append('Protection set to "%s"' % options.protection)
631
632 self.attributes = {}
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700633 if options.attribute:
634 for pair in options.attribute:
635 m = re.match(self.attribute_regex, pair)
636 if not m:
637 raise topic_common.CliError('Attribute must be in key=value '
638 'syntax.')
639 elif m.group('attribute') in self.attributes:
xixuand6011f12016-12-08 15:01:58 -0800640 raise topic_common.CliError(
641 'Multiple values provided for attribute '
642 '%s.' % m.group('attribute'))
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700643 self.attributes[m.group('attribute')] = m.group('value')
Justin Giorgi16bba562016-06-22 10:42:13 -0700644
645 self.platform = options.platform
mblighbe630eb2008-08-01 16:41:48 +0000646 return (options, leftover)
647
648
Justin Giorgi16bba562016-06-22 10:42:13 -0700649 def _set_acls(self, hosts, acls):
650 """Add hosts to acls (and remove from all other acls).
651
652 @param hosts: list of hostnames
653 @param acls: list of acl names
654 """
655 # Remove from all ACLs except 'Everyone' and ACLs in list
656 # Skip hosts that don't exist
657 for host in hosts:
658 if host not in self.host_ids:
659 continue
660 host_id = self.host_ids[host]
661 for a in self.execute_rpc('get_acl_groups', hosts=host_id):
662 if a['name'] not in self.acls and a['id'] != 1:
663 self.execute_rpc('acl_group_remove_hosts', id=a['id'],
664 hosts=self.hosts)
665
666 # Add hosts to the ACLs
667 self.check_and_create_items('get_acl_groups', 'add_acl_group',
668 self.acls)
669 for a in acls:
670 self.execute_rpc('acl_group_add_hosts', id=a, hosts=hosts)
671
672
673 def _remove_labels(self, host, condition):
674 """Remove all labels from host that meet condition(label).
675
676 @param host: hostname
677 @param condition: callable that returns bool when given a label
678 """
679 if host in self.host_ids:
680 host_id = self.host_ids[host]
681 labels_to_remove = []
682 for l in self.execute_rpc('get_labels', host=host_id):
683 if condition(l):
684 labels_to_remove.append(l['id'])
685 if labels_to_remove:
686 self.execute_rpc('host_remove_labels', id=host_id,
687 labels=labels_to_remove)
688
689
690 def _set_labels(self, host, labels):
691 """Apply labels to host (and remove all other labels).
692
693 @param host: hostname
694 @param labels: list of label names
695 """
696 condition = lambda l: l['name'] not in labels and not l['platform']
697 self._remove_labels(host, condition)
698 self.check_and_create_items('get_labels', 'add_label', labels)
699 self.execute_rpc('host_add_labels', id=host, labels=labels)
700
701
702 def _set_platform_label(self, host, platform_label):
703 """Apply the platform label to host (and remove existing).
704
705 @param host: hostname
706 @param platform_label: platform label's name
707 """
708 self._remove_labels(host, lambda l: l['platform'])
709 self.check_and_create_items('get_labels', 'add_label', [platform_label],
710 platform=True)
711 self.execute_rpc('host_add_labels', id=host, labels=[platform_label])
712
713
714 def _set_attributes(self, host, attributes):
715 """Set attributes on host.
716
717 @param host: hostname
718 @param attributes: attribute dictionary
719 """
720 for attr, value in self.attributes.iteritems():
721 self.execute_rpc('set_host_attribute', attribute=attr,
722 value=value, hostname=host)
723
724
725class host_mod(BaseHostModCreate):
726 """atest host mod [--lock|--unlock --force_modify_locking
727 --platform <arch>
728 --labels <labels>|--blist <label_file>
729 --acls <acls>|--alist <acl_file>
730 --protection <protection_type>
731 --attributes <attr>=<value>;<attr>=<value>
732 --mlist <mach_file>] <hosts>"""
733 usage_action = 'mod'
Justin Giorgi16bba562016-06-22 10:42:13 -0700734
735 def __init__(self):
736 """Add the options specific to the mod action"""
737 super(host_mod, self).__init__()
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700738 self.parser.add_option('--unlock-lock-id',
739 help=('Unlock the lock with the lock-id. %s' %
740 skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
741 default=None)
Justin Giorgi16bba562016-06-22 10:42:13 -0700742 self.parser.add_option('-f', '--force_modify_locking',
743 help='Forcefully lock\unlock a host',
744 action='store_true')
745 self.parser.add_option('--remove_acls',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700746 help=('Remove all active acls. %s' %
747 skylab_utils.MSG_INVALID_IN_SKYLAB),
Justin Giorgi16bba562016-06-22 10:42:13 -0700748 action='store_true')
749 self.parser.add_option('--remove_labels',
750 help='Remove all labels.',
751 action='store_true')
752
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700753 self.add_skylab_options()
Ningning Xia1dd82ee2018-06-08 11:41:03 -0700754 self.parser.add_option('--new-env',
755 dest='new_env',
756 choices=['staging', 'prod'],
757 help=('The new environment ("staging" or '
758 '"prod") of the hosts. %s' %
759 skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
760 default=None)
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700761
762
763 def _parse_unlock_options(self, options):
764 """Parse unlock related options."""
765 if self.skylab and options.unlock and options.unlock_lock_id is None:
766 self.invalid_syntax('Must provide --unlock-lock-id with "--skylab '
767 '--unlock".')
768
769 if (not (self.skylab and options.unlock) and
770 options.unlock_lock_id is not None):
771 self.invalid_syntax('--unlock-lock-id is only valid with '
772 '"--skylab --unlock".')
773
774 self.unlock_lock_id = options.unlock_lock_id
775
Justin Giorgi16bba562016-06-22 10:42:13 -0700776
777 def parse(self):
778 """Consume the specific options"""
779 (options, leftover) = super(host_mod, self).parse()
780
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700781 self._parse_unlock_options(options)
782
Justin Giorgi16bba562016-06-22 10:42:13 -0700783 if options.force_modify_locking:
784 self.data['force_modify_locking'] = True
785
Ningning Xiaef35cb52018-05-04 17:58:20 -0700786 if self.skylab and options.remove_acls:
787 # TODO(nxia): drop the flag when all hosts are migrated to skylab
788 self.invalid_syntax('--remove_acls is not supported with --skylab.')
789
Justin Giorgi16bba562016-06-22 10:42:13 -0700790 self.remove_acls = options.remove_acls
791 self.remove_labels = options.remove_labels
Ningning Xia1dd82ee2018-06-08 11:41:03 -0700792 self.new_env = options.new_env
Justin Giorgi16bba562016-06-22 10:42:13 -0700793
794 return (options, leftover)
795
796
Ningning Xiaef35cb52018-05-04 17:58:20 -0700797 def execute_skylab(self):
798 """Execute atest host mod with --skylab.
799
800 @return A list of hostnames which have been successfully modified.
801 """
802 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
803 inventory_repo.initialize()
804 data_dir = inventory_repo.get_data_dir()
805 lab = text_manager.load_lab(data_dir)
806
807 locked_by = None
808 if self.lock:
809 locked_by = inventory_repo.git_repo.config('user.email')
810
811 successes = []
812 for hostname in self.hosts:
813 try:
814 device.modify(
815 lab,
816 'duts',
817 hostname,
818 self.environment,
819 lock=self.lock,
820 locked_by=locked_by,
821 lock_reason = self.lock_reason,
822 unlock=self.unlock,
823 unlock_lock_id=self.unlock_lock_id,
824 attributes=self.attributes,
825 remove_labels=self.remove_labels,
Ningning Xia1dd82ee2018-06-08 11:41:03 -0700826 label_map=self.label_map,
827 new_env=self.new_env)
Ningning Xiaef35cb52018-05-04 17:58:20 -0700828 successes.append(hostname)
829 except device.SkylabDeviceActionError as e:
830 print('Cannot modify host %s: %s' % (hostname, e))
831
832 if successes:
833 text_manager.dump_lab(data_dir, lab)
834
835 status = inventory_repo.git_repo.status()
836 if not status:
837 print('Nothing is changed for hosts %s.' % successes)
838 return []
839
840 message = skylab_utils.construct_commit_message(
841 'Modify %d hosts.\n\n%s' % (len(successes), successes))
842 self.change_number = inventory_repo.upload_change(
843 message, draft=self.draft, dryrun=self.dryrun,
844 submit=self.submit)
845
846 return successes
847
848
Justin Giorgi16bba562016-06-22 10:42:13 -0700849 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800850 """Execute 'atest host mod'."""
Ningning Xiaef35cb52018-05-04 17:58:20 -0700851 if self.skylab:
852 return self.execute_skylab()
853
Justin Giorgi16bba562016-06-22 10:42:13 -0700854 successes = []
855 for host in self.execute_rpc('get_hosts', hostname__in=self.hosts):
856 self.host_ids[host['hostname']] = host['id']
857 for host in self.hosts:
858 if host not in self.host_ids:
859 self.failure('Cannot modify non-existant host %s.' % host)
860 continue
861 host_id = self.host_ids[host]
862
863 try:
864 if self.data:
865 self.execute_rpc('modify_host', item=host,
866 id=host, **self.data)
867
868 if self.attributes:
869 self._set_attributes(host, self.attributes)
870
871 if self.labels or self.remove_labels:
872 self._set_labels(host, self.labels)
873
874 if self.platform:
875 self._set_platform_label(host, self.platform)
876
877 # TODO: Make the AFE return True or False,
878 # especially for lock
879 successes.append(host)
880 except topic_common.CliError, full_error:
881 # Already logged by execute_rpc()
882 pass
883
884 if self.acls or self.remove_acls:
885 self._set_acls(self.hosts, self.acls)
886
887 return successes
888
889
890 def output(self, hosts):
xixuand6011f12016-12-08 15:01:58 -0800891 """Print output of 'atest host mod'.
892
893 @param hosts: the host list to be printed.
894 """
Justin Giorgi16bba562016-06-22 10:42:13 -0700895 for msg in self.messages:
896 self.print_wrapped(msg, hosts)
897
Ningning Xiaef35cb52018-05-04 17:58:20 -0700898 if hosts and self.skylab:
Ningning Xiac8b430d2018-05-17 15:10:25 -0700899 print('Modified hosts: %s.' % ', '.join(hosts))
Ningning Xiaef35cb52018-05-04 17:58:20 -0700900 if self.skylab and not self.dryrun and not self.submit:
Ningning Xiac8b430d2018-05-17 15:10:25 -0700901 print(skylab_utils.get_cl_message(self.change_number))
Ningning Xiaef35cb52018-05-04 17:58:20 -0700902
Justin Giorgi16bba562016-06-22 10:42:13 -0700903
904class HostInfo(object):
905 """Store host information so we don't have to keep looking it up."""
906 def __init__(self, hostname, platform, labels):
907 self.hostname = hostname
908 self.platform = platform
909 self.labels = labels
910
911
912class host_create(BaseHostModCreate):
913 """atest host create [--lock|--unlock --platform <arch>
914 --labels <labels>|--blist <label_file>
915 --acls <acls>|--alist <acl_file>
916 --protection <protection_type>
917 --attributes <attr>=<value>;<attr>=<value>
918 --mlist <mach_file>] <hosts>"""
919 usage_action = 'create'
920
921 def parse(self):
922 """Option logic specific to create action.
923 """
924 (options, leftovers) = super(host_create, self).parse()
925 self.locked = options.lock
926 if 'serials' in self.attributes:
927 if len(self.hosts) > 1:
928 raise topic_common.CliError('Can not specify serials with '
929 'multiple hosts.')
930
931
932 @classmethod
933 def construct_without_parse(
934 cls, web_server, hosts, platform=None,
935 locked=False, lock_reason='', labels=[], acls=[],
936 protection=host_protections.Protection.NO_PROTECTION):
937 """Construct a host_create object and fill in data from args.
938
939 Do not need to call parse after the construction.
940
941 Return an object of site_host_create ready to execute.
942
943 @param web_server: A string specifies the autotest webserver url.
944 It is needed to setup comm to make rpc.
945 @param hosts: A list of hostnames as strings.
946 @param platform: A string or None.
947 @param locked: A boolean.
948 @param lock_reason: A string.
949 @param labels: A list of labels as strings.
950 @param acls: A list of acls as strings.
951 @param protection: An enum defined in host_protections.
952 """
953 obj = cls()
954 obj.web_server = web_server
955 try:
956 # Setup stuff needed for afe comm.
957 obj.afe = rpc.afe_comm(web_server)
958 except rpc.AuthError, s:
959 obj.failure(str(s), fatal=True)
960 obj.hosts = hosts
961 obj.platform = platform
962 obj.locked = locked
963 if locked and lock_reason.strip():
964 obj.data['lock_reason'] = lock_reason.strip()
965 obj.labels = labels
966 obj.acls = acls
967 if protection:
968 obj.data['protection'] = protection
969 obj.attributes = {}
970 return obj
971
972
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700973 def _detect_host_info(self, host):
974 """Detect platform and labels from the host.
975
976 @param host: hostname
977
978 @return: HostInfo object
979 """
980 # Mock an afe_host object so that the host is constructed as if the
981 # data was already in afe
982 data = {'attributes': self.attributes, 'labels': self.labels}
983 afe_host = frontend.Host(None, data)
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700984 store = host_info.InMemoryHostInfoStore(
985 host_info.HostInfo(labels=self.labels,
986 attributes=self.attributes))
987 machine = {
988 'hostname': host,
989 'afe_host': afe_host,
990 'host_info_store': store
991 }
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700992 try:
Allen Li2c32d6b2017-02-03 15:28:10 -0800993 if bin_utils.ping(host, tries=1, deadline=1) == 0:
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700994 serials = self.attributes.get('serials', '').split(',')
Richard Barnette9db80682018-04-26 00:55:15 +0000995 adb_serial = self.attributes.get('serials')
996 host_dut = hosts.create_host(machine,
997 adb_serial=adb_serial)
xixuand6011f12016-12-08 15:01:58 -0800998
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700999 info = HostInfo(host, host_dut.get_platform(),
1000 host_dut.get_labels())
xixuand6011f12016-12-08 15:01:58 -08001001 # Clean host to make sure nothing left after calling it,
1002 # e.g. tunnels.
1003 if hasattr(host_dut, 'close'):
1004 host_dut.close()
Justin Giorgi5208eaa2016-07-02 20:12:12 -07001005 else:
1006 # Can't ping the host, use default information.
Prathmesh Prabhud252d262017-05-31 11:46:34 -07001007 info = HostInfo(host, None, [])
Justin Giorgi5208eaa2016-07-02 20:12:12 -07001008 except (socket.gaierror, error.AutoservRunError,
1009 error.AutoservSSHTimeout):
1010 # We may be adding a host that does not exist yet or we can't
1011 # reach due to hostname/address issues or if the host is down.
Prathmesh Prabhud252d262017-05-31 11:46:34 -07001012 info = HostInfo(host, None, [])
1013 return info
Justin Giorgi5208eaa2016-07-02 20:12:12 -07001014
1015
mblighbe630eb2008-08-01 16:41:48 +00001016 def _execute_add_one_host(self, host):
mbligh719e14a2008-12-04 01:17:08 +00001017 # Always add the hosts as locked to avoid the host
Matthew Sartori68186332015-04-27 17:19:53 -07001018 # being picked up by the scheduler before it's ACL'ed.
mbligh719e14a2008-12-04 01:17:08 +00001019 self.data['locked'] = True
Matthew Sartori68186332015-04-27 17:19:53 -07001020 if not self.locked:
1021 self.data['lock_reason'] = 'Forced lock on device creation'
Justin Giorgi16bba562016-06-22 10:42:13 -07001022 self.execute_rpc('add_host', hostname=host, status="Ready", **self.data)
mblighbe630eb2008-08-01 16:41:48 +00001023
Justin Giorgi16bba562016-06-22 10:42:13 -07001024 # If there are labels avaliable for host, use them.
Prathmesh Prabhud252d262017-05-31 11:46:34 -07001025 info = self._detect_host_info(host)
Justin Giorgi16bba562016-06-22 10:42:13 -07001026 labels = set(self.labels)
Prathmesh Prabhud252d262017-05-31 11:46:34 -07001027 if info.labels:
1028 labels.update(info.labels)
Justin Giorgi16bba562016-06-22 10:42:13 -07001029
1030 if labels:
1031 self._set_labels(host, list(labels))
1032
1033 # Now add the platform label.
1034 # If a platform was not provided and we were able to retrieve it
1035 # from the host, use the retrieved platform.
Prathmesh Prabhud252d262017-05-31 11:46:34 -07001036 platform = self.platform if self.platform else info.platform
Justin Giorgi16bba562016-06-22 10:42:13 -07001037 if platform:
1038 self._set_platform_label(host, platform)
1039
1040 if self.attributes:
1041 self._set_attributes(host, self.attributes)
mblighbe630eb2008-08-01 16:41:48 +00001042
1043
Justin Giorgi5208eaa2016-07-02 20:12:12 -07001044 def execute(self):
xixuand6011f12016-12-08 15:01:58 -08001045 """Execute 'atest host create'."""
Justin Giorgi16bba562016-06-22 10:42:13 -07001046 successful_hosts = []
1047 for host in self.hosts:
1048 try:
1049 self._execute_add_one_host(host)
1050 successful_hosts.append(host)
1051 except topic_common.CliError:
1052 pass
Jiaxi Luoc342f9f2014-05-19 16:22:03 -07001053
1054 if successful_hosts:
Justin Giorgi16bba562016-06-22 10:42:13 -07001055 self._set_acls(successful_hosts, self.acls)
Jiaxi Luoc342f9f2014-05-19 16:22:03 -07001056
1057 if not self.locked:
1058 for host in successful_hosts:
Matthew Sartori68186332015-04-27 17:19:53 -07001059 self.execute_rpc('modify_host', id=host, locked=False,
1060 lock_reason='')
Jiaxi Luoc342f9f2014-05-19 16:22:03 -07001061 return successful_hosts
1062
1063
mblighbe630eb2008-08-01 16:41:48 +00001064 def output(self, hosts):
xixuand6011f12016-12-08 15:01:58 -08001065 """Print output of 'atest host create'.
1066
1067 @param hosts: the added host list to be printed.
1068 """
mblighbe630eb2008-08-01 16:41:48 +00001069 self.print_wrapped('Added host', hosts)
1070
1071
1072class host_delete(action_common.atest_delete, host):
1073 """atest host delete [--mlist <mach_file>] <hosts>"""
Ningning Xiac8b430d2018-05-17 15:10:25 -07001074
1075 def __init__(self):
1076 super(host_delete, self).__init__()
1077
1078 self.add_skylab_options()
1079
1080
1081 def execute_skylab(self):
1082 """Execute 'atest host delete' with '--skylab'.
1083
1084 @return A list of hostnames which have been successfully deleted.
1085 """
1086 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
1087 inventory_repo.initialize()
1088 data_dir = inventory_repo.get_data_dir()
1089 lab = text_manager.load_lab(data_dir)
1090
1091 successes = []
1092 for hostname in self.hosts:
1093 try:
1094 device.delete(
1095 lab,
1096 'duts',
1097 hostname,
1098 self.environment)
1099 successes.append(hostname)
1100 except device.SkylabDeviceActionError as e:
1101 print('Cannot delete host %s: %s' % (hostname, e))
1102
1103 if successes:
1104 text_manager.dump_lab(data_dir, lab)
1105 message = skylab_utils.construct_commit_message(
1106 'Delete %d hosts.\n\n%s' % (len(successes), successes))
1107 self.change_number = inventory_repo.upload_change(
1108 message, draft=self.draft, dryrun=self.dryrun,
1109 submit=self.submit)
1110
1111 return successes
1112
1113
1114 def execute(self):
1115 """Execute 'atest host delete'.
1116
1117 @return A list of hostnames which have been successfully deleted.
1118 """
1119 if self.skylab:
1120 return self.execute_skylab()
1121
1122 return super(host_delete, self).execute()
Ningning Xiac46bdd12018-05-29 11:24:14 -07001123
1124
1125class InvalidHostnameError(Exception):
1126 """Cannot perform actions on the host because of invalid hostname."""
1127
1128
1129def _add_hostname_suffix(hostname, suffix):
1130 """Add the suffix to the hostname."""
1131 if hostname.endswith(suffix):
1132 raise InvalidHostnameError(
1133 'Cannot add "%s" as it already contains the suffix.' % suffix)
1134
1135 return hostname + suffix
1136
1137
Gregory Nisbet48f1eea2019-08-05 16:18:56 -07001138def _remove_hostname_suffix_if_present(hostname, suffix):
Ningning Xiac46bdd12018-05-29 11:24:14 -07001139 """Remove the suffix from the hostname."""
Gregory Nisbet48f1eea2019-08-05 16:18:56 -07001140 if hostname.endswith(suffix):
1141 return hostname[:len(hostname) - len(suffix)]
1142 else:
1143 return hostname
Ningning Xiac46bdd12018-05-29 11:24:14 -07001144
1145
1146class host_rename(host):
1147 """Host rename is only for migrating hosts between skylab and AFE DB."""
1148
1149 usage_action = 'rename'
1150
1151 def __init__(self):
1152 """Add the options specific to the rename action."""
1153 super(host_rename, self).__init__()
1154
1155 self.parser.add_option('--for-migration',
1156 help=('Rename hostnames for migration. Rename '
1157 'each "hostname" to "hostname%s". '
1158 'The original "hostname" must not contain '
1159 'suffix.' % MIGRATED_HOST_SUFFIX),
1160 action='store_true',
1161 default=False)
1162 self.parser.add_option('--for-rollback',
1163 help=('Rename hostnames for migration rollback. '
1164 'Rename each "hostname%s" to its original '
1165 '"hostname".' % MIGRATED_HOST_SUFFIX),
1166 action='store_true',
1167 default=False)
1168 self.parser.add_option('--dryrun',
1169 help='Execute the action as a dryrun.',
1170 action='store_true',
1171 default=False)
Gregory Nisbetee457f62019-07-08 15:47:26 -07001172 self.parser.add_option('--non-interactive',
1173 help='run non-interactively',
Gregory Nisbet9744ea92019-07-02 12:36:59 -07001174 action='store_true',
1175 default=False)
Ningning Xiac46bdd12018-05-29 11:24:14 -07001176
1177
1178 def parse(self):
1179 """Consume the options common to host rename."""
1180 (options, leftovers) = super(host_rename, self).parse()
1181 self.for_migration = options.for_migration
1182 self.for_rollback = options.for_rollback
1183 self.dryrun = options.dryrun
Gregory Nisbetee457f62019-07-08 15:47:26 -07001184 self.interactive = not options.non_interactive
Ningning Xiac46bdd12018-05-29 11:24:14 -07001185 self.host_ids = {}
1186
1187 if not (self.for_migration ^ self.for_rollback):
1188 self.invalid_syntax('--for-migration and --for-rollback are '
1189 'exclusive, and one of them must be enabled.')
1190
1191 if not self.hosts:
1192 self.invalid_syntax('Must provide hostname(s).')
1193
1194 if self.dryrun:
1195 print('This will be a dryrun and will not rename hostnames.')
1196
1197 return (options, leftovers)
1198
1199
1200 def execute(self):
1201 """Execute 'atest host rename'."""
Gregory Nisbetee457f62019-07-08 15:47:26 -07001202 if self.interactive:
1203 if self.prompt_confirmation():
1204 pass
1205 else:
1206 return
Ningning Xiac46bdd12018-05-29 11:24:14 -07001207
1208 successes = []
1209 for host in self.execute_rpc('get_hosts', hostname__in=self.hosts):
1210 self.host_ids[host['hostname']] = host['id']
1211 for host in self.hosts:
1212 if host not in self.host_ids:
1213 self.failure('Cannot rename non-existant host %s.' % host,
1214 item=host, what_failed='Failed to rename')
1215 continue
1216 try:
Ningning Xiab261b162018-06-07 17:24:59 -07001217 host_id = self.host_ids[host]
Ningning Xiac46bdd12018-05-29 11:24:14 -07001218 if self.for_migration:
1219 new_hostname = _add_hostname_suffix(
1220 host, MIGRATED_HOST_SUFFIX)
1221 else:
1222 #for_rollback
Gregory Nisbet917ee332019-09-05 11:41:23 -07001223 new_hostname = _remove_hostname_suffix_if_present(
Ningning Xiac46bdd12018-05-29 11:24:14 -07001224 host, MIGRATED_HOST_SUFFIX)
1225
1226 if not self.dryrun:
Ningning Xia423c7962018-06-07 15:27:18 -07001227 # TODO(crbug.com/850737): delete and abort HQE.
Ningning Xiac46bdd12018-05-29 11:24:14 -07001228 data = {'hostname': new_hostname}
Ningning Xiab261b162018-06-07 17:24:59 -07001229 self.execute_rpc('modify_host', item=host, id=host_id,
1230 **data)
Ningning Xiac46bdd12018-05-29 11:24:14 -07001231 successes.append((host, new_hostname))
1232 except InvalidHostnameError as e:
1233 self.failure('Cannot rename host %s: %s' % (host, e), item=host,
1234 what_failed='Failed to rename')
1235 except topic_common.CliError, full_error:
1236 # Already logged by execute_rpc()
1237 pass
1238
1239 return successes
1240
1241
1242 def output(self, results):
1243 """Print output of 'atest host rename'."""
1244 if results:
1245 print('Successfully renamed:')
1246 for old_hostname, new_hostname in results:
1247 print('%s to %s' % (old_hostname, new_hostname))
Ningning Xia9df3d152018-05-23 17:15:14 -07001248
1249
1250class host_migrate(action_common.atest_list, host):
1251 """'atest host migrate' to migrate or rollback hosts."""
1252
1253 usage_action = 'migrate'
1254
1255 def __init__(self):
1256 super(host_migrate, self).__init__()
1257
1258 self.parser.add_option('--migration',
1259 dest='migration',
1260 help='Migrate the hosts to skylab.',
1261 action='store_true',
1262 default=False)
1263 self.parser.add_option('--rollback',
1264 dest='rollback',
1265 help='Rollback the hosts migrated to skylab.',
1266 action='store_true',
1267 default=False)
1268 self.parser.add_option('--model',
1269 help='Model of the hosts to migrate.',
1270 dest='model',
1271 default=None)
Xixuan Wu84621e52018-08-28 14:29:53 -07001272 self.parser.add_option('--board',
1273 help='Board of the hosts to migrate.',
1274 dest='board',
1275 default=None)
Ningning Xia9df3d152018-05-23 17:15:14 -07001276 self.parser.add_option('--pool',
1277 help=('Pool of the hosts to migrate. Must '
1278 'specify --model for the pool.'),
1279 dest='pool',
1280 default=None)
1281
1282 self.add_skylab_options(enforce_skylab=True)
1283
1284
1285 def parse(self):
1286 """Consume the specific options"""
1287 (options, leftover) = super(host_migrate, self).parse()
1288
1289 self.migration = options.migration
1290 self.rollback = options.rollback
1291 self.model = options.model
1292 self.pool = options.pool
Xixuan Wu84621e52018-08-28 14:29:53 -07001293 self.board = options.board
Ningning Xia9df3d152018-05-23 17:15:14 -07001294 self.host_ids = {}
1295
1296 if not (self.migration ^ self.rollback):
1297 self.invalid_syntax('--migration and --rollback are exclusive, '
1298 'and one of them must be enabled.')
1299
Xixuan Wu84621e52018-08-28 14:29:53 -07001300 if self.pool is not None and (self.model is None and
1301 self.board is None):
1302 self.invalid_syntax('Must provide --model or --board with --pool.')
Ningning Xia9df3d152018-05-23 17:15:14 -07001303
Xixuan Wu84621e52018-08-28 14:29:53 -07001304 if not self.hosts and not (self.model or self.board):
1305 self.invalid_syntax('Must provide hosts or --model or --board.')
Ningning Xia9df3d152018-05-23 17:15:14 -07001306
1307 return (options, leftover)
1308
1309
Ningning Xia56d68432018-06-06 17:28:20 -07001310 def _remove_invalid_hostnames(self, hostnames, log_failure=False):
1311 """Remove hostnames with MIGRATED_HOST_SUFFIX.
1312
1313 @param hostnames: A list of hostnames.
1314 @param log_failure: Bool indicating whether to log invalid hostsnames.
1315
1316 @return A list of valid hostnames.
1317 """
1318 invalid_hostnames = set()
Ningning Xia9df3d152018-05-23 17:15:14 -07001319 for hostname in hostnames:
1320 if hostname.endswith(MIGRATED_HOST_SUFFIX):
Ningning Xia56d68432018-06-06 17:28:20 -07001321 if log_failure:
1322 self.failure('Cannot migrate host with suffix "%s" %s.' %
1323 (MIGRATED_HOST_SUFFIX, hostname),
1324 item=hostname, what_failed='Failed to rename')
1325 invalid_hostnames.add(hostname)
1326
1327 hostnames = list(set(hostnames) - invalid_hostnames)
1328
1329 return hostnames
1330
1331
1332 def execute(self):
1333 """Execute 'atest host migrate'."""
1334 hostnames = self._remove_invalid_hostnames(self.hosts, log_failure=True)
Ningning Xia9df3d152018-05-23 17:15:14 -07001335
1336 filters = {}
1337 check_results = {}
1338 if hostnames:
1339 check_results['hostname__in'] = 'hostname'
1340 if self.migration:
1341 filters['hostname__in'] = hostnames
1342 else:
1343 # rollback
1344 hostnames_with_suffix = [
1345 _add_hostname_suffix(h, MIGRATED_HOST_SUFFIX)
1346 for h in hostnames]
1347 filters['hostname__in'] = hostnames_with_suffix
Ningning Xia56d68432018-06-06 17:28:20 -07001348 else:
1349 # TODO(nxia): add exclude_filter {'hostname__endswith':
1350 # MIGRATED_HOST_SUFFIX} for --migration
1351 if self.rollback:
1352 filters['hostname__endswith'] = MIGRATED_HOST_SUFFIX
Ningning Xia9df3d152018-05-23 17:15:14 -07001353
1354 labels = []
1355 if self.model:
1356 labels.append('model:%s' % self.model)
1357 if self.pool:
1358 labels.append('pool:%s' % self.pool)
Xixuan Wu84621e52018-08-28 14:29:53 -07001359 if self.board:
1360 labels.append('board:%s' % self.board)
Ningning Xia9df3d152018-05-23 17:15:14 -07001361
1362 if labels:
1363 if len(labels) == 1:
1364 filters['labels__name__in'] = labels
1365 check_results['labels__name__in'] = None
1366 else:
1367 filters['multiple_labels'] = labels
1368 check_results['multiple_labels'] = None
1369
1370 results = super(host_migrate, self).execute(
1371 op='get_hosts', filters=filters, check_results=check_results)
1372 hostnames = [h['hostname'] for h in results]
1373
Ningning Xia56d68432018-06-06 17:28:20 -07001374 if self.migration:
1375 hostnames = self._remove_invalid_hostnames(hostnames)
1376 else:
1377 # rollback
1378 hostnames = [_remove_hostname_suffix(h, MIGRATED_HOST_SUFFIX)
1379 for h in hostnames]
1380
Ningning Xia9df3d152018-05-23 17:15:14 -07001381 return self.execute_skylab_migration(hostnames)
1382
1383
Xixuan Wua8d93c82018-08-03 16:23:26 -07001384 def assign_duts_to_drone(self, infra, devices, environment):
Ningning Xia877817f2018-06-08 12:23:48 -07001385 """Assign uids of the devices to a random skylab drone.
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001386
1387 @param infra: An instance of lab_pb2.Infrastructure.
Xixuan Wua8d93c82018-08-03 16:23:26 -07001388 @param devices: A list of device_pb2.Device to be assigned to the drone.
1389 @param environment: 'staging' or 'prod'.
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001390 """
1391 skylab_drones = skylab_server.get_servers(
Xixuan Wua8d93c82018-08-03 16:23:26 -07001392 infra, environment, role='skylab_drone', status='primary')
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001393
1394 if len(skylab_drones) == 0:
1395 raise device.SkylabDeviceActionError(
1396 'No skylab drone is found in primary status and staging '
1397 'environment. Please confirm there is at least one valid skylab'
1398 ' drone added in skylab inventory.')
1399
Xixuan Wudfc2de22018-08-06 09:29:01 -07001400 for device in devices:
1401 # Randomly distribute each device to a skylab_drone.
1402 skylab_drone = random.choice(skylab_drones)
1403 skylab_server.add_dut_uids(skylab_drone, [device])
Ningning Xia877817f2018-06-08 12:23:48 -07001404
1405
1406 def remove_duts_from_drone(self, infra, devices):
1407 """Remove uids of the devices from their skylab drones.
1408
1409 @param infra: An instance of lab_pb2.Infrastructure.
1410 @devices: A list of device_pb2.Device to be remove from the drone.
1411 """
1412 skylab_drones = skylab_server.get_servers(
1413 infra, 'staging', role='skylab_drone', status='primary')
1414
1415 for skylab_drone in skylab_drones:
1416 skylab_server.remove_dut_uids(skylab_drone, devices)
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001417
1418
Ningning Xia9df3d152018-05-23 17:15:14 -07001419 def execute_skylab_migration(self, hostnames):
1420 """Execute migration in skylab_inventory.
1421
1422 @param hostnames: A list of hostnames to migrate.
1423 @return If there're hosts to migrate, return a list of the hostnames and
1424 a message instructing actions after the migration; else return
1425 None.
1426 """
1427 if not hostnames:
1428 return
1429
1430 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
1431 inventory_repo.initialize()
1432
1433 subdirs = ['skylab', 'prod', 'staging']
1434 data_dirs = skylab_data_dir, prod_data_dir, staging_data_dir = [
1435 inventory_repo.get_data_dir(data_subdir=d) for d in subdirs]
1436 skylab_lab, prod_lab, staging_lab = [
1437 text_manager.load_lab(d) for d in data_dirs]
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001438 infra = text_manager.load_infrastructure(skylab_data_dir)
Ningning Xia9df3d152018-05-23 17:15:14 -07001439
1440 label_map = None
1441 labels = []
Xixuan Wu46f8e202018-12-13 14:40:58 -08001442 if self.board:
1443 labels.append('board:%s' % self.board)
Ningning Xia9df3d152018-05-23 17:15:14 -07001444 if self.model:
Xixuan Wu46f8e202018-12-13 14:40:58 -08001445 labels.append('model:%s' % self.model)
Ningning Xia9df3d152018-05-23 17:15:14 -07001446 if self.pool:
1447 labels.append('critical_pool:%s' % self.pool)
1448 if labels:
1449 label_map = device.convert_to_label_map(labels)
1450
1451 if self.migration:
1452 prod_devices = device.move_devices(
1453 prod_lab, skylab_lab, 'duts', label_map=label_map,
1454 hostnames=hostnames)
1455 staging_devices = device.move_devices(
1456 staging_lab, skylab_lab, 'duts', label_map=label_map,
1457 hostnames=hostnames)
1458
1459 all_devices = prod_devices + staging_devices
1460 # Hostnames in afe_hosts tabel.
1461 device_hostnames = [str(d.common.hostname) for d in all_devices]
1462 message = (
1463 'Migration: move %s hosts into skylab_inventory.\n\n'
Ningning Xia423c7962018-06-07 15:27:18 -07001464 'Please run this command after the CL is submitted:\n'
Ningning Xia9df3d152018-05-23 17:15:14 -07001465 'atest host rename --for-migration %s' %
1466 (len(all_devices), ' '.join(device_hostnames)))
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001467
Xixuan Wua8d93c82018-08-03 16:23:26 -07001468 self.assign_duts_to_drone(infra, prod_devices, 'prod')
1469 self.assign_duts_to_drone(infra, staging_devices, 'staging')
Ningning Xia9df3d152018-05-23 17:15:14 -07001470 else:
1471 # rollback
1472 prod_devices = device.move_devices(
1473 skylab_lab, prod_lab, 'duts', environment='prod',
Ningning Xia56d68432018-06-06 17:28:20 -07001474 label_map=label_map, hostnames=hostnames)
Ningning Xia9df3d152018-05-23 17:15:14 -07001475 staging_devices = device.move_devices(
Ningning Xia877817f2018-06-08 12:23:48 -07001476 skylab_lab, staging_lab, 'duts', environment='staging',
Ningning Xia56d68432018-06-06 17:28:20 -07001477 label_map=label_map, hostnames=hostnames)
Ningning Xia9df3d152018-05-23 17:15:14 -07001478
1479 all_devices = prod_devices + staging_devices
1480 # Hostnames in afe_hosts tabel.
1481 device_hostnames = [_add_hostname_suffix(str(d.common.hostname),
1482 MIGRATED_HOST_SUFFIX)
1483 for d in all_devices]
1484 message = (
1485 'Rollback: remove %s hosts from skylab_inventory.\n\n'
Ningning Xia423c7962018-06-07 15:27:18 -07001486 'Please run this command after the CL is submitted:\n'
Ningning Xia9df3d152018-05-23 17:15:14 -07001487 'atest host rename --for-rollback %s' %
1488 (len(all_devices), ' '.join(device_hostnames)))
1489
Ningning Xia877817f2018-06-08 12:23:48 -07001490 self.remove_duts_from_drone(infra, all_devices)
1491
Ningning Xia9df3d152018-05-23 17:15:14 -07001492 if all_devices:
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001493 text_manager.dump_infrastructure(skylab_data_dir, infra)
1494
Ningning Xia9df3d152018-05-23 17:15:14 -07001495 if prod_devices:
1496 text_manager.dump_lab(prod_data_dir, prod_lab)
1497
1498 if staging_devices:
1499 text_manager.dump_lab(staging_data_dir, staging_lab)
1500
1501 text_manager.dump_lab(skylab_data_dir, skylab_lab)
1502
1503 self.change_number = inventory_repo.upload_change(
1504 message, draft=self.draft, dryrun=self.dryrun,
1505 submit=self.submit)
1506
1507 return all_devices, message
1508
1509
1510 def output(self, result):
1511 """Print output of 'atest host list'.
1512
1513 @param result: the result to be printed.
1514 """
1515 if result:
1516 devices, message = result
1517
1518 if devices:
1519 hostnames = [h.common.hostname for h in devices]
1520 if self.migration:
1521 print('Migrating hosts: %s' % ','.join(hostnames))
1522 else:
1523 # rollback
1524 print('Rolling back hosts: %s' % ','.join(hostnames))
1525
1526 if not self.dryrun:
1527 if not self.submit:
1528 print(skylab_utils.get_cl_message(self.change_number))
1529 else:
1530 # Print the instruction command for renaming hosts.
1531 print('%s' % message)
1532 else:
1533 print('No hosts were migrated.')
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001534
1535
Gregory Nisbet82350232019-09-10 11:12:47 -07001536
1537def _host_skylab_migrate_get_hostnames(obj, class_, model=None, pool=None, board=None):
1538 """
1539 @params : in 'model', 'pool', 'board'
1540
1541 """
1542 # TODO(gregorynisbet)
1543 # this just gets all the hostnames, it doesn't filter by
1544 # presence or absence of migrated-do-not-use.
1545 labels = []
1546 for key, value in ({'model': model, 'board': board, 'pool': pool}).items():
1547 if value:
1548 labels.append(key + ":" + value)
1549 filters = {}
1550 check_results = {}
1551 # Copy the filter and check_results initialization logic from
1552 # the 'execute' method of the class 'host_migrate'.
1553 if not labels:
1554 return []
1555 elif len(labels) == 1:
1556 filters['labels__name__in'] = labels
1557 check_results['labels__name__in'] = None
1558 elif len(labels) > 1:
1559 filters['multiple_labels'] = labels
1560 check_results['multiple_labels'] = None
1561 else:
1562 assert False
1563
1564 results = super(class_, obj).execute(
1565 op='get_hosts', filters=filters, check_results=check_results)
1566 return [result['hostname'] for result in results]
1567
1568
1569
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001570class host_skylab_migrate(action_common.atest_list, host):
1571 usage_action = 'skylab_migrate'
1572
1573 def __init__(self):
1574 super(host_skylab_migrate, self).__init__()
1575 self.parser.add_option('--dry-run',
1576 help='Dry run. Show only candidate hosts.',
1577 action='store_true',
1578 dest='dry_run')
1579 self.parser.add_option('--ratio',
1580 help='ratio of hosts to migrate as number from 0 to 1.',
1581 type=float,
1582 dest='ratio',
1583 default=1)
1584 self.parser.add_option('--bug-number',
1585 help='bug number for tracking purposes.',
1586 dest='bug_number',
1587 default=None)
1588 self.parser.add_option('--board',
1589 help='Board of the hosts to migrate',
1590 dest='board',
1591 default=None)
1592 self.parser.add_option('--model',
1593 help='Model of the hosts to migrate',
1594 dest='model',
1595 default=None)
1596 self.parser.add_option('--pool',
1597 help='Pool of the hosts to migrate',
1598 dest='pool',
1599 default=None)
Gregory Nisbet917ee332019-09-05 11:41:23 -07001600 # TODO(gregorynisbet): remove this flag and make quick-add-duts default.
1601 self.parser.add_option('-q',
1602 '--use-quick-add',
1603 help='whether to use "skylab quick-add-duts"',
1604 dest='use_quick_add',
1605 action='store_true')
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001606 def parse(self):
1607 (options, leftover) = super(host_skylab_migrate, self).parse()
1608 self.dry_run = options.dry_run
1609 self.ratio = options.ratio
1610 self.bug_number = options.bug_number
1611 self.model = options.model
1612 self.pool = options.pool
1613 self.board = options.board
1614 self._reason = "migration to skylab: %s" % self.bug_number
Gregory Nisbet917ee332019-09-05 11:41:23 -07001615 self.use_quick_add = options.use_quick_add
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001616 return (options, leftover)
1617
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001618 def _validate_one_hostname_source(self):
1619 """Validate that hostname source is explicit hostnames or valid query.
1620
1621 Hostnames must either be provided explicitly or be the result of a
1622 query defined by 'model', 'board', and 'pool'.
1623
1624 @returns : whether the hostnames come from exactly one valid source.
1625 """
1626 has_criteria = any([(self.model and self.board), self.board, self.pool])
1627 has_command_line_hosts = bool(self.hosts)
1628 if has_criteria != has_command_line_hosts:
1629 # all good, one data source
1630 return True
1631 if has_criteria and has_command_line_hosts:
1632 self.failure(
1633 '--model/host/board and explicit hostnames are alternatives. Provide exactly one.',
1634 item='cli',
1635 what_failed='user')
1636 return False
1637 self.failure(
1638 'no explicit hosts and no criteria provided.',
1639 item='cli',
1640 what_failed='user')
1641 return False
1642
1643
1644 def execute(self):
1645 if not self._validate_one_hostname_source():
1646 return None
1647 if self.hosts:
1648 hostnames = self.hosts
1649 else:
Gregory Nisbet82350232019-09-10 11:12:47 -07001650 hostnames = _host_skylab_migrate_get_hostnames(
1651 obj=self,
1652 class_=host_skylab_migrate,
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001653 model=self.model,
1654 board=self.board,
1655 pool=self.pool,
1656 )
1657 if self.dry_run:
1658 return hostnames
1659 if not hostnames:
1660 return {'error': 'no hosts to migrate'}
1661 res = skylab_migration.migrate(
1662 ratio=self.ratio,
1663 reason=self._reason,
1664 hostnames=hostnames,
1665 max_duration=10 * 60,
1666 interval_len=2,
1667 min_ready_intervals=10,
1668 immediately=True,
Gregory Nisbet917ee332019-09-05 11:41:23 -07001669 use_quick_add=self.use_quick_add,
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001670 )
1671 return res
1672
1673
1674 def output(self, result):
1675 if result is not None:
1676 print json.dumps(result, indent=4, sort_keys=True)
Gregory Nisbetac7f7f72019-09-10 16:25:20 -07001677
1678
Gregory Nisbet89eb9212019-09-13 11:21:30 -07001679class host_skylab_rollback(action_common.atest_list, host):
Gregory Nisbetac7f7f72019-09-10 16:25:20 -07001680 usage_action = "skylab_rollback"
1681
1682 def __init__(self):
Gregory Nisbet89eb9212019-09-13 11:21:30 -07001683 super(host_skylab_rollback, self).__init__()
Gregory Nisbetac7f7f72019-09-10 16:25:20 -07001684 self.parser.add_option('--bug-number',
1685 help='bug number for tracking purposes.',
1686 dest='bug_number',
1687 default=None)
1688
1689 def parse(self):
Gregory Nisbet89eb9212019-09-13 11:21:30 -07001690 (options, leftover) = super(host_skylab_rollback, self).parse()
Gregory Nisbetac7f7f72019-09-10 16:25:20 -07001691 self.bug_number = options.bug_number
1692 return (options, leftover)
1693
1694 def execute(self):
1695 if self.hosts:
1696 hostnames = self.hosts
1697 else:
1698 hostnames = _host_skylab_migrate_get_hostnames(
1699 obj=self,
1700 class_=host_skylab_migrate,
1701 model=self.model,
1702 board=self.board,
1703 pool=self.pool,
1704 )
1705 if not hostnames:
1706 return {'error': 'no hosts to migrate'}
1707 res = skylab_rollback.rollback(
1708 hosts=hostnames,
1709 bug=self.bug_number,
1710 dry_run=False,
1711 )
1712 return res