blob: 63a4e8c358349ab0bd2fdfd14da374ed914ef098 [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 Nisbetf74a7fe2019-09-25 10:45:12 -070027import sys
Gregory Nisbet93fa5b52019-07-19 14:56:35 -070028import time
mblighbe630eb2008-08-01 16:41:48 +000029
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -070030from autotest_lib.cli import action_common, rpc, topic_common, skylab_utils, skylab_migration
Gregory Nisbet93fa5b52019-07-19 14:56:35 -070031from autotest_lib.cli import fair_partition
Xixuan Wu06f7e232019-07-12 23:33:28 +000032from autotest_lib.client.bin import utils as bin_utils
Gregory Nisbet9c05d202019-09-25 22:29:06 -070033from autotest_lib.cli.skylab_json_utils import process_labels, print_textpb
Gregory Nisbetac7f7f72019-09-10 16:25:20 -070034from autotest_lib.cli import skylab_rollback
Gregory Nisbetf37da462019-09-11 10:19:03 -070035from autotest_lib.cli.skylab_json_utils import process_labels, validate_required_fields_for_skylab
Justin Giorgi16bba562016-06-22 10:42:13 -070036from autotest_lib.client.common_lib import error, host_protections
Justin Giorgi5208eaa2016-07-02 20:12:12 -070037from autotest_lib.server import frontend, hosts
Prathmesh Prabhud252d262017-05-31 11:46:34 -070038from autotest_lib.server.hosts import host_info
Gregory Nisbet93fa5b52019-07-19 14:56:35 -070039from autotest_lib.server.lib.status_history import HostJobHistory
40from autotest_lib.server.lib.status_history import UNUSED, WORKING
41from autotest_lib.server.lib.status_history import BROKEN, UNKNOWN
mblighbe630eb2008-08-01 16:41:48 +000042
43
Ningning Xiaea02ab12018-05-11 15:08:57 -070044try:
45 from skylab_inventory import text_manager
46 from skylab_inventory.lib import device
Ningning Xiaffb3c1f2018-06-07 18:42:32 -070047 from skylab_inventory.lib import server as skylab_server
Ningning Xiaea02ab12018-05-11 15:08:57 -070048except ImportError:
49 pass
50
51
Ningning Xiac46bdd12018-05-29 11:24:14 -070052MIGRATED_HOST_SUFFIX = '-migrated-do-not-use'
53
54
Gregory Nisbet99b61942019-07-08 09:43:06 -070055ID_AUTOGEN_MESSAGE = ("[IGNORED]. Do not edit (crbug.com/950553). ID is "
56 "auto-generated.")
57
58
59
mblighbe630eb2008-08-01 16:41:48 +000060class host(topic_common.atest):
61 """Host class
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -070062 atest host [create|delete|list|stat|mod|jobs|rename|migrate|skylab_migrate|statjson] <options>"""
63 usage_action = '[create|delete|list|stat|mod|jobs|rename|migrate|skylab_migrate|statjson]'
mblighbe630eb2008-08-01 16:41:48 +000064 topic = msg_topic = 'host'
65 msg_items = '<hosts>'
66
mblighaed47e82009-03-17 19:06:18 +000067 protections = host_protections.Protection.names
68
mblighbe630eb2008-08-01 16:41:48 +000069
70 def __init__(self):
71 """Add to the parser the options common to all the
72 host actions"""
73 super(host, self).__init__()
74
75 self.parser.add_option('-M', '--mlist',
76 help='File listing the machines',
77 type='string',
78 default=None,
79 metavar='MACHINE_FLIST')
80
mbligh9deeefa2009-05-01 23:11:08 +000081 self.topic_parse_info = topic_common.item_parse_info(
82 attribute_name='hosts',
83 filename_option='mlist',
84 use_leftover=True)
mblighbe630eb2008-08-01 16:41:48 +000085
86
87 def _parse_lock_options(self, options):
88 if options.lock and options.unlock:
89 self.invalid_syntax('Only specify one of '
90 '--lock and --unlock.')
91
Ningning Xiaef35cb52018-05-04 17:58:20 -070092 self.lock = options.lock
93 self.unlock = options.unlock
94 self.lock_reason = options.lock_reason
Ningning Xiaef35cb52018-05-04 17:58:20 -070095
mblighbe630eb2008-08-01 16:41:48 +000096 if options.lock:
97 self.data['locked'] = True
98 self.messages.append('Locked host')
99 elif options.unlock:
100 self.data['locked'] = False
Matthew Sartori68186332015-04-27 17:19:53 -0700101 self.data['lock_reason'] = ''
mblighbe630eb2008-08-01 16:41:48 +0000102 self.messages.append('Unlocked host')
103
Matthew Sartori68186332015-04-27 17:19:53 -0700104 if options.lock and options.lock_reason:
105 self.data['lock_reason'] = options.lock_reason
106
mblighbe630eb2008-08-01 16:41:48 +0000107
108 def _cleanup_labels(self, labels, platform=None):
109 """Removes the platform label from the overall labels"""
110 if platform:
111 return [label for label in labels
112 if label != platform]
113 else:
114 try:
115 return [label for label in labels
116 if not label['platform']]
117 except TypeError:
118 # This is a hack - the server will soon
119 # do this, so all this code should be removed.
120 return labels
121
122
123 def get_items(self):
124 return self.hosts
125
126
127class host_help(host):
128 """Just here to get the atest logic working.
129 Usage is set by its parent"""
130 pass
131
132
133class host_list(action_common.atest_list, host):
134 """atest host list [--mlist <file>|<hosts>] [--label <label>]
mbligh536a5242008-10-18 14:35:54 +0000135 [--status <status1,status2>] [--acl <ACL>] [--user <user>]"""
mblighbe630eb2008-08-01 16:41:48 +0000136
137 def __init__(self):
138 super(host_list, self).__init__()
139
140 self.parser.add_option('-b', '--label',
mbligh6444c6b2008-10-27 20:55:13 +0000141 default='',
142 help='Only list hosts with all these labels '
Ningning Xiaea02ab12018-05-11 15:08:57 -0700143 '(comma separated). When --skylab is provided, '
144 'a label must be in the format of '
145 'label-key:label-value (e.g., board:lumpy).')
mblighbe630eb2008-08-01 16:41:48 +0000146 self.parser.add_option('-s', '--status',
mbligh6444c6b2008-10-27 20:55:13 +0000147 default='',
148 help='Only list hosts with any of these '
149 'statuses (comma separated)')
mbligh536a5242008-10-18 14:35:54 +0000150 self.parser.add_option('-a', '--acl',
mbligh6444c6b2008-10-27 20:55:13 +0000151 default='',
Ningning Xiaea02ab12018-05-11 15:08:57 -0700152 help=('Only list hosts within this ACL. %s' %
153 skylab_utils.MSG_INVALID_IN_SKYLAB))
mbligh536a5242008-10-18 14:35:54 +0000154 self.parser.add_option('-u', '--user',
mbligh6444c6b2008-10-27 20:55:13 +0000155 default='',
Ningning Xiaea02ab12018-05-11 15:08:57 -0700156 help=('Only list hosts available to this user. '
157 '%s' % skylab_utils.MSG_INVALID_IN_SKYLAB))
mbligh70b9bf42008-11-18 15:00:46 +0000158 self.parser.add_option('-N', '--hostnames-only', help='Only return '
159 'hostnames for the machines queried.',
160 action='store_true')
mbligh91e0efd2009-02-26 01:02:16 +0000161 self.parser.add_option('--locked',
162 default=False,
163 help='Only list locked hosts',
164 action='store_true')
165 self.parser.add_option('--unlocked',
166 default=False,
167 help='Only list unlocked hosts',
168 action='store_true')
Ningning Xiaea02ab12018-05-11 15:08:57 -0700169 self.parser.add_option('--full-output',
170 default=False,
171 help=('Print out the full content of the hosts. '
172 'Only supported with --skylab.'),
173 action='store_true',
174 dest='full_output')
mbligh91e0efd2009-02-26 01:02:16 +0000175
Ningning Xiaea02ab12018-05-11 15:08:57 -0700176 self.add_skylab_options()
mblighbe630eb2008-08-01 16:41:48 +0000177
178
179 def parse(self):
180 """Consume the specific options"""
jamesrenc2863162010-07-12 21:20:51 +0000181 label_info = topic_common.item_parse_info(attribute_name='labels',
182 inline_option='label')
183
184 (options, leftover) = super(host_list, self).parse([label_info])
185
mblighbe630eb2008-08-01 16:41:48 +0000186 self.status = options.status
mbligh536a5242008-10-18 14:35:54 +0000187 self.acl = options.acl
188 self.user = options.user
mbligh70b9bf42008-11-18 15:00:46 +0000189 self.hostnames_only = options.hostnames_only
mbligh91e0efd2009-02-26 01:02:16 +0000190
191 if options.locked and options.unlocked:
192 self.invalid_syntax('--locked and --unlocked are '
193 'mutually exclusive')
Ningning Xiaea02ab12018-05-11 15:08:57 -0700194
mbligh91e0efd2009-02-26 01:02:16 +0000195 self.locked = options.locked
196 self.unlocked = options.unlocked
Ningning Xia64ced002018-05-16 17:36:20 -0700197 self.label_map = None
Ningning Xiaea02ab12018-05-11 15:08:57 -0700198
199 if self.skylab:
200 if options.user or options.acl or options.status:
201 self.invalid_syntax('--user, --acl or --status is not '
202 'supported with --skylab.')
203 self.full_output = options.full_output
204 if self.full_output and self.hostnames_only:
205 self.invalid_syntax('--full-output is conflicted with '
206 '--hostnames-only.')
Ningning Xia64ced002018-05-16 17:36:20 -0700207
208 if self.labels:
209 self.label_map = device.convert_to_label_map(self.labels)
Ningning Xiaea02ab12018-05-11 15:08:57 -0700210 else:
211 if options.full_output:
212 self.invalid_syntax('--full_output is only supported with '
213 '--skylab.')
214
mblighbe630eb2008-08-01 16:41:48 +0000215 return (options, leftover)
216
217
Ningning Xiaea02ab12018-05-11 15:08:57 -0700218 def execute_skylab(self):
219 """Execute 'atest host list' with --skylab."""
220 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
221 inventory_repo.initialize()
222 lab = text_manager.load_lab(inventory_repo.get_data_dir())
223
224 # TODO(nxia): support filtering on run-time labels and status.
225 return device.get_devices(
226 lab,
227 'duts',
228 self.environment,
Ningning Xia64ced002018-05-16 17:36:20 -0700229 label_map=self.label_map,
Ningning Xiaea02ab12018-05-11 15:08:57 -0700230 hostnames=self.hosts,
231 locked=self.locked,
232 unlocked=self.unlocked)
233
234
mblighbe630eb2008-08-01 16:41:48 +0000235 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800236 """Execute 'atest host list'."""
Ningning Xiaea02ab12018-05-11 15:08:57 -0700237 if self.skylab:
238 return self.execute_skylab()
239
mblighbe630eb2008-08-01 16:41:48 +0000240 filters = {}
241 check_results = {}
242 if self.hosts:
243 filters['hostname__in'] = self.hosts
244 check_results['hostname__in'] = 'hostname'
mbligh6444c6b2008-10-27 20:55:13 +0000245
246 if self.labels:
jamesrenc2863162010-07-12 21:20:51 +0000247 if len(self.labels) == 1:
mblighf703fb42009-01-30 00:35:05 +0000248 # This is needed for labels with wildcards (x86*)
jamesrenc2863162010-07-12 21:20:51 +0000249 filters['labels__name__in'] = self.labels
mblighf703fb42009-01-30 00:35:05 +0000250 check_results['labels__name__in'] = None
251 else:
jamesrenc2863162010-07-12 21:20:51 +0000252 filters['multiple_labels'] = self.labels
mblighf703fb42009-01-30 00:35:05 +0000253 check_results['multiple_labels'] = None
mbligh6444c6b2008-10-27 20:55:13 +0000254
mblighbe630eb2008-08-01 16:41:48 +0000255 if self.status:
mbligh6444c6b2008-10-27 20:55:13 +0000256 statuses = self.status.split(',')
257 statuses = [status.strip() for status in statuses
258 if status.strip()]
259
260 filters['status__in'] = statuses
mblighcd8eb972008-08-25 19:20:39 +0000261 check_results['status__in'] = None
mbligh6444c6b2008-10-27 20:55:13 +0000262
mbligh536a5242008-10-18 14:35:54 +0000263 if self.acl:
showardd9ac4452009-02-07 02:04:37 +0000264 filters['aclgroup__name'] = self.acl
265 check_results['aclgroup__name'] = None
mbligh536a5242008-10-18 14:35:54 +0000266 if self.user:
showardd9ac4452009-02-07 02:04:37 +0000267 filters['aclgroup__users__login'] = self.user
268 check_results['aclgroup__users__login'] = None
mbligh91e0efd2009-02-26 01:02:16 +0000269
270 if self.locked or self.unlocked:
271 filters['locked'] = self.locked
272 check_results['locked'] = None
273
mblighbe630eb2008-08-01 16:41:48 +0000274 return super(host_list, self).execute(op='get_hosts',
275 filters=filters,
276 check_results=check_results)
277
278
279 def output(self, results):
xixuand6011f12016-12-08 15:01:58 -0800280 """Print output of 'atest host list'.
281
282 @param results: the results to be printed.
283 """
Ningning Xiaea02ab12018-05-11 15:08:57 -0700284 if results and not self.skylab:
mblighbe630eb2008-08-01 16:41:48 +0000285 # Remove the platform from the labels.
286 for result in results:
287 result['labels'] = self._cleanup_labels(result['labels'],
288 result['platform'])
Ningning Xiaea02ab12018-05-11 15:08:57 -0700289 if self.skylab and self.full_output:
290 print results
291 return
292
293 if self.skylab:
294 results = device.convert_to_autotest_hosts(results)
295
mbligh70b9bf42008-11-18 15:00:46 +0000296 if self.hostnames_only:
mblighdf75f8b2008-11-18 19:07:42 +0000297 self.print_list(results, key='hostname')
mbligh70b9bf42008-11-18 15:00:46 +0000298 else:
Ningning Xiaea02ab12018-05-11 15:08:57 -0700299 keys = ['hostname', 'status', 'shard', 'locked', 'lock_reason',
300 'locked_by', 'platform', 'labels']
Prashanth Balasubramaniana5048562014-12-12 10:14:11 -0800301 super(host_list, self).output(results, keys=keys)
mblighbe630eb2008-08-01 16:41:48 +0000302
303
304class host_stat(host):
305 """atest host stat --mlist <file>|<hosts>"""
306 usage_action = 'stat'
307
308 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800309 """Execute 'atest host stat'."""
mblighbe630eb2008-08-01 16:41:48 +0000310 results = []
311 # Convert wildcards into real host stats.
312 existing_hosts = []
313 for host in self.hosts:
314 if host.endswith('*'):
315 stats = self.execute_rpc('get_hosts',
316 hostname__startswith=host.rstrip('*'))
317 if len(stats) == 0:
318 self.failure('No hosts matching %s' % host, item=host,
319 what_failed='Failed to stat')
320 continue
321 else:
322 stats = self.execute_rpc('get_hosts', hostname=host)
323 if len(stats) == 0:
324 self.failure('Unknown host %s' % host, item=host,
325 what_failed='Failed to stat')
326 continue
327 existing_hosts.extend(stats)
328
329 for stat in existing_hosts:
330 host = stat['hostname']
331 # The host exists, these should succeed
332 acls = self.execute_rpc('get_acl_groups', hosts__hostname=host)
333
334 labels = self.execute_rpc('get_labels', host__hostname=host)
Simran Basi0739d682015-02-25 16:22:56 -0800335 results.append([[stat], acls, labels, stat['attributes']])
mblighbe630eb2008-08-01 16:41:48 +0000336 return results
337
338
339 def output(self, results):
xixuand6011f12016-12-08 15:01:58 -0800340 """Print output of 'atest host stat'.
341
342 @param results: the results to be printed.
343 """
Simran Basi0739d682015-02-25 16:22:56 -0800344 for stats, acls, labels, attributes in results:
mblighbe630eb2008-08-01 16:41:48 +0000345 print '-'*5
346 self.print_fields(stats,
Aviv Kesheta40d1272017-11-16 00:56:35 -0800347 keys=['hostname', 'id', 'platform',
mblighe163b032008-10-18 14:30:27 +0000348 'status', 'locked', 'locked_by',
Matthew Sartori68186332015-04-27 17:19:53 -0700349 'lock_time', 'lock_reason', 'protection',])
mblighbe630eb2008-08-01 16:41:48 +0000350 self.print_by_ids(acls, 'ACLs', line_before=True)
351 labels = self._cleanup_labels(labels)
352 self.print_by_ids(labels, 'Labels', line_before=True)
Simran Basi0739d682015-02-25 16:22:56 -0800353 self.print_dict(attributes, 'Host Attributes', line_before=True)
mblighbe630eb2008-08-01 16:41:48 +0000354
355
Gregory Nisbet93fa5b52019-07-19 14:56:35 -0700356class host_get_migration_plan(host_stat):
357 """atest host get_migration_plan --mlist <file>|<hosts>"""
358 usage_action = "get_migration_plan"
359
360 def __init__(self):
361 super(host_get_migration_plan, self).__init__()
362 self.parser.add_option("--ratio", default=0.5, type=float, dest="ratio")
363 self.add_skylab_options()
364
365 def parse(self):
366 (options, leftover) = super(host_get_migration_plan, self).parse()
367 self.ratio = options.ratio
368 return (options, leftover)
369
370 def execute(self):
371 afe = frontend.AFE()
372 results = super(host_get_migration_plan, self).execute()
373 working = []
374 non_working = []
375 for stats, _, _, _ in results:
376 assert len(stats) == 1
377 stats = stats[0]
378 hostname = stats["hostname"]
379 now = time.time()
380 history = HostJobHistory.get_host_history(
381 afe=afe,
382 hostname=hostname,
383 start_time=now,
384 end_time=now - 24 * 60 * 60,
385 )
386 dut_status, _ = history.last_diagnosis()
387 if dut_status in [UNUSED, WORKING]:
388 working.append(hostname)
389 elif dut_status == BROKEN:
390 non_working.append(hostname)
391 elif dut_status == UNKNOWN:
392 # if it's unknown, randomly assign it to working or
393 # nonworking, since we don't know.
394 # The two choices aren't actually equiprobable, but it
395 # should be fine.
396 random.choice([working, non_working]).append(hostname)
397 else:
398 raise ValueError("unknown status %s" % dut_status)
399 working_transfer, working_retain = fair_partition.partition(working, self.ratio)
400 non_working_transfer, non_working_retain = \
401 fair_partition.partition(non_working, self.ratio)
402 return {
403 "transfer": working_transfer + non_working_transfer,
404 "retain": working_retain + non_working_retain,
405 }
406
407 def output(self, results):
408 print json.dumps(results, indent=4, sort_keys=True)
409
Gregory Nisbet99b61942019-07-08 09:43:06 -0700410
411class host_statjson(host_stat):
412 """atest host statjson --mlist <file>|<hosts>
413
414 exposes the same information that 'atest host stat' does, but in the json
415 format that 'skylab add-dut' expects
416 """
417
418 usage_action = "statjson"
419
Gregory Nisbetf37da462019-09-11 10:19:03 -0700420 def __init__(self):
421 super(host_statjson, self).__init__()
422 self.parser.add_option('--verify',
423 default=False,
424 help='Verify that required fields are provided',
425 action='store_true',
426 dest='verify')
Gregory Nisbet9c05d202019-09-25 22:29:06 -0700427 self.parse.add_option('--textpb',
428 default=False,
429 help='Print in best effort textpb format',
430 action='store_true',
431 dest='textpb')
Gregory Nisbetf37da462019-09-11 10:19:03 -0700432
433 def parse(self):
434 (options, leftover) = super(host_statjson, self).parse()
435 self.verify = options.verify
Gregory Nisbet9c05d202019-09-25 22:29:06 -0700436 self.textpb = options.textpb
Gregory Nisbetf37da462019-09-11 10:19:03 -0700437 return (options, leftover)
Gregory Nisbet99b61942019-07-08 09:43:06 -0700438
439 def output(self, results):
Gregory Nisbetf37da462019-09-11 10:19:03 -0700440 """Print output of 'atest host statjson <...>'"""
Gregory Nisbet99b61942019-07-08 09:43:06 -0700441 for row in results:
442 stats, acls, labels, attributes = row
443 # TODO(gregorynisbet): under what circumstances is stats
444 # not a list of length 1?
445 assert len(stats) == 1
446 stats_map = stats[0]
Gregory Nisbetce77cef2019-07-29 12:27:21 -0700447
448 # Stripping the MIGRATED_HOST_SUFFIX makes it possible to
449 # migrate a DUT from autotest to skylab even after its hostname
450 # has been changed.
451 # This enables the steps (renaming the host,
452 # copying the inventory information to skylab) to be doable in
453 # either order.
Gregory Nisbet48f1eea2019-08-05 16:18:56 -0700454 hostname = _remove_hostname_suffix_if_present(
455 stats_map["hostname"],
456 MIGRATED_HOST_SUFFIX
457 )
458
Gregory Nisbet932f5a92019-09-05 14:02:31 -0700459 # TODO(gregorynisbet): clean up servo information
460 if "servo_host" not in attributes:
461 attributes["servo_host"] = "dummy_host"
462 if "servo_port" not in attributes:
463 attributes["servo_port"] = "dummy_port"
464
Gregory Nisbet99b61942019-07-08 09:43:06 -0700465 labels = self._cleanup_labels(labels)
466 attrs = [{"key": k, "value": v} for k, v in attributes.iteritems()]
467 out_labels = process_labels(labels, platform=stats_map["platform"])
468 skylab_json = {
469 "common": {
470 "attributes": attrs,
Gregory Nisbet23a06b92019-07-11 16:44:43 -0700471 "environment": "ENVIRONMENT_PROD",
Gregory Nisbetce77cef2019-07-29 12:27:21 -0700472 "hostname": hostname,
Gregory Nisbet99b61942019-07-08 09:43:06 -0700473 "id": ID_AUTOGEN_MESSAGE,
474 "labels": out_labels,
Gregory Nisbetcc0941f2019-08-29 13:56:05 -0700475 "serialNumber": attributes.get("serial_number", None),
Gregory Nisbet99b61942019-07-08 09:43:06 -0700476 }
477 }
Gregory Nisbetf37da462019-09-11 10:19:03 -0700478 # if the validate flag is provided, check that a given json blob
479 # has all the required fields for skylab.
480 if self.verify:
481 validate_required_fields_for_skylab(skylab_json)
Gregory Nisbet9c05d202019-09-25 22:29:06 -0700482 if self.textpb:
483 print_textpb(skylab_json)
484 else:
485 print json.dumps(skylab_json, indent=4, sort_keys=True)
Gregory Nisbet99b61942019-07-08 09:43:06 -0700486
487
mblighbe630eb2008-08-01 16:41:48 +0000488class host_jobs(host):
mbligh1494eca2008-08-13 21:24:22 +0000489 """atest host jobs [--max-query] --mlist <file>|<hosts>"""
mblighbe630eb2008-08-01 16:41:48 +0000490 usage_action = 'jobs'
491
mbligh6996fe82008-08-13 00:32:27 +0000492 def __init__(self):
493 super(host_jobs, self).__init__()
494 self.parser.add_option('-q', '--max-query',
495 help='Limits the number of results '
496 '(20 by default)',
497 type='int', default=20)
498
499
500 def parse(self):
501 """Consume the specific options"""
mbligh9deeefa2009-05-01 23:11:08 +0000502 (options, leftover) = super(host_jobs, self).parse()
mbligh6996fe82008-08-13 00:32:27 +0000503 self.max_queries = options.max_query
504 return (options, leftover)
505
506
mblighbe630eb2008-08-01 16:41:48 +0000507 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800508 """Execute 'atest host jobs'."""
mblighbe630eb2008-08-01 16:41:48 +0000509 results = []
510 real_hosts = []
511 for host in self.hosts:
512 if host.endswith('*'):
513 stats = self.execute_rpc('get_hosts',
514 hostname__startswith=host.rstrip('*'))
515 if len(stats) == 0:
516 self.failure('No host matching %s' % host, item=host,
517 what_failed='Failed to stat')
518 [real_hosts.append(stat['hostname']) for stat in stats]
519 else:
520 real_hosts.append(host)
521
522 for host in real_hosts:
523 queue_entries = self.execute_rpc('get_host_queue_entries',
mbligh6996fe82008-08-13 00:32:27 +0000524 host__hostname=host,
mbligh1494eca2008-08-13 21:24:22 +0000525 query_limit=self.max_queries,
showard6958c722009-09-23 20:03:16 +0000526 sort_by=['-job__id'])
mblighbe630eb2008-08-01 16:41:48 +0000527 jobs = []
528 for entry in queue_entries:
529 job = {'job_id': entry['job']['id'],
530 'job_owner': entry['job']['owner'],
531 'job_name': entry['job']['name'],
532 'status': entry['status']}
533 jobs.append(job)
534 results.append((host, jobs))
535 return results
536
537
538 def output(self, results):
xixuand6011f12016-12-08 15:01:58 -0800539 """Print output of 'atest host jobs'.
540
541 @param results: the results to be printed.
542 """
mblighbe630eb2008-08-01 16:41:48 +0000543 for host, jobs in results:
544 print '-'*5
545 print 'Hostname: %s' % host
546 self.print_table(jobs, keys_header=['job_id',
showardbe0d8692009-08-20 23:42:44 +0000547 'job_owner',
548 'job_name',
549 'status'])
mblighbe630eb2008-08-01 16:41:48 +0000550
Justin Giorgi16bba562016-06-22 10:42:13 -0700551class BaseHostModCreate(host):
xixuand6011f12016-12-08 15:01:58 -0800552 """The base class for host_mod and host_create"""
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700553 # Matches one attribute=value pair
554 attribute_regex = r'(?P<attribute>\w+)=(?P<value>.+)?'
mblighbe630eb2008-08-01 16:41:48 +0000555
556 def __init__(self):
Justin Giorgi16bba562016-06-22 10:42:13 -0700557 """Add the options shared between host mod and host create actions."""
mblighbe630eb2008-08-01 16:41:48 +0000558 self.messages = []
Justin Giorgi16bba562016-06-22 10:42:13 -0700559 self.host_ids = {}
560 super(BaseHostModCreate, self).__init__()
mblighbe630eb2008-08-01 16:41:48 +0000561 self.parser.add_option('-l', '--lock',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700562 help='Lock hosts.',
mblighbe630eb2008-08-01 16:41:48 +0000563 action='store_true')
Matthew Sartori68186332015-04-27 17:19:53 -0700564 self.parser.add_option('-r', '--lock_reason',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700565 help='Reason for locking hosts.',
Matthew Sartori68186332015-04-27 17:19:53 -0700566 default='')
Ningning Xiaef35cb52018-05-04 17:58:20 -0700567 self.parser.add_option('-u', '--unlock',
568 help='Unlock hosts.',
569 action='store_true')
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700570
mblighe163b032008-10-18 14:30:27 +0000571 self.parser.add_option('-p', '--protection', type='choice',
mblighaed47e82009-03-17 19:06:18 +0000572 help=('Set the protection level on a host. '
Ningning Xiaef35cb52018-05-04 17:58:20 -0700573 'Must be one of: %s. %s' %
574 (', '.join('"%s"' % p
575 for p in self.protections),
576 skylab_utils.MSG_INVALID_IN_SKYLAB)),
mblighaed47e82009-03-17 19:06:18 +0000577 choices=self.protections)
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700578 self._attributes = []
579 self.parser.add_option('--attribute', '-i',
580 help=('Host attribute to add or change. Format '
581 'is <attribute>=<value>. Multiple '
582 'attributes can be set by passing the '
583 'argument multiple times. Attributes can '
584 'be unset by providing an empty value.'),
585 action='append')
mblighbe630eb2008-08-01 16:41:48 +0000586 self.parser.add_option('-b', '--labels',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700587 help=('Comma separated list of labels. '
588 'When --skylab is provided, a label must '
589 'be in the format of label-key:label-value'
590 ' (e.g., board:lumpy).'))
mblighbe630eb2008-08-01 16:41:48 +0000591 self.parser.add_option('-B', '--blist',
592 help='File listing the labels',
593 type='string',
594 metavar='LABEL_FLIST')
595 self.parser.add_option('-a', '--acls',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700596 help=('Comma separated list of ACLs. %s' %
597 skylab_utils.MSG_INVALID_IN_SKYLAB))
mblighbe630eb2008-08-01 16:41:48 +0000598 self.parser.add_option('-A', '--alist',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700599 help=('File listing the acls. %s' %
600 skylab_utils.MSG_INVALID_IN_SKYLAB),
mblighbe630eb2008-08-01 16:41:48 +0000601 type='string',
602 metavar='ACL_FLIST')
Justin Giorgi16bba562016-06-22 10:42:13 -0700603 self.parser.add_option('-t', '--platform',
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700604 help=('Sets the platform label. %s Please set '
605 'platform in labels (e.g., -b '
606 'platform:platform_name) with --skylab.' %
607 skylab_utils.MSG_INVALID_IN_SKYLAB))
mblighbe630eb2008-08-01 16:41:48 +0000608
609
610 def parse(self):
Justin Giorgi16bba562016-06-22 10:42:13 -0700611 """Consume the options common to host create and host mod.
612 """
mbligh9deeefa2009-05-01 23:11:08 +0000613 label_info = topic_common.item_parse_info(attribute_name='labels',
614 inline_option='labels',
615 filename_option='blist')
616 acl_info = topic_common.item_parse_info(attribute_name='acls',
617 inline_option='acls',
618 filename_option='alist')
619
Justin Giorgi16bba562016-06-22 10:42:13 -0700620 (options, leftover) = super(BaseHostModCreate, self).parse([label_info,
mbligh9deeefa2009-05-01 23:11:08 +0000621 acl_info],
622 req_items='hosts')
mblighbe630eb2008-08-01 16:41:48 +0000623
624 self._parse_lock_options(options)
Justin Giorgi16bba562016-06-22 10:42:13 -0700625
Ningning Xia64ced002018-05-16 17:36:20 -0700626 self.label_map = None
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700627 if self.allow_skylab and self.skylab:
Ningning Xiaef35cb52018-05-04 17:58:20 -0700628 # TODO(nxia): drop these flags when all hosts are migrated to skylab
Ningning Xia64ced002018-05-16 17:36:20 -0700629 if (options.protection or options.acls or options.alist or
630 options.platform):
631 self.invalid_syntax(
632 '--protection, --acls, --alist or --platform is not '
633 'supported with --skylab.')
634
635 if self.labels:
636 self.label_map = device.convert_to_label_map(self.labels)
Ningning Xiaef35cb52018-05-04 17:58:20 -0700637
mblighaed47e82009-03-17 19:06:18 +0000638 if options.protection:
639 self.data['protection'] = options.protection
Justin Giorgi16bba562016-06-22 10:42:13 -0700640 self.messages.append('Protection set to "%s"' % options.protection)
641
642 self.attributes = {}
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700643 if options.attribute:
644 for pair in options.attribute:
645 m = re.match(self.attribute_regex, pair)
646 if not m:
647 raise topic_common.CliError('Attribute must be in key=value '
648 'syntax.')
649 elif m.group('attribute') in self.attributes:
xixuand6011f12016-12-08 15:01:58 -0800650 raise topic_common.CliError(
651 'Multiple values provided for attribute '
652 '%s.' % m.group('attribute'))
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700653 self.attributes[m.group('attribute')] = m.group('value')
Justin Giorgi16bba562016-06-22 10:42:13 -0700654
655 self.platform = options.platform
mblighbe630eb2008-08-01 16:41:48 +0000656 return (options, leftover)
657
658
Justin Giorgi16bba562016-06-22 10:42:13 -0700659 def _set_acls(self, hosts, acls):
660 """Add hosts to acls (and remove from all other acls).
661
662 @param hosts: list of hostnames
663 @param acls: list of acl names
664 """
665 # Remove from all ACLs except 'Everyone' and ACLs in list
666 # Skip hosts that don't exist
667 for host in hosts:
668 if host not in self.host_ids:
669 continue
670 host_id = self.host_ids[host]
671 for a in self.execute_rpc('get_acl_groups', hosts=host_id):
672 if a['name'] not in self.acls and a['id'] != 1:
673 self.execute_rpc('acl_group_remove_hosts', id=a['id'],
674 hosts=self.hosts)
675
676 # Add hosts to the ACLs
677 self.check_and_create_items('get_acl_groups', 'add_acl_group',
678 self.acls)
679 for a in acls:
680 self.execute_rpc('acl_group_add_hosts', id=a, hosts=hosts)
681
682
683 def _remove_labels(self, host, condition):
684 """Remove all labels from host that meet condition(label).
685
686 @param host: hostname
687 @param condition: callable that returns bool when given a label
688 """
689 if host in self.host_ids:
690 host_id = self.host_ids[host]
691 labels_to_remove = []
692 for l in self.execute_rpc('get_labels', host=host_id):
693 if condition(l):
694 labels_to_remove.append(l['id'])
695 if labels_to_remove:
696 self.execute_rpc('host_remove_labels', id=host_id,
697 labels=labels_to_remove)
698
699
700 def _set_labels(self, host, labels):
701 """Apply labels to host (and remove all other labels).
702
703 @param host: hostname
704 @param labels: list of label names
705 """
706 condition = lambda l: l['name'] not in labels and not l['platform']
707 self._remove_labels(host, condition)
708 self.check_and_create_items('get_labels', 'add_label', labels)
709 self.execute_rpc('host_add_labels', id=host, labels=labels)
710
711
712 def _set_platform_label(self, host, platform_label):
713 """Apply the platform label to host (and remove existing).
714
715 @param host: hostname
716 @param platform_label: platform label's name
717 """
718 self._remove_labels(host, lambda l: l['platform'])
719 self.check_and_create_items('get_labels', 'add_label', [platform_label],
720 platform=True)
721 self.execute_rpc('host_add_labels', id=host, labels=[platform_label])
722
723
724 def _set_attributes(self, host, attributes):
725 """Set attributes on host.
726
727 @param host: hostname
728 @param attributes: attribute dictionary
729 """
730 for attr, value in self.attributes.iteritems():
731 self.execute_rpc('set_host_attribute', attribute=attr,
732 value=value, hostname=host)
733
734
735class host_mod(BaseHostModCreate):
736 """atest host mod [--lock|--unlock --force_modify_locking
737 --platform <arch>
738 --labels <labels>|--blist <label_file>
739 --acls <acls>|--alist <acl_file>
740 --protection <protection_type>
741 --attributes <attr>=<value>;<attr>=<value>
742 --mlist <mach_file>] <hosts>"""
743 usage_action = 'mod'
Justin Giorgi16bba562016-06-22 10:42:13 -0700744
745 def __init__(self):
746 """Add the options specific to the mod action"""
747 super(host_mod, self).__init__()
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700748 self.parser.add_option('--unlock-lock-id',
749 help=('Unlock the lock with the lock-id. %s' %
750 skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
751 default=None)
Justin Giorgi16bba562016-06-22 10:42:13 -0700752 self.parser.add_option('-f', '--force_modify_locking',
753 help='Forcefully lock\unlock a host',
754 action='store_true')
755 self.parser.add_option('--remove_acls',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700756 help=('Remove all active acls. %s' %
757 skylab_utils.MSG_INVALID_IN_SKYLAB),
Justin Giorgi16bba562016-06-22 10:42:13 -0700758 action='store_true')
759 self.parser.add_option('--remove_labels',
760 help='Remove all labels.',
761 action='store_true')
762
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700763 self.add_skylab_options()
Ningning Xia1dd82ee2018-06-08 11:41:03 -0700764 self.parser.add_option('--new-env',
765 dest='new_env',
766 choices=['staging', 'prod'],
767 help=('The new environment ("staging" or '
768 '"prod") of the hosts. %s' %
769 skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
770 default=None)
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700771
772
773 def _parse_unlock_options(self, options):
774 """Parse unlock related options."""
775 if self.skylab and options.unlock and options.unlock_lock_id is None:
776 self.invalid_syntax('Must provide --unlock-lock-id with "--skylab '
777 '--unlock".')
778
779 if (not (self.skylab and options.unlock) and
780 options.unlock_lock_id is not None):
781 self.invalid_syntax('--unlock-lock-id is only valid with '
782 '"--skylab --unlock".')
783
784 self.unlock_lock_id = options.unlock_lock_id
785
Justin Giorgi16bba562016-06-22 10:42:13 -0700786
787 def parse(self):
788 """Consume the specific options"""
789 (options, leftover) = super(host_mod, self).parse()
790
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700791 self._parse_unlock_options(options)
792
Justin Giorgi16bba562016-06-22 10:42:13 -0700793 if options.force_modify_locking:
794 self.data['force_modify_locking'] = True
795
Ningning Xiaef35cb52018-05-04 17:58:20 -0700796 if self.skylab and options.remove_acls:
797 # TODO(nxia): drop the flag when all hosts are migrated to skylab
798 self.invalid_syntax('--remove_acls is not supported with --skylab.')
799
Justin Giorgi16bba562016-06-22 10:42:13 -0700800 self.remove_acls = options.remove_acls
801 self.remove_labels = options.remove_labels
Ningning Xia1dd82ee2018-06-08 11:41:03 -0700802 self.new_env = options.new_env
Justin Giorgi16bba562016-06-22 10:42:13 -0700803
804 return (options, leftover)
805
806
Ningning Xiaef35cb52018-05-04 17:58:20 -0700807 def execute_skylab(self):
808 """Execute atest host mod with --skylab.
809
810 @return A list of hostnames which have been successfully modified.
811 """
812 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
813 inventory_repo.initialize()
814 data_dir = inventory_repo.get_data_dir()
815 lab = text_manager.load_lab(data_dir)
816
817 locked_by = None
818 if self.lock:
819 locked_by = inventory_repo.git_repo.config('user.email')
820
821 successes = []
822 for hostname in self.hosts:
823 try:
824 device.modify(
825 lab,
826 'duts',
827 hostname,
828 self.environment,
829 lock=self.lock,
830 locked_by=locked_by,
831 lock_reason = self.lock_reason,
832 unlock=self.unlock,
833 unlock_lock_id=self.unlock_lock_id,
834 attributes=self.attributes,
835 remove_labels=self.remove_labels,
Ningning Xia1dd82ee2018-06-08 11:41:03 -0700836 label_map=self.label_map,
837 new_env=self.new_env)
Ningning Xiaef35cb52018-05-04 17:58:20 -0700838 successes.append(hostname)
839 except device.SkylabDeviceActionError as e:
840 print('Cannot modify host %s: %s' % (hostname, e))
841
842 if successes:
843 text_manager.dump_lab(data_dir, lab)
844
845 status = inventory_repo.git_repo.status()
846 if not status:
847 print('Nothing is changed for hosts %s.' % successes)
848 return []
849
850 message = skylab_utils.construct_commit_message(
851 'Modify %d hosts.\n\n%s' % (len(successes), successes))
852 self.change_number = inventory_repo.upload_change(
853 message, draft=self.draft, dryrun=self.dryrun,
854 submit=self.submit)
855
856 return successes
857
858
Justin Giorgi16bba562016-06-22 10:42:13 -0700859 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800860 """Execute 'atest host mod'."""
Ningning Xiaef35cb52018-05-04 17:58:20 -0700861 if self.skylab:
862 return self.execute_skylab()
863
Justin Giorgi16bba562016-06-22 10:42:13 -0700864 successes = []
865 for host in self.execute_rpc('get_hosts', hostname__in=self.hosts):
866 self.host_ids[host['hostname']] = host['id']
867 for host in self.hosts:
868 if host not in self.host_ids:
869 self.failure('Cannot modify non-existant host %s.' % host)
870 continue
871 host_id = self.host_ids[host]
872
873 try:
874 if self.data:
875 self.execute_rpc('modify_host', item=host,
876 id=host, **self.data)
877
878 if self.attributes:
879 self._set_attributes(host, self.attributes)
880
881 if self.labels or self.remove_labels:
882 self._set_labels(host, self.labels)
883
884 if self.platform:
885 self._set_platform_label(host, self.platform)
886
887 # TODO: Make the AFE return True or False,
888 # especially for lock
889 successes.append(host)
890 except topic_common.CliError, full_error:
891 # Already logged by execute_rpc()
892 pass
893
894 if self.acls or self.remove_acls:
895 self._set_acls(self.hosts, self.acls)
896
897 return successes
898
899
900 def output(self, hosts):
xixuand6011f12016-12-08 15:01:58 -0800901 """Print output of 'atest host mod'.
902
903 @param hosts: the host list to be printed.
904 """
Justin Giorgi16bba562016-06-22 10:42:13 -0700905 for msg in self.messages:
906 self.print_wrapped(msg, hosts)
907
Ningning Xiaef35cb52018-05-04 17:58:20 -0700908 if hosts and self.skylab:
Ningning Xiac8b430d2018-05-17 15:10:25 -0700909 print('Modified hosts: %s.' % ', '.join(hosts))
Ningning Xiaef35cb52018-05-04 17:58:20 -0700910 if self.skylab and not self.dryrun and not self.submit:
Ningning Xiac8b430d2018-05-17 15:10:25 -0700911 print(skylab_utils.get_cl_message(self.change_number))
Ningning Xiaef35cb52018-05-04 17:58:20 -0700912
Justin Giorgi16bba562016-06-22 10:42:13 -0700913
914class HostInfo(object):
915 """Store host information so we don't have to keep looking it up."""
916 def __init__(self, hostname, platform, labels):
917 self.hostname = hostname
918 self.platform = platform
919 self.labels = labels
920
921
922class host_create(BaseHostModCreate):
923 """atest host create [--lock|--unlock --platform <arch>
924 --labels <labels>|--blist <label_file>
925 --acls <acls>|--alist <acl_file>
926 --protection <protection_type>
927 --attributes <attr>=<value>;<attr>=<value>
928 --mlist <mach_file>] <hosts>"""
929 usage_action = 'create'
930
931 def parse(self):
932 """Option logic specific to create action.
933 """
934 (options, leftovers) = super(host_create, self).parse()
935 self.locked = options.lock
936 if 'serials' in self.attributes:
937 if len(self.hosts) > 1:
938 raise topic_common.CliError('Can not specify serials with '
939 'multiple hosts.')
940
941
942 @classmethod
943 def construct_without_parse(
944 cls, web_server, hosts, platform=None,
945 locked=False, lock_reason='', labels=[], acls=[],
946 protection=host_protections.Protection.NO_PROTECTION):
947 """Construct a host_create object and fill in data from args.
948
949 Do not need to call parse after the construction.
950
951 Return an object of site_host_create ready to execute.
952
953 @param web_server: A string specifies the autotest webserver url.
954 It is needed to setup comm to make rpc.
955 @param hosts: A list of hostnames as strings.
956 @param platform: A string or None.
957 @param locked: A boolean.
958 @param lock_reason: A string.
959 @param labels: A list of labels as strings.
960 @param acls: A list of acls as strings.
961 @param protection: An enum defined in host_protections.
962 """
963 obj = cls()
964 obj.web_server = web_server
965 try:
966 # Setup stuff needed for afe comm.
967 obj.afe = rpc.afe_comm(web_server)
968 except rpc.AuthError, s:
969 obj.failure(str(s), fatal=True)
970 obj.hosts = hosts
971 obj.platform = platform
972 obj.locked = locked
973 if locked and lock_reason.strip():
974 obj.data['lock_reason'] = lock_reason.strip()
975 obj.labels = labels
976 obj.acls = acls
977 if protection:
978 obj.data['protection'] = protection
979 obj.attributes = {}
980 return obj
981
982
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700983 def _detect_host_info(self, host):
984 """Detect platform and labels from the host.
985
986 @param host: hostname
987
988 @return: HostInfo object
989 """
990 # Mock an afe_host object so that the host is constructed as if the
991 # data was already in afe
992 data = {'attributes': self.attributes, 'labels': self.labels}
993 afe_host = frontend.Host(None, data)
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700994 store = host_info.InMemoryHostInfoStore(
995 host_info.HostInfo(labels=self.labels,
996 attributes=self.attributes))
997 machine = {
998 'hostname': host,
999 'afe_host': afe_host,
1000 'host_info_store': store
1001 }
Justin Giorgi5208eaa2016-07-02 20:12:12 -07001002 try:
Allen Li2c32d6b2017-02-03 15:28:10 -08001003 if bin_utils.ping(host, tries=1, deadline=1) == 0:
Justin Giorgi5208eaa2016-07-02 20:12:12 -07001004 serials = self.attributes.get('serials', '').split(',')
Richard Barnette9db80682018-04-26 00:55:15 +00001005 adb_serial = self.attributes.get('serials')
1006 host_dut = hosts.create_host(machine,
1007 adb_serial=adb_serial)
xixuand6011f12016-12-08 15:01:58 -08001008
Prathmesh Prabhud252d262017-05-31 11:46:34 -07001009 info = HostInfo(host, host_dut.get_platform(),
1010 host_dut.get_labels())
xixuand6011f12016-12-08 15:01:58 -08001011 # Clean host to make sure nothing left after calling it,
1012 # e.g. tunnels.
1013 if hasattr(host_dut, 'close'):
1014 host_dut.close()
Justin Giorgi5208eaa2016-07-02 20:12:12 -07001015 else:
1016 # Can't ping the host, use default information.
Prathmesh Prabhud252d262017-05-31 11:46:34 -07001017 info = HostInfo(host, None, [])
Justin Giorgi5208eaa2016-07-02 20:12:12 -07001018 except (socket.gaierror, error.AutoservRunError,
1019 error.AutoservSSHTimeout):
1020 # We may be adding a host that does not exist yet or we can't
1021 # reach due to hostname/address issues or if the host is down.
Prathmesh Prabhud252d262017-05-31 11:46:34 -07001022 info = HostInfo(host, None, [])
1023 return info
Justin Giorgi5208eaa2016-07-02 20:12:12 -07001024
1025
mblighbe630eb2008-08-01 16:41:48 +00001026 def _execute_add_one_host(self, host):
mbligh719e14a2008-12-04 01:17:08 +00001027 # Always add the hosts as locked to avoid the host
Matthew Sartori68186332015-04-27 17:19:53 -07001028 # being picked up by the scheduler before it's ACL'ed.
mbligh719e14a2008-12-04 01:17:08 +00001029 self.data['locked'] = True
Matthew Sartori68186332015-04-27 17:19:53 -07001030 if not self.locked:
1031 self.data['lock_reason'] = 'Forced lock on device creation'
Justin Giorgi16bba562016-06-22 10:42:13 -07001032 self.execute_rpc('add_host', hostname=host, status="Ready", **self.data)
mblighbe630eb2008-08-01 16:41:48 +00001033
Justin Giorgi16bba562016-06-22 10:42:13 -07001034 # If there are labels avaliable for host, use them.
Prathmesh Prabhud252d262017-05-31 11:46:34 -07001035 info = self._detect_host_info(host)
Justin Giorgi16bba562016-06-22 10:42:13 -07001036 labels = set(self.labels)
Prathmesh Prabhud252d262017-05-31 11:46:34 -07001037 if info.labels:
1038 labels.update(info.labels)
Justin Giorgi16bba562016-06-22 10:42:13 -07001039
1040 if labels:
1041 self._set_labels(host, list(labels))
1042
1043 # Now add the platform label.
1044 # If a platform was not provided and we were able to retrieve it
1045 # from the host, use the retrieved platform.
Prathmesh Prabhud252d262017-05-31 11:46:34 -07001046 platform = self.platform if self.platform else info.platform
Justin Giorgi16bba562016-06-22 10:42:13 -07001047 if platform:
1048 self._set_platform_label(host, platform)
1049
1050 if self.attributes:
1051 self._set_attributes(host, self.attributes)
mblighbe630eb2008-08-01 16:41:48 +00001052
1053
Justin Giorgi5208eaa2016-07-02 20:12:12 -07001054 def execute(self):
xixuand6011f12016-12-08 15:01:58 -08001055 """Execute 'atest host create'."""
Justin Giorgi16bba562016-06-22 10:42:13 -07001056 successful_hosts = []
1057 for host in self.hosts:
1058 try:
1059 self._execute_add_one_host(host)
1060 successful_hosts.append(host)
1061 except topic_common.CliError:
1062 pass
Jiaxi Luoc342f9f2014-05-19 16:22:03 -07001063
1064 if successful_hosts:
Justin Giorgi16bba562016-06-22 10:42:13 -07001065 self._set_acls(successful_hosts, self.acls)
Jiaxi Luoc342f9f2014-05-19 16:22:03 -07001066
1067 if not self.locked:
1068 for host in successful_hosts:
Matthew Sartori68186332015-04-27 17:19:53 -07001069 self.execute_rpc('modify_host', id=host, locked=False,
1070 lock_reason='')
Jiaxi Luoc342f9f2014-05-19 16:22:03 -07001071 return successful_hosts
1072
1073
mblighbe630eb2008-08-01 16:41:48 +00001074 def output(self, hosts):
xixuand6011f12016-12-08 15:01:58 -08001075 """Print output of 'atest host create'.
1076
1077 @param hosts: the added host list to be printed.
1078 """
mblighbe630eb2008-08-01 16:41:48 +00001079 self.print_wrapped('Added host', hosts)
1080
1081
1082class host_delete(action_common.atest_delete, host):
1083 """atest host delete [--mlist <mach_file>] <hosts>"""
Ningning Xiac8b430d2018-05-17 15:10:25 -07001084
1085 def __init__(self):
1086 super(host_delete, self).__init__()
1087
1088 self.add_skylab_options()
1089
1090
1091 def execute_skylab(self):
1092 """Execute 'atest host delete' with '--skylab'.
1093
1094 @return A list of hostnames which have been successfully deleted.
1095 """
1096 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
1097 inventory_repo.initialize()
1098 data_dir = inventory_repo.get_data_dir()
1099 lab = text_manager.load_lab(data_dir)
1100
1101 successes = []
1102 for hostname in self.hosts:
1103 try:
1104 device.delete(
1105 lab,
1106 'duts',
1107 hostname,
1108 self.environment)
1109 successes.append(hostname)
1110 except device.SkylabDeviceActionError as e:
1111 print('Cannot delete host %s: %s' % (hostname, e))
1112
1113 if successes:
1114 text_manager.dump_lab(data_dir, lab)
1115 message = skylab_utils.construct_commit_message(
1116 'Delete %d hosts.\n\n%s' % (len(successes), successes))
1117 self.change_number = inventory_repo.upload_change(
1118 message, draft=self.draft, dryrun=self.dryrun,
1119 submit=self.submit)
1120
1121 return successes
1122
1123
1124 def execute(self):
1125 """Execute 'atest host delete'.
1126
1127 @return A list of hostnames which have been successfully deleted.
1128 """
1129 if self.skylab:
1130 return self.execute_skylab()
1131
1132 return super(host_delete, self).execute()
Ningning Xiac46bdd12018-05-29 11:24:14 -07001133
1134
1135class InvalidHostnameError(Exception):
1136 """Cannot perform actions on the host because of invalid hostname."""
1137
1138
1139def _add_hostname_suffix(hostname, suffix):
1140 """Add the suffix to the hostname."""
1141 if hostname.endswith(suffix):
1142 raise InvalidHostnameError(
1143 'Cannot add "%s" as it already contains the suffix.' % suffix)
1144
1145 return hostname + suffix
1146
1147
Gregory Nisbet48f1eea2019-08-05 16:18:56 -07001148def _remove_hostname_suffix_if_present(hostname, suffix):
Ningning Xiac46bdd12018-05-29 11:24:14 -07001149 """Remove the suffix from the hostname."""
Gregory Nisbet48f1eea2019-08-05 16:18:56 -07001150 if hostname.endswith(suffix):
1151 return hostname[:len(hostname) - len(suffix)]
1152 else:
1153 return hostname
Ningning Xiac46bdd12018-05-29 11:24:14 -07001154
1155
1156class host_rename(host):
1157 """Host rename is only for migrating hosts between skylab and AFE DB."""
1158
1159 usage_action = 'rename'
1160
1161 def __init__(self):
1162 """Add the options specific to the rename action."""
1163 super(host_rename, self).__init__()
1164
1165 self.parser.add_option('--for-migration',
1166 help=('Rename hostnames for migration. Rename '
1167 'each "hostname" to "hostname%s". '
1168 'The original "hostname" must not contain '
1169 'suffix.' % MIGRATED_HOST_SUFFIX),
1170 action='store_true',
1171 default=False)
1172 self.parser.add_option('--for-rollback',
1173 help=('Rename hostnames for migration rollback. '
1174 'Rename each "hostname%s" to its original '
1175 '"hostname".' % MIGRATED_HOST_SUFFIX),
1176 action='store_true',
1177 default=False)
1178 self.parser.add_option('--dryrun',
1179 help='Execute the action as a dryrun.',
1180 action='store_true',
1181 default=False)
Gregory Nisbetee457f62019-07-08 15:47:26 -07001182 self.parser.add_option('--non-interactive',
1183 help='run non-interactively',
Gregory Nisbet9744ea92019-07-02 12:36:59 -07001184 action='store_true',
1185 default=False)
Ningning Xiac46bdd12018-05-29 11:24:14 -07001186
1187
1188 def parse(self):
1189 """Consume the options common to host rename."""
1190 (options, leftovers) = super(host_rename, self).parse()
1191 self.for_migration = options.for_migration
1192 self.for_rollback = options.for_rollback
1193 self.dryrun = options.dryrun
Gregory Nisbetee457f62019-07-08 15:47:26 -07001194 self.interactive = not options.non_interactive
Ningning Xiac46bdd12018-05-29 11:24:14 -07001195 self.host_ids = {}
1196
1197 if not (self.for_migration ^ self.for_rollback):
1198 self.invalid_syntax('--for-migration and --for-rollback are '
1199 'exclusive, and one of them must be enabled.')
1200
1201 if not self.hosts:
1202 self.invalid_syntax('Must provide hostname(s).')
1203
1204 if self.dryrun:
1205 print('This will be a dryrun and will not rename hostnames.')
1206
1207 return (options, leftovers)
1208
1209
1210 def execute(self):
1211 """Execute 'atest host rename'."""
Gregory Nisbetee457f62019-07-08 15:47:26 -07001212 if self.interactive:
1213 if self.prompt_confirmation():
1214 pass
1215 else:
1216 return
Ningning Xiac46bdd12018-05-29 11:24:14 -07001217
1218 successes = []
1219 for host in self.execute_rpc('get_hosts', hostname__in=self.hosts):
1220 self.host_ids[host['hostname']] = host['id']
1221 for host in self.hosts:
1222 if host not in self.host_ids:
1223 self.failure('Cannot rename non-existant host %s.' % host,
1224 item=host, what_failed='Failed to rename')
1225 continue
1226 try:
Ningning Xiab261b162018-06-07 17:24:59 -07001227 host_id = self.host_ids[host]
Ningning Xiac46bdd12018-05-29 11:24:14 -07001228 if self.for_migration:
1229 new_hostname = _add_hostname_suffix(
1230 host, MIGRATED_HOST_SUFFIX)
1231 else:
1232 #for_rollback
Gregory Nisbet917ee332019-09-05 11:41:23 -07001233 new_hostname = _remove_hostname_suffix_if_present(
Ningning Xiac46bdd12018-05-29 11:24:14 -07001234 host, MIGRATED_HOST_SUFFIX)
1235
1236 if not self.dryrun:
Ningning Xia423c7962018-06-07 15:27:18 -07001237 # TODO(crbug.com/850737): delete and abort HQE.
Ningning Xiac46bdd12018-05-29 11:24:14 -07001238 data = {'hostname': new_hostname}
Ningning Xiab261b162018-06-07 17:24:59 -07001239 self.execute_rpc('modify_host', item=host, id=host_id,
1240 **data)
Ningning Xiac46bdd12018-05-29 11:24:14 -07001241 successes.append((host, new_hostname))
1242 except InvalidHostnameError as e:
1243 self.failure('Cannot rename host %s: %s' % (host, e), item=host,
1244 what_failed='Failed to rename')
1245 except topic_common.CliError, full_error:
1246 # Already logged by execute_rpc()
1247 pass
1248
1249 return successes
1250
1251
1252 def output(self, results):
1253 """Print output of 'atest host rename'."""
1254 if results:
1255 print('Successfully renamed:')
1256 for old_hostname, new_hostname in results:
1257 print('%s to %s' % (old_hostname, new_hostname))
Ningning Xia9df3d152018-05-23 17:15:14 -07001258
1259
1260class host_migrate(action_common.atest_list, host):
1261 """'atest host migrate' to migrate or rollback hosts."""
1262
1263 usage_action = 'migrate'
1264
1265 def __init__(self):
1266 super(host_migrate, self).__init__()
1267
1268 self.parser.add_option('--migration',
1269 dest='migration',
1270 help='Migrate the hosts to skylab.',
1271 action='store_true',
1272 default=False)
1273 self.parser.add_option('--rollback',
1274 dest='rollback',
1275 help='Rollback the hosts migrated to skylab.',
1276 action='store_true',
1277 default=False)
1278 self.parser.add_option('--model',
1279 help='Model of the hosts to migrate.',
1280 dest='model',
1281 default=None)
Xixuan Wu84621e52018-08-28 14:29:53 -07001282 self.parser.add_option('--board',
1283 help='Board of the hosts to migrate.',
1284 dest='board',
1285 default=None)
Ningning Xia9df3d152018-05-23 17:15:14 -07001286 self.parser.add_option('--pool',
1287 help=('Pool of the hosts to migrate. Must '
1288 'specify --model for the pool.'),
1289 dest='pool',
1290 default=None)
1291
1292 self.add_skylab_options(enforce_skylab=True)
1293
1294
1295 def parse(self):
1296 """Consume the specific options"""
1297 (options, leftover) = super(host_migrate, self).parse()
1298
1299 self.migration = options.migration
1300 self.rollback = options.rollback
1301 self.model = options.model
1302 self.pool = options.pool
Xixuan Wu84621e52018-08-28 14:29:53 -07001303 self.board = options.board
Ningning Xia9df3d152018-05-23 17:15:14 -07001304 self.host_ids = {}
1305
1306 if not (self.migration ^ self.rollback):
1307 self.invalid_syntax('--migration and --rollback are exclusive, '
1308 'and one of them must be enabled.')
1309
Xixuan Wu84621e52018-08-28 14:29:53 -07001310 if self.pool is not None and (self.model is None and
1311 self.board is None):
1312 self.invalid_syntax('Must provide --model or --board with --pool.')
Ningning Xia9df3d152018-05-23 17:15:14 -07001313
Xixuan Wu84621e52018-08-28 14:29:53 -07001314 if not self.hosts and not (self.model or self.board):
1315 self.invalid_syntax('Must provide hosts or --model or --board.')
Ningning Xia9df3d152018-05-23 17:15:14 -07001316
1317 return (options, leftover)
1318
1319
Ningning Xia56d68432018-06-06 17:28:20 -07001320 def _remove_invalid_hostnames(self, hostnames, log_failure=False):
1321 """Remove hostnames with MIGRATED_HOST_SUFFIX.
1322
1323 @param hostnames: A list of hostnames.
1324 @param log_failure: Bool indicating whether to log invalid hostsnames.
1325
1326 @return A list of valid hostnames.
1327 """
1328 invalid_hostnames = set()
Ningning Xia9df3d152018-05-23 17:15:14 -07001329 for hostname in hostnames:
1330 if hostname.endswith(MIGRATED_HOST_SUFFIX):
Ningning Xia56d68432018-06-06 17:28:20 -07001331 if log_failure:
1332 self.failure('Cannot migrate host with suffix "%s" %s.' %
1333 (MIGRATED_HOST_SUFFIX, hostname),
1334 item=hostname, what_failed='Failed to rename')
1335 invalid_hostnames.add(hostname)
1336
1337 hostnames = list(set(hostnames) - invalid_hostnames)
1338
1339 return hostnames
1340
1341
1342 def execute(self):
1343 """Execute 'atest host migrate'."""
1344 hostnames = self._remove_invalid_hostnames(self.hosts, log_failure=True)
Ningning Xia9df3d152018-05-23 17:15:14 -07001345
1346 filters = {}
1347 check_results = {}
1348 if hostnames:
1349 check_results['hostname__in'] = 'hostname'
1350 if self.migration:
1351 filters['hostname__in'] = hostnames
1352 else:
1353 # rollback
1354 hostnames_with_suffix = [
1355 _add_hostname_suffix(h, MIGRATED_HOST_SUFFIX)
1356 for h in hostnames]
1357 filters['hostname__in'] = hostnames_with_suffix
Ningning Xia56d68432018-06-06 17:28:20 -07001358 else:
1359 # TODO(nxia): add exclude_filter {'hostname__endswith':
1360 # MIGRATED_HOST_SUFFIX} for --migration
1361 if self.rollback:
1362 filters['hostname__endswith'] = MIGRATED_HOST_SUFFIX
Ningning Xia9df3d152018-05-23 17:15:14 -07001363
1364 labels = []
1365 if self.model:
1366 labels.append('model:%s' % self.model)
1367 if self.pool:
1368 labels.append('pool:%s' % self.pool)
Xixuan Wu84621e52018-08-28 14:29:53 -07001369 if self.board:
1370 labels.append('board:%s' % self.board)
Ningning Xia9df3d152018-05-23 17:15:14 -07001371
1372 if labels:
1373 if len(labels) == 1:
1374 filters['labels__name__in'] = labels
1375 check_results['labels__name__in'] = None
1376 else:
1377 filters['multiple_labels'] = labels
1378 check_results['multiple_labels'] = None
1379
1380 results = super(host_migrate, self).execute(
1381 op='get_hosts', filters=filters, check_results=check_results)
1382 hostnames = [h['hostname'] for h in results]
1383
Ningning Xia56d68432018-06-06 17:28:20 -07001384 if self.migration:
1385 hostnames = self._remove_invalid_hostnames(hostnames)
1386 else:
1387 # rollback
1388 hostnames = [_remove_hostname_suffix(h, MIGRATED_HOST_SUFFIX)
1389 for h in hostnames]
1390
Ningning Xia9df3d152018-05-23 17:15:14 -07001391 return self.execute_skylab_migration(hostnames)
1392
1393
Xixuan Wua8d93c82018-08-03 16:23:26 -07001394 def assign_duts_to_drone(self, infra, devices, environment):
Ningning Xia877817f2018-06-08 12:23:48 -07001395 """Assign uids of the devices to a random skylab drone.
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001396
1397 @param infra: An instance of lab_pb2.Infrastructure.
Xixuan Wua8d93c82018-08-03 16:23:26 -07001398 @param devices: A list of device_pb2.Device to be assigned to the drone.
1399 @param environment: 'staging' or 'prod'.
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001400 """
1401 skylab_drones = skylab_server.get_servers(
Xixuan Wua8d93c82018-08-03 16:23:26 -07001402 infra, environment, role='skylab_drone', status='primary')
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001403
1404 if len(skylab_drones) == 0:
1405 raise device.SkylabDeviceActionError(
1406 'No skylab drone is found in primary status and staging '
1407 'environment. Please confirm there is at least one valid skylab'
1408 ' drone added in skylab inventory.')
1409
Xixuan Wudfc2de22018-08-06 09:29:01 -07001410 for device in devices:
1411 # Randomly distribute each device to a skylab_drone.
1412 skylab_drone = random.choice(skylab_drones)
1413 skylab_server.add_dut_uids(skylab_drone, [device])
Ningning Xia877817f2018-06-08 12:23:48 -07001414
1415
1416 def remove_duts_from_drone(self, infra, devices):
1417 """Remove uids of the devices from their skylab drones.
1418
1419 @param infra: An instance of lab_pb2.Infrastructure.
1420 @devices: A list of device_pb2.Device to be remove from the drone.
1421 """
1422 skylab_drones = skylab_server.get_servers(
1423 infra, 'staging', role='skylab_drone', status='primary')
1424
1425 for skylab_drone in skylab_drones:
1426 skylab_server.remove_dut_uids(skylab_drone, devices)
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001427
1428
Ningning Xia9df3d152018-05-23 17:15:14 -07001429 def execute_skylab_migration(self, hostnames):
1430 """Execute migration in skylab_inventory.
1431
1432 @param hostnames: A list of hostnames to migrate.
1433 @return If there're hosts to migrate, return a list of the hostnames and
1434 a message instructing actions after the migration; else return
1435 None.
1436 """
1437 if not hostnames:
1438 return
1439
1440 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
1441 inventory_repo.initialize()
1442
1443 subdirs = ['skylab', 'prod', 'staging']
1444 data_dirs = skylab_data_dir, prod_data_dir, staging_data_dir = [
1445 inventory_repo.get_data_dir(data_subdir=d) for d in subdirs]
1446 skylab_lab, prod_lab, staging_lab = [
1447 text_manager.load_lab(d) for d in data_dirs]
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001448 infra = text_manager.load_infrastructure(skylab_data_dir)
Ningning Xia9df3d152018-05-23 17:15:14 -07001449
1450 label_map = None
1451 labels = []
Xixuan Wu46f8e202018-12-13 14:40:58 -08001452 if self.board:
1453 labels.append('board:%s' % self.board)
Ningning Xia9df3d152018-05-23 17:15:14 -07001454 if self.model:
Xixuan Wu46f8e202018-12-13 14:40:58 -08001455 labels.append('model:%s' % self.model)
Ningning Xia9df3d152018-05-23 17:15:14 -07001456 if self.pool:
1457 labels.append('critical_pool:%s' % self.pool)
1458 if labels:
1459 label_map = device.convert_to_label_map(labels)
1460
1461 if self.migration:
1462 prod_devices = device.move_devices(
1463 prod_lab, skylab_lab, 'duts', label_map=label_map,
1464 hostnames=hostnames)
1465 staging_devices = device.move_devices(
1466 staging_lab, skylab_lab, 'duts', label_map=label_map,
1467 hostnames=hostnames)
1468
1469 all_devices = prod_devices + staging_devices
1470 # Hostnames in afe_hosts tabel.
1471 device_hostnames = [str(d.common.hostname) for d in all_devices]
1472 message = (
1473 'Migration: move %s hosts into skylab_inventory.\n\n'
Ningning Xia423c7962018-06-07 15:27:18 -07001474 'Please run this command after the CL is submitted:\n'
Ningning Xia9df3d152018-05-23 17:15:14 -07001475 'atest host rename --for-migration %s' %
1476 (len(all_devices), ' '.join(device_hostnames)))
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001477
Xixuan Wua8d93c82018-08-03 16:23:26 -07001478 self.assign_duts_to_drone(infra, prod_devices, 'prod')
1479 self.assign_duts_to_drone(infra, staging_devices, 'staging')
Ningning Xia9df3d152018-05-23 17:15:14 -07001480 else:
1481 # rollback
1482 prod_devices = device.move_devices(
1483 skylab_lab, prod_lab, 'duts', environment='prod',
Ningning Xia56d68432018-06-06 17:28:20 -07001484 label_map=label_map, hostnames=hostnames)
Ningning Xia9df3d152018-05-23 17:15:14 -07001485 staging_devices = device.move_devices(
Ningning Xia877817f2018-06-08 12:23:48 -07001486 skylab_lab, staging_lab, 'duts', environment='staging',
Ningning Xia56d68432018-06-06 17:28:20 -07001487 label_map=label_map, hostnames=hostnames)
Ningning Xia9df3d152018-05-23 17:15:14 -07001488
1489 all_devices = prod_devices + staging_devices
1490 # Hostnames in afe_hosts tabel.
1491 device_hostnames = [_add_hostname_suffix(str(d.common.hostname),
1492 MIGRATED_HOST_SUFFIX)
1493 for d in all_devices]
1494 message = (
1495 'Rollback: remove %s hosts from skylab_inventory.\n\n'
Ningning Xia423c7962018-06-07 15:27:18 -07001496 'Please run this command after the CL is submitted:\n'
Ningning Xia9df3d152018-05-23 17:15:14 -07001497 'atest host rename --for-rollback %s' %
1498 (len(all_devices), ' '.join(device_hostnames)))
1499
Ningning Xia877817f2018-06-08 12:23:48 -07001500 self.remove_duts_from_drone(infra, all_devices)
1501
Ningning Xia9df3d152018-05-23 17:15:14 -07001502 if all_devices:
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001503 text_manager.dump_infrastructure(skylab_data_dir, infra)
1504
Ningning Xia9df3d152018-05-23 17:15:14 -07001505 if prod_devices:
1506 text_manager.dump_lab(prod_data_dir, prod_lab)
1507
1508 if staging_devices:
1509 text_manager.dump_lab(staging_data_dir, staging_lab)
1510
1511 text_manager.dump_lab(skylab_data_dir, skylab_lab)
1512
1513 self.change_number = inventory_repo.upload_change(
1514 message, draft=self.draft, dryrun=self.dryrun,
1515 submit=self.submit)
1516
1517 return all_devices, message
1518
1519
1520 def output(self, result):
1521 """Print output of 'atest host list'.
1522
1523 @param result: the result to be printed.
1524 """
1525 if result:
1526 devices, message = result
1527
1528 if devices:
1529 hostnames = [h.common.hostname for h in devices]
1530 if self.migration:
1531 print('Migrating hosts: %s' % ','.join(hostnames))
1532 else:
1533 # rollback
1534 print('Rolling back hosts: %s' % ','.join(hostnames))
1535
1536 if not self.dryrun:
1537 if not self.submit:
1538 print(skylab_utils.get_cl_message(self.change_number))
1539 else:
1540 # Print the instruction command for renaming hosts.
1541 print('%s' % message)
1542 else:
1543 print('No hosts were migrated.')
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001544
1545
Gregory Nisbet82350232019-09-10 11:12:47 -07001546
1547def _host_skylab_migrate_get_hostnames(obj, class_, model=None, pool=None, board=None):
1548 """
1549 @params : in 'model', 'pool', 'board'
1550
1551 """
1552 # TODO(gregorynisbet)
1553 # this just gets all the hostnames, it doesn't filter by
1554 # presence or absence of migrated-do-not-use.
1555 labels = []
1556 for key, value in ({'model': model, 'board': board, 'pool': pool}).items():
1557 if value:
1558 labels.append(key + ":" + value)
1559 filters = {}
1560 check_results = {}
1561 # Copy the filter and check_results initialization logic from
1562 # the 'execute' method of the class 'host_migrate'.
1563 if not labels:
1564 return []
1565 elif len(labels) == 1:
1566 filters['labels__name__in'] = labels
1567 check_results['labels__name__in'] = None
1568 elif len(labels) > 1:
1569 filters['multiple_labels'] = labels
1570 check_results['multiple_labels'] = None
1571 else:
1572 assert False
1573
1574 results = super(class_, obj).execute(
1575 op='get_hosts', filters=filters, check_results=check_results)
1576 return [result['hostname'] for result in results]
1577
1578
1579
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001580class host_skylab_migrate(action_common.atest_list, host):
1581 usage_action = 'skylab_migrate'
1582
1583 def __init__(self):
1584 super(host_skylab_migrate, self).__init__()
1585 self.parser.add_option('--dry-run',
1586 help='Dry run. Show only candidate hosts.',
1587 action='store_true',
1588 dest='dry_run')
1589 self.parser.add_option('--ratio',
1590 help='ratio of hosts to migrate as number from 0 to 1.',
1591 type=float,
1592 dest='ratio',
1593 default=1)
1594 self.parser.add_option('--bug-number',
1595 help='bug number for tracking purposes.',
1596 dest='bug_number',
1597 default=None)
1598 self.parser.add_option('--board',
1599 help='Board of the hosts to migrate',
1600 dest='board',
1601 default=None)
1602 self.parser.add_option('--model',
1603 help='Model of the hosts to migrate',
1604 dest='model',
1605 default=None)
1606 self.parser.add_option('--pool',
1607 help='Pool of the hosts to migrate',
1608 dest='pool',
1609 default=None)
Gregory Nisbet917ee332019-09-05 11:41:23 -07001610 self.parser.add_option('-q',
Gregory Nisbet876f0892019-09-13 15:55:56 -07001611 '--quick',
1612 help='use quick-add-duts',
Gregory Nisbet917ee332019-09-05 11:41:23 -07001613 dest='use_quick_add',
1614 action='store_true')
Gregory Nisbet876f0892019-09-13 15:55:56 -07001615 self.parser.add_option('-s',
1616 '--slow',
Gregory Nisbetefc93972019-09-25 21:53:44 -07001617 help='don\'t use quick-add-duts',
Gregory Nisbet876f0892019-09-13 15:55:56 -07001618 dest='no_use_quick_add',
1619 action='store_true')
Gregory Nisbetefc93972019-09-25 21:53:44 -07001620 self.parser.add_option('-b',
1621 '--batch-size',
1622 help='process n duts at a time',
1623 dest="batch_size",
1624 default=None)
Gregory Nisbet876f0892019-09-13 15:55:56 -07001625
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001626 def parse(self):
1627 (options, leftover) = super(host_skylab_migrate, self).parse()
1628 self.dry_run = options.dry_run
1629 self.ratio = options.ratio
1630 self.bug_number = options.bug_number
1631 self.model = options.model
1632 self.pool = options.pool
1633 self.board = options.board
1634 self._reason = "migration to skylab: %s" % self.bug_number
Gregory Nisbet876f0892019-09-13 15:55:56 -07001635 use_quick_add = options.use_quick_add
1636 no_use_quick_add = options.no_use_quick_add
1637 if use_quick_add:
1638 if no_use_quick_add:
1639 self.invalid_syntax('cannot supply both --quick and --slow.')
1640 else:
1641 self.use_quick_add = True
1642 else:
1643 if no_use_quick_add:
1644 self.use_quick_add = False
1645 else:
1646 self.invalid_syntax('must include either --quick or --slow.')
Gregory Nisbetefc93972019-09-25 21:53:44 -07001647 self.batch_size = options.batch_size
Gregory Nisbet876f0892019-09-13 15:55:56 -07001648
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001649 return (options, leftover)
1650
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001651 def _validate_one_hostname_source(self):
1652 """Validate that hostname source is explicit hostnames or valid query.
1653
1654 Hostnames must either be provided explicitly or be the result of a
1655 query defined by 'model', 'board', and 'pool'.
1656
1657 @returns : whether the hostnames come from exactly one valid source.
1658 """
1659 has_criteria = any([(self.model and self.board), self.board, self.pool])
1660 has_command_line_hosts = bool(self.hosts)
1661 if has_criteria != has_command_line_hosts:
1662 # all good, one data source
1663 return True
1664 if has_criteria and has_command_line_hosts:
1665 self.failure(
1666 '--model/host/board and explicit hostnames are alternatives. Provide exactly one.',
1667 item='cli',
1668 what_failed='user')
1669 return False
1670 self.failure(
1671 'no explicit hosts and no criteria provided.',
1672 item='cli',
1673 what_failed='user')
1674 return False
1675
1676
1677 def execute(self):
1678 if not self._validate_one_hostname_source():
1679 return None
1680 if self.hosts:
1681 hostnames = self.hosts
1682 else:
Gregory Nisbet82350232019-09-10 11:12:47 -07001683 hostnames = _host_skylab_migrate_get_hostnames(
1684 obj=self,
1685 class_=host_skylab_migrate,
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001686 model=self.model,
1687 board=self.board,
1688 pool=self.pool,
1689 )
1690 if self.dry_run:
1691 return hostnames
1692 if not hostnames:
1693 return {'error': 'no hosts to migrate'}
1694 res = skylab_migration.migrate(
1695 ratio=self.ratio,
1696 reason=self._reason,
1697 hostnames=hostnames,
1698 max_duration=10 * 60,
1699 interval_len=2,
1700 min_ready_intervals=10,
1701 immediately=True,
Gregory Nisbet917ee332019-09-05 11:41:23 -07001702 use_quick_add=self.use_quick_add,
Gregory Nisbetefc93972019-09-25 21:53:44 -07001703 batch_size=self.batch_size,
Gregory Nisbetc4d63ca2019-08-08 10:46:40 -07001704 )
1705 return res
1706
1707
1708 def output(self, result):
1709 if result is not None:
1710 print json.dumps(result, indent=4, sort_keys=True)
Gregory Nisbetac7f7f72019-09-10 16:25:20 -07001711
1712
Gregory Nisbet89eb9212019-09-13 11:21:30 -07001713class host_skylab_rollback(action_common.atest_list, host):
Gregory Nisbetac7f7f72019-09-10 16:25:20 -07001714 usage_action = "skylab_rollback"
1715
1716 def __init__(self):
Gregory Nisbet89eb9212019-09-13 11:21:30 -07001717 super(host_skylab_rollback, self).__init__()
Gregory Nisbetac7f7f72019-09-10 16:25:20 -07001718 self.parser.add_option('--bug-number',
1719 help='bug number for tracking purposes.',
1720 dest='bug_number',
1721 default=None)
1722
1723 def parse(self):
Gregory Nisbet89eb9212019-09-13 11:21:30 -07001724 (options, leftover) = super(host_skylab_rollback, self).parse()
Gregory Nisbetac7f7f72019-09-10 16:25:20 -07001725 self.bug_number = options.bug_number
1726 return (options, leftover)
1727
1728 def execute(self):
1729 if self.hosts:
1730 hostnames = self.hosts
1731 else:
1732 hostnames = _host_skylab_migrate_get_hostnames(
1733 obj=self,
1734 class_=host_skylab_migrate,
1735 model=self.model,
1736 board=self.board,
1737 pool=self.pool,
1738 )
1739 if not hostnames:
1740 return {'error': 'no hosts to migrate'}
1741 res = skylab_rollback.rollback(
1742 hosts=hostnames,
1743 bug=self.bug_number,
1744 dry_run=False,
1745 )
1746 return res
Gregory Nisbete0cdbd62019-09-19 18:09:36 -07001747
1748
1749 def output(self, result):
1750 print result
Gregory Nisbetf74a7fe2019-09-25 10:45:12 -07001751
1752
1753class host_skylab_verify(action_common.atest_list, host):
1754 usage_action = "skylab_verify"
1755
1756 def __init__(self):
1757 super(host_skylab_verify, self).__init__()
1758
1759 def parse(self):
1760 (options, leftover) = super(host_skylab_verify, self).parse()
1761 return (options, leftover)
1762
1763 def execute(self):
1764 if self.hosts:
1765 hostnames = self.hosts
1766 else:
1767 hostnames = _host_skylab_migrate_get_hostnames(
1768 obj=self,
1769 class_=host_skylab_migrate,
1770 model=self.model,
1771 board=self.board,
1772 pool=self.pool,
1773 )
1774 if not hostnames:
1775 return {'error': 'no hosts to migrate'}
1776 res = skylab_migration.hostname_migrated_status(
1777 hostnames=hostnames,
1778 )
1779 return res
1780
1781
1782 def output(self, result):
1783 json.dump(result, sys.stdout, indent=4)