blob: c49e1f3c7f8129d3a0118548b39732572c78b018 [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
Ningning Xiaffb3c1f2018-06-07 18:42:32 -070023import random
Simran Basi0739d682015-02-25 16:22:56 -080024import re
Justin Giorgi16bba562016-06-22 10:42:13 -070025import socket
mblighbe630eb2008-08-01 16:41:48 +000026
Ningning Xiaea02ab12018-05-11 15:08:57 -070027from autotest_lib.cli import action_common, rpc, topic_common, skylab_utils
Allen Li2c32d6b2017-02-03 15:28:10 -080028from autotest_lib.client.bin import utils as bin_utils
Justin Giorgi16bba562016-06-22 10:42:13 -070029from autotest_lib.client.common_lib import error, host_protections
Justin Giorgi5208eaa2016-07-02 20:12:12 -070030from autotest_lib.server import frontend, hosts
Prathmesh Prabhud252d262017-05-31 11:46:34 -070031from autotest_lib.server.hosts import host_info
mblighbe630eb2008-08-01 16:41:48 +000032
33
Ningning Xiaea02ab12018-05-11 15:08:57 -070034try:
35 from skylab_inventory import text_manager
36 from skylab_inventory.lib import device
Ningning Xiaffb3c1f2018-06-07 18:42:32 -070037 from skylab_inventory.lib import server as skylab_server
Ningning Xiaea02ab12018-05-11 15:08:57 -070038except ImportError:
39 pass
40
41
Ningning Xiac46bdd12018-05-29 11:24:14 -070042MIGRATED_HOST_SUFFIX = '-migrated-do-not-use'
43
44
mblighbe630eb2008-08-01 16:41:48 +000045class host(topic_common.atest):
46 """Host class
Ningning Xia9df3d152018-05-23 17:15:14 -070047 atest host [create|delete|list|stat|mod|jobs|rename|migrate] <options>"""
48 usage_action = '[create|delete|list|stat|mod|jobs|rename|migrate]'
mblighbe630eb2008-08-01 16:41:48 +000049 topic = msg_topic = 'host'
50 msg_items = '<hosts>'
51
mblighaed47e82009-03-17 19:06:18 +000052 protections = host_protections.Protection.names
53
mblighbe630eb2008-08-01 16:41:48 +000054
55 def __init__(self):
56 """Add to the parser the options common to all the
57 host actions"""
58 super(host, self).__init__()
59
60 self.parser.add_option('-M', '--mlist',
61 help='File listing the machines',
62 type='string',
63 default=None,
64 metavar='MACHINE_FLIST')
65
mbligh9deeefa2009-05-01 23:11:08 +000066 self.topic_parse_info = topic_common.item_parse_info(
67 attribute_name='hosts',
68 filename_option='mlist',
69 use_leftover=True)
mblighbe630eb2008-08-01 16:41:48 +000070
71
72 def _parse_lock_options(self, options):
73 if options.lock and options.unlock:
74 self.invalid_syntax('Only specify one of '
75 '--lock and --unlock.')
76
Ningning Xiaef35cb52018-05-04 17:58:20 -070077 self.lock = options.lock
78 self.unlock = options.unlock
79 self.lock_reason = options.lock_reason
Ningning Xiaef35cb52018-05-04 17:58:20 -070080
mblighbe630eb2008-08-01 16:41:48 +000081 if options.lock:
82 self.data['locked'] = True
83 self.messages.append('Locked host')
84 elif options.unlock:
85 self.data['locked'] = False
Matthew Sartori68186332015-04-27 17:19:53 -070086 self.data['lock_reason'] = ''
mblighbe630eb2008-08-01 16:41:48 +000087 self.messages.append('Unlocked host')
88
Matthew Sartori68186332015-04-27 17:19:53 -070089 if options.lock and options.lock_reason:
90 self.data['lock_reason'] = options.lock_reason
91
mblighbe630eb2008-08-01 16:41:48 +000092
93 def _cleanup_labels(self, labels, platform=None):
94 """Removes the platform label from the overall labels"""
95 if platform:
96 return [label for label in labels
97 if label != platform]
98 else:
99 try:
100 return [label for label in labels
101 if not label['platform']]
102 except TypeError:
103 # This is a hack - the server will soon
104 # do this, so all this code should be removed.
105 return labels
106
107
108 def get_items(self):
109 return self.hosts
110
111
112class host_help(host):
113 """Just here to get the atest logic working.
114 Usage is set by its parent"""
115 pass
116
117
118class host_list(action_common.atest_list, host):
119 """atest host list [--mlist <file>|<hosts>] [--label <label>]
mbligh536a5242008-10-18 14:35:54 +0000120 [--status <status1,status2>] [--acl <ACL>] [--user <user>]"""
mblighbe630eb2008-08-01 16:41:48 +0000121
122 def __init__(self):
123 super(host_list, self).__init__()
124
125 self.parser.add_option('-b', '--label',
mbligh6444c6b2008-10-27 20:55:13 +0000126 default='',
127 help='Only list hosts with all these labels '
Ningning Xiaea02ab12018-05-11 15:08:57 -0700128 '(comma separated). When --skylab is provided, '
129 'a label must be in the format of '
130 'label-key:label-value (e.g., board:lumpy).')
mblighbe630eb2008-08-01 16:41:48 +0000131 self.parser.add_option('-s', '--status',
mbligh6444c6b2008-10-27 20:55:13 +0000132 default='',
133 help='Only list hosts with any of these '
134 'statuses (comma separated)')
mbligh536a5242008-10-18 14:35:54 +0000135 self.parser.add_option('-a', '--acl',
mbligh6444c6b2008-10-27 20:55:13 +0000136 default='',
Ningning Xiaea02ab12018-05-11 15:08:57 -0700137 help=('Only list hosts within this ACL. %s' %
138 skylab_utils.MSG_INVALID_IN_SKYLAB))
mbligh536a5242008-10-18 14:35:54 +0000139 self.parser.add_option('-u', '--user',
mbligh6444c6b2008-10-27 20:55:13 +0000140 default='',
Ningning Xiaea02ab12018-05-11 15:08:57 -0700141 help=('Only list hosts available to this user. '
142 '%s' % skylab_utils.MSG_INVALID_IN_SKYLAB))
mbligh70b9bf42008-11-18 15:00:46 +0000143 self.parser.add_option('-N', '--hostnames-only', help='Only return '
144 'hostnames for the machines queried.',
145 action='store_true')
mbligh91e0efd2009-02-26 01:02:16 +0000146 self.parser.add_option('--locked',
147 default=False,
148 help='Only list locked hosts',
149 action='store_true')
150 self.parser.add_option('--unlocked',
151 default=False,
152 help='Only list unlocked hosts',
153 action='store_true')
Ningning Xiaea02ab12018-05-11 15:08:57 -0700154 self.parser.add_option('--full-output',
155 default=False,
156 help=('Print out the full content of the hosts. '
157 'Only supported with --skylab.'),
158 action='store_true',
159 dest='full_output')
mbligh91e0efd2009-02-26 01:02:16 +0000160
Ningning Xiaea02ab12018-05-11 15:08:57 -0700161 self.add_skylab_options()
mblighbe630eb2008-08-01 16:41:48 +0000162
163
164 def parse(self):
165 """Consume the specific options"""
jamesrenc2863162010-07-12 21:20:51 +0000166 label_info = topic_common.item_parse_info(attribute_name='labels',
167 inline_option='label')
168
169 (options, leftover) = super(host_list, self).parse([label_info])
170
mblighbe630eb2008-08-01 16:41:48 +0000171 self.status = options.status
mbligh536a5242008-10-18 14:35:54 +0000172 self.acl = options.acl
173 self.user = options.user
mbligh70b9bf42008-11-18 15:00:46 +0000174 self.hostnames_only = options.hostnames_only
mbligh91e0efd2009-02-26 01:02:16 +0000175
176 if options.locked and options.unlocked:
177 self.invalid_syntax('--locked and --unlocked are '
178 'mutually exclusive')
Ningning Xiaea02ab12018-05-11 15:08:57 -0700179
mbligh91e0efd2009-02-26 01:02:16 +0000180 self.locked = options.locked
181 self.unlocked = options.unlocked
Ningning Xia64ced002018-05-16 17:36:20 -0700182 self.label_map = None
Ningning Xiaea02ab12018-05-11 15:08:57 -0700183
184 if self.skylab:
185 if options.user or options.acl or options.status:
186 self.invalid_syntax('--user, --acl or --status is not '
187 'supported with --skylab.')
188 self.full_output = options.full_output
189 if self.full_output and self.hostnames_only:
190 self.invalid_syntax('--full-output is conflicted with '
191 '--hostnames-only.')
Ningning Xia64ced002018-05-16 17:36:20 -0700192
193 if self.labels:
194 self.label_map = device.convert_to_label_map(self.labels)
Ningning Xiaea02ab12018-05-11 15:08:57 -0700195 else:
196 if options.full_output:
197 self.invalid_syntax('--full_output is only supported with '
198 '--skylab.')
199
mblighbe630eb2008-08-01 16:41:48 +0000200 return (options, leftover)
201
202
Ningning Xiaea02ab12018-05-11 15:08:57 -0700203 def execute_skylab(self):
204 """Execute 'atest host list' with --skylab."""
205 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
206 inventory_repo.initialize()
207 lab = text_manager.load_lab(inventory_repo.get_data_dir())
208
209 # TODO(nxia): support filtering on run-time labels and status.
210 return device.get_devices(
211 lab,
212 'duts',
213 self.environment,
Ningning Xia64ced002018-05-16 17:36:20 -0700214 label_map=self.label_map,
Ningning Xiaea02ab12018-05-11 15:08:57 -0700215 hostnames=self.hosts,
216 locked=self.locked,
217 unlocked=self.unlocked)
218
219
mblighbe630eb2008-08-01 16:41:48 +0000220 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800221 """Execute 'atest host list'."""
Ningning Xiaea02ab12018-05-11 15:08:57 -0700222 if self.skylab:
223 return self.execute_skylab()
224
mblighbe630eb2008-08-01 16:41:48 +0000225 filters = {}
226 check_results = {}
227 if self.hosts:
228 filters['hostname__in'] = self.hosts
229 check_results['hostname__in'] = 'hostname'
mbligh6444c6b2008-10-27 20:55:13 +0000230
231 if self.labels:
jamesrenc2863162010-07-12 21:20:51 +0000232 if len(self.labels) == 1:
mblighf703fb42009-01-30 00:35:05 +0000233 # This is needed for labels with wildcards (x86*)
jamesrenc2863162010-07-12 21:20:51 +0000234 filters['labels__name__in'] = self.labels
mblighf703fb42009-01-30 00:35:05 +0000235 check_results['labels__name__in'] = None
236 else:
jamesrenc2863162010-07-12 21:20:51 +0000237 filters['multiple_labels'] = self.labels
mblighf703fb42009-01-30 00:35:05 +0000238 check_results['multiple_labels'] = None
mbligh6444c6b2008-10-27 20:55:13 +0000239
mblighbe630eb2008-08-01 16:41:48 +0000240 if self.status:
mbligh6444c6b2008-10-27 20:55:13 +0000241 statuses = self.status.split(',')
242 statuses = [status.strip() for status in statuses
243 if status.strip()]
244
245 filters['status__in'] = statuses
mblighcd8eb972008-08-25 19:20:39 +0000246 check_results['status__in'] = None
mbligh6444c6b2008-10-27 20:55:13 +0000247
mbligh536a5242008-10-18 14:35:54 +0000248 if self.acl:
showardd9ac4452009-02-07 02:04:37 +0000249 filters['aclgroup__name'] = self.acl
250 check_results['aclgroup__name'] = None
mbligh536a5242008-10-18 14:35:54 +0000251 if self.user:
showardd9ac4452009-02-07 02:04:37 +0000252 filters['aclgroup__users__login'] = self.user
253 check_results['aclgroup__users__login'] = None
mbligh91e0efd2009-02-26 01:02:16 +0000254
255 if self.locked or self.unlocked:
256 filters['locked'] = self.locked
257 check_results['locked'] = None
258
mblighbe630eb2008-08-01 16:41:48 +0000259 return super(host_list, self).execute(op='get_hosts',
260 filters=filters,
261 check_results=check_results)
262
263
264 def output(self, results):
xixuand6011f12016-12-08 15:01:58 -0800265 """Print output of 'atest host list'.
266
267 @param results: the results to be printed.
268 """
Ningning Xiaea02ab12018-05-11 15:08:57 -0700269 if results and not self.skylab:
mblighbe630eb2008-08-01 16:41:48 +0000270 # Remove the platform from the labels.
271 for result in results:
272 result['labels'] = self._cleanup_labels(result['labels'],
273 result['platform'])
Ningning Xiaea02ab12018-05-11 15:08:57 -0700274 if self.skylab and self.full_output:
275 print results
276 return
277
278 if self.skylab:
279 results = device.convert_to_autotest_hosts(results)
280
mbligh70b9bf42008-11-18 15:00:46 +0000281 if self.hostnames_only:
mblighdf75f8b2008-11-18 19:07:42 +0000282 self.print_list(results, key='hostname')
mbligh70b9bf42008-11-18 15:00:46 +0000283 else:
Ningning Xiaea02ab12018-05-11 15:08:57 -0700284 keys = ['hostname', 'status', 'shard', 'locked', 'lock_reason',
285 'locked_by', 'platform', 'labels']
Prashanth Balasubramaniana5048562014-12-12 10:14:11 -0800286 super(host_list, self).output(results, keys=keys)
mblighbe630eb2008-08-01 16:41:48 +0000287
288
289class host_stat(host):
290 """atest host stat --mlist <file>|<hosts>"""
291 usage_action = 'stat'
292
293 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800294 """Execute 'atest host stat'."""
mblighbe630eb2008-08-01 16:41:48 +0000295 results = []
296 # Convert wildcards into real host stats.
297 existing_hosts = []
298 for host in self.hosts:
299 if host.endswith('*'):
300 stats = self.execute_rpc('get_hosts',
301 hostname__startswith=host.rstrip('*'))
302 if len(stats) == 0:
303 self.failure('No hosts matching %s' % host, item=host,
304 what_failed='Failed to stat')
305 continue
306 else:
307 stats = self.execute_rpc('get_hosts', hostname=host)
308 if len(stats) == 0:
309 self.failure('Unknown host %s' % host, item=host,
310 what_failed='Failed to stat')
311 continue
312 existing_hosts.extend(stats)
313
314 for stat in existing_hosts:
315 host = stat['hostname']
316 # The host exists, these should succeed
317 acls = self.execute_rpc('get_acl_groups', hosts__hostname=host)
318
319 labels = self.execute_rpc('get_labels', host__hostname=host)
Simran Basi0739d682015-02-25 16:22:56 -0800320 results.append([[stat], acls, labels, stat['attributes']])
mblighbe630eb2008-08-01 16:41:48 +0000321 return results
322
323
324 def output(self, results):
xixuand6011f12016-12-08 15:01:58 -0800325 """Print output of 'atest host stat'.
326
327 @param results: the results to be printed.
328 """
Simran Basi0739d682015-02-25 16:22:56 -0800329 for stats, acls, labels, attributes in results:
mblighbe630eb2008-08-01 16:41:48 +0000330 print '-'*5
331 self.print_fields(stats,
Aviv Kesheta40d1272017-11-16 00:56:35 -0800332 keys=['hostname', 'id', 'platform',
mblighe163b032008-10-18 14:30:27 +0000333 'status', 'locked', 'locked_by',
Matthew Sartori68186332015-04-27 17:19:53 -0700334 'lock_time', 'lock_reason', 'protection',])
mblighbe630eb2008-08-01 16:41:48 +0000335 self.print_by_ids(acls, 'ACLs', line_before=True)
336 labels = self._cleanup_labels(labels)
337 self.print_by_ids(labels, 'Labels', line_before=True)
Simran Basi0739d682015-02-25 16:22:56 -0800338 self.print_dict(attributes, 'Host Attributes', line_before=True)
mblighbe630eb2008-08-01 16:41:48 +0000339
340
341class host_jobs(host):
mbligh1494eca2008-08-13 21:24:22 +0000342 """atest host jobs [--max-query] --mlist <file>|<hosts>"""
mblighbe630eb2008-08-01 16:41:48 +0000343 usage_action = 'jobs'
344
mbligh6996fe82008-08-13 00:32:27 +0000345 def __init__(self):
346 super(host_jobs, self).__init__()
347 self.parser.add_option('-q', '--max-query',
348 help='Limits the number of results '
349 '(20 by default)',
350 type='int', default=20)
351
352
353 def parse(self):
354 """Consume the specific options"""
mbligh9deeefa2009-05-01 23:11:08 +0000355 (options, leftover) = super(host_jobs, self).parse()
mbligh6996fe82008-08-13 00:32:27 +0000356 self.max_queries = options.max_query
357 return (options, leftover)
358
359
mblighbe630eb2008-08-01 16:41:48 +0000360 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800361 """Execute 'atest host jobs'."""
mblighbe630eb2008-08-01 16:41:48 +0000362 results = []
363 real_hosts = []
364 for host in self.hosts:
365 if host.endswith('*'):
366 stats = self.execute_rpc('get_hosts',
367 hostname__startswith=host.rstrip('*'))
368 if len(stats) == 0:
369 self.failure('No host matching %s' % host, item=host,
370 what_failed='Failed to stat')
371 [real_hosts.append(stat['hostname']) for stat in stats]
372 else:
373 real_hosts.append(host)
374
375 for host in real_hosts:
376 queue_entries = self.execute_rpc('get_host_queue_entries',
mbligh6996fe82008-08-13 00:32:27 +0000377 host__hostname=host,
mbligh1494eca2008-08-13 21:24:22 +0000378 query_limit=self.max_queries,
showard6958c722009-09-23 20:03:16 +0000379 sort_by=['-job__id'])
mblighbe630eb2008-08-01 16:41:48 +0000380 jobs = []
381 for entry in queue_entries:
382 job = {'job_id': entry['job']['id'],
383 'job_owner': entry['job']['owner'],
384 'job_name': entry['job']['name'],
385 'status': entry['status']}
386 jobs.append(job)
387 results.append((host, jobs))
388 return results
389
390
391 def output(self, results):
xixuand6011f12016-12-08 15:01:58 -0800392 """Print output of 'atest host jobs'.
393
394 @param results: the results to be printed.
395 """
mblighbe630eb2008-08-01 16:41:48 +0000396 for host, jobs in results:
397 print '-'*5
398 print 'Hostname: %s' % host
399 self.print_table(jobs, keys_header=['job_id',
showardbe0d8692009-08-20 23:42:44 +0000400 'job_owner',
401 'job_name',
402 'status'])
mblighbe630eb2008-08-01 16:41:48 +0000403
Justin Giorgi16bba562016-06-22 10:42:13 -0700404class BaseHostModCreate(host):
xixuand6011f12016-12-08 15:01:58 -0800405 """The base class for host_mod and host_create"""
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700406 # Matches one attribute=value pair
407 attribute_regex = r'(?P<attribute>\w+)=(?P<value>.+)?'
mblighbe630eb2008-08-01 16:41:48 +0000408
409 def __init__(self):
Justin Giorgi16bba562016-06-22 10:42:13 -0700410 """Add the options shared between host mod and host create actions."""
mblighbe630eb2008-08-01 16:41:48 +0000411 self.messages = []
Justin Giorgi16bba562016-06-22 10:42:13 -0700412 self.host_ids = {}
413 super(BaseHostModCreate, self).__init__()
mblighbe630eb2008-08-01 16:41:48 +0000414 self.parser.add_option('-l', '--lock',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700415 help='Lock hosts.',
mblighbe630eb2008-08-01 16:41:48 +0000416 action='store_true')
Matthew Sartori68186332015-04-27 17:19:53 -0700417 self.parser.add_option('-r', '--lock_reason',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700418 help='Reason for locking hosts.',
Matthew Sartori68186332015-04-27 17:19:53 -0700419 default='')
Ningning Xiaef35cb52018-05-04 17:58:20 -0700420 self.parser.add_option('-u', '--unlock',
421 help='Unlock hosts.',
422 action='store_true')
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700423
mblighe163b032008-10-18 14:30:27 +0000424 self.parser.add_option('-p', '--protection', type='choice',
mblighaed47e82009-03-17 19:06:18 +0000425 help=('Set the protection level on a host. '
Ningning Xiaef35cb52018-05-04 17:58:20 -0700426 'Must be one of: %s. %s' %
427 (', '.join('"%s"' % p
428 for p in self.protections),
429 skylab_utils.MSG_INVALID_IN_SKYLAB)),
mblighaed47e82009-03-17 19:06:18 +0000430 choices=self.protections)
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700431 self._attributes = []
432 self.parser.add_option('--attribute', '-i',
433 help=('Host attribute to add or change. Format '
434 'is <attribute>=<value>. Multiple '
435 'attributes can be set by passing the '
436 'argument multiple times. Attributes can '
437 'be unset by providing an empty value.'),
438 action='append')
mblighbe630eb2008-08-01 16:41:48 +0000439 self.parser.add_option('-b', '--labels',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700440 help=('Comma separated list of labels. '
441 'When --skylab is provided, a label must '
442 'be in the format of label-key:label-value'
443 ' (e.g., board:lumpy).'))
mblighbe630eb2008-08-01 16:41:48 +0000444 self.parser.add_option('-B', '--blist',
445 help='File listing the labels',
446 type='string',
447 metavar='LABEL_FLIST')
448 self.parser.add_option('-a', '--acls',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700449 help=('Comma separated list of ACLs. %s' %
450 skylab_utils.MSG_INVALID_IN_SKYLAB))
mblighbe630eb2008-08-01 16:41:48 +0000451 self.parser.add_option('-A', '--alist',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700452 help=('File listing the acls. %s' %
453 skylab_utils.MSG_INVALID_IN_SKYLAB),
mblighbe630eb2008-08-01 16:41:48 +0000454 type='string',
455 metavar='ACL_FLIST')
Justin Giorgi16bba562016-06-22 10:42:13 -0700456 self.parser.add_option('-t', '--platform',
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700457 help=('Sets the platform label. %s Please set '
458 'platform in labels (e.g., -b '
459 'platform:platform_name) with --skylab.' %
460 skylab_utils.MSG_INVALID_IN_SKYLAB))
mblighbe630eb2008-08-01 16:41:48 +0000461
462
463 def parse(self):
Justin Giorgi16bba562016-06-22 10:42:13 -0700464 """Consume the options common to host create and host mod.
465 """
mbligh9deeefa2009-05-01 23:11:08 +0000466 label_info = topic_common.item_parse_info(attribute_name='labels',
467 inline_option='labels',
468 filename_option='blist')
469 acl_info = topic_common.item_parse_info(attribute_name='acls',
470 inline_option='acls',
471 filename_option='alist')
472
Justin Giorgi16bba562016-06-22 10:42:13 -0700473 (options, leftover) = super(BaseHostModCreate, self).parse([label_info,
mbligh9deeefa2009-05-01 23:11:08 +0000474 acl_info],
475 req_items='hosts')
mblighbe630eb2008-08-01 16:41:48 +0000476
477 self._parse_lock_options(options)
Justin Giorgi16bba562016-06-22 10:42:13 -0700478
Ningning Xia64ced002018-05-16 17:36:20 -0700479 self.label_map = None
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700480 if self.allow_skylab and self.skylab:
Ningning Xiaef35cb52018-05-04 17:58:20 -0700481 # TODO(nxia): drop these flags when all hosts are migrated to skylab
Ningning Xia64ced002018-05-16 17:36:20 -0700482 if (options.protection or options.acls or options.alist or
483 options.platform):
484 self.invalid_syntax(
485 '--protection, --acls, --alist or --platform is not '
486 'supported with --skylab.')
487
488 if self.labels:
489 self.label_map = device.convert_to_label_map(self.labels)
Ningning Xiaef35cb52018-05-04 17:58:20 -0700490
mblighaed47e82009-03-17 19:06:18 +0000491 if options.protection:
492 self.data['protection'] = options.protection
Justin Giorgi16bba562016-06-22 10:42:13 -0700493 self.messages.append('Protection set to "%s"' % options.protection)
494
495 self.attributes = {}
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700496 if options.attribute:
497 for pair in options.attribute:
498 m = re.match(self.attribute_regex, pair)
499 if not m:
500 raise topic_common.CliError('Attribute must be in key=value '
501 'syntax.')
502 elif m.group('attribute') in self.attributes:
xixuand6011f12016-12-08 15:01:58 -0800503 raise topic_common.CliError(
504 'Multiple values provided for attribute '
505 '%s.' % m.group('attribute'))
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700506 self.attributes[m.group('attribute')] = m.group('value')
Justin Giorgi16bba562016-06-22 10:42:13 -0700507
508 self.platform = options.platform
mblighbe630eb2008-08-01 16:41:48 +0000509 return (options, leftover)
510
511
Justin Giorgi16bba562016-06-22 10:42:13 -0700512 def _set_acls(self, hosts, acls):
513 """Add hosts to acls (and remove from all other acls).
514
515 @param hosts: list of hostnames
516 @param acls: list of acl names
517 """
518 # Remove from all ACLs except 'Everyone' and ACLs in list
519 # Skip hosts that don't exist
520 for host in hosts:
521 if host not in self.host_ids:
522 continue
523 host_id = self.host_ids[host]
524 for a in self.execute_rpc('get_acl_groups', hosts=host_id):
525 if a['name'] not in self.acls and a['id'] != 1:
526 self.execute_rpc('acl_group_remove_hosts', id=a['id'],
527 hosts=self.hosts)
528
529 # Add hosts to the ACLs
530 self.check_and_create_items('get_acl_groups', 'add_acl_group',
531 self.acls)
532 for a in acls:
533 self.execute_rpc('acl_group_add_hosts', id=a, hosts=hosts)
534
535
536 def _remove_labels(self, host, condition):
537 """Remove all labels from host that meet condition(label).
538
539 @param host: hostname
540 @param condition: callable that returns bool when given a label
541 """
542 if host in self.host_ids:
543 host_id = self.host_ids[host]
544 labels_to_remove = []
545 for l in self.execute_rpc('get_labels', host=host_id):
546 if condition(l):
547 labels_to_remove.append(l['id'])
548 if labels_to_remove:
549 self.execute_rpc('host_remove_labels', id=host_id,
550 labels=labels_to_remove)
551
552
553 def _set_labels(self, host, labels):
554 """Apply labels to host (and remove all other labels).
555
556 @param host: hostname
557 @param labels: list of label names
558 """
559 condition = lambda l: l['name'] not in labels and not l['platform']
560 self._remove_labels(host, condition)
561 self.check_and_create_items('get_labels', 'add_label', labels)
562 self.execute_rpc('host_add_labels', id=host, labels=labels)
563
564
565 def _set_platform_label(self, host, platform_label):
566 """Apply the platform label to host (and remove existing).
567
568 @param host: hostname
569 @param platform_label: platform label's name
570 """
571 self._remove_labels(host, lambda l: l['platform'])
572 self.check_and_create_items('get_labels', 'add_label', [platform_label],
573 platform=True)
574 self.execute_rpc('host_add_labels', id=host, labels=[platform_label])
575
576
577 def _set_attributes(self, host, attributes):
578 """Set attributes on host.
579
580 @param host: hostname
581 @param attributes: attribute dictionary
582 """
583 for attr, value in self.attributes.iteritems():
584 self.execute_rpc('set_host_attribute', attribute=attr,
585 value=value, hostname=host)
586
587
588class host_mod(BaseHostModCreate):
589 """atest host mod [--lock|--unlock --force_modify_locking
590 --platform <arch>
591 --labels <labels>|--blist <label_file>
592 --acls <acls>|--alist <acl_file>
593 --protection <protection_type>
594 --attributes <attr>=<value>;<attr>=<value>
595 --mlist <mach_file>] <hosts>"""
596 usage_action = 'mod'
Justin Giorgi16bba562016-06-22 10:42:13 -0700597
598 def __init__(self):
599 """Add the options specific to the mod action"""
600 super(host_mod, self).__init__()
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700601 self.parser.add_option('--unlock-lock-id',
602 help=('Unlock the lock with the lock-id. %s' %
603 skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
604 default=None)
Justin Giorgi16bba562016-06-22 10:42:13 -0700605 self.parser.add_option('-f', '--force_modify_locking',
606 help='Forcefully lock\unlock a host',
607 action='store_true')
608 self.parser.add_option('--remove_acls',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700609 help=('Remove all active acls. %s' %
610 skylab_utils.MSG_INVALID_IN_SKYLAB),
Justin Giorgi16bba562016-06-22 10:42:13 -0700611 action='store_true')
612 self.parser.add_option('--remove_labels',
613 help='Remove all labels.',
614 action='store_true')
615
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700616 self.add_skylab_options()
617
618
619 def _parse_unlock_options(self, options):
620 """Parse unlock related options."""
621 if self.skylab and options.unlock and options.unlock_lock_id is None:
622 self.invalid_syntax('Must provide --unlock-lock-id with "--skylab '
623 '--unlock".')
624
625 if (not (self.skylab and options.unlock) and
626 options.unlock_lock_id is not None):
627 self.invalid_syntax('--unlock-lock-id is only valid with '
628 '"--skylab --unlock".')
629
630 self.unlock_lock_id = options.unlock_lock_id
631
Justin Giorgi16bba562016-06-22 10:42:13 -0700632
633 def parse(self):
634 """Consume the specific options"""
635 (options, leftover) = super(host_mod, self).parse()
636
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700637 self._parse_unlock_options(options)
638
Justin Giorgi16bba562016-06-22 10:42:13 -0700639 if options.force_modify_locking:
640 self.data['force_modify_locking'] = True
641
Ningning Xiaef35cb52018-05-04 17:58:20 -0700642 if self.skylab and options.remove_acls:
643 # TODO(nxia): drop the flag when all hosts are migrated to skylab
644 self.invalid_syntax('--remove_acls is not supported with --skylab.')
645
Justin Giorgi16bba562016-06-22 10:42:13 -0700646 self.remove_acls = options.remove_acls
647 self.remove_labels = options.remove_labels
648
649 return (options, leftover)
650
651
Ningning Xiaef35cb52018-05-04 17:58:20 -0700652 def execute_skylab(self):
653 """Execute atest host mod with --skylab.
654
655 @return A list of hostnames which have been successfully modified.
656 """
657 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
658 inventory_repo.initialize()
659 data_dir = inventory_repo.get_data_dir()
660 lab = text_manager.load_lab(data_dir)
661
662 locked_by = None
663 if self.lock:
664 locked_by = inventory_repo.git_repo.config('user.email')
665
666 successes = []
667 for hostname in self.hosts:
668 try:
669 device.modify(
670 lab,
671 'duts',
672 hostname,
673 self.environment,
674 lock=self.lock,
675 locked_by=locked_by,
676 lock_reason = self.lock_reason,
677 unlock=self.unlock,
678 unlock_lock_id=self.unlock_lock_id,
679 attributes=self.attributes,
680 remove_labels=self.remove_labels,
Ningning Xia64ced002018-05-16 17:36:20 -0700681 label_map=self.label_map)
Ningning Xiaef35cb52018-05-04 17:58:20 -0700682 successes.append(hostname)
683 except device.SkylabDeviceActionError as e:
684 print('Cannot modify host %s: %s' % (hostname, e))
685
686 if successes:
687 text_manager.dump_lab(data_dir, lab)
688
689 status = inventory_repo.git_repo.status()
690 if not status:
691 print('Nothing is changed for hosts %s.' % successes)
692 return []
693
694 message = skylab_utils.construct_commit_message(
695 'Modify %d hosts.\n\n%s' % (len(successes), successes))
696 self.change_number = inventory_repo.upload_change(
697 message, draft=self.draft, dryrun=self.dryrun,
698 submit=self.submit)
699
700 return successes
701
702
Justin Giorgi16bba562016-06-22 10:42:13 -0700703 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800704 """Execute 'atest host mod'."""
Ningning Xiaef35cb52018-05-04 17:58:20 -0700705 if self.skylab:
706 return self.execute_skylab()
707
Justin Giorgi16bba562016-06-22 10:42:13 -0700708 successes = []
709 for host in self.execute_rpc('get_hosts', hostname__in=self.hosts):
710 self.host_ids[host['hostname']] = host['id']
711 for host in self.hosts:
712 if host not in self.host_ids:
713 self.failure('Cannot modify non-existant host %s.' % host)
714 continue
715 host_id = self.host_ids[host]
716
717 try:
718 if self.data:
719 self.execute_rpc('modify_host', item=host,
720 id=host, **self.data)
721
722 if self.attributes:
723 self._set_attributes(host, self.attributes)
724
725 if self.labels or self.remove_labels:
726 self._set_labels(host, self.labels)
727
728 if self.platform:
729 self._set_platform_label(host, self.platform)
730
731 # TODO: Make the AFE return True or False,
732 # especially for lock
733 successes.append(host)
734 except topic_common.CliError, full_error:
735 # Already logged by execute_rpc()
736 pass
737
738 if self.acls or self.remove_acls:
739 self._set_acls(self.hosts, self.acls)
740
741 return successes
742
743
744 def output(self, hosts):
xixuand6011f12016-12-08 15:01:58 -0800745 """Print output of 'atest host mod'.
746
747 @param hosts: the host list to be printed.
748 """
Justin Giorgi16bba562016-06-22 10:42:13 -0700749 for msg in self.messages:
750 self.print_wrapped(msg, hosts)
751
Ningning Xiaef35cb52018-05-04 17:58:20 -0700752 if hosts and self.skylab:
Ningning Xiac8b430d2018-05-17 15:10:25 -0700753 print('Modified hosts: %s.' % ', '.join(hosts))
Ningning Xiaef35cb52018-05-04 17:58:20 -0700754 if self.skylab and not self.dryrun and not self.submit:
Ningning Xiac8b430d2018-05-17 15:10:25 -0700755 print(skylab_utils.get_cl_message(self.change_number))
Ningning Xiaef35cb52018-05-04 17:58:20 -0700756
Justin Giorgi16bba562016-06-22 10:42:13 -0700757
758class HostInfo(object):
759 """Store host information so we don't have to keep looking it up."""
760 def __init__(self, hostname, platform, labels):
761 self.hostname = hostname
762 self.platform = platform
763 self.labels = labels
764
765
766class host_create(BaseHostModCreate):
767 """atest host create [--lock|--unlock --platform <arch>
768 --labels <labels>|--blist <label_file>
769 --acls <acls>|--alist <acl_file>
770 --protection <protection_type>
771 --attributes <attr>=<value>;<attr>=<value>
772 --mlist <mach_file>] <hosts>"""
773 usage_action = 'create'
774
775 def parse(self):
776 """Option logic specific to create action.
777 """
778 (options, leftovers) = super(host_create, self).parse()
779 self.locked = options.lock
780 if 'serials' in self.attributes:
781 if len(self.hosts) > 1:
782 raise topic_common.CliError('Can not specify serials with '
783 'multiple hosts.')
784
785
786 @classmethod
787 def construct_without_parse(
788 cls, web_server, hosts, platform=None,
789 locked=False, lock_reason='', labels=[], acls=[],
790 protection=host_protections.Protection.NO_PROTECTION):
791 """Construct a host_create object and fill in data from args.
792
793 Do not need to call parse after the construction.
794
795 Return an object of site_host_create ready to execute.
796
797 @param web_server: A string specifies the autotest webserver url.
798 It is needed to setup comm to make rpc.
799 @param hosts: A list of hostnames as strings.
800 @param platform: A string or None.
801 @param locked: A boolean.
802 @param lock_reason: A string.
803 @param labels: A list of labels as strings.
804 @param acls: A list of acls as strings.
805 @param protection: An enum defined in host_protections.
806 """
807 obj = cls()
808 obj.web_server = web_server
809 try:
810 # Setup stuff needed for afe comm.
811 obj.afe = rpc.afe_comm(web_server)
812 except rpc.AuthError, s:
813 obj.failure(str(s), fatal=True)
814 obj.hosts = hosts
815 obj.platform = platform
816 obj.locked = locked
817 if locked and lock_reason.strip():
818 obj.data['lock_reason'] = lock_reason.strip()
819 obj.labels = labels
820 obj.acls = acls
821 if protection:
822 obj.data['protection'] = protection
823 obj.attributes = {}
824 return obj
825
826
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700827 def _detect_host_info(self, host):
828 """Detect platform and labels from the host.
829
830 @param host: hostname
831
832 @return: HostInfo object
833 """
834 # Mock an afe_host object so that the host is constructed as if the
835 # data was already in afe
836 data = {'attributes': self.attributes, 'labels': self.labels}
837 afe_host = frontend.Host(None, data)
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700838 store = host_info.InMemoryHostInfoStore(
839 host_info.HostInfo(labels=self.labels,
840 attributes=self.attributes))
841 machine = {
842 'hostname': host,
843 'afe_host': afe_host,
844 'host_info_store': store
845 }
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700846 try:
Allen Li2c32d6b2017-02-03 15:28:10 -0800847 if bin_utils.ping(host, tries=1, deadline=1) == 0:
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700848 serials = self.attributes.get('serials', '').split(',')
Richard Barnette9db80682018-04-26 00:55:15 +0000849 adb_serial = self.attributes.get('serials')
850 host_dut = hosts.create_host(machine,
851 adb_serial=adb_serial)
xixuand6011f12016-12-08 15:01:58 -0800852
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700853 info = HostInfo(host, host_dut.get_platform(),
854 host_dut.get_labels())
xixuand6011f12016-12-08 15:01:58 -0800855 # Clean host to make sure nothing left after calling it,
856 # e.g. tunnels.
857 if hasattr(host_dut, 'close'):
858 host_dut.close()
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700859 else:
860 # Can't ping the host, use default information.
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700861 info = HostInfo(host, None, [])
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700862 except (socket.gaierror, error.AutoservRunError,
863 error.AutoservSSHTimeout):
864 # We may be adding a host that does not exist yet or we can't
865 # reach due to hostname/address issues or if the host is down.
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700866 info = HostInfo(host, None, [])
867 return info
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700868
869
mblighbe630eb2008-08-01 16:41:48 +0000870 def _execute_add_one_host(self, host):
mbligh719e14a2008-12-04 01:17:08 +0000871 # Always add the hosts as locked to avoid the host
Matthew Sartori68186332015-04-27 17:19:53 -0700872 # being picked up by the scheduler before it's ACL'ed.
mbligh719e14a2008-12-04 01:17:08 +0000873 self.data['locked'] = True
Matthew Sartori68186332015-04-27 17:19:53 -0700874 if not self.locked:
875 self.data['lock_reason'] = 'Forced lock on device creation'
Justin Giorgi16bba562016-06-22 10:42:13 -0700876 self.execute_rpc('add_host', hostname=host, status="Ready", **self.data)
mblighbe630eb2008-08-01 16:41:48 +0000877
Justin Giorgi16bba562016-06-22 10:42:13 -0700878 # If there are labels avaliable for host, use them.
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700879 info = self._detect_host_info(host)
Justin Giorgi16bba562016-06-22 10:42:13 -0700880 labels = set(self.labels)
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700881 if info.labels:
882 labels.update(info.labels)
Justin Giorgi16bba562016-06-22 10:42:13 -0700883
884 if labels:
885 self._set_labels(host, list(labels))
886
887 # Now add the platform label.
888 # If a platform was not provided and we were able to retrieve it
889 # from the host, use the retrieved platform.
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700890 platform = self.platform if self.platform else info.platform
Justin Giorgi16bba562016-06-22 10:42:13 -0700891 if platform:
892 self._set_platform_label(host, platform)
893
894 if self.attributes:
895 self._set_attributes(host, self.attributes)
mblighbe630eb2008-08-01 16:41:48 +0000896
897
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700898 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800899 """Execute 'atest host create'."""
Justin Giorgi16bba562016-06-22 10:42:13 -0700900 successful_hosts = []
901 for host in self.hosts:
902 try:
903 self._execute_add_one_host(host)
904 successful_hosts.append(host)
905 except topic_common.CliError:
906 pass
Jiaxi Luoc342f9f2014-05-19 16:22:03 -0700907
908 if successful_hosts:
Justin Giorgi16bba562016-06-22 10:42:13 -0700909 self._set_acls(successful_hosts, self.acls)
Jiaxi Luoc342f9f2014-05-19 16:22:03 -0700910
911 if not self.locked:
912 for host in successful_hosts:
Matthew Sartori68186332015-04-27 17:19:53 -0700913 self.execute_rpc('modify_host', id=host, locked=False,
914 lock_reason='')
Jiaxi Luoc342f9f2014-05-19 16:22:03 -0700915 return successful_hosts
916
917
mblighbe630eb2008-08-01 16:41:48 +0000918 def output(self, hosts):
xixuand6011f12016-12-08 15:01:58 -0800919 """Print output of 'atest host create'.
920
921 @param hosts: the added host list to be printed.
922 """
mblighbe630eb2008-08-01 16:41:48 +0000923 self.print_wrapped('Added host', hosts)
924
925
926class host_delete(action_common.atest_delete, host):
927 """atest host delete [--mlist <mach_file>] <hosts>"""
Ningning Xiac8b430d2018-05-17 15:10:25 -0700928
929 def __init__(self):
930 super(host_delete, self).__init__()
931
932 self.add_skylab_options()
933
934
935 def execute_skylab(self):
936 """Execute 'atest host delete' with '--skylab'.
937
938 @return A list of hostnames which have been successfully deleted.
939 """
940 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
941 inventory_repo.initialize()
942 data_dir = inventory_repo.get_data_dir()
943 lab = text_manager.load_lab(data_dir)
944
945 successes = []
946 for hostname in self.hosts:
947 try:
948 device.delete(
949 lab,
950 'duts',
951 hostname,
952 self.environment)
953 successes.append(hostname)
954 except device.SkylabDeviceActionError as e:
955 print('Cannot delete host %s: %s' % (hostname, e))
956
957 if successes:
958 text_manager.dump_lab(data_dir, lab)
959 message = skylab_utils.construct_commit_message(
960 'Delete %d hosts.\n\n%s' % (len(successes), successes))
961 self.change_number = inventory_repo.upload_change(
962 message, draft=self.draft, dryrun=self.dryrun,
963 submit=self.submit)
964
965 return successes
966
967
968 def execute(self):
969 """Execute 'atest host delete'.
970
971 @return A list of hostnames which have been successfully deleted.
972 """
973 if self.skylab:
974 return self.execute_skylab()
975
976 return super(host_delete, self).execute()
Ningning Xiac46bdd12018-05-29 11:24:14 -0700977
978
979class InvalidHostnameError(Exception):
980 """Cannot perform actions on the host because of invalid hostname."""
981
982
983def _add_hostname_suffix(hostname, suffix):
984 """Add the suffix to the hostname."""
985 if hostname.endswith(suffix):
986 raise InvalidHostnameError(
987 'Cannot add "%s" as it already contains the suffix.' % suffix)
988
989 return hostname + suffix
990
991
992def _remove_hostname_suffix(hostname, suffix):
993 """Remove the suffix from the hostname."""
994 if not hostname.endswith(suffix):
995 raise InvalidHostnameError(
996 'Cannot remove "%s" as it doesn\'t contain the suffix.' %
997 suffix)
998
999 return hostname[:len(hostname) - len(suffix)]
1000
1001
1002class host_rename(host):
1003 """Host rename is only for migrating hosts between skylab and AFE DB."""
1004
1005 usage_action = 'rename'
1006
1007 def __init__(self):
1008 """Add the options specific to the rename action."""
1009 super(host_rename, self).__init__()
1010
1011 self.parser.add_option('--for-migration',
1012 help=('Rename hostnames for migration. Rename '
1013 'each "hostname" to "hostname%s". '
1014 'The original "hostname" must not contain '
1015 'suffix.' % MIGRATED_HOST_SUFFIX),
1016 action='store_true',
1017 default=False)
1018 self.parser.add_option('--for-rollback',
1019 help=('Rename hostnames for migration rollback. '
1020 'Rename each "hostname%s" to its original '
1021 '"hostname".' % MIGRATED_HOST_SUFFIX),
1022 action='store_true',
1023 default=False)
1024 self.parser.add_option('--dryrun',
1025 help='Execute the action as a dryrun.',
1026 action='store_true',
1027 default=False)
1028
1029
1030 def parse(self):
1031 """Consume the options common to host rename."""
1032 (options, leftovers) = super(host_rename, self).parse()
1033 self.for_migration = options.for_migration
1034 self.for_rollback = options.for_rollback
1035 self.dryrun = options.dryrun
1036 self.host_ids = {}
1037
1038 if not (self.for_migration ^ self.for_rollback):
1039 self.invalid_syntax('--for-migration and --for-rollback are '
1040 'exclusive, and one of them must be enabled.')
1041
1042 if not self.hosts:
1043 self.invalid_syntax('Must provide hostname(s).')
1044
1045 if self.dryrun:
1046 print('This will be a dryrun and will not rename hostnames.')
1047
1048 return (options, leftovers)
1049
1050
1051 def execute(self):
1052 """Execute 'atest host rename'."""
1053 if not self.prompt_confirmation():
1054 return
1055
1056 successes = []
1057 for host in self.execute_rpc('get_hosts', hostname__in=self.hosts):
1058 self.host_ids[host['hostname']] = host['id']
1059 for host in self.hosts:
1060 if host not in self.host_ids:
1061 self.failure('Cannot rename non-existant host %s.' % host,
1062 item=host, what_failed='Failed to rename')
1063 continue
1064 try:
Ningning Xiab261b162018-06-07 17:24:59 -07001065 host_id = self.host_ids[host]
Ningning Xiac46bdd12018-05-29 11:24:14 -07001066 if self.for_migration:
1067 new_hostname = _add_hostname_suffix(
1068 host, MIGRATED_HOST_SUFFIX)
1069 else:
1070 #for_rollback
1071 new_hostname = _remove_hostname_suffix(
1072 host, MIGRATED_HOST_SUFFIX)
1073
1074 if not self.dryrun:
Ningning Xia423c7962018-06-07 15:27:18 -07001075 # TODO(crbug.com/850737): delete and abort HQE.
Ningning Xiac46bdd12018-05-29 11:24:14 -07001076 data = {'hostname': new_hostname}
Ningning Xiab261b162018-06-07 17:24:59 -07001077 self.execute_rpc('modify_host', item=host, id=host_id,
1078 **data)
Ningning Xiac46bdd12018-05-29 11:24:14 -07001079 successes.append((host, new_hostname))
1080 except InvalidHostnameError as e:
1081 self.failure('Cannot rename host %s: %s' % (host, e), item=host,
1082 what_failed='Failed to rename')
1083 except topic_common.CliError, full_error:
1084 # Already logged by execute_rpc()
1085 pass
1086
1087 return successes
1088
1089
1090 def output(self, results):
1091 """Print output of 'atest host rename'."""
1092 if results:
1093 print('Successfully renamed:')
1094 for old_hostname, new_hostname in results:
1095 print('%s to %s' % (old_hostname, new_hostname))
Ningning Xia9df3d152018-05-23 17:15:14 -07001096
1097
1098class host_migrate(action_common.atest_list, host):
1099 """'atest host migrate' to migrate or rollback hosts."""
1100
1101 usage_action = 'migrate'
1102
1103 def __init__(self):
1104 super(host_migrate, self).__init__()
1105
1106 self.parser.add_option('--migration',
1107 dest='migration',
1108 help='Migrate the hosts to skylab.',
1109 action='store_true',
1110 default=False)
1111 self.parser.add_option('--rollback',
1112 dest='rollback',
1113 help='Rollback the hosts migrated to skylab.',
1114 action='store_true',
1115 default=False)
1116 self.parser.add_option('--model',
1117 help='Model of the hosts to migrate.',
1118 dest='model',
1119 default=None)
1120 self.parser.add_option('--pool',
1121 help=('Pool of the hosts to migrate. Must '
1122 'specify --model for the pool.'),
1123 dest='pool',
1124 default=None)
1125
1126 self.add_skylab_options(enforce_skylab=True)
1127
1128
1129 def parse(self):
1130 """Consume the specific options"""
1131 (options, leftover) = super(host_migrate, self).parse()
1132
1133 self.migration = options.migration
1134 self.rollback = options.rollback
1135 self.model = options.model
1136 self.pool = options.pool
1137 self.host_ids = {}
1138
1139 if not (self.migration ^ self.rollback):
1140 self.invalid_syntax('--migration and --rollback are exclusive, '
1141 'and one of them must be enabled.')
1142
1143 if self.pool is not None and self.model is None:
1144 self.invalid_syntax('Must provide --model with --pool.')
1145
1146 if not self.hosts and not self.model:
1147 self.invalid_syntax('Must provide hosts or --model.')
1148
1149 return (options, leftover)
1150
1151
Ningning Xia56d68432018-06-06 17:28:20 -07001152 def _remove_invalid_hostnames(self, hostnames, log_failure=False):
1153 """Remove hostnames with MIGRATED_HOST_SUFFIX.
1154
1155 @param hostnames: A list of hostnames.
1156 @param log_failure: Bool indicating whether to log invalid hostsnames.
1157
1158 @return A list of valid hostnames.
1159 """
1160 invalid_hostnames = set()
Ningning Xia9df3d152018-05-23 17:15:14 -07001161 for hostname in hostnames:
1162 if hostname.endswith(MIGRATED_HOST_SUFFIX):
Ningning Xia56d68432018-06-06 17:28:20 -07001163 if log_failure:
1164 self.failure('Cannot migrate host with suffix "%s" %s.' %
1165 (MIGRATED_HOST_SUFFIX, hostname),
1166 item=hostname, what_failed='Failed to rename')
1167 invalid_hostnames.add(hostname)
1168
1169 hostnames = list(set(hostnames) - invalid_hostnames)
1170
1171 return hostnames
1172
1173
1174 def execute(self):
1175 """Execute 'atest host migrate'."""
1176 hostnames = self._remove_invalid_hostnames(self.hosts, log_failure=True)
Ningning Xia9df3d152018-05-23 17:15:14 -07001177
1178 filters = {}
1179 check_results = {}
1180 if hostnames:
1181 check_results['hostname__in'] = 'hostname'
1182 if self.migration:
1183 filters['hostname__in'] = hostnames
1184 else:
1185 # rollback
1186 hostnames_with_suffix = [
1187 _add_hostname_suffix(h, MIGRATED_HOST_SUFFIX)
1188 for h in hostnames]
1189 filters['hostname__in'] = hostnames_with_suffix
Ningning Xia56d68432018-06-06 17:28:20 -07001190 else:
1191 # TODO(nxia): add exclude_filter {'hostname__endswith':
1192 # MIGRATED_HOST_SUFFIX} for --migration
1193 if self.rollback:
1194 filters['hostname__endswith'] = MIGRATED_HOST_SUFFIX
Ningning Xia9df3d152018-05-23 17:15:14 -07001195
1196 labels = []
1197 if self.model:
1198 labels.append('model:%s' % self.model)
1199 if self.pool:
1200 labels.append('pool:%s' % self.pool)
1201
1202 if labels:
1203 if len(labels) == 1:
1204 filters['labels__name__in'] = labels
1205 check_results['labels__name__in'] = None
1206 else:
1207 filters['multiple_labels'] = labels
1208 check_results['multiple_labels'] = None
1209
1210 results = super(host_migrate, self).execute(
1211 op='get_hosts', filters=filters, check_results=check_results)
1212 hostnames = [h['hostname'] for h in results]
1213
Ningning Xia56d68432018-06-06 17:28:20 -07001214 if self.migration:
1215 hostnames = self._remove_invalid_hostnames(hostnames)
1216 else:
1217 # rollback
1218 hostnames = [_remove_hostname_suffix(h, MIGRATED_HOST_SUFFIX)
1219 for h in hostnames]
1220
Ningning Xia9df3d152018-05-23 17:15:14 -07001221 return self.execute_skylab_migration(hostnames)
1222
1223
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001224 def assign_duts_to_drone(self, infra, all_devices):
1225 """Assign dut uids of the devices to a random skylab drone.
1226
1227 @param infra: An instance of lab_pb2.Infrastructure.
1228 @all_devices: A list of device_pb2.Device to be assigned to the drone.
1229 """
1230 skylab_drones = skylab_server.get_servers(
1231 infra, 'staging', role='skylab_drone', status='primary')
1232
1233 if len(skylab_drones) == 0:
1234 raise device.SkylabDeviceActionError(
1235 'No skylab drone is found in primary status and staging '
1236 'environment. Please confirm there is at least one valid skylab'
1237 ' drone added in skylab inventory.')
1238
1239 skylab_drone = random.choice(skylab_drones)
1240 skylab_server.add_dut_uids(skylab_drone, all_devices)
1241
1242
Ningning Xia9df3d152018-05-23 17:15:14 -07001243 def execute_skylab_migration(self, hostnames):
1244 """Execute migration in skylab_inventory.
1245
1246 @param hostnames: A list of hostnames to migrate.
1247 @return If there're hosts to migrate, return a list of the hostnames and
1248 a message instructing actions after the migration; else return
1249 None.
1250 """
1251 if not hostnames:
1252 return
1253
1254 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
1255 inventory_repo.initialize()
1256
1257 subdirs = ['skylab', 'prod', 'staging']
1258 data_dirs = skylab_data_dir, prod_data_dir, staging_data_dir = [
1259 inventory_repo.get_data_dir(data_subdir=d) for d in subdirs]
1260 skylab_lab, prod_lab, staging_lab = [
1261 text_manager.load_lab(d) for d in data_dirs]
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001262 infra = text_manager.load_infrastructure(skylab_data_dir)
Ningning Xia9df3d152018-05-23 17:15:14 -07001263
1264 label_map = None
1265 labels = []
1266 if self.model:
1267 labels.append('board:%s' % self.model)
1268 if self.pool:
1269 labels.append('critical_pool:%s' % self.pool)
1270 if labels:
1271 label_map = device.convert_to_label_map(labels)
1272
1273 if self.migration:
1274 prod_devices = device.move_devices(
1275 prod_lab, skylab_lab, 'duts', label_map=label_map,
1276 hostnames=hostnames)
1277 staging_devices = device.move_devices(
1278 staging_lab, skylab_lab, 'duts', label_map=label_map,
1279 hostnames=hostnames)
1280
1281 all_devices = prod_devices + staging_devices
1282 # Hostnames in afe_hosts tabel.
1283 device_hostnames = [str(d.common.hostname) for d in all_devices]
1284 message = (
1285 'Migration: move %s hosts into skylab_inventory.\n\n'
Ningning Xia423c7962018-06-07 15:27:18 -07001286 'Please run this command after the CL is submitted:\n'
Ningning Xia9df3d152018-05-23 17:15:14 -07001287 'atest host rename --for-migration %s' %
1288 (len(all_devices), ' '.join(device_hostnames)))
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001289
1290 self.assign_duts_to_drone(infra, all_devices)
Ningning Xia9df3d152018-05-23 17:15:14 -07001291 else:
1292 # rollback
1293 prod_devices = device.move_devices(
1294 skylab_lab, prod_lab, 'duts', environment='prod',
Ningning Xia56d68432018-06-06 17:28:20 -07001295 label_map=label_map, hostnames=hostnames)
Ningning Xia9df3d152018-05-23 17:15:14 -07001296 staging_devices = device.move_devices(
1297 staging_lab, skylab_lab, 'duts', environment='staging',
Ningning Xia56d68432018-06-06 17:28:20 -07001298 label_map=label_map, hostnames=hostnames)
Ningning Xia9df3d152018-05-23 17:15:14 -07001299
1300 all_devices = prod_devices + staging_devices
1301 # Hostnames in afe_hosts tabel.
1302 device_hostnames = [_add_hostname_suffix(str(d.common.hostname),
1303 MIGRATED_HOST_SUFFIX)
1304 for d in all_devices]
1305 message = (
1306 'Rollback: remove %s hosts from skylab_inventory.\n\n'
Ningning Xia423c7962018-06-07 15:27:18 -07001307 'Please run this command after the CL is submitted:\n'
Ningning Xia9df3d152018-05-23 17:15:14 -07001308 'atest host rename --for-rollback %s' %
1309 (len(all_devices), ' '.join(device_hostnames)))
1310
1311 if all_devices:
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001312 text_manager.dump_infrastructure(skylab_data_dir, infra)
1313
Ningning Xia9df3d152018-05-23 17:15:14 -07001314 if prod_devices:
1315 text_manager.dump_lab(prod_data_dir, prod_lab)
1316
1317 if staging_devices:
1318 text_manager.dump_lab(staging_data_dir, staging_lab)
1319
1320 text_manager.dump_lab(skylab_data_dir, skylab_lab)
1321
1322 self.change_number = inventory_repo.upload_change(
1323 message, draft=self.draft, dryrun=self.dryrun,
1324 submit=self.submit)
1325
1326 return all_devices, message
1327
1328
1329 def output(self, result):
1330 """Print output of 'atest host list'.
1331
1332 @param result: the result to be printed.
1333 """
1334 if result:
1335 devices, message = result
1336
1337 if devices:
1338 hostnames = [h.common.hostname for h in devices]
1339 if self.migration:
1340 print('Migrating hosts: %s' % ','.join(hostnames))
1341 else:
1342 # rollback
1343 print('Rolling back hosts: %s' % ','.join(hostnames))
1344
1345 if not self.dryrun:
1346 if not self.submit:
1347 print(skylab_utils.get_cl_message(self.change_number))
1348 else:
1349 # Print the instruction command for renaming hosts.
1350 print('%s' % message)
1351 else:
1352 print('No hosts were migrated.')