blob: 384101660db4e786f9722e8c7844d169cb8c6e98 [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
Simran Basi0739d682015-02-25 16:22:56 -080023import re
Justin Giorgi16bba562016-06-22 10:42:13 -070024import socket
mblighbe630eb2008-08-01 16:41:48 +000025
Ningning Xiaea02ab12018-05-11 15:08:57 -070026from autotest_lib.cli import action_common, rpc, topic_common, skylab_utils
Allen Li2c32d6b2017-02-03 15:28:10 -080027from autotest_lib.client.bin import utils as bin_utils
Justin Giorgi16bba562016-06-22 10:42:13 -070028from autotest_lib.client.common_lib import error, host_protections
Justin Giorgi5208eaa2016-07-02 20:12:12 -070029from autotest_lib.server import frontend, hosts
Prathmesh Prabhud252d262017-05-31 11:46:34 -070030from autotest_lib.server.hosts import host_info
mblighbe630eb2008-08-01 16:41:48 +000031
32
Ningning Xiaea02ab12018-05-11 15:08:57 -070033try:
34 from skylab_inventory import text_manager
35 from skylab_inventory.lib import device
36except ImportError:
37 pass
38
39
Ningning Xiac46bdd12018-05-29 11:24:14 -070040MIGRATED_HOST_SUFFIX = '-migrated-do-not-use'
41
42
mblighbe630eb2008-08-01 16:41:48 +000043class host(topic_common.atest):
44 """Host class
Ningning Xia9df3d152018-05-23 17:15:14 -070045 atest host [create|delete|list|stat|mod|jobs|rename|migrate] <options>"""
46 usage_action = '[create|delete|list|stat|mod|jobs|rename|migrate]'
mblighbe630eb2008-08-01 16:41:48 +000047 topic = msg_topic = 'host'
48 msg_items = '<hosts>'
49
mblighaed47e82009-03-17 19:06:18 +000050 protections = host_protections.Protection.names
51
mblighbe630eb2008-08-01 16:41:48 +000052
53 def __init__(self):
54 """Add to the parser the options common to all the
55 host actions"""
56 super(host, self).__init__()
57
58 self.parser.add_option('-M', '--mlist',
59 help='File listing the machines',
60 type='string',
61 default=None,
62 metavar='MACHINE_FLIST')
63
mbligh9deeefa2009-05-01 23:11:08 +000064 self.topic_parse_info = topic_common.item_parse_info(
65 attribute_name='hosts',
66 filename_option='mlist',
67 use_leftover=True)
mblighbe630eb2008-08-01 16:41:48 +000068
69
70 def _parse_lock_options(self, options):
71 if options.lock and options.unlock:
72 self.invalid_syntax('Only specify one of '
73 '--lock and --unlock.')
74
Ningning Xiaef35cb52018-05-04 17:58:20 -070075 self.lock = options.lock
76 self.unlock = options.unlock
77 self.lock_reason = options.lock_reason
Ningning Xiaef35cb52018-05-04 17:58:20 -070078
mblighbe630eb2008-08-01 16:41:48 +000079 if options.lock:
80 self.data['locked'] = True
81 self.messages.append('Locked host')
82 elif options.unlock:
83 self.data['locked'] = False
Matthew Sartori68186332015-04-27 17:19:53 -070084 self.data['lock_reason'] = ''
mblighbe630eb2008-08-01 16:41:48 +000085 self.messages.append('Unlocked host')
86
Matthew Sartori68186332015-04-27 17:19:53 -070087 if options.lock and options.lock_reason:
88 self.data['lock_reason'] = options.lock_reason
89
mblighbe630eb2008-08-01 16:41:48 +000090
91 def _cleanup_labels(self, labels, platform=None):
92 """Removes the platform label from the overall labels"""
93 if platform:
94 return [label for label in labels
95 if label != platform]
96 else:
97 try:
98 return [label for label in labels
99 if not label['platform']]
100 except TypeError:
101 # This is a hack - the server will soon
102 # do this, so all this code should be removed.
103 return labels
104
105
106 def get_items(self):
107 return self.hosts
108
109
110class host_help(host):
111 """Just here to get the atest logic working.
112 Usage is set by its parent"""
113 pass
114
115
116class host_list(action_common.atest_list, host):
117 """atest host list [--mlist <file>|<hosts>] [--label <label>]
mbligh536a5242008-10-18 14:35:54 +0000118 [--status <status1,status2>] [--acl <ACL>] [--user <user>]"""
mblighbe630eb2008-08-01 16:41:48 +0000119
120 def __init__(self):
121 super(host_list, self).__init__()
122
123 self.parser.add_option('-b', '--label',
mbligh6444c6b2008-10-27 20:55:13 +0000124 default='',
125 help='Only list hosts with all these labels '
Ningning Xiaea02ab12018-05-11 15:08:57 -0700126 '(comma separated). When --skylab is provided, '
127 'a label must be in the format of '
128 'label-key:label-value (e.g., board:lumpy).')
mblighbe630eb2008-08-01 16:41:48 +0000129 self.parser.add_option('-s', '--status',
mbligh6444c6b2008-10-27 20:55:13 +0000130 default='',
131 help='Only list hosts with any of these '
132 'statuses (comma separated)')
mbligh536a5242008-10-18 14:35:54 +0000133 self.parser.add_option('-a', '--acl',
mbligh6444c6b2008-10-27 20:55:13 +0000134 default='',
Ningning Xiaea02ab12018-05-11 15:08:57 -0700135 help=('Only list hosts within this ACL. %s' %
136 skylab_utils.MSG_INVALID_IN_SKYLAB))
mbligh536a5242008-10-18 14:35:54 +0000137 self.parser.add_option('-u', '--user',
mbligh6444c6b2008-10-27 20:55:13 +0000138 default='',
Ningning Xiaea02ab12018-05-11 15:08:57 -0700139 help=('Only list hosts available to this user. '
140 '%s' % skylab_utils.MSG_INVALID_IN_SKYLAB))
mbligh70b9bf42008-11-18 15:00:46 +0000141 self.parser.add_option('-N', '--hostnames-only', help='Only return '
142 'hostnames for the machines queried.',
143 action='store_true')
mbligh91e0efd2009-02-26 01:02:16 +0000144 self.parser.add_option('--locked',
145 default=False,
146 help='Only list locked hosts',
147 action='store_true')
148 self.parser.add_option('--unlocked',
149 default=False,
150 help='Only list unlocked hosts',
151 action='store_true')
Ningning Xiaea02ab12018-05-11 15:08:57 -0700152 self.parser.add_option('--full-output',
153 default=False,
154 help=('Print out the full content of the hosts. '
155 'Only supported with --skylab.'),
156 action='store_true',
157 dest='full_output')
mbligh91e0efd2009-02-26 01:02:16 +0000158
Ningning Xiaea02ab12018-05-11 15:08:57 -0700159 self.add_skylab_options()
mblighbe630eb2008-08-01 16:41:48 +0000160
161
162 def parse(self):
163 """Consume the specific options"""
jamesrenc2863162010-07-12 21:20:51 +0000164 label_info = topic_common.item_parse_info(attribute_name='labels',
165 inline_option='label')
166
167 (options, leftover) = super(host_list, self).parse([label_info])
168
mblighbe630eb2008-08-01 16:41:48 +0000169 self.status = options.status
mbligh536a5242008-10-18 14:35:54 +0000170 self.acl = options.acl
171 self.user = options.user
mbligh70b9bf42008-11-18 15:00:46 +0000172 self.hostnames_only = options.hostnames_only
mbligh91e0efd2009-02-26 01:02:16 +0000173
174 if options.locked and options.unlocked:
175 self.invalid_syntax('--locked and --unlocked are '
176 'mutually exclusive')
Ningning Xiaea02ab12018-05-11 15:08:57 -0700177
mbligh91e0efd2009-02-26 01:02:16 +0000178 self.locked = options.locked
179 self.unlocked = options.unlocked
Ningning Xia64ced002018-05-16 17:36:20 -0700180 self.label_map = None
Ningning Xiaea02ab12018-05-11 15:08:57 -0700181
182 if self.skylab:
183 if options.user or options.acl or options.status:
184 self.invalid_syntax('--user, --acl or --status is not '
185 'supported with --skylab.')
186 self.full_output = options.full_output
187 if self.full_output and self.hostnames_only:
188 self.invalid_syntax('--full-output is conflicted with '
189 '--hostnames-only.')
Ningning Xia64ced002018-05-16 17:36:20 -0700190
191 if self.labels:
192 self.label_map = device.convert_to_label_map(self.labels)
Ningning Xiaea02ab12018-05-11 15:08:57 -0700193 else:
194 if options.full_output:
195 self.invalid_syntax('--full_output is only supported with '
196 '--skylab.')
197
mblighbe630eb2008-08-01 16:41:48 +0000198 return (options, leftover)
199
200
Ningning Xiaea02ab12018-05-11 15:08:57 -0700201 def execute_skylab(self):
202 """Execute 'atest host list' with --skylab."""
203 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
204 inventory_repo.initialize()
205 lab = text_manager.load_lab(inventory_repo.get_data_dir())
206
207 # TODO(nxia): support filtering on run-time labels and status.
208 return device.get_devices(
209 lab,
210 'duts',
211 self.environment,
Ningning Xia64ced002018-05-16 17:36:20 -0700212 label_map=self.label_map,
Ningning Xiaea02ab12018-05-11 15:08:57 -0700213 hostnames=self.hosts,
214 locked=self.locked,
215 unlocked=self.unlocked)
216
217
mblighbe630eb2008-08-01 16:41:48 +0000218 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800219 """Execute 'atest host list'."""
Ningning Xiaea02ab12018-05-11 15:08:57 -0700220 if self.skylab:
221 return self.execute_skylab()
222
mblighbe630eb2008-08-01 16:41:48 +0000223 filters = {}
224 check_results = {}
225 if self.hosts:
226 filters['hostname__in'] = self.hosts
227 check_results['hostname__in'] = 'hostname'
mbligh6444c6b2008-10-27 20:55:13 +0000228
229 if self.labels:
jamesrenc2863162010-07-12 21:20:51 +0000230 if len(self.labels) == 1:
mblighf703fb42009-01-30 00:35:05 +0000231 # This is needed for labels with wildcards (x86*)
jamesrenc2863162010-07-12 21:20:51 +0000232 filters['labels__name__in'] = self.labels
mblighf703fb42009-01-30 00:35:05 +0000233 check_results['labels__name__in'] = None
234 else:
jamesrenc2863162010-07-12 21:20:51 +0000235 filters['multiple_labels'] = self.labels
mblighf703fb42009-01-30 00:35:05 +0000236 check_results['multiple_labels'] = None
mbligh6444c6b2008-10-27 20:55:13 +0000237
mblighbe630eb2008-08-01 16:41:48 +0000238 if self.status:
mbligh6444c6b2008-10-27 20:55:13 +0000239 statuses = self.status.split(',')
240 statuses = [status.strip() for status in statuses
241 if status.strip()]
242
243 filters['status__in'] = statuses
mblighcd8eb972008-08-25 19:20:39 +0000244 check_results['status__in'] = None
mbligh6444c6b2008-10-27 20:55:13 +0000245
mbligh536a5242008-10-18 14:35:54 +0000246 if self.acl:
showardd9ac4452009-02-07 02:04:37 +0000247 filters['aclgroup__name'] = self.acl
248 check_results['aclgroup__name'] = None
mbligh536a5242008-10-18 14:35:54 +0000249 if self.user:
showardd9ac4452009-02-07 02:04:37 +0000250 filters['aclgroup__users__login'] = self.user
251 check_results['aclgroup__users__login'] = None
mbligh91e0efd2009-02-26 01:02:16 +0000252
253 if self.locked or self.unlocked:
254 filters['locked'] = self.locked
255 check_results['locked'] = None
256
mblighbe630eb2008-08-01 16:41:48 +0000257 return super(host_list, self).execute(op='get_hosts',
258 filters=filters,
259 check_results=check_results)
260
261
262 def output(self, results):
xixuand6011f12016-12-08 15:01:58 -0800263 """Print output of 'atest host list'.
264
265 @param results: the results to be printed.
266 """
Ningning Xiaea02ab12018-05-11 15:08:57 -0700267 if results and not self.skylab:
mblighbe630eb2008-08-01 16:41:48 +0000268 # Remove the platform from the labels.
269 for result in results:
270 result['labels'] = self._cleanup_labels(result['labels'],
271 result['platform'])
Ningning Xiaea02ab12018-05-11 15:08:57 -0700272 if self.skylab and self.full_output:
273 print results
274 return
275
276 if self.skylab:
277 results = device.convert_to_autotest_hosts(results)
278
mbligh70b9bf42008-11-18 15:00:46 +0000279 if self.hostnames_only:
mblighdf75f8b2008-11-18 19:07:42 +0000280 self.print_list(results, key='hostname')
mbligh70b9bf42008-11-18 15:00:46 +0000281 else:
Ningning Xiaea02ab12018-05-11 15:08:57 -0700282 keys = ['hostname', 'status', 'shard', 'locked', 'lock_reason',
283 'locked_by', 'platform', 'labels']
Prashanth Balasubramaniana5048562014-12-12 10:14:11 -0800284 super(host_list, self).output(results, keys=keys)
mblighbe630eb2008-08-01 16:41:48 +0000285
286
287class host_stat(host):
288 """atest host stat --mlist <file>|<hosts>"""
289 usage_action = 'stat'
290
291 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800292 """Execute 'atest host stat'."""
mblighbe630eb2008-08-01 16:41:48 +0000293 results = []
294 # Convert wildcards into real host stats.
295 existing_hosts = []
296 for host in self.hosts:
297 if host.endswith('*'):
298 stats = self.execute_rpc('get_hosts',
299 hostname__startswith=host.rstrip('*'))
300 if len(stats) == 0:
301 self.failure('No hosts matching %s' % host, item=host,
302 what_failed='Failed to stat')
303 continue
304 else:
305 stats = self.execute_rpc('get_hosts', hostname=host)
306 if len(stats) == 0:
307 self.failure('Unknown host %s' % host, item=host,
308 what_failed='Failed to stat')
309 continue
310 existing_hosts.extend(stats)
311
312 for stat in existing_hosts:
313 host = stat['hostname']
314 # The host exists, these should succeed
315 acls = self.execute_rpc('get_acl_groups', hosts__hostname=host)
316
317 labels = self.execute_rpc('get_labels', host__hostname=host)
Simran Basi0739d682015-02-25 16:22:56 -0800318 results.append([[stat], acls, labels, stat['attributes']])
mblighbe630eb2008-08-01 16:41:48 +0000319 return results
320
321
322 def output(self, results):
xixuand6011f12016-12-08 15:01:58 -0800323 """Print output of 'atest host stat'.
324
325 @param results: the results to be printed.
326 """
Simran Basi0739d682015-02-25 16:22:56 -0800327 for stats, acls, labels, attributes in results:
mblighbe630eb2008-08-01 16:41:48 +0000328 print '-'*5
329 self.print_fields(stats,
Aviv Kesheta40d1272017-11-16 00:56:35 -0800330 keys=['hostname', 'id', 'platform',
mblighe163b032008-10-18 14:30:27 +0000331 'status', 'locked', 'locked_by',
Matthew Sartori68186332015-04-27 17:19:53 -0700332 'lock_time', 'lock_reason', 'protection',])
mblighbe630eb2008-08-01 16:41:48 +0000333 self.print_by_ids(acls, 'ACLs', line_before=True)
334 labels = self._cleanup_labels(labels)
335 self.print_by_ids(labels, 'Labels', line_before=True)
Simran Basi0739d682015-02-25 16:22:56 -0800336 self.print_dict(attributes, 'Host Attributes', line_before=True)
mblighbe630eb2008-08-01 16:41:48 +0000337
338
339class host_jobs(host):
mbligh1494eca2008-08-13 21:24:22 +0000340 """atest host jobs [--max-query] --mlist <file>|<hosts>"""
mblighbe630eb2008-08-01 16:41:48 +0000341 usage_action = 'jobs'
342
mbligh6996fe82008-08-13 00:32:27 +0000343 def __init__(self):
344 super(host_jobs, self).__init__()
345 self.parser.add_option('-q', '--max-query',
346 help='Limits the number of results '
347 '(20 by default)',
348 type='int', default=20)
349
350
351 def parse(self):
352 """Consume the specific options"""
mbligh9deeefa2009-05-01 23:11:08 +0000353 (options, leftover) = super(host_jobs, self).parse()
mbligh6996fe82008-08-13 00:32:27 +0000354 self.max_queries = options.max_query
355 return (options, leftover)
356
357
mblighbe630eb2008-08-01 16:41:48 +0000358 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800359 """Execute 'atest host jobs'."""
mblighbe630eb2008-08-01 16:41:48 +0000360 results = []
361 real_hosts = []
362 for host in self.hosts:
363 if host.endswith('*'):
364 stats = self.execute_rpc('get_hosts',
365 hostname__startswith=host.rstrip('*'))
366 if len(stats) == 0:
367 self.failure('No host matching %s' % host, item=host,
368 what_failed='Failed to stat')
369 [real_hosts.append(stat['hostname']) for stat in stats]
370 else:
371 real_hosts.append(host)
372
373 for host in real_hosts:
374 queue_entries = self.execute_rpc('get_host_queue_entries',
mbligh6996fe82008-08-13 00:32:27 +0000375 host__hostname=host,
mbligh1494eca2008-08-13 21:24:22 +0000376 query_limit=self.max_queries,
showard6958c722009-09-23 20:03:16 +0000377 sort_by=['-job__id'])
mblighbe630eb2008-08-01 16:41:48 +0000378 jobs = []
379 for entry in queue_entries:
380 job = {'job_id': entry['job']['id'],
381 'job_owner': entry['job']['owner'],
382 'job_name': entry['job']['name'],
383 'status': entry['status']}
384 jobs.append(job)
385 results.append((host, jobs))
386 return results
387
388
389 def output(self, results):
xixuand6011f12016-12-08 15:01:58 -0800390 """Print output of 'atest host jobs'.
391
392 @param results: the results to be printed.
393 """
mblighbe630eb2008-08-01 16:41:48 +0000394 for host, jobs in results:
395 print '-'*5
396 print 'Hostname: %s' % host
397 self.print_table(jobs, keys_header=['job_id',
showardbe0d8692009-08-20 23:42:44 +0000398 'job_owner',
399 'job_name',
400 'status'])
mblighbe630eb2008-08-01 16:41:48 +0000401
Justin Giorgi16bba562016-06-22 10:42:13 -0700402class BaseHostModCreate(host):
xixuand6011f12016-12-08 15:01:58 -0800403 """The base class for host_mod and host_create"""
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700404 # Matches one attribute=value pair
405 attribute_regex = r'(?P<attribute>\w+)=(?P<value>.+)?'
mblighbe630eb2008-08-01 16:41:48 +0000406
407 def __init__(self):
Justin Giorgi16bba562016-06-22 10:42:13 -0700408 """Add the options shared between host mod and host create actions."""
mblighbe630eb2008-08-01 16:41:48 +0000409 self.messages = []
Justin Giorgi16bba562016-06-22 10:42:13 -0700410 self.host_ids = {}
411 super(BaseHostModCreate, self).__init__()
mblighbe630eb2008-08-01 16:41:48 +0000412 self.parser.add_option('-l', '--lock',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700413 help='Lock hosts.',
mblighbe630eb2008-08-01 16:41:48 +0000414 action='store_true')
Matthew Sartori68186332015-04-27 17:19:53 -0700415 self.parser.add_option('-r', '--lock_reason',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700416 help='Reason for locking hosts.',
Matthew Sartori68186332015-04-27 17:19:53 -0700417 default='')
Ningning Xiaef35cb52018-05-04 17:58:20 -0700418 self.parser.add_option('-u', '--unlock',
419 help='Unlock hosts.',
420 action='store_true')
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700421
mblighe163b032008-10-18 14:30:27 +0000422 self.parser.add_option('-p', '--protection', type='choice',
mblighaed47e82009-03-17 19:06:18 +0000423 help=('Set the protection level on a host. '
Ningning Xiaef35cb52018-05-04 17:58:20 -0700424 'Must be one of: %s. %s' %
425 (', '.join('"%s"' % p
426 for p in self.protections),
427 skylab_utils.MSG_INVALID_IN_SKYLAB)),
mblighaed47e82009-03-17 19:06:18 +0000428 choices=self.protections)
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700429 self._attributes = []
430 self.parser.add_option('--attribute', '-i',
431 help=('Host attribute to add or change. Format '
432 'is <attribute>=<value>. Multiple '
433 'attributes can be set by passing the '
434 'argument multiple times. Attributes can '
435 'be unset by providing an empty value.'),
436 action='append')
mblighbe630eb2008-08-01 16:41:48 +0000437 self.parser.add_option('-b', '--labels',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700438 help=('Comma separated list of labels. '
439 'When --skylab is provided, a label must '
440 'be in the format of label-key:label-value'
441 ' (e.g., board:lumpy).'))
mblighbe630eb2008-08-01 16:41:48 +0000442 self.parser.add_option('-B', '--blist',
443 help='File listing the labels',
444 type='string',
445 metavar='LABEL_FLIST')
446 self.parser.add_option('-a', '--acls',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700447 help=('Comma separated list of ACLs. %s' %
448 skylab_utils.MSG_INVALID_IN_SKYLAB))
mblighbe630eb2008-08-01 16:41:48 +0000449 self.parser.add_option('-A', '--alist',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700450 help=('File listing the acls. %s' %
451 skylab_utils.MSG_INVALID_IN_SKYLAB),
mblighbe630eb2008-08-01 16:41:48 +0000452 type='string',
453 metavar='ACL_FLIST')
Justin Giorgi16bba562016-06-22 10:42:13 -0700454 self.parser.add_option('-t', '--platform',
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700455 help=('Sets the platform label. %s Please set '
456 'platform in labels (e.g., -b '
457 'platform:platform_name) with --skylab.' %
458 skylab_utils.MSG_INVALID_IN_SKYLAB))
mblighbe630eb2008-08-01 16:41:48 +0000459
460
461 def parse(self):
Justin Giorgi16bba562016-06-22 10:42:13 -0700462 """Consume the options common to host create and host mod.
463 """
mbligh9deeefa2009-05-01 23:11:08 +0000464 label_info = topic_common.item_parse_info(attribute_name='labels',
465 inline_option='labels',
466 filename_option='blist')
467 acl_info = topic_common.item_parse_info(attribute_name='acls',
468 inline_option='acls',
469 filename_option='alist')
470
Justin Giorgi16bba562016-06-22 10:42:13 -0700471 (options, leftover) = super(BaseHostModCreate, self).parse([label_info,
mbligh9deeefa2009-05-01 23:11:08 +0000472 acl_info],
473 req_items='hosts')
mblighbe630eb2008-08-01 16:41:48 +0000474
475 self._parse_lock_options(options)
Justin Giorgi16bba562016-06-22 10:42:13 -0700476
Ningning Xia64ced002018-05-16 17:36:20 -0700477 self.label_map = None
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700478 if self.allow_skylab and self.skylab:
Ningning Xiaef35cb52018-05-04 17:58:20 -0700479 # TODO(nxia): drop these flags when all hosts are migrated to skylab
Ningning Xia64ced002018-05-16 17:36:20 -0700480 if (options.protection or options.acls or options.alist or
481 options.platform):
482 self.invalid_syntax(
483 '--protection, --acls, --alist or --platform is not '
484 'supported with --skylab.')
485
486 if self.labels:
487 self.label_map = device.convert_to_label_map(self.labels)
Ningning Xiaef35cb52018-05-04 17:58:20 -0700488
mblighaed47e82009-03-17 19:06:18 +0000489 if options.protection:
490 self.data['protection'] = options.protection
Justin Giorgi16bba562016-06-22 10:42:13 -0700491 self.messages.append('Protection set to "%s"' % options.protection)
492
493 self.attributes = {}
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700494 if options.attribute:
495 for pair in options.attribute:
496 m = re.match(self.attribute_regex, pair)
497 if not m:
498 raise topic_common.CliError('Attribute must be in key=value '
499 'syntax.')
500 elif m.group('attribute') in self.attributes:
xixuand6011f12016-12-08 15:01:58 -0800501 raise topic_common.CliError(
502 'Multiple values provided for attribute '
503 '%s.' % m.group('attribute'))
Justin Giorgi9261f9f2016-07-11 17:48:52 -0700504 self.attributes[m.group('attribute')] = m.group('value')
Justin Giorgi16bba562016-06-22 10:42:13 -0700505
506 self.platform = options.platform
mblighbe630eb2008-08-01 16:41:48 +0000507 return (options, leftover)
508
509
Justin Giorgi16bba562016-06-22 10:42:13 -0700510 def _set_acls(self, hosts, acls):
511 """Add hosts to acls (and remove from all other acls).
512
513 @param hosts: list of hostnames
514 @param acls: list of acl names
515 """
516 # Remove from all ACLs except 'Everyone' and ACLs in list
517 # Skip hosts that don't exist
518 for host in hosts:
519 if host not in self.host_ids:
520 continue
521 host_id = self.host_ids[host]
522 for a in self.execute_rpc('get_acl_groups', hosts=host_id):
523 if a['name'] not in self.acls and a['id'] != 1:
524 self.execute_rpc('acl_group_remove_hosts', id=a['id'],
525 hosts=self.hosts)
526
527 # Add hosts to the ACLs
528 self.check_and_create_items('get_acl_groups', 'add_acl_group',
529 self.acls)
530 for a in acls:
531 self.execute_rpc('acl_group_add_hosts', id=a, hosts=hosts)
532
533
534 def _remove_labels(self, host, condition):
535 """Remove all labels from host that meet condition(label).
536
537 @param host: hostname
538 @param condition: callable that returns bool when given a label
539 """
540 if host in self.host_ids:
541 host_id = self.host_ids[host]
542 labels_to_remove = []
543 for l in self.execute_rpc('get_labels', host=host_id):
544 if condition(l):
545 labels_to_remove.append(l['id'])
546 if labels_to_remove:
547 self.execute_rpc('host_remove_labels', id=host_id,
548 labels=labels_to_remove)
549
550
551 def _set_labels(self, host, labels):
552 """Apply labels to host (and remove all other labels).
553
554 @param host: hostname
555 @param labels: list of label names
556 """
557 condition = lambda l: l['name'] not in labels and not l['platform']
558 self._remove_labels(host, condition)
559 self.check_and_create_items('get_labels', 'add_label', labels)
560 self.execute_rpc('host_add_labels', id=host, labels=labels)
561
562
563 def _set_platform_label(self, host, platform_label):
564 """Apply the platform label to host (and remove existing).
565
566 @param host: hostname
567 @param platform_label: platform label's name
568 """
569 self._remove_labels(host, lambda l: l['platform'])
570 self.check_and_create_items('get_labels', 'add_label', [platform_label],
571 platform=True)
572 self.execute_rpc('host_add_labels', id=host, labels=[platform_label])
573
574
575 def _set_attributes(self, host, attributes):
576 """Set attributes on host.
577
578 @param host: hostname
579 @param attributes: attribute dictionary
580 """
581 for attr, value in self.attributes.iteritems():
582 self.execute_rpc('set_host_attribute', attribute=attr,
583 value=value, hostname=host)
584
585
586class host_mod(BaseHostModCreate):
587 """atest host mod [--lock|--unlock --force_modify_locking
588 --platform <arch>
589 --labels <labels>|--blist <label_file>
590 --acls <acls>|--alist <acl_file>
591 --protection <protection_type>
592 --attributes <attr>=<value>;<attr>=<value>
593 --mlist <mach_file>] <hosts>"""
594 usage_action = 'mod'
Justin Giorgi16bba562016-06-22 10:42:13 -0700595
596 def __init__(self):
597 """Add the options specific to the mod action"""
598 super(host_mod, self).__init__()
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700599 self.parser.add_option('--unlock-lock-id',
600 help=('Unlock the lock with the lock-id. %s' %
601 skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
602 default=None)
Justin Giorgi16bba562016-06-22 10:42:13 -0700603 self.parser.add_option('-f', '--force_modify_locking',
604 help='Forcefully lock\unlock a host',
605 action='store_true')
606 self.parser.add_option('--remove_acls',
Ningning Xiaef35cb52018-05-04 17:58:20 -0700607 help=('Remove all active acls. %s' %
608 skylab_utils.MSG_INVALID_IN_SKYLAB),
Justin Giorgi16bba562016-06-22 10:42:13 -0700609 action='store_true')
610 self.parser.add_option('--remove_labels',
611 help='Remove all labels.',
612 action='store_true')
613
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700614 self.add_skylab_options()
615
616
617 def _parse_unlock_options(self, options):
618 """Parse unlock related options."""
619 if self.skylab and options.unlock and options.unlock_lock_id is None:
620 self.invalid_syntax('Must provide --unlock-lock-id with "--skylab '
621 '--unlock".')
622
623 if (not (self.skylab and options.unlock) and
624 options.unlock_lock_id is not None):
625 self.invalid_syntax('--unlock-lock-id is only valid with '
626 '"--skylab --unlock".')
627
628 self.unlock_lock_id = options.unlock_lock_id
629
Justin Giorgi16bba562016-06-22 10:42:13 -0700630
631 def parse(self):
632 """Consume the specific options"""
633 (options, leftover) = super(host_mod, self).parse()
634
Ningning Xia5c0e8f32018-05-21 16:26:50 -0700635 self._parse_unlock_options(options)
636
Justin Giorgi16bba562016-06-22 10:42:13 -0700637 if options.force_modify_locking:
638 self.data['force_modify_locking'] = True
639
Ningning Xiaef35cb52018-05-04 17:58:20 -0700640 if self.skylab and options.remove_acls:
641 # TODO(nxia): drop the flag when all hosts are migrated to skylab
642 self.invalid_syntax('--remove_acls is not supported with --skylab.')
643
Justin Giorgi16bba562016-06-22 10:42:13 -0700644 self.remove_acls = options.remove_acls
645 self.remove_labels = options.remove_labels
646
647 return (options, leftover)
648
649
Ningning Xiaef35cb52018-05-04 17:58:20 -0700650 def execute_skylab(self):
651 """Execute atest host mod with --skylab.
652
653 @return A list of hostnames which have been successfully modified.
654 """
655 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
656 inventory_repo.initialize()
657 data_dir = inventory_repo.get_data_dir()
658 lab = text_manager.load_lab(data_dir)
659
660 locked_by = None
661 if self.lock:
662 locked_by = inventory_repo.git_repo.config('user.email')
663
664 successes = []
665 for hostname in self.hosts:
666 try:
667 device.modify(
668 lab,
669 'duts',
670 hostname,
671 self.environment,
672 lock=self.lock,
673 locked_by=locked_by,
674 lock_reason = self.lock_reason,
675 unlock=self.unlock,
676 unlock_lock_id=self.unlock_lock_id,
677 attributes=self.attributes,
678 remove_labels=self.remove_labels,
Ningning Xia64ced002018-05-16 17:36:20 -0700679 label_map=self.label_map)
Ningning Xiaef35cb52018-05-04 17:58:20 -0700680 successes.append(hostname)
681 except device.SkylabDeviceActionError as e:
682 print('Cannot modify host %s: %s' % (hostname, e))
683
684 if successes:
685 text_manager.dump_lab(data_dir, lab)
686
687 status = inventory_repo.git_repo.status()
688 if not status:
689 print('Nothing is changed for hosts %s.' % successes)
690 return []
691
692 message = skylab_utils.construct_commit_message(
693 'Modify %d hosts.\n\n%s' % (len(successes), successes))
694 self.change_number = inventory_repo.upload_change(
695 message, draft=self.draft, dryrun=self.dryrun,
696 submit=self.submit)
697
698 return successes
699
700
Justin Giorgi16bba562016-06-22 10:42:13 -0700701 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800702 """Execute 'atest host mod'."""
Ningning Xiaef35cb52018-05-04 17:58:20 -0700703 if self.skylab:
704 return self.execute_skylab()
705
Justin Giorgi16bba562016-06-22 10:42:13 -0700706 successes = []
707 for host in self.execute_rpc('get_hosts', hostname__in=self.hosts):
708 self.host_ids[host['hostname']] = host['id']
709 for host in self.hosts:
710 if host not in self.host_ids:
711 self.failure('Cannot modify non-existant host %s.' % host)
712 continue
713 host_id = self.host_ids[host]
714
715 try:
716 if self.data:
717 self.execute_rpc('modify_host', item=host,
718 id=host, **self.data)
719
720 if self.attributes:
721 self._set_attributes(host, self.attributes)
722
723 if self.labels or self.remove_labels:
724 self._set_labels(host, self.labels)
725
726 if self.platform:
727 self._set_platform_label(host, self.platform)
728
729 # TODO: Make the AFE return True or False,
730 # especially for lock
731 successes.append(host)
732 except topic_common.CliError, full_error:
733 # Already logged by execute_rpc()
734 pass
735
736 if self.acls or self.remove_acls:
737 self._set_acls(self.hosts, self.acls)
738
739 return successes
740
741
742 def output(self, hosts):
xixuand6011f12016-12-08 15:01:58 -0800743 """Print output of 'atest host mod'.
744
745 @param hosts: the host list to be printed.
746 """
Justin Giorgi16bba562016-06-22 10:42:13 -0700747 for msg in self.messages:
748 self.print_wrapped(msg, hosts)
749
Ningning Xiaef35cb52018-05-04 17:58:20 -0700750 if hosts and self.skylab:
Ningning Xiac8b430d2018-05-17 15:10:25 -0700751 print('Modified hosts: %s.' % ', '.join(hosts))
Ningning Xiaef35cb52018-05-04 17:58:20 -0700752 if self.skylab and not self.dryrun and not self.submit:
Ningning Xiac8b430d2018-05-17 15:10:25 -0700753 print(skylab_utils.get_cl_message(self.change_number))
Ningning Xiaef35cb52018-05-04 17:58:20 -0700754
Justin Giorgi16bba562016-06-22 10:42:13 -0700755
756class HostInfo(object):
757 """Store host information so we don't have to keep looking it up."""
758 def __init__(self, hostname, platform, labels):
759 self.hostname = hostname
760 self.platform = platform
761 self.labels = labels
762
763
764class host_create(BaseHostModCreate):
765 """atest host create [--lock|--unlock --platform <arch>
766 --labels <labels>|--blist <label_file>
767 --acls <acls>|--alist <acl_file>
768 --protection <protection_type>
769 --attributes <attr>=<value>;<attr>=<value>
770 --mlist <mach_file>] <hosts>"""
771 usage_action = 'create'
772
773 def parse(self):
774 """Option logic specific to create action.
775 """
776 (options, leftovers) = super(host_create, self).parse()
777 self.locked = options.lock
778 if 'serials' in self.attributes:
779 if len(self.hosts) > 1:
780 raise topic_common.CliError('Can not specify serials with '
781 'multiple hosts.')
782
783
784 @classmethod
785 def construct_without_parse(
786 cls, web_server, hosts, platform=None,
787 locked=False, lock_reason='', labels=[], acls=[],
788 protection=host_protections.Protection.NO_PROTECTION):
789 """Construct a host_create object and fill in data from args.
790
791 Do not need to call parse after the construction.
792
793 Return an object of site_host_create ready to execute.
794
795 @param web_server: A string specifies the autotest webserver url.
796 It is needed to setup comm to make rpc.
797 @param hosts: A list of hostnames as strings.
798 @param platform: A string or None.
799 @param locked: A boolean.
800 @param lock_reason: A string.
801 @param labels: A list of labels as strings.
802 @param acls: A list of acls as strings.
803 @param protection: An enum defined in host_protections.
804 """
805 obj = cls()
806 obj.web_server = web_server
807 try:
808 # Setup stuff needed for afe comm.
809 obj.afe = rpc.afe_comm(web_server)
810 except rpc.AuthError, s:
811 obj.failure(str(s), fatal=True)
812 obj.hosts = hosts
813 obj.platform = platform
814 obj.locked = locked
815 if locked and lock_reason.strip():
816 obj.data['lock_reason'] = lock_reason.strip()
817 obj.labels = labels
818 obj.acls = acls
819 if protection:
820 obj.data['protection'] = protection
821 obj.attributes = {}
822 return obj
823
824
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700825 def _detect_host_info(self, host):
826 """Detect platform and labels from the host.
827
828 @param host: hostname
829
830 @return: HostInfo object
831 """
832 # Mock an afe_host object so that the host is constructed as if the
833 # data was already in afe
834 data = {'attributes': self.attributes, 'labels': self.labels}
835 afe_host = frontend.Host(None, data)
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700836 store = host_info.InMemoryHostInfoStore(
837 host_info.HostInfo(labels=self.labels,
838 attributes=self.attributes))
839 machine = {
840 'hostname': host,
841 'afe_host': afe_host,
842 'host_info_store': store
843 }
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700844 try:
Allen Li2c32d6b2017-02-03 15:28:10 -0800845 if bin_utils.ping(host, tries=1, deadline=1) == 0:
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700846 serials = self.attributes.get('serials', '').split(',')
Richard Barnette9db80682018-04-26 00:55:15 +0000847 adb_serial = self.attributes.get('serials')
848 host_dut = hosts.create_host(machine,
849 adb_serial=adb_serial)
xixuand6011f12016-12-08 15:01:58 -0800850
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700851 info = HostInfo(host, host_dut.get_platform(),
852 host_dut.get_labels())
xixuand6011f12016-12-08 15:01:58 -0800853 # Clean host to make sure nothing left after calling it,
854 # e.g. tunnels.
855 if hasattr(host_dut, 'close'):
856 host_dut.close()
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700857 else:
858 # Can't ping the host, use default information.
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700859 info = HostInfo(host, None, [])
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700860 except (socket.gaierror, error.AutoservRunError,
861 error.AutoservSSHTimeout):
862 # We may be adding a host that does not exist yet or we can't
863 # reach due to hostname/address issues or if the host is down.
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700864 info = HostInfo(host, None, [])
865 return info
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700866
867
mblighbe630eb2008-08-01 16:41:48 +0000868 def _execute_add_one_host(self, host):
mbligh719e14a2008-12-04 01:17:08 +0000869 # Always add the hosts as locked to avoid the host
Matthew Sartori68186332015-04-27 17:19:53 -0700870 # being picked up by the scheduler before it's ACL'ed.
mbligh719e14a2008-12-04 01:17:08 +0000871 self.data['locked'] = True
Matthew Sartori68186332015-04-27 17:19:53 -0700872 if not self.locked:
873 self.data['lock_reason'] = 'Forced lock on device creation'
Justin Giorgi16bba562016-06-22 10:42:13 -0700874 self.execute_rpc('add_host', hostname=host, status="Ready", **self.data)
mblighbe630eb2008-08-01 16:41:48 +0000875
Justin Giorgi16bba562016-06-22 10:42:13 -0700876 # If there are labels avaliable for host, use them.
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700877 info = self._detect_host_info(host)
Justin Giorgi16bba562016-06-22 10:42:13 -0700878 labels = set(self.labels)
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700879 if info.labels:
880 labels.update(info.labels)
Justin Giorgi16bba562016-06-22 10:42:13 -0700881
882 if labels:
883 self._set_labels(host, list(labels))
884
885 # Now add the platform label.
886 # If a platform was not provided and we were able to retrieve it
887 # from the host, use the retrieved platform.
Prathmesh Prabhud252d262017-05-31 11:46:34 -0700888 platform = self.platform if self.platform else info.platform
Justin Giorgi16bba562016-06-22 10:42:13 -0700889 if platform:
890 self._set_platform_label(host, platform)
891
892 if self.attributes:
893 self._set_attributes(host, self.attributes)
mblighbe630eb2008-08-01 16:41:48 +0000894
895
Justin Giorgi5208eaa2016-07-02 20:12:12 -0700896 def execute(self):
xixuand6011f12016-12-08 15:01:58 -0800897 """Execute 'atest host create'."""
Justin Giorgi16bba562016-06-22 10:42:13 -0700898 successful_hosts = []
899 for host in self.hosts:
900 try:
901 self._execute_add_one_host(host)
902 successful_hosts.append(host)
903 except topic_common.CliError:
904 pass
Jiaxi Luoc342f9f2014-05-19 16:22:03 -0700905
906 if successful_hosts:
Justin Giorgi16bba562016-06-22 10:42:13 -0700907 self._set_acls(successful_hosts, self.acls)
Jiaxi Luoc342f9f2014-05-19 16:22:03 -0700908
909 if not self.locked:
910 for host in successful_hosts:
Matthew Sartori68186332015-04-27 17:19:53 -0700911 self.execute_rpc('modify_host', id=host, locked=False,
912 lock_reason='')
Jiaxi Luoc342f9f2014-05-19 16:22:03 -0700913 return successful_hosts
914
915
mblighbe630eb2008-08-01 16:41:48 +0000916 def output(self, hosts):
xixuand6011f12016-12-08 15:01:58 -0800917 """Print output of 'atest host create'.
918
919 @param hosts: the added host list to be printed.
920 """
mblighbe630eb2008-08-01 16:41:48 +0000921 self.print_wrapped('Added host', hosts)
922
923
924class host_delete(action_common.atest_delete, host):
925 """atest host delete [--mlist <mach_file>] <hosts>"""
Ningning Xiac8b430d2018-05-17 15:10:25 -0700926
927 def __init__(self):
928 super(host_delete, self).__init__()
929
930 self.add_skylab_options()
931
932
933 def execute_skylab(self):
934 """Execute 'atest host delete' with '--skylab'.
935
936 @return A list of hostnames which have been successfully deleted.
937 """
938 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
939 inventory_repo.initialize()
940 data_dir = inventory_repo.get_data_dir()
941 lab = text_manager.load_lab(data_dir)
942
943 successes = []
944 for hostname in self.hosts:
945 try:
946 device.delete(
947 lab,
948 'duts',
949 hostname,
950 self.environment)
951 successes.append(hostname)
952 except device.SkylabDeviceActionError as e:
953 print('Cannot delete host %s: %s' % (hostname, e))
954
955 if successes:
956 text_manager.dump_lab(data_dir, lab)
957 message = skylab_utils.construct_commit_message(
958 'Delete %d hosts.\n\n%s' % (len(successes), successes))
959 self.change_number = inventory_repo.upload_change(
960 message, draft=self.draft, dryrun=self.dryrun,
961 submit=self.submit)
962
963 return successes
964
965
966 def execute(self):
967 """Execute 'atest host delete'.
968
969 @return A list of hostnames which have been successfully deleted.
970 """
971 if self.skylab:
972 return self.execute_skylab()
973
974 return super(host_delete, self).execute()
Ningning Xiac46bdd12018-05-29 11:24:14 -0700975
976
977class InvalidHostnameError(Exception):
978 """Cannot perform actions on the host because of invalid hostname."""
979
980
981def _add_hostname_suffix(hostname, suffix):
982 """Add the suffix to the hostname."""
983 if hostname.endswith(suffix):
984 raise InvalidHostnameError(
985 'Cannot add "%s" as it already contains the suffix.' % suffix)
986
987 return hostname + suffix
988
989
990def _remove_hostname_suffix(hostname, suffix):
991 """Remove the suffix from the hostname."""
992 if not hostname.endswith(suffix):
993 raise InvalidHostnameError(
994 'Cannot remove "%s" as it doesn\'t contain the suffix.' %
995 suffix)
996
997 return hostname[:len(hostname) - len(suffix)]
998
999
1000class host_rename(host):
1001 """Host rename is only for migrating hosts between skylab and AFE DB."""
1002
1003 usage_action = 'rename'
1004
1005 def __init__(self):
1006 """Add the options specific to the rename action."""
1007 super(host_rename, self).__init__()
1008
1009 self.parser.add_option('--for-migration',
1010 help=('Rename hostnames for migration. Rename '
1011 'each "hostname" to "hostname%s". '
1012 'The original "hostname" must not contain '
1013 'suffix.' % MIGRATED_HOST_SUFFIX),
1014 action='store_true',
1015 default=False)
1016 self.parser.add_option('--for-rollback',
1017 help=('Rename hostnames for migration rollback. '
1018 'Rename each "hostname%s" to its original '
1019 '"hostname".' % MIGRATED_HOST_SUFFIX),
1020 action='store_true',
1021 default=False)
1022 self.parser.add_option('--dryrun',
1023 help='Execute the action as a dryrun.',
1024 action='store_true',
1025 default=False)
1026
1027
1028 def parse(self):
1029 """Consume the options common to host rename."""
1030 (options, leftovers) = super(host_rename, self).parse()
1031 self.for_migration = options.for_migration
1032 self.for_rollback = options.for_rollback
1033 self.dryrun = options.dryrun
1034 self.host_ids = {}
1035
1036 if not (self.for_migration ^ self.for_rollback):
1037 self.invalid_syntax('--for-migration and --for-rollback are '
1038 'exclusive, and one of them must be enabled.')
1039
1040 if not self.hosts:
1041 self.invalid_syntax('Must provide hostname(s).')
1042
1043 if self.dryrun:
1044 print('This will be a dryrun and will not rename hostnames.')
1045
1046 return (options, leftovers)
1047
1048
1049 def execute(self):
1050 """Execute 'atest host rename'."""
1051 if not self.prompt_confirmation():
1052 return
1053
1054 successes = []
1055 for host in self.execute_rpc('get_hosts', hostname__in=self.hosts):
1056 self.host_ids[host['hostname']] = host['id']
1057 for host in self.hosts:
1058 if host not in self.host_ids:
1059 self.failure('Cannot rename non-existant host %s.' % host,
1060 item=host, what_failed='Failed to rename')
1061 continue
1062 try:
1063 if self.for_migration:
1064 new_hostname = _add_hostname_suffix(
1065 host, MIGRATED_HOST_SUFFIX)
1066 else:
1067 #for_rollback
1068 new_hostname = _remove_hostname_suffix(
1069 host, MIGRATED_HOST_SUFFIX)
1070
1071 if not self.dryrun:
1072 data = {'hostname': new_hostname}
1073 self.execute_rpc('modify_host', item=host, id=host, **data)
1074 successes.append((host, new_hostname))
1075 except InvalidHostnameError as e:
1076 self.failure('Cannot rename host %s: %s' % (host, e), item=host,
1077 what_failed='Failed to rename')
1078 except topic_common.CliError, full_error:
1079 # Already logged by execute_rpc()
1080 pass
1081
1082 return successes
1083
1084
1085 def output(self, results):
1086 """Print output of 'atest host rename'."""
1087 if results:
1088 print('Successfully renamed:')
1089 for old_hostname, new_hostname in results:
1090 print('%s to %s' % (old_hostname, new_hostname))
Ningning Xia9df3d152018-05-23 17:15:14 -07001091
1092
1093class host_migrate(action_common.atest_list, host):
1094 """'atest host migrate' to migrate or rollback hosts."""
1095
1096 usage_action = 'migrate'
1097
1098 def __init__(self):
1099 super(host_migrate, self).__init__()
1100
1101 self.parser.add_option('--migration',
1102 dest='migration',
1103 help='Migrate the hosts to skylab.',
1104 action='store_true',
1105 default=False)
1106 self.parser.add_option('--rollback',
1107 dest='rollback',
1108 help='Rollback the hosts migrated to skylab.',
1109 action='store_true',
1110 default=False)
1111 self.parser.add_option('--model',
1112 help='Model of the hosts to migrate.',
1113 dest='model',
1114 default=None)
1115 self.parser.add_option('--pool',
1116 help=('Pool of the hosts to migrate. Must '
1117 'specify --model for the pool.'),
1118 dest='pool',
1119 default=None)
1120
1121 self.add_skylab_options(enforce_skylab=True)
1122
1123
1124 def parse(self):
1125 """Consume the specific options"""
1126 (options, leftover) = super(host_migrate, self).parse()
1127
1128 self.migration = options.migration
1129 self.rollback = options.rollback
1130 self.model = options.model
1131 self.pool = options.pool
1132 self.host_ids = {}
1133
1134 if not (self.migration ^ self.rollback):
1135 self.invalid_syntax('--migration and --rollback are exclusive, '
1136 'and one of them must be enabled.')
1137
1138 if self.pool is not None and self.model is None:
1139 self.invalid_syntax('Must provide --model with --pool.')
1140
1141 if not self.hosts and not self.model:
1142 self.invalid_syntax('Must provide hosts or --model.')
1143
1144 return (options, leftover)
1145
1146
1147 def execute(self):
1148 """Execute 'atest host migrate'."""
1149 hostnames = self.hosts
1150 for hostname in hostnames:
1151 if hostname.endswith(MIGRATED_HOST_SUFFIX):
1152 self.failure('Cannot migrate host with suffix "%s" %s.' %
1153 (MIGRATED_HOST_SUFFIX, hostname),
1154 item=hostname, what_failed='Failed to rename')
1155 hostnames.remove(hostname)
1156
1157 filters = {}
1158 check_results = {}
1159 if hostnames:
1160 check_results['hostname__in'] = 'hostname'
1161 if self.migration:
1162 filters['hostname__in'] = hostnames
1163 else:
1164 # rollback
1165 hostnames_with_suffix = [
1166 _add_hostname_suffix(h, MIGRATED_HOST_SUFFIX)
1167 for h in hostnames]
1168 filters['hostname__in'] = hostnames_with_suffix
1169
1170 labels = []
1171 if self.model:
1172 labels.append('model:%s' % self.model)
1173 if self.pool:
1174 labels.append('pool:%s' % self.pool)
1175
1176 if labels:
1177 if len(labels) == 1:
1178 filters['labels__name__in'] = labels
1179 check_results['labels__name__in'] = None
1180 else:
1181 filters['multiple_labels'] = labels
1182 check_results['multiple_labels'] = None
1183
1184 results = super(host_migrate, self).execute(
1185 op='get_hosts', filters=filters, check_results=check_results)
1186 hostnames = [h['hostname'] for h in results]
1187
1188 return self.execute_skylab_migration(hostnames)
1189
1190
1191 def execute_skylab_migration(self, hostnames):
1192 """Execute migration in skylab_inventory.
1193
1194 @param hostnames: A list of hostnames to migrate.
1195 @return If there're hosts to migrate, return a list of the hostnames and
1196 a message instructing actions after the migration; else return
1197 None.
1198 """
1199 if not hostnames:
1200 return
1201
1202 inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
1203 inventory_repo.initialize()
1204
1205 subdirs = ['skylab', 'prod', 'staging']
1206 data_dirs = skylab_data_dir, prod_data_dir, staging_data_dir = [
1207 inventory_repo.get_data_dir(data_subdir=d) for d in subdirs]
1208 skylab_lab, prod_lab, staging_lab = [
1209 text_manager.load_lab(d) for d in data_dirs]
1210
1211 label_map = None
1212 labels = []
1213 if self.model:
1214 labels.append('board:%s' % self.model)
1215 if self.pool:
1216 labels.append('critical_pool:%s' % self.pool)
1217 if labels:
1218 label_map = device.convert_to_label_map(labels)
1219
1220 if self.migration:
1221 prod_devices = device.move_devices(
1222 prod_lab, skylab_lab, 'duts', label_map=label_map,
1223 hostnames=hostnames)
1224 staging_devices = device.move_devices(
1225 staging_lab, skylab_lab, 'duts', label_map=label_map,
1226 hostnames=hostnames)
1227
1228 all_devices = prod_devices + staging_devices
1229 # Hostnames in afe_hosts tabel.
1230 device_hostnames = [str(d.common.hostname) for d in all_devices]
1231 message = (
1232 'Migration: move %s hosts into skylab_inventory.\n\n'
1233 'Please run command:\n'
1234 'atest host rename --for-migration %s' %
1235 (len(all_devices), ' '.join(device_hostnames)))
1236 else:
1237 # rollback
1238 prod_devices = device.move_devices(
1239 skylab_lab, prod_lab, 'duts', environment='prod',
1240 label_map=label_map, hostnames=self.hosts)
1241 staging_devices = device.move_devices(
1242 staging_lab, skylab_lab, 'duts', environment='staging',
1243 label_map=label_map, hostnames=self.hosts)
1244
1245 all_devices = prod_devices + staging_devices
1246 # Hostnames in afe_hosts tabel.
1247 device_hostnames = [_add_hostname_suffix(str(d.common.hostname),
1248 MIGRATED_HOST_SUFFIX)
1249 for d in all_devices]
1250 message = (
1251 'Rollback: remove %s hosts from skylab_inventory.\n\n'
1252 'Please run command:\n'
1253 'atest host rename --for-rollback %s' %
1254 (len(all_devices), ' '.join(device_hostnames)))
1255
1256 if all_devices:
1257 if prod_devices:
1258 text_manager.dump_lab(prod_data_dir, prod_lab)
1259
1260 if staging_devices:
1261 text_manager.dump_lab(staging_data_dir, staging_lab)
1262
1263 text_manager.dump_lab(skylab_data_dir, skylab_lab)
1264
1265 self.change_number = inventory_repo.upload_change(
1266 message, draft=self.draft, dryrun=self.dryrun,
1267 submit=self.submit)
1268
1269 return all_devices, message
1270
1271
1272 def output(self, result):
1273 """Print output of 'atest host list'.
1274
1275 @param result: the result to be printed.
1276 """
1277 if result:
1278 devices, message = result
1279
1280 if devices:
1281 hostnames = [h.common.hostname for h in devices]
1282 if self.migration:
1283 print('Migrating hosts: %s' % ','.join(hostnames))
1284 else:
1285 # rollback
1286 print('Rolling back hosts: %s' % ','.join(hostnames))
1287
1288 if not self.dryrun:
1289 if not self.submit:
1290 print(skylab_utils.get_cl_message(self.change_number))
1291 else:
1292 # Print the instruction command for renaming hosts.
1293 print('%s' % message)
1294 else:
1295 print('No hosts were migrated.')