blob: 033c2cc110261d4dd8328f8c5ee93e46b4984178 [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 Nisbet79751f12020-02-11 21:23:18 -080030from autotest_lib.cli import action_common, rpc, topic_common, skylab_utils
Xixuan Wu06f7e232019-07-12 23:33:28 +000031from autotest_lib.client.bin import utils as bin_utils
Justin Giorgi16bba562016-06-22 10:42:13 -070032from autotest_lib.client.common_lib import error, host_protections
Justin Giorgi5208eaa2016-07-02 20:12:12 -070033from autotest_lib.server import frontend, hosts
Prathmesh Prabhud252d262017-05-31 11:46:34 -070034from autotest_lib.server.hosts import host_info
Gregory Nisbet93fa5b52019-07-19 14:56:35 -070035from autotest_lib.server.lib.status_history import HostJobHistory
36from autotest_lib.server.lib.status_history import UNUSED, WORKING
37from autotest_lib.server.lib.status_history import BROKEN, UNKNOWN
mblighbe630eb2008-08-01 16:41:48 +000038
39
Ningning Xiaea02ab12018-05-11 15:08:57 -070040try:
41 from skylab_inventory import text_manager
42 from skylab_inventory.lib import device
Ningning Xiaffb3c1f2018-06-07 18:42:32 -070043 from skylab_inventory.lib import server as skylab_server
Ningning Xiaea02ab12018-05-11 15:08:57 -070044except ImportError:
45 pass
46
47
Ningning Xiac46bdd12018-05-29 11:24:14 -070048MIGRATED_HOST_SUFFIX = '-migrated-do-not-use'
49
50
Gregory Nisbet99b61942019-07-08 09:43:06 -070051ID_AUTOGEN_MESSAGE = ("[IGNORED]. Do not edit (crbug.com/950553). ID is "
52 "auto-generated.")
53
54
55
mblighbe630eb2008-08-01 16:41:48 +000056class host(topic_common.atest):
57 """Host class
Gregory Nisbet79751f12020-02-11 21:23:18 -080058 atest host [create|delete|list|stat|mod|jobs|rename|migrate] <options>"""
59 usage_action = '[create|delete|list|stat|mod|jobs|rename|migrate]'
mblighbe630eb2008-08-01 16:41:48 +000060 topic = msg_topic = 'host'
61 msg_items = '<hosts>'
62
mblighaed47e82009-03-17 19:06:18 +000063 protections = host_protections.Protection.names
64
mblighbe630eb2008-08-01 16:41:48 +000065
66 def __init__(self):
67 """Add to the parser the options common to all the
68 host actions"""
69 super(host, self).__init__()
70
71 self.parser.add_option('-M', '--mlist',
72 help='File listing the machines',
73 type='string',
74 default=None,
75 metavar='MACHINE_FLIST')
76
mbligh9deeefa2009-05-01 23:11:08 +000077 self.topic_parse_info = topic_common.item_parse_info(
78 attribute_name='hosts',
79 filename_option='mlist',
80 use_leftover=True)
mblighbe630eb2008-08-01 16:41:48 +000081
82
83 def _parse_lock_options(self, options):
84 if options.lock and options.unlock:
85 self.invalid_syntax('Only specify one of '
86 '--lock and --unlock.')
87
Ningning Xiaef35cb52018-05-04 17:58:20 -070088 self.lock = options.lock
89 self.unlock = options.unlock
90 self.lock_reason = options.lock_reason
Ningning Xiaef35cb52018-05-04 17:58:20 -070091
mblighbe630eb2008-08-01 16:41:48 +000092 if options.lock:
93 self.data['locked'] = True
94 self.messages.append('Locked host')
95 elif options.unlock:
96 self.data['locked'] = False
Matthew Sartori68186332015-04-27 17:19:53 -070097 self.data['lock_reason'] = ''
mblighbe630eb2008-08-01 16:41:48 +000098 self.messages.append('Unlocked host')
99
Matthew Sartori68186332015-04-27 17:19:53 -0700100 if options.lock and options.lock_reason:
101 self.data['lock_reason'] = options.lock_reason
102
mblighbe630eb2008-08-01 16:41:48 +0000103
104 def _cleanup_labels(self, labels, platform=None):
105 """Removes the platform label from the overall labels"""
106 if platform:
107 return [label for label in labels
108 if label != platform]
109 else:
110 try:
111 return [label for label in labels
112 if not label['platform']]
113 except TypeError:
114 # This is a hack - the server will soon
115 # do this, so all this code should be removed.
116 return labels
117
118
119 def get_items(self):
120 return self.hosts
121
122
123class host_help(host):
124 """Just here to get the atest logic working.
125 Usage is set by its parent"""
126 pass
127
128
129class host_list(action_common.atest_list, host):
130 """atest host list [--mlist <file>|<hosts>] [--label <label>]
mbligh536a5242008-10-18 14:35:54 +0000131 [--status <status1,status2>] [--acl <ACL>] [--user <user>]"""
mblighbe630eb2008-08-01 16:41:48 +0000132
133 def __init__(self):
134 super(host_list, self).__init__()
135
136 self.parser.add_option('-b', '--label',
mbligh6444c6b2008-10-27 20:55:13 +0000137 default='',
138 help='Only list hosts with all these labels '
Ningning Xiaea02ab12018-05-11 15:08:57 -0700139 '(comma separated). When --skylab is provided, '
140 'a label must be in the format of '
141 'label-key:label-value (e.g., board:lumpy).')
mblighbe630eb2008-08-01 16:41:48 +0000142 self.parser.add_option('-s', '--status',
mbligh6444c6b2008-10-27 20:55:13 +0000143 default='',
144 help='Only list hosts with any of these '
145 'statuses (comma separated)')
mbligh536a5242008-10-18 14:35:54 +0000146 self.parser.add_option('-a', '--acl',
mbligh6444c6b2008-10-27 20:55:13 +0000147 default='',
Ningning Xiaea02ab12018-05-11 15:08:57 -0700148 help=('Only list hosts within this ACL. %s' %
149 skylab_utils.MSG_INVALID_IN_SKYLAB))
mbligh536a5242008-10-18 14:35:54 +0000150 self.parser.add_option('-u', '--user',
mbligh6444c6b2008-10-27 20:55:13 +0000151 default='',
Ningning Xiaea02ab12018-05-11 15:08:57 -0700152 help=('Only list hosts available to this user. '
153 '%s' % skylab_utils.MSG_INVALID_IN_SKYLAB))
mbligh70b9bf42008-11-18 15:00:46 +0000154 self.parser.add_option('-N', '--hostnames-only', help='Only return '
155 'hostnames for the machines queried.',
156 action='store_true')
mbligh91e0efd2009-02-26 01:02:16 +0000157 self.parser.add_option('--locked',
158 default=False,
159 help='Only list locked hosts',
160 action='store_true')
161 self.parser.add_option('--unlocked',
162 default=False,
163 help='Only list unlocked hosts',
164 action='store_true')
Ningning Xiaea02ab12018-05-11 15:08:57 -0700165 self.parser.add_option('--full-output',
166 default=False,
167 help=('Print out the full content of the hosts. '
168 'Only supported with --skylab.'),
169 action='store_true',
170 dest='full_output')
mbligh91e0efd2009-02-26 01:02:16 +0000171
Ningning Xiaea02ab12018-05-11 15:08:57 -0700172 self.add_skylab_options()
mblighbe630eb2008-08-01 16:41:48 +0000173
174
175 def parse(self):
176 """Consume the specific options"""
jamesrenc2863162010-07-12 21:20:51 +0000177 label_info = topic_common.item_parse_info(attribute_name='labels',
178 inline_option='label')
179
180 (options, leftover) = super(host_list, self).parse([label_info])
181
mblighbe630eb2008-08-01 16:41:48 +0000182 self.status = options.status
mbligh536a5242008-10-18 14:35:54 +0000183 self.acl = options.acl
184 self.user = options.user
mbligh70b9bf42008-11-18 15:00:46 +0000185 self.hostnames_only = options.hostnames_only
mbligh91e0efd2009-02-26 01:02:16 +0000186
187 if options.locked and options.unlocked:
188 self.invalid_syntax('--locked and --unlocked are '
189 'mutually exclusive')
Ningning Xiaea02ab12018-05-11 15:08:57 -0700190
mbligh91e0efd2009-02-26 01:02:16 +0000191 self.locked = options.locked
192 self.unlocked = options.unlocked
Ningning Xia64ced002018-05-16 17:36:20 -0700193 self.label_map = None
Ningning Xiaea02ab12018-05-11 15:08:57 -0700194
195 if self.skylab:
196 if options.user or options.acl or options.status:
197 self.invalid_syntax('--user, --acl or --status is not '
198 'supported with --skylab.')
199 self.full_output = options.full_output
200 if self.full_output and self.hostnames_only:
201 self.invalid_syntax('--full-output is conflicted with '
202 '--hostnames-only.')
Ningning Xia64ced002018-05-16 17:36:20 -0700203
204 if self.labels:
205 self.label_map = device.convert_to_label_map(self.labels)
Ningning Xiaea02ab12018-05-11 15:08:57 -0700206 else:
207 if options.full_output:
208 self.invalid_syntax('--full_output is only supported with '
209 '--skylab.')
210
mblighbe630eb2008-08-01 16:41:48 +0000211 return (options, leftover)
212
213
Ningning Xiaea02ab12018-05-11 15:08:57 -0700214 def execute_skylab(self):
215 """Execute 'atest host list' with --skylab."""
216 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
217 inventory_repo.initialize()
218 lab = text_manager.load_lab(inventory_repo.get_data_dir())
219
220 # TODO(nxia): support filtering on run-time labels and status.
221 return device.get_devices(
222 lab,
223 'duts',
224 self.environment,
Ningning Xia64ced002018-05-16 17:36:20 -0700225 label_map=self.label_map,
Ningning Xiaea02ab12018-05-11 15:08:57 -0700226 hostnames=self.hosts,
227 locked=self.locked,
228 unlocked=self.unlocked)
229
230
mblighbe630eb2008-08-01 16:41:48 +0000231 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800232 """Execute 'atest host list'."""
Ningning Xiaea02ab12018-05-11 15:08:57 -0700233 if self.skylab:
234 return self.execute_skylab()
235
mblighbe630eb2008-08-01 16:41:48 +0000236 filters = {}
237 check_results = {}
238 if self.hosts:
239 filters['hostname__in'] = self.hosts
240 check_results['hostname__in'] = 'hostname'
mbligh6444c6b2008-10-27 20:55:13 +0000241
242 if self.labels:
jamesrenc2863162010-07-12 21:20:51 +0000243 if len(self.labels) == 1:
mblighf703fb42009-01-30 00:35:05 +0000244 # This is needed for labels with wildcards (x86*)
jamesrenc2863162010-07-12 21:20:51 +0000245 filters['labels__name__in'] = self.labels
mblighf703fb42009-01-30 00:35:05 +0000246 check_results['labels__name__in'] = None
247 else:
jamesrenc2863162010-07-12 21:20:51 +0000248 filters['multiple_labels'] = self.labels
mblighf703fb42009-01-30 00:35:05 +0000249 check_results['multiple_labels'] = None
mbligh6444c6b2008-10-27 20:55:13 +0000250
mblighbe630eb2008-08-01 16:41:48 +0000251 if self.status:
mbligh6444c6b2008-10-27 20:55:13 +0000252 statuses = self.status.split(',')
253 statuses = [status.strip() for status in statuses
254 if status.strip()]
255
256 filters['status__in'] = statuses
mblighcd8eb972008-08-25 19:20:39 +0000257 check_results['status__in'] = None
mbligh6444c6b2008-10-27 20:55:13 +0000258
mbligh536a5242008-10-18 14:35:54 +0000259 if self.acl:
showardd9ac4452009-02-07 02:04:37 +0000260 filters['aclgroup__name'] = self.acl
261 check_results['aclgroup__name'] = None
mbligh536a5242008-10-18 14:35:54 +0000262 if self.user:
showardd9ac4452009-02-07 02:04:37 +0000263 filters['aclgroup__users__login'] = self.user
264 check_results['aclgroup__users__login'] = None
mbligh91e0efd2009-02-26 01:02:16 +0000265
266 if self.locked or self.unlocked:
267 filters['locked'] = self.locked
268 check_results['locked'] = None
269
mblighbe630eb2008-08-01 16:41:48 +0000270 return super(host_list, self).execute(op='get_hosts',
271 filters=filters,
272 check_results=check_results)
273
274
275 def output(self, results):
xixuand6011f12016-12-08 15:01:58 -0800276 """Print output of 'atest host list'.
277
278 @param results: the results to be printed.
279 """
Ningning Xiaea02ab12018-05-11 15:08:57 -0700280 if results and not self.skylab:
mblighbe630eb2008-08-01 16:41:48 +0000281 # Remove the platform from the labels.
282 for result in results:
283 result['labels'] = self._cleanup_labels(result['labels'],
284 result['platform'])
Ningning Xiaea02ab12018-05-11 15:08:57 -0700285 if self.skylab and self.full_output:
286 print results
287 return
288
289 if self.skylab:
290 results = device.convert_to_autotest_hosts(results)
291
mbligh70b9bf42008-11-18 15:00:46 +0000292 if self.hostnames_only:
mblighdf75f8b2008-11-18 19:07:42 +0000293 self.print_list(results, key='hostname')
mbligh70b9bf42008-11-18 15:00:46 +0000294 else:
Ningning Xiaea02ab12018-05-11 15:08:57 -0700295 keys = ['hostname', 'status', 'shard', 'locked', 'lock_reason',
296 'locked_by', 'platform', 'labels']
Prashanth Balasubramaniana5048562014-12-12 10:14:11 -0800297 super(host_list, self).output(results, keys=keys)
mblighbe630eb2008-08-01 16:41:48 +0000298
299
300class host_stat(host):
301 """atest host stat --mlist <file>|<hosts>"""
302 usage_action = 'stat'
303
304 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800305 """Execute 'atest host stat'."""
mblighbe630eb2008-08-01 16:41:48 +0000306 results = []
307 # Convert wildcards into real host stats.
308 existing_hosts = []
309 for host in self.hosts:
310 if host.endswith('*'):
311 stats = self.execute_rpc('get_hosts',
312 hostname__startswith=host.rstrip('*'))
313 if len(stats) == 0:
314 self.failure('No hosts matching %s' % host, item=host,
315 what_failed='Failed to stat')
316 continue
317 else:
318 stats = self.execute_rpc('get_hosts', hostname=host)
319 if len(stats) == 0:
320 self.failure('Unknown host %s' % host, item=host,
321 what_failed='Failed to stat')
322 continue
323 existing_hosts.extend(stats)
324
325 for stat in existing_hosts:
326 host = stat['hostname']
327 # The host exists, these should succeed
328 acls = self.execute_rpc('get_acl_groups', hosts__hostname=host)
329
330 labels = self.execute_rpc('get_labels', host__hostname=host)
Simran Basi0739d682015-02-25 16:22:56 -0800331 results.append([[stat], acls, labels, stat['attributes']])
mblighbe630eb2008-08-01 16:41:48 +0000332 return results
333
334
335 def output(self, results):
xixuand6011f12016-12-08 15:01:58 -0800336 """Print output of 'atest host stat'.
337
338 @param results: the results to be printed.
339 """
Simran Basi0739d682015-02-25 16:22:56 -0800340 for stats, acls, labels, attributes in results:
mblighbe630eb2008-08-01 16:41:48 +0000341 print '-'*5
342 self.print_fields(stats,
Aviv Kesheta40d1272017-11-16 00:56:35 -0800343 keys=['hostname', 'id', 'platform',
mblighe163b032008-10-18 14:30:27 +0000344 'status', 'locked', 'locked_by',
Matthew Sartori68186332015-04-27 17:19:53 -0700345 'lock_time', 'lock_reason', 'protection',])
mblighbe630eb2008-08-01 16:41:48 +0000346 self.print_by_ids(acls, 'ACLs', line_before=True)
347 labels = self._cleanup_labels(labels)
348 self.print_by_ids(labels, 'Labels', line_before=True)
Simran Basi0739d682015-02-25 16:22:56 -0800349 self.print_dict(attributes, 'Host Attributes', line_before=True)
mblighbe630eb2008-08-01 16:41:48 +0000350
351
Gregory Nisbet93fa5b52019-07-19 14:56:35 -0700352class host_get_migration_plan(host_stat):
353 """atest host get_migration_plan --mlist <file>|<hosts>"""
354 usage_action = "get_migration_plan"
355
356 def __init__(self):
357 super(host_get_migration_plan, self).__init__()
358 self.parser.add_option("--ratio", default=0.5, type=float, dest="ratio")
359 self.add_skylab_options()
360
361 def parse(self):
362 (options, leftover) = super(host_get_migration_plan, self).parse()
363 self.ratio = options.ratio
364 return (options, leftover)
365
366 def execute(self):
367 afe = frontend.AFE()
368 results = super(host_get_migration_plan, self).execute()
369 working = []
370 non_working = []
371 for stats, _, _, _ in results:
372 assert len(stats) == 1
373 stats = stats[0]
374 hostname = stats["hostname"]
375 now = time.time()
376 history = HostJobHistory.get_host_history(
377 afe=afe,
378 hostname=hostname,
379 start_time=now,
380 end_time=now - 24 * 60 * 60,
381 )
382 dut_status, _ = history.last_diagnosis()
383 if dut_status in [UNUSED, WORKING]:
384 working.append(hostname)
385 elif dut_status == BROKEN:
386 non_working.append(hostname)
387 elif dut_status == UNKNOWN:
388 # if it's unknown, randomly assign it to working or
389 # nonworking, since we don't know.
390 # The two choices aren't actually equiprobable, but it
391 # should be fine.
392 random.choice([working, non_working]).append(hostname)
393 else:
394 raise ValueError("unknown status %s" % dut_status)
395 working_transfer, working_retain = fair_partition.partition(working, self.ratio)
396 non_working_transfer, non_working_retain = \
397 fair_partition.partition(non_working, self.ratio)
398 return {
399 "transfer": working_transfer + non_working_transfer,
400 "retain": working_retain + non_working_retain,
401 }
402
403 def output(self, results):
404 print json.dumps(results, indent=4, sort_keys=True)
405
Gregory Nisbet99b61942019-07-08 09:43:06 -0700406
mblighbe630eb2008-08-01 16:41:48 +0000407class host_jobs(host):
mbligh1494eca2008-08-13 21:24:22 +0000408 """atest host jobs [--max-query] --mlist <file>|<hosts>"""
mblighbe630eb2008-08-01 16:41:48 +0000409 usage_action = 'jobs'
410
mbligh6996fe82008-08-13 00:32:27 +0000411 def __init__(self):
412 super(host_jobs, self).__init__()
413 self.parser.add_option('-q', '--max-query',
414 help='Limits the number of results '
415 '(20 by default)',
416 type='int', default=20)
417
418
419 def parse(self):
420 """Consume the specific options"""
mbligh9deeefa2009-05-01 23:11:08 +0000421 (options, leftover) = super(host_jobs, self).parse()
mbligh6996fe82008-08-13 00:32:27 +0000422 self.max_queries = options.max_query
423 return (options, leftover)
424
425
mblighbe630eb2008-08-01 16:41:48 +0000426 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800427 """Execute 'atest host jobs'."""
mblighbe630eb2008-08-01 16:41:48 +0000428 results = []
429 real_hosts = []
430 for host in self.hosts:
431 if host.endswith('*'):
432 stats = self.execute_rpc('get_hosts',
433 hostname__startswith=host.rstrip('*'))
434 if len(stats) == 0:
435 self.failure('No host matching %s' % host, item=host,
436 what_failed='Failed to stat')
437 [real_hosts.append(stat['hostname']) for stat in stats]
438 else:
439 real_hosts.append(host)
440
441 for host in real_hosts:
442 queue_entries = self.execute_rpc('get_host_queue_entries',
mbligh6996fe82008-08-13 00:32:27 +0000443 host__hostname=host,
mbligh1494eca2008-08-13 21:24:22 +0000444 query_limit=self.max_queries,
showard6958c722009-09-23 20:03:16 +0000445 sort_by=['-job__id'])
mblighbe630eb2008-08-01 16:41:48 +0000446 jobs = []
447 for entry in queue_entries:
448 job = {'job_id': entry['job']['id'],
449 'job_owner': entry['job']['owner'],
450 'job_name': entry['job']['name'],
451 'status': entry['status']}
452 jobs.append(job)
453 results.append((host, jobs))
454 return results
455
456
457 def output(self, results):
xixuand6011f12016-12-08 15:01:58 -0800458 """Print output of 'atest host jobs'.
459
460 @param results: the results to be printed.
461 """
mblighbe630eb2008-08-01 16:41:48 +0000462 for host, jobs in results:
463 print '-'*5
464 print 'Hostname: %s' % host
465 self.print_table(jobs, keys_header=['job_id',
showardbe0d8692009-08-20 23:42:44 +0000466 'job_owner',
467 'job_name',
468 'status'])
mblighbe630eb2008-08-01 16:41:48 +0000469
Justin Giorgi16bba562016-06-22 10:42:13 -0700470class BaseHostModCreate(host):
xixuand6011f12016-12-08 15:01:58 -0800471 """The base class for host_mod and host_create"""
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700472 # Matches one attribute=value pair
473 attribute_regex = r'(?P<attribute>\w+)=(?P<value>.+)?'
mblighbe630eb2008-08-01 16:41:48 +0000474
475 def __init__(self):
Justin Giorgi16bba562016-06-22 10:42:13 -0700476 """Add the options shared between host mod and host create actions."""
mblighbe630eb2008-08-01 16:41:48 +0000477 self.messages = []
Justin Giorgi16bba562016-06-22 10:42:13 -0700478 self.host_ids = {}
479 super(BaseHostModCreate, self).__init__()
mblighbe630eb2008-08-01 16:41:48 +0000480 self.parser.add_option('-l', '--lock',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700481 help='Lock hosts.',
mblighbe630eb2008-08-01 16:41:48 +0000482 action='store_true')
Matthew Sartori68186332015-04-27 17:19:53 -0700483 self.parser.add_option('-r', '--lock_reason',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700484 help='Reason for locking hosts.',
Matthew Sartori68186332015-04-27 17:19:53 -0700485 default='')
Ningning Xiaef35cb52018-05-04 17:58:20 -0700486 self.parser.add_option('-u', '--unlock',
487 help='Unlock hosts.',
488 action='store_true')
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700489
mblighe163b032008-10-18 14:30:27 +0000490 self.parser.add_option('-p', '--protection', type='choice',
mblighaed47e82009-03-17 19:06:18 +0000491 help=('Set the protection level on a host. '
Ningning Xiaef35cb52018-05-04 17:58:20 -0700492 'Must be one of: %s. %s' %
493 (', '.join('"%s"' % p
494 for p in self.protections),
495 skylab_utils.MSG_INVALID_IN_SKYLAB)),
mblighaed47e82009-03-17 19:06:18 +0000496 choices=self.protections)
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700497 self._attributes = []
498 self.parser.add_option('--attribute', '-i',
499 help=('Host attribute to add or change. Format '
500 'is <attribute>=<value>. Multiple '
501 'attributes can be set by passing the '
502 'argument multiple times. Attributes can '
503 'be unset by providing an empty value.'),
504 action='append')
mblighbe630eb2008-08-01 16:41:48 +0000505 self.parser.add_option('-b', '--labels',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700506 help=('Comma separated list of labels. '
507 'When --skylab is provided, a label must '
508 'be in the format of label-key:label-value'
509 ' (e.g., board:lumpy).'))
mblighbe630eb2008-08-01 16:41:48 +0000510 self.parser.add_option('-B', '--blist',
511 help='File listing the labels',
512 type='string',
513 metavar='LABEL_FLIST')
514 self.parser.add_option('-a', '--acls',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700515 help=('Comma separated list of ACLs. %s' %
516 skylab_utils.MSG_INVALID_IN_SKYLAB))
mblighbe630eb2008-08-01 16:41:48 +0000517 self.parser.add_option('-A', '--alist',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700518 help=('File listing the acls. %s' %
519 skylab_utils.MSG_INVALID_IN_SKYLAB),
mblighbe630eb2008-08-01 16:41:48 +0000520 type='string',
521 metavar='ACL_FLIST')
Justin Giorgi16bba562016-06-22 10:42:13 -0700522 self.parser.add_option('-t', '--platform',
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700523 help=('Sets the platform label. %s Please set '
524 'platform in labels (e.g., -b '
525 'platform:platform_name) with --skylab.' %
526 skylab_utils.MSG_INVALID_IN_SKYLAB))
mblighbe630eb2008-08-01 16:41:48 +0000527
528
529 def parse(self):
Justin Giorgi16bba562016-06-22 10:42:13 -0700530 """Consume the options common to host create and host mod.
531 """
mbligh9deeefa2009-05-01 23:11:08 +0000532 label_info = topic_common.item_parse_info(attribute_name='labels',
533 inline_option='labels',
534 filename_option='blist')
535 acl_info = topic_common.item_parse_info(attribute_name='acls',
536 inline_option='acls',
537 filename_option='alist')
538
Justin Giorgi16bba562016-06-22 10:42:13 -0700539 (options, leftover) = super(BaseHostModCreate, self).parse([label_info,
mbligh9deeefa2009-05-01 23:11:08 +0000540 acl_info],
541 req_items='hosts')
mblighbe630eb2008-08-01 16:41:48 +0000542
543 self._parse_lock_options(options)
Justin Giorgi16bba562016-06-22 10:42:13 -0700544
Ningning Xia64ced002018-05-16 17:36:20 -0700545 self.label_map = None
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700546 if self.allow_skylab and self.skylab:
Ningning Xiaef35cb52018-05-04 17:58:20 -0700547 # TODO(nxia): drop these flags when all hosts are migrated to skylab
Ningning Xia64ced002018-05-16 17:36:20 -0700548 if (options.protection or options.acls or options.alist or
549 options.platform):
550 self.invalid_syntax(
551 '--protection, --acls, --alist or --platform is not '
552 'supported with --skylab.')
553
554 if self.labels:
555 self.label_map = device.convert_to_label_map(self.labels)
Ningning Xiaef35cb52018-05-04 17:58:20 -0700556
mblighaed47e82009-03-17 19:06:18 +0000557 if options.protection:
558 self.data['protection'] = options.protection
Justin Giorgi16bba562016-06-22 10:42:13 -0700559 self.messages.append('Protection set to "%s"' % options.protection)
560
561 self.attributes = {}
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700562 if options.attribute:
563 for pair in options.attribute:
564 m = re.match(self.attribute_regex, pair)
565 if not m:
566 raise topic_common.CliError('Attribute must be in key=value '
567 'syntax.')
568 elif m.group('attribute') in self.attributes:
xixuand6011f12016-12-08 15:01:58 -0800569 raise topic_common.CliError(
570 'Multiple values provided for attribute '
571 '%s.' % m.group('attribute'))
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700572 self.attributes[m.group('attribute')] = m.group('value')
Justin Giorgi16bba562016-06-22 10:42:13 -0700573
574 self.platform = options.platform
mblighbe630eb2008-08-01 16:41:48 +0000575 return (options, leftover)
576
577
Justin Giorgi16bba562016-06-22 10:42:13 -0700578 def _set_acls(self, hosts, acls):
579 """Add hosts to acls (and remove from all other acls).
580
581 @param hosts: list of hostnames
582 @param acls: list of acl names
583 """
584 # Remove from all ACLs except 'Everyone' and ACLs in list
585 # Skip hosts that don't exist
586 for host in hosts:
587 if host not in self.host_ids:
588 continue
589 host_id = self.host_ids[host]
590 for a in self.execute_rpc('get_acl_groups', hosts=host_id):
591 if a['name'] not in self.acls and a['id'] != 1:
592 self.execute_rpc('acl_group_remove_hosts', id=a['id'],
593 hosts=self.hosts)
594
595 # Add hosts to the ACLs
596 self.check_and_create_items('get_acl_groups', 'add_acl_group',
597 self.acls)
598 for a in acls:
599 self.execute_rpc('acl_group_add_hosts', id=a, hosts=hosts)
600
601
602 def _remove_labels(self, host, condition):
603 """Remove all labels from host that meet condition(label).
604
605 @param host: hostname
606 @param condition: callable that returns bool when given a label
607 """
608 if host in self.host_ids:
609 host_id = self.host_ids[host]
610 labels_to_remove = []
611 for l in self.execute_rpc('get_labels', host=host_id):
612 if condition(l):
613 labels_to_remove.append(l['id'])
614 if labels_to_remove:
615 self.execute_rpc('host_remove_labels', id=host_id,
616 labels=labels_to_remove)
617
618
619 def _set_labels(self, host, labels):
620 """Apply labels to host (and remove all other labels).
621
622 @param host: hostname
623 @param labels: list of label names
624 """
625 condition = lambda l: l['name'] not in labels and not l['platform']
626 self._remove_labels(host, condition)
627 self.check_and_create_items('get_labels', 'add_label', labels)
628 self.execute_rpc('host_add_labels', id=host, labels=labels)
629
630
631 def _set_platform_label(self, host, platform_label):
632 """Apply the platform label to host (and remove existing).
633
634 @param host: hostname
635 @param platform_label: platform label's name
636 """
637 self._remove_labels(host, lambda l: l['platform'])
638 self.check_and_create_items('get_labels', 'add_label', [platform_label],
639 platform=True)
640 self.execute_rpc('host_add_labels', id=host, labels=[platform_label])
641
642
643 def _set_attributes(self, host, attributes):
644 """Set attributes on host.
645
646 @param host: hostname
647 @param attributes: attribute dictionary
648 """
649 for attr, value in self.attributes.iteritems():
650 self.execute_rpc('set_host_attribute', attribute=attr,
651 value=value, hostname=host)
652
653
654class host_mod(BaseHostModCreate):
655 """atest host mod [--lock|--unlock --force_modify_locking
656 --platform <arch>
657 --labels <labels>|--blist <label_file>
658 --acls <acls>|--alist <acl_file>
659 --protection <protection_type>
660 --attributes <attr>=<value>;<attr>=<value>
661 --mlist <mach_file>] <hosts>"""
662 usage_action = 'mod'
Justin Giorgi16bba562016-06-22 10:42:13 -0700663
664 def __init__(self):
665 """Add the options specific to the mod action"""
666 super(host_mod, self).__init__()
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700667 self.parser.add_option('--unlock-lock-id',
668 help=('Unlock the lock with the lock-id. %s' %
669 skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
670 default=None)
Justin Giorgi16bba562016-06-22 10:42:13 -0700671 self.parser.add_option('-f', '--force_modify_locking',
672 help='Forcefully lock\unlock a host',
673 action='store_true')
674 self.parser.add_option('--remove_acls',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700675 help=('Remove all active acls. %s' %
676 skylab_utils.MSG_INVALID_IN_SKYLAB),
Justin Giorgi16bba562016-06-22 10:42:13 -0700677 action='store_true')
678 self.parser.add_option('--remove_labels',
679 help='Remove all labels.',
680 action='store_true')
681
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700682 self.add_skylab_options()
Ningning Xia1dd82ee2018-06-08 11:41:03 -0700683 self.parser.add_option('--new-env',
684 dest='new_env',
685 choices=['staging', 'prod'],
686 help=('The new environment ("staging" or '
687 '"prod") of the hosts. %s' %
688 skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
689 default=None)
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700690
691
692 def _parse_unlock_options(self, options):
693 """Parse unlock related options."""
694 if self.skylab and options.unlock and options.unlock_lock_id is None:
695 self.invalid_syntax('Must provide --unlock-lock-id with "--skylab '
696 '--unlock".')
697
698 if (not (self.skylab and options.unlock) and
699 options.unlock_lock_id is not None):
700 self.invalid_syntax('--unlock-lock-id is only valid with '
701 '"--skylab --unlock".')
702
703 self.unlock_lock_id = options.unlock_lock_id
704
Justin Giorgi16bba562016-06-22 10:42:13 -0700705
706 def parse(self):
707 """Consume the specific options"""
708 (options, leftover) = super(host_mod, self).parse()
709
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700710 self._parse_unlock_options(options)
711
Justin Giorgi16bba562016-06-22 10:42:13 -0700712 if options.force_modify_locking:
713 self.data['force_modify_locking'] = True
714
Ningning Xiaef35cb52018-05-04 17:58:20 -0700715 if self.skylab and options.remove_acls:
716 # TODO(nxia): drop the flag when all hosts are migrated to skylab
717 self.invalid_syntax('--remove_acls is not supported with --skylab.')
718
Justin Giorgi16bba562016-06-22 10:42:13 -0700719 self.remove_acls = options.remove_acls
720 self.remove_labels = options.remove_labels
Ningning Xia1dd82ee2018-06-08 11:41:03 -0700721 self.new_env = options.new_env
Justin Giorgi16bba562016-06-22 10:42:13 -0700722
723 return (options, leftover)
724
725
Ningning Xiaef35cb52018-05-04 17:58:20 -0700726 def execute_skylab(self):
727 """Execute atest host mod with --skylab.
728
729 @return A list of hostnames which have been successfully modified.
730 """
731 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
732 inventory_repo.initialize()
733 data_dir = inventory_repo.get_data_dir()
734 lab = text_manager.load_lab(data_dir)
735
736 locked_by = None
737 if self.lock:
738 locked_by = inventory_repo.git_repo.config('user.email')
739
740 successes = []
741 for hostname in self.hosts:
742 try:
743 device.modify(
744 lab,
745 'duts',
746 hostname,
747 self.environment,
748 lock=self.lock,
749 locked_by=locked_by,
750 lock_reason = self.lock_reason,
751 unlock=self.unlock,
752 unlock_lock_id=self.unlock_lock_id,
753 attributes=self.attributes,
754 remove_labels=self.remove_labels,
Ningning Xia1dd82ee2018-06-08 11:41:03 -0700755 label_map=self.label_map,
756 new_env=self.new_env)
Ningning Xiaef35cb52018-05-04 17:58:20 -0700757 successes.append(hostname)
758 except device.SkylabDeviceActionError as e:
759 print('Cannot modify host %s: %s' % (hostname, e))
760
761 if successes:
762 text_manager.dump_lab(data_dir, lab)
763
764 status = inventory_repo.git_repo.status()
765 if not status:
766 print('Nothing is changed for hosts %s.' % successes)
767 return []
768
769 message = skylab_utils.construct_commit_message(
770 'Modify %d hosts.\n\n%s' % (len(successes), successes))
771 self.change_number = inventory_repo.upload_change(
772 message, draft=self.draft, dryrun=self.dryrun,
773 submit=self.submit)
774
775 return successes
776
777
Justin Giorgi16bba562016-06-22 10:42:13 -0700778 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800779 """Execute 'atest host mod'."""
Ningning Xiaef35cb52018-05-04 17:58:20 -0700780 if self.skylab:
781 return self.execute_skylab()
782
Justin Giorgi16bba562016-06-22 10:42:13 -0700783 successes = []
784 for host in self.execute_rpc('get_hosts', hostname__in=self.hosts):
785 self.host_ids[host['hostname']] = host['id']
786 for host in self.hosts:
787 if host not in self.host_ids:
788 self.failure('Cannot modify non-existant host %s.' % host)
789 continue
790 host_id = self.host_ids[host]
791
792 try:
793 if self.data:
794 self.execute_rpc('modify_host', item=host,
795 id=host, **self.data)
796
797 if self.attributes:
798 self._set_attributes(host, self.attributes)
799
800 if self.labels or self.remove_labels:
801 self._set_labels(host, self.labels)
802
803 if self.platform:
804 self._set_platform_label(host, self.platform)
805
806 # TODO: Make the AFE return True or False,
807 # especially for lock
808 successes.append(host)
809 except topic_common.CliError, full_error:
810 # Already logged by execute_rpc()
811 pass
812
813 if self.acls or self.remove_acls:
814 self._set_acls(self.hosts, self.acls)
815
816 return successes
817
818
819 def output(self, hosts):
xixuand6011f12016-12-08 15:01:58 -0800820 """Print output of 'atest host mod'.
821
822 @param hosts: the host list to be printed.
823 """
Justin Giorgi16bba562016-06-22 10:42:13 -0700824 for msg in self.messages:
825 self.print_wrapped(msg, hosts)
826
Ningning Xiaef35cb52018-05-04 17:58:20 -0700827 if hosts and self.skylab:
Ningning Xiac8b430d2018-05-17 15:10:25 -0700828 print('Modified hosts: %s.' % ', '.join(hosts))
Ningning Xiaef35cb52018-05-04 17:58:20 -0700829 if self.skylab and not self.dryrun and not self.submit:
Ningning Xiac8b430d2018-05-17 15:10:25 -0700830 print(skylab_utils.get_cl_message(self.change_number))
Ningning Xiaef35cb52018-05-04 17:58:20 -0700831
Justin Giorgi16bba562016-06-22 10:42:13 -0700832
833class HostInfo(object):
834 """Store host information so we don't have to keep looking it up."""
835 def __init__(self, hostname, platform, labels):
836 self.hostname = hostname
837 self.platform = platform
838 self.labels = labels
839
840
841class host_create(BaseHostModCreate):
842 """atest host create [--lock|--unlock --platform <arch>
843 --labels <labels>|--blist <label_file>
844 --acls <acls>|--alist <acl_file>
845 --protection <protection_type>
846 --attributes <attr>=<value>;<attr>=<value>
847 --mlist <mach_file>] <hosts>"""
848 usage_action = 'create'
849
850 def parse(self):
851 """Option logic specific to create action.
852 """
853 (options, leftovers) = super(host_create, self).parse()
854 self.locked = options.lock
855 if 'serials' in self.attributes:
856 if len(self.hosts) > 1:
857 raise topic_common.CliError('Can not specify serials with '
858 'multiple hosts.')
859
860
861 @classmethod
862 def construct_without_parse(
863 cls, web_server, hosts, platform=None,
864 locked=False, lock_reason='', labels=[], acls=[],
865 protection=host_protections.Protection.NO_PROTECTION):
866 """Construct a host_create object and fill in data from args.
867
868 Do not need to call parse after the construction.
869
870 Return an object of site_host_create ready to execute.
871
872 @param web_server: A string specifies the autotest webserver url.
873 It is needed to setup comm to make rpc.
874 @param hosts: A list of hostnames as strings.
875 @param platform: A string or None.
876 @param locked: A boolean.
877 @param lock_reason: A string.
878 @param labels: A list of labels as strings.
879 @param acls: A list of acls as strings.
880 @param protection: An enum defined in host_protections.
881 """
882 obj = cls()
883 obj.web_server = web_server
884 try:
885 # Setup stuff needed for afe comm.
886 obj.afe = rpc.afe_comm(web_server)
887 except rpc.AuthError, s:
888 obj.failure(str(s), fatal=True)
889 obj.hosts = hosts
890 obj.platform = platform
891 obj.locked = locked
892 if locked and lock_reason.strip():
893 obj.data['lock_reason'] = lock_reason.strip()
894 obj.labels = labels
895 obj.acls = acls
896 if protection:
897 obj.data['protection'] = protection
898 obj.attributes = {}
899 return obj
900
901
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700902 def _detect_host_info(self, host):
903 """Detect platform and labels from the host.
904
905 @param host: hostname
906
907 @return: HostInfo object
908 """
909 # Mock an afe_host object so that the host is constructed as if the
910 # data was already in afe
911 data = {'attributes': self.attributes, 'labels': self.labels}
912 afe_host = frontend.Host(None, data)
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700913 store = host_info.InMemoryHostInfoStore(
914 host_info.HostInfo(labels=self.labels,
915 attributes=self.attributes))
916 machine = {
917 'hostname': host,
918 'afe_host': afe_host,
919 'host_info_store': store
920 }
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700921 try:
Allen Li2c32d6b2017-02-03 15:28:10 -0800922 if bin_utils.ping(host, tries=1, deadline=1) == 0:
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700923 serials = self.attributes.get('serials', '').split(',')
Richard Barnette9db80682018-04-26 00:55:15 +0000924 adb_serial = self.attributes.get('serials')
925 host_dut = hosts.create_host(machine,
926 adb_serial=adb_serial)
xixuand6011f12016-12-08 15:01:58 -0800927
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700928 info = HostInfo(host, host_dut.get_platform(),
929 host_dut.get_labels())
xixuand6011f12016-12-08 15:01:58 -0800930 # Clean host to make sure nothing left after calling it,
931 # e.g. tunnels.
932 if hasattr(host_dut, 'close'):
933 host_dut.close()
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700934 else:
935 # Can't ping the host, use default information.
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700936 info = HostInfo(host, None, [])
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700937 except (socket.gaierror, error.AutoservRunError,
938 error.AutoservSSHTimeout):
939 # We may be adding a host that does not exist yet or we can't
940 # reach due to hostname/address issues or if the host is down.
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700941 info = HostInfo(host, None, [])
942 return info
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700943
944
mblighbe630eb2008-08-01 16:41:48 +0000945 def _execute_add_one_host(self, host):
mbligh719e14a2008-12-04 01:17:08 +0000946 # Always add the hosts as locked to avoid the host
Matthew Sartori68186332015-04-27 17:19:53 -0700947 # being picked up by the scheduler before it's ACL'ed.
mbligh719e14a2008-12-04 01:17:08 +0000948 self.data['locked'] = True
Matthew Sartori68186332015-04-27 17:19:53 -0700949 if not self.locked:
950 self.data['lock_reason'] = 'Forced lock on device creation'
Justin Giorgi16bba562016-06-22 10:42:13 -0700951 self.execute_rpc('add_host', hostname=host, status="Ready", **self.data)
mblighbe630eb2008-08-01 16:41:48 +0000952
Justin Giorgi16bba562016-06-22 10:42:13 -0700953 # If there are labels avaliable for host, use them.
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700954 info = self._detect_host_info(host)
Justin Giorgi16bba562016-06-22 10:42:13 -0700955 labels = set(self.labels)
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700956 if info.labels:
957 labels.update(info.labels)
Justin Giorgi16bba562016-06-22 10:42:13 -0700958
959 if labels:
960 self._set_labels(host, list(labels))
961
962 # Now add the platform label.
963 # If a platform was not provided and we were able to retrieve it
964 # from the host, use the retrieved platform.
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700965 platform = self.platform if self.platform else info.platform
Justin Giorgi16bba562016-06-22 10:42:13 -0700966 if platform:
967 self._set_platform_label(host, platform)
968
969 if self.attributes:
970 self._set_attributes(host, self.attributes)
mblighbe630eb2008-08-01 16:41:48 +0000971
972
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700973 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800974 """Execute 'atest host create'."""
Justin Giorgi16bba562016-06-22 10:42:13 -0700975 successful_hosts = []
976 for host in self.hosts:
977 try:
978 self._execute_add_one_host(host)
979 successful_hosts.append(host)
980 except topic_common.CliError:
981 pass
Jiaxi Luoc342f9f2014-05-19 16:22:03 -0700982
983 if successful_hosts:
Justin Giorgi16bba562016-06-22 10:42:13 -0700984 self._set_acls(successful_hosts, self.acls)
Jiaxi Luoc342f9f2014-05-19 16:22:03 -0700985
986 if not self.locked:
987 for host in successful_hosts:
Matthew Sartori68186332015-04-27 17:19:53 -0700988 self.execute_rpc('modify_host', id=host, locked=False,
989 lock_reason='')
Jiaxi Luoc342f9f2014-05-19 16:22:03 -0700990 return successful_hosts
991
992
mblighbe630eb2008-08-01 16:41:48 +0000993 def output(self, hosts):
xixuand6011f12016-12-08 15:01:58 -0800994 """Print output of 'atest host create'.
995
996 @param hosts: the added host list to be printed.
997 """
mblighbe630eb2008-08-01 16:41:48 +0000998 self.print_wrapped('Added host', hosts)
999
1000
1001class host_delete(action_common.atest_delete, host):
1002 """atest host delete [--mlist <mach_file>] <hosts>"""
Ningning Xiac8b430d2018-05-17 15:10:25 -07001003
1004 def __init__(self):
1005 super(host_delete, self).__init__()
1006
1007 self.add_skylab_options()
1008
1009
1010 def execute_skylab(self):
1011 """Execute 'atest host delete' with '--skylab'.
1012
1013 @return A list of hostnames which have been successfully deleted.
1014 """
1015 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
1016 inventory_repo.initialize()
1017 data_dir = inventory_repo.get_data_dir()
1018 lab = text_manager.load_lab(data_dir)
1019
1020 successes = []
1021 for hostname in self.hosts:
1022 try:
1023 device.delete(
1024 lab,
1025 'duts',
1026 hostname,
1027 self.environment)
1028 successes.append(hostname)
1029 except device.SkylabDeviceActionError as e:
1030 print('Cannot delete host %s: %s' % (hostname, e))
1031
1032 if successes:
1033 text_manager.dump_lab(data_dir, lab)
1034 message = skylab_utils.construct_commit_message(
1035 'Delete %d hosts.\n\n%s' % (len(successes), successes))
1036 self.change_number = inventory_repo.upload_change(
1037 message, draft=self.draft, dryrun=self.dryrun,
1038 submit=self.submit)
1039
1040 return successes
1041
1042
1043 def execute(self):
1044 """Execute 'atest host delete'.
1045
1046 @return A list of hostnames which have been successfully deleted.
1047 """
1048 if self.skylab:
1049 return self.execute_skylab()
1050
1051 return super(host_delete, self).execute()
Ningning Xiac46bdd12018-05-29 11:24:14 -07001052
1053
1054class InvalidHostnameError(Exception):
1055 """Cannot perform actions on the host because of invalid hostname."""
1056
1057
1058def _add_hostname_suffix(hostname, suffix):
1059 """Add the suffix to the hostname."""
1060 if hostname.endswith(suffix):
1061 raise InvalidHostnameError(
1062 'Cannot add "%s" as it already contains the suffix.' % suffix)
1063
1064 return hostname + suffix
1065
1066
Gregory Nisbet48f1eea2019-08-05 16:18:56 -07001067def _remove_hostname_suffix_if_present(hostname, suffix):
Ningning Xiac46bdd12018-05-29 11:24:14 -07001068 """Remove the suffix from the hostname."""
Gregory Nisbet48f1eea2019-08-05 16:18:56 -07001069 if hostname.endswith(suffix):
1070 return hostname[:len(hostname) - len(suffix)]
1071 else:
1072 return hostname
Ningning Xiac46bdd12018-05-29 11:24:14 -07001073
1074
1075class host_rename(host):
1076 """Host rename is only for migrating hosts between skylab and AFE DB."""
1077
1078 usage_action = 'rename'
1079
1080 def __init__(self):
1081 """Add the options specific to the rename action."""
1082 super(host_rename, self).__init__()
1083
1084 self.parser.add_option('--for-migration',
1085 help=('Rename hostnames for migration. Rename '
1086 'each "hostname" to "hostname%s". '
1087 'The original "hostname" must not contain '
1088 'suffix.' % MIGRATED_HOST_SUFFIX),
1089 action='store_true',
1090 default=False)
1091 self.parser.add_option('--for-rollback',
1092 help=('Rename hostnames for migration rollback. '
1093 'Rename each "hostname%s" to its original '
1094 '"hostname".' % MIGRATED_HOST_SUFFIX),
1095 action='store_true',
1096 default=False)
1097 self.parser.add_option('--dryrun',
1098 help='Execute the action as a dryrun.',
1099 action='store_true',
1100 default=False)
Gregory Nisbetee457f62019-07-08 15:47:26 -07001101 self.parser.add_option('--non-interactive',
1102 help='run non-interactively',
Gregory Nisbet9744ea92019-07-02 12:36:59 -07001103 action='store_true',
1104 default=False)
Ningning Xiac46bdd12018-05-29 11:24:14 -07001105
1106
1107 def parse(self):
1108 """Consume the options common to host rename."""
1109 (options, leftovers) = super(host_rename, self).parse()
1110 self.for_migration = options.for_migration
1111 self.for_rollback = options.for_rollback
1112 self.dryrun = options.dryrun
Gregory Nisbetee457f62019-07-08 15:47:26 -07001113 self.interactive = not options.non_interactive
Ningning Xiac46bdd12018-05-29 11:24:14 -07001114 self.host_ids = {}
1115
1116 if not (self.for_migration ^ self.for_rollback):
1117 self.invalid_syntax('--for-migration and --for-rollback are '
1118 'exclusive, and one of them must be enabled.')
1119
1120 if not self.hosts:
1121 self.invalid_syntax('Must provide hostname(s).')
1122
1123 if self.dryrun:
1124 print('This will be a dryrun and will not rename hostnames.')
1125
1126 return (options, leftovers)
1127
1128
1129 def execute(self):
1130 """Execute 'atest host rename'."""
Gregory Nisbetee457f62019-07-08 15:47:26 -07001131 if self.interactive:
1132 if self.prompt_confirmation():
1133 pass
1134 else:
1135 return
Ningning Xiac46bdd12018-05-29 11:24:14 -07001136
1137 successes = []
1138 for host in self.execute_rpc('get_hosts', hostname__in=self.hosts):
1139 self.host_ids[host['hostname']] = host['id']
1140 for host in self.hosts:
1141 if host not in self.host_ids:
1142 self.failure('Cannot rename non-existant host %s.' % host,
1143 item=host, what_failed='Failed to rename')
1144 continue
1145 try:
Ningning Xiab261b162018-06-07 17:24:59 -07001146 host_id = self.host_ids[host]
Ningning Xiac46bdd12018-05-29 11:24:14 -07001147 if self.for_migration:
1148 new_hostname = _add_hostname_suffix(
1149 host, MIGRATED_HOST_SUFFIX)
1150 else:
1151 #for_rollback
Gregory Nisbet917ee332019-09-05 11:41:23 -07001152 new_hostname = _remove_hostname_suffix_if_present(
Ningning Xiac46bdd12018-05-29 11:24:14 -07001153 host, MIGRATED_HOST_SUFFIX)
1154
1155 if not self.dryrun:
Ningning Xia423c7962018-06-07 15:27:18 -07001156 # TODO(crbug.com/850737): delete and abort HQE.
Ningning Xiac46bdd12018-05-29 11:24:14 -07001157 data = {'hostname': new_hostname}
Ningning Xiab261b162018-06-07 17:24:59 -07001158 self.execute_rpc('modify_host', item=host, id=host_id,
1159 **data)
Ningning Xiac46bdd12018-05-29 11:24:14 -07001160 successes.append((host, new_hostname))
1161 except InvalidHostnameError as e:
1162 self.failure('Cannot rename host %s: %s' % (host, e), item=host,
1163 what_failed='Failed to rename')
1164 except topic_common.CliError, full_error:
1165 # Already logged by execute_rpc()
1166 pass
1167
1168 return successes
1169
1170
1171 def output(self, results):
1172 """Print output of 'atest host rename'."""
1173 if results:
1174 print('Successfully renamed:')
1175 for old_hostname, new_hostname in results:
1176 print('%s to %s' % (old_hostname, new_hostname))
Ningning Xia9df3d152018-05-23 17:15:14 -07001177
1178
1179class host_migrate(action_common.atest_list, host):
1180 """'atest host migrate' to migrate or rollback hosts."""
1181
1182 usage_action = 'migrate'
1183
1184 def __init__(self):
1185 super(host_migrate, self).__init__()
1186
1187 self.parser.add_option('--migration',
1188 dest='migration',
1189 help='Migrate the hosts to skylab.',
1190 action='store_true',
1191 default=False)
1192 self.parser.add_option('--rollback',
1193 dest='rollback',
1194 help='Rollback the hosts migrated to skylab.',
1195 action='store_true',
1196 default=False)
1197 self.parser.add_option('--model',
1198 help='Model of the hosts to migrate.',
1199 dest='model',
1200 default=None)
Xixuan Wu84621e52018-08-28 14:29:53 -07001201 self.parser.add_option('--board',
1202 help='Board of the hosts to migrate.',
1203 dest='board',
1204 default=None)
Ningning Xia9df3d152018-05-23 17:15:14 -07001205 self.parser.add_option('--pool',
1206 help=('Pool of the hosts to migrate. Must '
1207 'specify --model for the pool.'),
1208 dest='pool',
1209 default=None)
1210
1211 self.add_skylab_options(enforce_skylab=True)
1212
1213
1214 def parse(self):
1215 """Consume the specific options"""
1216 (options, leftover) = super(host_migrate, self).parse()
1217
1218 self.migration = options.migration
1219 self.rollback = options.rollback
1220 self.model = options.model
1221 self.pool = options.pool
Xixuan Wu84621e52018-08-28 14:29:53 -07001222 self.board = options.board
Ningning Xia9df3d152018-05-23 17:15:14 -07001223 self.host_ids = {}
1224
1225 if not (self.migration ^ self.rollback):
1226 self.invalid_syntax('--migration and --rollback are exclusive, '
1227 'and one of them must be enabled.')
1228
Xixuan Wu84621e52018-08-28 14:29:53 -07001229 if self.pool is not None and (self.model is None and
1230 self.board is None):
1231 self.invalid_syntax('Must provide --model or --board with --pool.')
Ningning Xia9df3d152018-05-23 17:15:14 -07001232
Xixuan Wu84621e52018-08-28 14:29:53 -07001233 if not self.hosts and not (self.model or self.board):
1234 self.invalid_syntax('Must provide hosts or --model or --board.')
Ningning Xia9df3d152018-05-23 17:15:14 -07001235
1236 return (options, leftover)
1237
1238
Ningning Xia56d68432018-06-06 17:28:20 -07001239 def _remove_invalid_hostnames(self, hostnames, log_failure=False):
1240 """Remove hostnames with MIGRATED_HOST_SUFFIX.
1241
1242 @param hostnames: A list of hostnames.
1243 @param log_failure: Bool indicating whether to log invalid hostsnames.
1244
1245 @return A list of valid hostnames.
1246 """
1247 invalid_hostnames = set()
Ningning Xia9df3d152018-05-23 17:15:14 -07001248 for hostname in hostnames:
1249 if hostname.endswith(MIGRATED_HOST_SUFFIX):
Ningning Xia56d68432018-06-06 17:28:20 -07001250 if log_failure:
1251 self.failure('Cannot migrate host with suffix "%s" %s.' %
1252 (MIGRATED_HOST_SUFFIX, hostname),
1253 item=hostname, what_failed='Failed to rename')
1254 invalid_hostnames.add(hostname)
1255
1256 hostnames = list(set(hostnames) - invalid_hostnames)
1257
1258 return hostnames
1259
1260
1261 def execute(self):
1262 """Execute 'atest host migrate'."""
1263 hostnames = self._remove_invalid_hostnames(self.hosts, log_failure=True)
Ningning Xia9df3d152018-05-23 17:15:14 -07001264
1265 filters = {}
1266 check_results = {}
1267 if hostnames:
1268 check_results['hostname__in'] = 'hostname'
1269 if self.migration:
1270 filters['hostname__in'] = hostnames
1271 else:
1272 # rollback
1273 hostnames_with_suffix = [
1274 _add_hostname_suffix(h, MIGRATED_HOST_SUFFIX)
1275 for h in hostnames]
1276 filters['hostname__in'] = hostnames_with_suffix
Ningning Xia56d68432018-06-06 17:28:20 -07001277 else:
1278 # TODO(nxia): add exclude_filter {'hostname__endswith':
1279 # MIGRATED_HOST_SUFFIX} for --migration
1280 if self.rollback:
1281 filters['hostname__endswith'] = MIGRATED_HOST_SUFFIX
Ningning Xia9df3d152018-05-23 17:15:14 -07001282
1283 labels = []
1284 if self.model:
1285 labels.append('model:%s' % self.model)
1286 if self.pool:
1287 labels.append('pool:%s' % self.pool)
Xixuan Wu84621e52018-08-28 14:29:53 -07001288 if self.board:
1289 labels.append('board:%s' % self.board)
Ningning Xia9df3d152018-05-23 17:15:14 -07001290
1291 if labels:
1292 if len(labels) == 1:
1293 filters['labels__name__in'] = labels
1294 check_results['labels__name__in'] = None
1295 else:
1296 filters['multiple_labels'] = labels
1297 check_results['multiple_labels'] = None
1298
1299 results = super(host_migrate, self).execute(
1300 op='get_hosts', filters=filters, check_results=check_results)
1301 hostnames = [h['hostname'] for h in results]
1302
Ningning Xia56d68432018-06-06 17:28:20 -07001303 if self.migration:
1304 hostnames = self._remove_invalid_hostnames(hostnames)
1305 else:
1306 # rollback
1307 hostnames = [_remove_hostname_suffix(h, MIGRATED_HOST_SUFFIX)
1308 for h in hostnames]
1309
Ningning Xia9df3d152018-05-23 17:15:14 -07001310 return self.execute_skylab_migration(hostnames)
1311
1312
Xixuan Wua8d93c82018-08-03 16:23:26 -07001313 def assign_duts_to_drone(self, infra, devices, environment):
Ningning Xia877817f2018-06-08 12:23:48 -07001314 """Assign uids of the devices to a random skylab drone.
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001315
1316 @param infra: An instance of lab_pb2.Infrastructure.
Xixuan Wua8d93c82018-08-03 16:23:26 -07001317 @param devices: A list of device_pb2.Device to be assigned to the drone.
1318 @param environment: 'staging' or 'prod'.
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001319 """
1320 skylab_drones = skylab_server.get_servers(
Xixuan Wua8d93c82018-08-03 16:23:26 -07001321 infra, environment, role='skylab_drone', status='primary')
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001322
1323 if len(skylab_drones) == 0:
1324 raise device.SkylabDeviceActionError(
1325 'No skylab drone is found in primary status and staging '
1326 'environment. Please confirm there is at least one valid skylab'
1327 ' drone added in skylab inventory.')
1328
Xixuan Wudfc2de22018-08-06 09:29:01 -07001329 for device in devices:
1330 # Randomly distribute each device to a skylab_drone.
1331 skylab_drone = random.choice(skylab_drones)
1332 skylab_server.add_dut_uids(skylab_drone, [device])
Ningning Xia877817f2018-06-08 12:23:48 -07001333
1334
1335 def remove_duts_from_drone(self, infra, devices):
1336 """Remove uids of the devices from their skylab drones.
1337
1338 @param infra: An instance of lab_pb2.Infrastructure.
1339 @devices: A list of device_pb2.Device to be remove from the drone.
1340 """
1341 skylab_drones = skylab_server.get_servers(
1342 infra, 'staging', role='skylab_drone', status='primary')
1343
1344 for skylab_drone in skylab_drones:
1345 skylab_server.remove_dut_uids(skylab_drone, devices)
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001346
1347
Ningning Xia9df3d152018-05-23 17:15:14 -07001348 def execute_skylab_migration(self, hostnames):
1349 """Execute migration in skylab_inventory.
1350
1351 @param hostnames: A list of hostnames to migrate.
1352 @return If there're hosts to migrate, return a list of the hostnames and
1353 a message instructing actions after the migration; else return
1354 None.
1355 """
1356 if not hostnames:
1357 return
1358
1359 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
1360 inventory_repo.initialize()
1361
1362 subdirs = ['skylab', 'prod', 'staging']
1363 data_dirs = skylab_data_dir, prod_data_dir, staging_data_dir = [
1364 inventory_repo.get_data_dir(data_subdir=d) for d in subdirs]
1365 skylab_lab, prod_lab, staging_lab = [
1366 text_manager.load_lab(d) for d in data_dirs]
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001367 infra = text_manager.load_infrastructure(skylab_data_dir)
Ningning Xia9df3d152018-05-23 17:15:14 -07001368
1369 label_map = None
1370 labels = []
Xixuan Wu46f8e202018-12-13 14:40:58 -08001371 if self.board:
1372 labels.append('board:%s' % self.board)
Ningning Xia9df3d152018-05-23 17:15:14 -07001373 if self.model:
Xixuan Wu46f8e202018-12-13 14:40:58 -08001374 labels.append('model:%s' % self.model)
Ningning Xia9df3d152018-05-23 17:15:14 -07001375 if self.pool:
1376 labels.append('critical_pool:%s' % self.pool)
1377 if labels:
1378 label_map = device.convert_to_label_map(labels)
1379
1380 if self.migration:
1381 prod_devices = device.move_devices(
1382 prod_lab, skylab_lab, 'duts', label_map=label_map,
1383 hostnames=hostnames)
1384 staging_devices = device.move_devices(
1385 staging_lab, skylab_lab, 'duts', label_map=label_map,
1386 hostnames=hostnames)
1387
1388 all_devices = prod_devices + staging_devices
1389 # Hostnames in afe_hosts tabel.
1390 device_hostnames = [str(d.common.hostname) for d in all_devices]
1391 message = (
1392 'Migration: move %s hosts into skylab_inventory.\n\n'
Ningning Xia423c7962018-06-07 15:27:18 -07001393 'Please run this command after the CL is submitted:\n'
Ningning Xia9df3d152018-05-23 17:15:14 -07001394 'atest host rename --for-migration %s' %
1395 (len(all_devices), ' '.join(device_hostnames)))
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001396
Xixuan Wua8d93c82018-08-03 16:23:26 -07001397 self.assign_duts_to_drone(infra, prod_devices, 'prod')
1398 self.assign_duts_to_drone(infra, staging_devices, 'staging')
Ningning Xia9df3d152018-05-23 17:15:14 -07001399 else:
1400 # rollback
1401 prod_devices = device.move_devices(
1402 skylab_lab, prod_lab, 'duts', environment='prod',
Ningning Xia56d68432018-06-06 17:28:20 -07001403 label_map=label_map, hostnames=hostnames)
Ningning Xia9df3d152018-05-23 17:15:14 -07001404 staging_devices = device.move_devices(
Ningning Xia877817f2018-06-08 12:23:48 -07001405 skylab_lab, staging_lab, 'duts', environment='staging',
Ningning Xia56d68432018-06-06 17:28:20 -07001406 label_map=label_map, hostnames=hostnames)
Ningning Xia9df3d152018-05-23 17:15:14 -07001407
1408 all_devices = prod_devices + staging_devices
1409 # Hostnames in afe_hosts tabel.
1410 device_hostnames = [_add_hostname_suffix(str(d.common.hostname),
1411 MIGRATED_HOST_SUFFIX)
1412 for d in all_devices]
1413 message = (
1414 'Rollback: remove %s hosts from skylab_inventory.\n\n'
Ningning Xia423c7962018-06-07 15:27:18 -07001415 'Please run this command after the CL is submitted:\n'
Ningning Xia9df3d152018-05-23 17:15:14 -07001416 'atest host rename --for-rollback %s' %
1417 (len(all_devices), ' '.join(device_hostnames)))
1418
Ningning Xia877817f2018-06-08 12:23:48 -07001419 self.remove_duts_from_drone(infra, all_devices)
1420
Ningning Xia9df3d152018-05-23 17:15:14 -07001421 if all_devices:
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001422 text_manager.dump_infrastructure(skylab_data_dir, infra)
1423
Ningning Xia9df3d152018-05-23 17:15:14 -07001424 if prod_devices:
1425 text_manager.dump_lab(prod_data_dir, prod_lab)
1426
1427 if staging_devices:
1428 text_manager.dump_lab(staging_data_dir, staging_lab)
1429
1430 text_manager.dump_lab(skylab_data_dir, skylab_lab)
1431
1432 self.change_number = inventory_repo.upload_change(
1433 message, draft=self.draft, dryrun=self.dryrun,
1434 submit=self.submit)
1435
1436 return all_devices, message
1437
1438
1439 def output(self, result):
1440 """Print output of 'atest host list'.
1441
1442 @param result: the result to be printed.
1443 """
1444 if result:
1445 devices, message = result
1446
1447 if devices:
1448 hostnames = [h.common.hostname for h in devices]
1449 if self.migration:
1450 print('Migrating hosts: %s' % ','.join(hostnames))
1451 else:
1452 # rollback
1453 print('Rolling back hosts: %s' % ','.join(hostnames))
1454
1455 if not self.dryrun:
1456 if not self.submit:
1457 print(skylab_utils.get_cl_message(self.change_number))
1458 else:
1459 # Print the instruction command for renaming hosts.
1460 print('%s' % message)
1461 else:
1462 print('No hosts were migrated.')