blob: 9f681c4ef481d47a884afd8bc83a0d44511536d1 [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()
Ningning Xia1dd82ee2018-06-08 11:41:03 -0700617 self.parser.add_option('--new-env',
618 dest='new_env',
619 choices=['staging', 'prod'],
620 help=('The new environment ("staging" or '
621 '"prod") of the hosts. %s' %
622 skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
623 default=None)
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700624
625
626 def _parse_unlock_options(self, options):
627 """Parse unlock related options."""
628 if self.skylab and options.unlock and options.unlock_lock_id is None:
629 self.invalid_syntax('Must provide --unlock-lock-id with "--skylab '
630 '--unlock".')
631
632 if (not (self.skylab and options.unlock) and
633 options.unlock_lock_id is not None):
634 self.invalid_syntax('--unlock-lock-id is only valid with '
635 '"--skylab --unlock".')
636
637 self.unlock_lock_id = options.unlock_lock_id
638
Justin Giorgi16bba562016-06-22 10:42:13 -0700639
640 def parse(self):
641 """Consume the specific options"""
642 (options, leftover) = super(host_mod, self).parse()
643
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700644 self._parse_unlock_options(options)
645
Justin Giorgi16bba562016-06-22 10:42:13 -0700646 if options.force_modify_locking:
647 self.data['force_modify_locking'] = True
648
Ningning Xiaef35cb52018-05-04 17:58:20 -0700649 if self.skylab and options.remove_acls:
650 # TODO(nxia): drop the flag when all hosts are migrated to skylab
651 self.invalid_syntax('--remove_acls is not supported with --skylab.')
652
Justin Giorgi16bba562016-06-22 10:42:13 -0700653 self.remove_acls = options.remove_acls
654 self.remove_labels = options.remove_labels
Ningning Xia1dd82ee2018-06-08 11:41:03 -0700655 self.new_env = options.new_env
Justin Giorgi16bba562016-06-22 10:42:13 -0700656
657 return (options, leftover)
658
659
Ningning Xiaef35cb52018-05-04 17:58:20 -0700660 def execute_skylab(self):
661 """Execute atest host mod with --skylab.
662
663 @return A list of hostnames which have been successfully modified.
664 """
665 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
666 inventory_repo.initialize()
667 data_dir = inventory_repo.get_data_dir()
668 lab = text_manager.load_lab(data_dir)
669
670 locked_by = None
671 if self.lock:
672 locked_by = inventory_repo.git_repo.config('user.email')
673
674 successes = []
675 for hostname in self.hosts:
676 try:
677 device.modify(
678 lab,
679 'duts',
680 hostname,
681 self.environment,
682 lock=self.lock,
683 locked_by=locked_by,
684 lock_reason = self.lock_reason,
685 unlock=self.unlock,
686 unlock_lock_id=self.unlock_lock_id,
687 attributes=self.attributes,
688 remove_labels=self.remove_labels,
Ningning Xia1dd82ee2018-06-08 11:41:03 -0700689 label_map=self.label_map,
690 new_env=self.new_env)
Ningning Xiaef35cb52018-05-04 17:58:20 -0700691 successes.append(hostname)
692 except device.SkylabDeviceActionError as e:
693 print('Cannot modify host %s: %s' % (hostname, e))
694
695 if successes:
696 text_manager.dump_lab(data_dir, lab)
697
698 status = inventory_repo.git_repo.status()
699 if not status:
700 print('Nothing is changed for hosts %s.' % successes)
701 return []
702
703 message = skylab_utils.construct_commit_message(
704 'Modify %d hosts.\n\n%s' % (len(successes), successes))
705 self.change_number = inventory_repo.upload_change(
706 message, draft=self.draft, dryrun=self.dryrun,
707 submit=self.submit)
708
709 return successes
710
711
Justin Giorgi16bba562016-06-22 10:42:13 -0700712 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800713 """Execute 'atest host mod'."""
Ningning Xiaef35cb52018-05-04 17:58:20 -0700714 if self.skylab:
715 return self.execute_skylab()
716
Justin Giorgi16bba562016-06-22 10:42:13 -0700717 successes = []
718 for host in self.execute_rpc('get_hosts', hostname__in=self.hosts):
719 self.host_ids[host['hostname']] = host['id']
720 for host in self.hosts:
721 if host not in self.host_ids:
722 self.failure('Cannot modify non-existant host %s.' % host)
723 continue
724 host_id = self.host_ids[host]
725
726 try:
727 if self.data:
728 self.execute_rpc('modify_host', item=host,
729 id=host, **self.data)
730
731 if self.attributes:
732 self._set_attributes(host, self.attributes)
733
734 if self.labels or self.remove_labels:
735 self._set_labels(host, self.labels)
736
737 if self.platform:
738 self._set_platform_label(host, self.platform)
739
740 # TODO: Make the AFE return True or False,
741 # especially for lock
742 successes.append(host)
743 except topic_common.CliError, full_error:
744 # Already logged by execute_rpc()
745 pass
746
747 if self.acls or self.remove_acls:
748 self._set_acls(self.hosts, self.acls)
749
750 return successes
751
752
753 def output(self, hosts):
xixuand6011f12016-12-08 15:01:58 -0800754 """Print output of 'atest host mod'.
755
756 @param hosts: the host list to be printed.
757 """
Justin Giorgi16bba562016-06-22 10:42:13 -0700758 for msg in self.messages:
759 self.print_wrapped(msg, hosts)
760
Ningning Xiaef35cb52018-05-04 17:58:20 -0700761 if hosts and self.skylab:
Ningning Xiac8b430d2018-05-17 15:10:25 -0700762 print('Modified hosts: %s.' % ', '.join(hosts))
Ningning Xiaef35cb52018-05-04 17:58:20 -0700763 if self.skylab and not self.dryrun and not self.submit:
Ningning Xiac8b430d2018-05-17 15:10:25 -0700764 print(skylab_utils.get_cl_message(self.change_number))
Ningning Xiaef35cb52018-05-04 17:58:20 -0700765
Justin Giorgi16bba562016-06-22 10:42:13 -0700766
767class HostInfo(object):
768 """Store host information so we don't have to keep looking it up."""
769 def __init__(self, hostname, platform, labels):
770 self.hostname = hostname
771 self.platform = platform
772 self.labels = labels
773
774
775class host_create(BaseHostModCreate):
776 """atest host create [--lock|--unlock --platform <arch>
777 --labels <labels>|--blist <label_file>
778 --acls <acls>|--alist <acl_file>
779 --protection <protection_type>
780 --attributes <attr>=<value>;<attr>=<value>
781 --mlist <mach_file>] <hosts>"""
782 usage_action = 'create'
783
784 def parse(self):
785 """Option logic specific to create action.
786 """
787 (options, leftovers) = super(host_create, self).parse()
788 self.locked = options.lock
789 if 'serials' in self.attributes:
790 if len(self.hosts) > 1:
791 raise topic_common.CliError('Can not specify serials with '
792 'multiple hosts.')
793
794
795 @classmethod
796 def construct_without_parse(
797 cls, web_server, hosts, platform=None,
798 locked=False, lock_reason='', labels=[], acls=[],
799 protection=host_protections.Protection.NO_PROTECTION):
800 """Construct a host_create object and fill in data from args.
801
802 Do not need to call parse after the construction.
803
804 Return an object of site_host_create ready to execute.
805
806 @param web_server: A string specifies the autotest webserver url.
807 It is needed to setup comm to make rpc.
808 @param hosts: A list of hostnames as strings.
809 @param platform: A string or None.
810 @param locked: A boolean.
811 @param lock_reason: A string.
812 @param labels: A list of labels as strings.
813 @param acls: A list of acls as strings.
814 @param protection: An enum defined in host_protections.
815 """
816 obj = cls()
817 obj.web_server = web_server
818 try:
819 # Setup stuff needed for afe comm.
820 obj.afe = rpc.afe_comm(web_server)
821 except rpc.AuthError, s:
822 obj.failure(str(s), fatal=True)
823 obj.hosts = hosts
824 obj.platform = platform
825 obj.locked = locked
826 if locked and lock_reason.strip():
827 obj.data['lock_reason'] = lock_reason.strip()
828 obj.labels = labels
829 obj.acls = acls
830 if protection:
831 obj.data['protection'] = protection
832 obj.attributes = {}
833 return obj
834
835
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700836 def _detect_host_info(self, host):
837 """Detect platform and labels from the host.
838
839 @param host: hostname
840
841 @return: HostInfo object
842 """
843 # Mock an afe_host object so that the host is constructed as if the
844 # data was already in afe
845 data = {'attributes': self.attributes, 'labels': self.labels}
846 afe_host = frontend.Host(None, data)
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700847 store = host_info.InMemoryHostInfoStore(
848 host_info.HostInfo(labels=self.labels,
849 attributes=self.attributes))
850 machine = {
851 'hostname': host,
852 'afe_host': afe_host,
853 'host_info_store': store
854 }
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700855 try:
Allen Li2c32d6b2017-02-03 15:28:10 -0800856 if bin_utils.ping(host, tries=1, deadline=1) == 0:
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700857 serials = self.attributes.get('serials', '').split(',')
Richard Barnette9db80682018-04-26 00:55:15 +0000858 adb_serial = self.attributes.get('serials')
859 host_dut = hosts.create_host(machine,
860 adb_serial=adb_serial)
xixuand6011f12016-12-08 15:01:58 -0800861
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700862 info = HostInfo(host, host_dut.get_platform(),
863 host_dut.get_labels())
xixuand6011f12016-12-08 15:01:58 -0800864 # Clean host to make sure nothing left after calling it,
865 # e.g. tunnels.
866 if hasattr(host_dut, 'close'):
867 host_dut.close()
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700868 else:
869 # Can't ping the host, use default information.
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700870 info = HostInfo(host, None, [])
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700871 except (socket.gaierror, error.AutoservRunError,
872 error.AutoservSSHTimeout):
873 # We may be adding a host that does not exist yet or we can't
874 # reach due to hostname/address issues or if the host is down.
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700875 info = HostInfo(host, None, [])
876 return info
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700877
878
mblighbe630eb2008-08-01 16:41:48 +0000879 def _execute_add_one_host(self, host):
mbligh719e14a2008-12-04 01:17:08 +0000880 # Always add the hosts as locked to avoid the host
Matthew Sartori68186332015-04-27 17:19:53 -0700881 # being picked up by the scheduler before it's ACL'ed.
mbligh719e14a2008-12-04 01:17:08 +0000882 self.data['locked'] = True
Matthew Sartori68186332015-04-27 17:19:53 -0700883 if not self.locked:
884 self.data['lock_reason'] = 'Forced lock on device creation'
Justin Giorgi16bba562016-06-22 10:42:13 -0700885 self.execute_rpc('add_host', hostname=host, status="Ready", **self.data)
mblighbe630eb2008-08-01 16:41:48 +0000886
Justin Giorgi16bba562016-06-22 10:42:13 -0700887 # If there are labels avaliable for host, use them.
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700888 info = self._detect_host_info(host)
Justin Giorgi16bba562016-06-22 10:42:13 -0700889 labels = set(self.labels)
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700890 if info.labels:
891 labels.update(info.labels)
Justin Giorgi16bba562016-06-22 10:42:13 -0700892
893 if labels:
894 self._set_labels(host, list(labels))
895
896 # Now add the platform label.
897 # If a platform was not provided and we were able to retrieve it
898 # from the host, use the retrieved platform.
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700899 platform = self.platform if self.platform else info.platform
Justin Giorgi16bba562016-06-22 10:42:13 -0700900 if platform:
901 self._set_platform_label(host, platform)
902
903 if self.attributes:
904 self._set_attributes(host, self.attributes)
mblighbe630eb2008-08-01 16:41:48 +0000905
906
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700907 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800908 """Execute 'atest host create'."""
Justin Giorgi16bba562016-06-22 10:42:13 -0700909 successful_hosts = []
910 for host in self.hosts:
911 try:
912 self._execute_add_one_host(host)
913 successful_hosts.append(host)
914 except topic_common.CliError:
915 pass
Jiaxi Luoc342f9f2014-05-19 16:22:03 -0700916
917 if successful_hosts:
Justin Giorgi16bba562016-06-22 10:42:13 -0700918 self._set_acls(successful_hosts, self.acls)
Jiaxi Luoc342f9f2014-05-19 16:22:03 -0700919
920 if not self.locked:
921 for host in successful_hosts:
Matthew Sartori68186332015-04-27 17:19:53 -0700922 self.execute_rpc('modify_host', id=host, locked=False,
923 lock_reason='')
Jiaxi Luoc342f9f2014-05-19 16:22:03 -0700924 return successful_hosts
925
926
mblighbe630eb2008-08-01 16:41:48 +0000927 def output(self, hosts):
xixuand6011f12016-12-08 15:01:58 -0800928 """Print output of 'atest host create'.
929
930 @param hosts: the added host list to be printed.
931 """
mblighbe630eb2008-08-01 16:41:48 +0000932 self.print_wrapped('Added host', hosts)
933
934
935class host_delete(action_common.atest_delete, host):
936 """atest host delete [--mlist <mach_file>] <hosts>"""
Ningning Xiac8b430d2018-05-17 15:10:25 -0700937
938 def __init__(self):
939 super(host_delete, self).__init__()
940
941 self.add_skylab_options()
942
943
944 def execute_skylab(self):
945 """Execute 'atest host delete' with '--skylab'.
946
947 @return A list of hostnames which have been successfully deleted.
948 """
949 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
950 inventory_repo.initialize()
951 data_dir = inventory_repo.get_data_dir()
952 lab = text_manager.load_lab(data_dir)
953
954 successes = []
955 for hostname in self.hosts:
956 try:
957 device.delete(
958 lab,
959 'duts',
960 hostname,
961 self.environment)
962 successes.append(hostname)
963 except device.SkylabDeviceActionError as e:
964 print('Cannot delete host %s: %s' % (hostname, e))
965
966 if successes:
967 text_manager.dump_lab(data_dir, lab)
968 message = skylab_utils.construct_commit_message(
969 'Delete %d hosts.\n\n%s' % (len(successes), successes))
970 self.change_number = inventory_repo.upload_change(
971 message, draft=self.draft, dryrun=self.dryrun,
972 submit=self.submit)
973
974 return successes
975
976
977 def execute(self):
978 """Execute 'atest host delete'.
979
980 @return A list of hostnames which have been successfully deleted.
981 """
982 if self.skylab:
983 return self.execute_skylab()
984
985 return super(host_delete, self).execute()
Ningning Xiac46bdd12018-05-29 11:24:14 -0700986
987
988class InvalidHostnameError(Exception):
989 """Cannot perform actions on the host because of invalid hostname."""
990
991
992def _add_hostname_suffix(hostname, suffix):
993 """Add the suffix to the hostname."""
994 if hostname.endswith(suffix):
995 raise InvalidHostnameError(
996 'Cannot add "%s" as it already contains the suffix.' % suffix)
997
998 return hostname + suffix
999
1000
1001def _remove_hostname_suffix(hostname, suffix):
1002 """Remove the suffix from the hostname."""
1003 if not hostname.endswith(suffix):
1004 raise InvalidHostnameError(
1005 'Cannot remove "%s" as it doesn\'t contain the suffix.' %
1006 suffix)
1007
1008 return hostname[:len(hostname) - len(suffix)]
1009
1010
1011class host_rename(host):
1012 """Host rename is only for migrating hosts between skylab and AFE DB."""
1013
1014 usage_action = 'rename'
1015
1016 def __init__(self):
1017 """Add the options specific to the rename action."""
1018 super(host_rename, self).__init__()
1019
1020 self.parser.add_option('--for-migration',
1021 help=('Rename hostnames for migration. Rename '
1022 'each "hostname" to "hostname%s". '
1023 'The original "hostname" must not contain '
1024 'suffix.' % MIGRATED_HOST_SUFFIX),
1025 action='store_true',
1026 default=False)
1027 self.parser.add_option('--for-rollback',
1028 help=('Rename hostnames for migration rollback. '
1029 'Rename each "hostname%s" to its original '
1030 '"hostname".' % MIGRATED_HOST_SUFFIX),
1031 action='store_true',
1032 default=False)
1033 self.parser.add_option('--dryrun',
1034 help='Execute the action as a dryrun.',
1035 action='store_true',
1036 default=False)
1037
1038
1039 def parse(self):
1040 """Consume the options common to host rename."""
1041 (options, leftovers) = super(host_rename, self).parse()
1042 self.for_migration = options.for_migration
1043 self.for_rollback = options.for_rollback
1044 self.dryrun = options.dryrun
1045 self.host_ids = {}
1046
1047 if not (self.for_migration ^ self.for_rollback):
1048 self.invalid_syntax('--for-migration and --for-rollback are '
1049 'exclusive, and one of them must be enabled.')
1050
1051 if not self.hosts:
1052 self.invalid_syntax('Must provide hostname(s).')
1053
1054 if self.dryrun:
1055 print('This will be a dryrun and will not rename hostnames.')
1056
1057 return (options, leftovers)
1058
1059
1060 def execute(self):
1061 """Execute 'atest host rename'."""
1062 if not self.prompt_confirmation():
1063 return
1064
1065 successes = []
1066 for host in self.execute_rpc('get_hosts', hostname__in=self.hosts):
1067 self.host_ids[host['hostname']] = host['id']
1068 for host in self.hosts:
1069 if host not in self.host_ids:
1070 self.failure('Cannot rename non-existant host %s.' % host,
1071 item=host, what_failed='Failed to rename')
1072 continue
1073 try:
Ningning Xiab261b162018-06-07 17:24:59 -07001074 host_id = self.host_ids[host]
Ningning Xiac46bdd12018-05-29 11:24:14 -07001075 if self.for_migration:
1076 new_hostname = _add_hostname_suffix(
1077 host, MIGRATED_HOST_SUFFIX)
1078 else:
1079 #for_rollback
1080 new_hostname = _remove_hostname_suffix(
1081 host, MIGRATED_HOST_SUFFIX)
1082
1083 if not self.dryrun:
Ningning Xia423c7962018-06-07 15:27:18 -07001084 # TODO(crbug.com/850737): delete and abort HQE.
Ningning Xiac46bdd12018-05-29 11:24:14 -07001085 data = {'hostname': new_hostname}
Ningning Xiab261b162018-06-07 17:24:59 -07001086 self.execute_rpc('modify_host', item=host, id=host_id,
1087 **data)
Ningning Xiac46bdd12018-05-29 11:24:14 -07001088 successes.append((host, new_hostname))
1089 except InvalidHostnameError as e:
1090 self.failure('Cannot rename host %s: %s' % (host, e), item=host,
1091 what_failed='Failed to rename')
1092 except topic_common.CliError, full_error:
1093 # Already logged by execute_rpc()
1094 pass
1095
1096 return successes
1097
1098
1099 def output(self, results):
1100 """Print output of 'atest host rename'."""
1101 if results:
1102 print('Successfully renamed:')
1103 for old_hostname, new_hostname in results:
1104 print('%s to %s' % (old_hostname, new_hostname))
Ningning Xia9df3d152018-05-23 17:15:14 -07001105
1106
1107class host_migrate(action_common.atest_list, host):
1108 """'atest host migrate' to migrate or rollback hosts."""
1109
1110 usage_action = 'migrate'
1111
1112 def __init__(self):
1113 super(host_migrate, self).__init__()
1114
1115 self.parser.add_option('--migration',
1116 dest='migration',
1117 help='Migrate the hosts to skylab.',
1118 action='store_true',
1119 default=False)
1120 self.parser.add_option('--rollback',
1121 dest='rollback',
1122 help='Rollback the hosts migrated to skylab.',
1123 action='store_true',
1124 default=False)
1125 self.parser.add_option('--model',
1126 help='Model of the hosts to migrate.',
1127 dest='model',
1128 default=None)
1129 self.parser.add_option('--pool',
1130 help=('Pool of the hosts to migrate. Must '
1131 'specify --model for the pool.'),
1132 dest='pool',
1133 default=None)
1134
1135 self.add_skylab_options(enforce_skylab=True)
1136
1137
1138 def parse(self):
1139 """Consume the specific options"""
1140 (options, leftover) = super(host_migrate, self).parse()
1141
1142 self.migration = options.migration
1143 self.rollback = options.rollback
1144 self.model = options.model
1145 self.pool = options.pool
1146 self.host_ids = {}
1147
1148 if not (self.migration ^ self.rollback):
1149 self.invalid_syntax('--migration and --rollback are exclusive, '
1150 'and one of them must be enabled.')
1151
1152 if self.pool is not None and self.model is None:
1153 self.invalid_syntax('Must provide --model with --pool.')
1154
1155 if not self.hosts and not self.model:
1156 self.invalid_syntax('Must provide hosts or --model.')
1157
1158 return (options, leftover)
1159
1160
Ningning Xia56d68432018-06-06 17:28:20 -07001161 def _remove_invalid_hostnames(self, hostnames, log_failure=False):
1162 """Remove hostnames with MIGRATED_HOST_SUFFIX.
1163
1164 @param hostnames: A list of hostnames.
1165 @param log_failure: Bool indicating whether to log invalid hostsnames.
1166
1167 @return A list of valid hostnames.
1168 """
1169 invalid_hostnames = set()
Ningning Xia9df3d152018-05-23 17:15:14 -07001170 for hostname in hostnames:
1171 if hostname.endswith(MIGRATED_HOST_SUFFIX):
Ningning Xia56d68432018-06-06 17:28:20 -07001172 if log_failure:
1173 self.failure('Cannot migrate host with suffix "%s" %s.' %
1174 (MIGRATED_HOST_SUFFIX, hostname),
1175 item=hostname, what_failed='Failed to rename')
1176 invalid_hostnames.add(hostname)
1177
1178 hostnames = list(set(hostnames) - invalid_hostnames)
1179
1180 return hostnames
1181
1182
1183 def execute(self):
1184 """Execute 'atest host migrate'."""
1185 hostnames = self._remove_invalid_hostnames(self.hosts, log_failure=True)
Ningning Xia9df3d152018-05-23 17:15:14 -07001186
1187 filters = {}
1188 check_results = {}
1189 if hostnames:
1190 check_results['hostname__in'] = 'hostname'
1191 if self.migration:
1192 filters['hostname__in'] = hostnames
1193 else:
1194 # rollback
1195 hostnames_with_suffix = [
1196 _add_hostname_suffix(h, MIGRATED_HOST_SUFFIX)
1197 for h in hostnames]
1198 filters['hostname__in'] = hostnames_with_suffix
Ningning Xia56d68432018-06-06 17:28:20 -07001199 else:
1200 # TODO(nxia): add exclude_filter {'hostname__endswith':
1201 # MIGRATED_HOST_SUFFIX} for --migration
1202 if self.rollback:
1203 filters['hostname__endswith'] = MIGRATED_HOST_SUFFIX
Ningning Xia9df3d152018-05-23 17:15:14 -07001204
1205 labels = []
1206 if self.model:
1207 labels.append('model:%s' % self.model)
1208 if self.pool:
1209 labels.append('pool:%s' % self.pool)
1210
1211 if labels:
1212 if len(labels) == 1:
1213 filters['labels__name__in'] = labels
1214 check_results['labels__name__in'] = None
1215 else:
1216 filters['multiple_labels'] = labels
1217 check_results['multiple_labels'] = None
1218
1219 results = super(host_migrate, self).execute(
1220 op='get_hosts', filters=filters, check_results=check_results)
1221 hostnames = [h['hostname'] for h in results]
1222
Ningning Xia56d68432018-06-06 17:28:20 -07001223 if self.migration:
1224 hostnames = self._remove_invalid_hostnames(hostnames)
1225 else:
1226 # rollback
1227 hostnames = [_remove_hostname_suffix(h, MIGRATED_HOST_SUFFIX)
1228 for h in hostnames]
1229
Ningning Xia9df3d152018-05-23 17:15:14 -07001230 return self.execute_skylab_migration(hostnames)
1231
1232
Xixuan Wua8d93c82018-08-03 16:23:26 -07001233 def assign_duts_to_drone(self, infra, devices, environment):
Ningning Xia877817f2018-06-08 12:23:48 -07001234 """Assign uids of the devices to a random skylab drone.
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001235
1236 @param infra: An instance of lab_pb2.Infrastructure.
Xixuan Wua8d93c82018-08-03 16:23:26 -07001237 @param devices: A list of device_pb2.Device to be assigned to the drone.
1238 @param environment: 'staging' or 'prod'.
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001239 """
1240 skylab_drones = skylab_server.get_servers(
Xixuan Wua8d93c82018-08-03 16:23:26 -07001241 infra, environment, role='skylab_drone', status='primary')
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001242
1243 if len(skylab_drones) == 0:
1244 raise device.SkylabDeviceActionError(
1245 'No skylab drone is found in primary status and staging '
1246 'environment. Please confirm there is at least one valid skylab'
1247 ' drone added in skylab inventory.')
1248
Xixuan Wudfc2de22018-08-06 09:29:01 -07001249 for device in devices:
1250 # Randomly distribute each device to a skylab_drone.
1251 skylab_drone = random.choice(skylab_drones)
1252 skylab_server.add_dut_uids(skylab_drone, [device])
Ningning Xia877817f2018-06-08 12:23:48 -07001253
1254
1255 def remove_duts_from_drone(self, infra, devices):
1256 """Remove uids of the devices from their skylab drones.
1257
1258 @param infra: An instance of lab_pb2.Infrastructure.
1259 @devices: A list of device_pb2.Device to be remove from the drone.
1260 """
1261 skylab_drones = skylab_server.get_servers(
1262 infra, 'staging', role='skylab_drone', status='primary')
1263
1264 for skylab_drone in skylab_drones:
1265 skylab_server.remove_dut_uids(skylab_drone, devices)
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001266
1267
Ningning Xia9df3d152018-05-23 17:15:14 -07001268 def execute_skylab_migration(self, hostnames):
1269 """Execute migration in skylab_inventory.
1270
1271 @param hostnames: A list of hostnames to migrate.
1272 @return If there're hosts to migrate, return a list of the hostnames and
1273 a message instructing actions after the migration; else return
1274 None.
1275 """
1276 if not hostnames:
1277 return
1278
1279 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
1280 inventory_repo.initialize()
1281
1282 subdirs = ['skylab', 'prod', 'staging']
1283 data_dirs = skylab_data_dir, prod_data_dir, staging_data_dir = [
1284 inventory_repo.get_data_dir(data_subdir=d) for d in subdirs]
1285 skylab_lab, prod_lab, staging_lab = [
1286 text_manager.load_lab(d) for d in data_dirs]
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001287 infra = text_manager.load_infrastructure(skylab_data_dir)
Ningning Xia9df3d152018-05-23 17:15:14 -07001288
1289 label_map = None
1290 labels = []
1291 if self.model:
1292 labels.append('board:%s' % self.model)
1293 if self.pool:
1294 labels.append('critical_pool:%s' % self.pool)
1295 if labels:
1296 label_map = device.convert_to_label_map(labels)
1297
1298 if self.migration:
1299 prod_devices = device.move_devices(
1300 prod_lab, skylab_lab, 'duts', label_map=label_map,
1301 hostnames=hostnames)
1302 staging_devices = device.move_devices(
1303 staging_lab, skylab_lab, 'duts', label_map=label_map,
1304 hostnames=hostnames)
1305
1306 all_devices = prod_devices + staging_devices
1307 # Hostnames in afe_hosts tabel.
1308 device_hostnames = [str(d.common.hostname) for d in all_devices]
1309 message = (
1310 'Migration: move %s hosts into skylab_inventory.\n\n'
Ningning Xia423c7962018-06-07 15:27:18 -07001311 'Please run this command after the CL is submitted:\n'
Ningning Xia9df3d152018-05-23 17:15:14 -07001312 'atest host rename --for-migration %s' %
1313 (len(all_devices), ' '.join(device_hostnames)))
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001314
Xixuan Wua8d93c82018-08-03 16:23:26 -07001315 self.assign_duts_to_drone(infra, prod_devices, 'prod')
1316 self.assign_duts_to_drone(infra, staging_devices, 'staging')
Ningning Xia9df3d152018-05-23 17:15:14 -07001317 else:
1318 # rollback
1319 prod_devices = device.move_devices(
1320 skylab_lab, prod_lab, 'duts', environment='prod',
Ningning Xia56d68432018-06-06 17:28:20 -07001321 label_map=label_map, hostnames=hostnames)
Ningning Xia9df3d152018-05-23 17:15:14 -07001322 staging_devices = device.move_devices(
Ningning Xia877817f2018-06-08 12:23:48 -07001323 skylab_lab, staging_lab, 'duts', environment='staging',
Ningning Xia56d68432018-06-06 17:28:20 -07001324 label_map=label_map, hostnames=hostnames)
Ningning Xia9df3d152018-05-23 17:15:14 -07001325
1326 all_devices = prod_devices + staging_devices
1327 # Hostnames in afe_hosts tabel.
1328 device_hostnames = [_add_hostname_suffix(str(d.common.hostname),
1329 MIGRATED_HOST_SUFFIX)
1330 for d in all_devices]
1331 message = (
1332 'Rollback: remove %s hosts from skylab_inventory.\n\n'
Ningning Xia423c7962018-06-07 15:27:18 -07001333 'Please run this command after the CL is submitted:\n'
Ningning Xia9df3d152018-05-23 17:15:14 -07001334 'atest host rename --for-rollback %s' %
1335 (len(all_devices), ' '.join(device_hostnames)))
1336
Ningning Xia877817f2018-06-08 12:23:48 -07001337 self.remove_duts_from_drone(infra, all_devices)
1338
Ningning Xia9df3d152018-05-23 17:15:14 -07001339 if all_devices:
Ningning Xiaffb3c1f2018-06-07 18:42:32 -07001340 text_manager.dump_infrastructure(skylab_data_dir, infra)
1341
Ningning Xia9df3d152018-05-23 17:15:14 -07001342 if prod_devices:
1343 text_manager.dump_lab(prod_data_dir, prod_lab)
1344
1345 if staging_devices:
1346 text_manager.dump_lab(staging_data_dir, staging_lab)
1347
1348 text_manager.dump_lab(skylab_data_dir, skylab_lab)
1349
1350 self.change_number = inventory_repo.upload_change(
1351 message, draft=self.draft, dryrun=self.dryrun,
1352 submit=self.submit)
1353
1354 return all_devices, message
1355
1356
1357 def output(self, result):
1358 """Print output of 'atest host list'.
1359
1360 @param result: the result to be printed.
1361 """
1362 if result:
1363 devices, message = result
1364
1365 if devices:
1366 hostnames = [h.common.hostname for h in devices]
1367 if self.migration:
1368 print('Migrating hosts: %s' % ','.join(hostnames))
1369 else:
1370 # rollback
1371 print('Rolling back hosts: %s' % ','.join(hostnames))
1372
1373 if not self.dryrun:
1374 if not self.submit:
1375 print(skylab_utils.get_cl_message(self.change_number))
1376 else:
1377 # Print the instruction command for renaming hosts.
1378 print('%s' % message)
1379 else:
1380 print('No hosts were migrated.')