| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 1 | # Copyright 2008 Google Inc. All Rights Reserved. | 
 | 2 |  | 
 | 3 | """ | 
 | 4 | The host module contains the objects and method used to | 
 | 5 | manage a host in Autotest. | 
 | 6 |  | 
 | 7 | The valid actions are: | 
 | 8 | create:  adds host(s) | 
 | 9 | delete:  deletes host(s) | 
 | 10 | list:    lists host(s) | 
 | 11 | stat:    displays host(s) information | 
 | 12 | mod:     modifies host(s) | 
 | 13 | jobs:    lists all jobs that ran on host(s) | 
 | 14 |  | 
 | 15 | The common options are: | 
 | 16 | -M|--mlist:   file containing a list of machines | 
 | 17 |  | 
 | 18 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 19 | See topic_common.py for a High Level Design and Algorithm. | 
 | 20 |  | 
 | 21 | """ | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 22 | import common | 
| Ningning Xia | ffb3c1f | 2018-06-07 18:42:32 -0700 | [diff] [blame] | 23 | import random | 
| Simran Basi | 0739d68 | 2015-02-25 16:22:56 -0800 | [diff] [blame] | 24 | import re | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 25 | import socket | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 26 |  | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 27 | from autotest_lib.cli import action_common, rpc, topic_common, skylab_utils | 
| Allen Li | 2c32d6b | 2017-02-03 15:28:10 -0800 | [diff] [blame] | 28 | from autotest_lib.client.bin import utils as bin_utils | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 29 | from autotest_lib.client.common_lib import error, host_protections | 
| Justin Giorgi | 5208eaa | 2016-07-02 20:12:12 -0700 | [diff] [blame] | 30 | from autotest_lib.server import frontend, hosts | 
| Prathmesh Prabhu | d252d26 | 2017-05-31 11:46:34 -0700 | [diff] [blame] | 31 | from autotest_lib.server.hosts import host_info | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 32 |  | 
 | 33 |  | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 34 | try: | 
 | 35 |     from skylab_inventory import text_manager | 
 | 36 |     from skylab_inventory.lib import device | 
| Ningning Xia | ffb3c1f | 2018-06-07 18:42:32 -0700 | [diff] [blame] | 37 |     from skylab_inventory.lib import server as skylab_server | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 38 | except ImportError: | 
 | 39 |     pass | 
 | 40 |  | 
 | 41 |  | 
| Ningning Xia | c46bdd1 | 2018-05-29 11:24:14 -0700 | [diff] [blame] | 42 | MIGRATED_HOST_SUFFIX = '-migrated-do-not-use' | 
 | 43 |  | 
 | 44 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 45 | class host(topic_common.atest): | 
 | 46 |     """Host class | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 47 |     atest host [create|delete|list|stat|mod|jobs|rename|migrate] <options>""" | 
 | 48 |     usage_action = '[create|delete|list|stat|mod|jobs|rename|migrate]' | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 49 |     topic = msg_topic = 'host' | 
 | 50 |     msg_items = '<hosts>' | 
 | 51 |  | 
| mbligh | aed47e8 | 2009-03-17 19:06:18 +0000 | [diff] [blame] | 52 |     protections = host_protections.Protection.names | 
 | 53 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 54 |  | 
 | 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 |  | 
| mbligh | 9deeefa | 2009-05-01 23:11:08 +0000 | [diff] [blame] | 66 |         self.topic_parse_info = topic_common.item_parse_info( | 
 | 67 |             attribute_name='hosts', | 
 | 68 |             filename_option='mlist', | 
 | 69 |             use_leftover=True) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 70 |  | 
 | 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 Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 77 |         self.lock = options.lock | 
 | 78 |         self.unlock = options.unlock | 
 | 79 |         self.lock_reason = options.lock_reason | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 80 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 81 |         if options.lock: | 
 | 82 |             self.data['locked'] = True | 
 | 83 |             self.messages.append('Locked host') | 
 | 84 |         elif options.unlock: | 
 | 85 |             self.data['locked'] = False | 
| Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 86 |             self.data['lock_reason'] = '' | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 87 |             self.messages.append('Unlocked host') | 
 | 88 |  | 
| Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 89 |         if options.lock and options.lock_reason: | 
 | 90 |             self.data['lock_reason'] = options.lock_reason | 
 | 91 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 92 |  | 
 | 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 |  | 
 | 112 | class host_help(host): | 
 | 113 |     """Just here to get the atest logic working. | 
 | 114 |     Usage is set by its parent""" | 
 | 115 |     pass | 
 | 116 |  | 
 | 117 |  | 
 | 118 | class host_list(action_common.atest_list, host): | 
 | 119 |     """atest host list [--mlist <file>|<hosts>] [--label <label>] | 
| mbligh | 536a524 | 2008-10-18 14:35:54 +0000 | [diff] [blame] | 120 |        [--status <status1,status2>] [--acl <ACL>] [--user <user>]""" | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 121 |  | 
 | 122 |     def __init__(self): | 
 | 123 |         super(host_list, self).__init__() | 
 | 124 |  | 
 | 125 |         self.parser.add_option('-b', '--label', | 
| mbligh | 6444c6b | 2008-10-27 20:55:13 +0000 | [diff] [blame] | 126 |                                default='', | 
 | 127 |                                help='Only list hosts with all these labels ' | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 128 |                                '(comma separated). When --skylab is provided, ' | 
 | 129 |                                'a label must be in the format of ' | 
 | 130 |                                'label-key:label-value (e.g., board:lumpy).') | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 131 |         self.parser.add_option('-s', '--status', | 
| mbligh | 6444c6b | 2008-10-27 20:55:13 +0000 | [diff] [blame] | 132 |                                default='', | 
 | 133 |                                help='Only list hosts with any of these ' | 
 | 134 |                                'statuses (comma separated)') | 
| mbligh | 536a524 | 2008-10-18 14:35:54 +0000 | [diff] [blame] | 135 |         self.parser.add_option('-a', '--acl', | 
| mbligh | 6444c6b | 2008-10-27 20:55:13 +0000 | [diff] [blame] | 136 |                                default='', | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 137 |                                help=('Only list hosts within this ACL. %s' % | 
 | 138 |                                      skylab_utils.MSG_INVALID_IN_SKYLAB)) | 
| mbligh | 536a524 | 2008-10-18 14:35:54 +0000 | [diff] [blame] | 139 |         self.parser.add_option('-u', '--user', | 
| mbligh | 6444c6b | 2008-10-27 20:55:13 +0000 | [diff] [blame] | 140 |                                default='', | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 141 |                                help=('Only list hosts available to this user. ' | 
 | 142 |                                      '%s' % skylab_utils.MSG_INVALID_IN_SKYLAB)) | 
| mbligh | 70b9bf4 | 2008-11-18 15:00:46 +0000 | [diff] [blame] | 143 |         self.parser.add_option('-N', '--hostnames-only', help='Only return ' | 
 | 144 |                                'hostnames for the machines queried.', | 
 | 145 |                                action='store_true') | 
| mbligh | 91e0efd | 2009-02-26 01:02:16 +0000 | [diff] [blame] | 146 |         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 Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 154 |         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') | 
| mbligh | 91e0efd | 2009-02-26 01:02:16 +0000 | [diff] [blame] | 160 |  | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 161 |         self.add_skylab_options() | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 162 |  | 
 | 163 |  | 
 | 164 |     def parse(self): | 
 | 165 |         """Consume the specific options""" | 
| jamesren | c286316 | 2010-07-12 21:20:51 +0000 | [diff] [blame] | 166 |         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 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 171 |         self.status = options.status | 
| mbligh | 536a524 | 2008-10-18 14:35:54 +0000 | [diff] [blame] | 172 |         self.acl = options.acl | 
 | 173 |         self.user = options.user | 
| mbligh | 70b9bf4 | 2008-11-18 15:00:46 +0000 | [diff] [blame] | 174 |         self.hostnames_only = options.hostnames_only | 
| mbligh | 91e0efd | 2009-02-26 01:02:16 +0000 | [diff] [blame] | 175 |  | 
 | 176 |         if options.locked and options.unlocked: | 
 | 177 |             self.invalid_syntax('--locked and --unlocked are ' | 
 | 178 |                                 'mutually exclusive') | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 179 |  | 
| mbligh | 91e0efd | 2009-02-26 01:02:16 +0000 | [diff] [blame] | 180 |         self.locked = options.locked | 
 | 181 |         self.unlocked = options.unlocked | 
| Ningning Xia | 64ced00 | 2018-05-16 17:36:20 -0700 | [diff] [blame] | 182 |         self.label_map = None | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 183 |  | 
 | 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 Xia | 64ced00 | 2018-05-16 17:36:20 -0700 | [diff] [blame] | 192 |  | 
 | 193 |             if self.labels: | 
 | 194 |                 self.label_map = device.convert_to_label_map(self.labels) | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 195 |         else: | 
 | 196 |             if options.full_output: | 
 | 197 |                 self.invalid_syntax('--full_output is only supported with ' | 
 | 198 |                                     '--skylab.') | 
 | 199 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 200 |         return (options, leftover) | 
 | 201 |  | 
 | 202 |  | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 203 |     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 Xia | 64ced00 | 2018-05-16 17:36:20 -0700 | [diff] [blame] | 214 |             label_map=self.label_map, | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 215 |             hostnames=self.hosts, | 
 | 216 |             locked=self.locked, | 
 | 217 |             unlocked=self.unlocked) | 
 | 218 |  | 
 | 219 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 220 |     def execute(self): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 221 |         """Execute 'atest host list'.""" | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 222 |         if self.skylab: | 
 | 223 |             return self.execute_skylab() | 
 | 224 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 225 |         filters = {} | 
 | 226 |         check_results = {} | 
 | 227 |         if self.hosts: | 
 | 228 |             filters['hostname__in'] = self.hosts | 
 | 229 |             check_results['hostname__in'] = 'hostname' | 
| mbligh | 6444c6b | 2008-10-27 20:55:13 +0000 | [diff] [blame] | 230 |  | 
 | 231 |         if self.labels: | 
| jamesren | c286316 | 2010-07-12 21:20:51 +0000 | [diff] [blame] | 232 |             if len(self.labels) == 1: | 
| mbligh | f703fb4 | 2009-01-30 00:35:05 +0000 | [diff] [blame] | 233 |                 # This is needed for labels with wildcards (x86*) | 
| jamesren | c286316 | 2010-07-12 21:20:51 +0000 | [diff] [blame] | 234 |                 filters['labels__name__in'] = self.labels | 
| mbligh | f703fb4 | 2009-01-30 00:35:05 +0000 | [diff] [blame] | 235 |                 check_results['labels__name__in'] = None | 
 | 236 |             else: | 
| jamesren | c286316 | 2010-07-12 21:20:51 +0000 | [diff] [blame] | 237 |                 filters['multiple_labels'] = self.labels | 
| mbligh | f703fb4 | 2009-01-30 00:35:05 +0000 | [diff] [blame] | 238 |                 check_results['multiple_labels'] = None | 
| mbligh | 6444c6b | 2008-10-27 20:55:13 +0000 | [diff] [blame] | 239 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 240 |         if self.status: | 
| mbligh | 6444c6b | 2008-10-27 20:55:13 +0000 | [diff] [blame] | 241 |             statuses = self.status.split(',') | 
 | 242 |             statuses = [status.strip() for status in statuses | 
 | 243 |                         if status.strip()] | 
 | 244 |  | 
 | 245 |             filters['status__in'] = statuses | 
| mbligh | cd8eb97 | 2008-08-25 19:20:39 +0000 | [diff] [blame] | 246 |             check_results['status__in'] = None | 
| mbligh | 6444c6b | 2008-10-27 20:55:13 +0000 | [diff] [blame] | 247 |  | 
| mbligh | 536a524 | 2008-10-18 14:35:54 +0000 | [diff] [blame] | 248 |         if self.acl: | 
| showard | d9ac445 | 2009-02-07 02:04:37 +0000 | [diff] [blame] | 249 |             filters['aclgroup__name'] = self.acl | 
 | 250 |             check_results['aclgroup__name'] = None | 
| mbligh | 536a524 | 2008-10-18 14:35:54 +0000 | [diff] [blame] | 251 |         if self.user: | 
| showard | d9ac445 | 2009-02-07 02:04:37 +0000 | [diff] [blame] | 252 |             filters['aclgroup__users__login'] = self.user | 
 | 253 |             check_results['aclgroup__users__login'] = None | 
| mbligh | 91e0efd | 2009-02-26 01:02:16 +0000 | [diff] [blame] | 254 |  | 
 | 255 |         if self.locked or self.unlocked: | 
 | 256 |             filters['locked'] = self.locked | 
 | 257 |             check_results['locked'] = None | 
 | 258 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 259 |         return super(host_list, self).execute(op='get_hosts', | 
 | 260 |                                               filters=filters, | 
 | 261 |                                               check_results=check_results) | 
 | 262 |  | 
 | 263 |  | 
 | 264 |     def output(self, results): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 265 |         """Print output of 'atest host list'. | 
 | 266 |  | 
 | 267 |         @param results: the results to be printed. | 
 | 268 |         """ | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 269 |         if results and not self.skylab: | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 270 |             # Remove the platform from the labels. | 
 | 271 |             for result in results: | 
 | 272 |                 result['labels'] = self._cleanup_labels(result['labels'], | 
 | 273 |                                                         result['platform']) | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 274 |         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 |  | 
| mbligh | 70b9bf4 | 2008-11-18 15:00:46 +0000 | [diff] [blame] | 281 |         if self.hostnames_only: | 
| mbligh | df75f8b | 2008-11-18 19:07:42 +0000 | [diff] [blame] | 282 |             self.print_list(results, key='hostname') | 
| mbligh | 70b9bf4 | 2008-11-18 15:00:46 +0000 | [diff] [blame] | 283 |         else: | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 284 |             keys = ['hostname', 'status', 'shard', 'locked', 'lock_reason', | 
 | 285 |                     'locked_by', 'platform', 'labels'] | 
| Prashanth Balasubramanian | a504856 | 2014-12-12 10:14:11 -0800 | [diff] [blame] | 286 |             super(host_list, self).output(results, keys=keys) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 287 |  | 
 | 288 |  | 
 | 289 | class host_stat(host): | 
 | 290 |     """atest host stat --mlist <file>|<hosts>""" | 
 | 291 |     usage_action = 'stat' | 
 | 292 |  | 
 | 293 |     def execute(self): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 294 |         """Execute 'atest host stat'.""" | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 295 |         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 Basi | 0739d68 | 2015-02-25 16:22:56 -0800 | [diff] [blame] | 320 |             results.append([[stat], acls, labels, stat['attributes']]) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 321 |         return results | 
 | 322 |  | 
 | 323 |  | 
 | 324 |     def output(self, results): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 325 |         """Print output of 'atest host stat'. | 
 | 326 |  | 
 | 327 |         @param results: the results to be printed. | 
 | 328 |         """ | 
| Simran Basi | 0739d68 | 2015-02-25 16:22:56 -0800 | [diff] [blame] | 329 |         for stats, acls, labels, attributes in results: | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 330 |             print '-'*5 | 
 | 331 |             self.print_fields(stats, | 
| Aviv Keshet | a40d127 | 2017-11-16 00:56:35 -0800 | [diff] [blame] | 332 |                               keys=['hostname', 'id', 'platform', | 
| mbligh | e163b03 | 2008-10-18 14:30:27 +0000 | [diff] [blame] | 333 |                                     'status', 'locked', 'locked_by', | 
| Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 334 |                                     'lock_time', 'lock_reason', 'protection',]) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 335 |             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 Basi | 0739d68 | 2015-02-25 16:22:56 -0800 | [diff] [blame] | 338 |             self.print_dict(attributes, 'Host Attributes', line_before=True) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 339 |  | 
 | 340 |  | 
 | 341 | class host_jobs(host): | 
| mbligh | 1494eca | 2008-08-13 21:24:22 +0000 | [diff] [blame] | 342 |     """atest host jobs [--max-query] --mlist <file>|<hosts>""" | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 343 |     usage_action = 'jobs' | 
 | 344 |  | 
| mbligh | 6996fe8 | 2008-08-13 00:32:27 +0000 | [diff] [blame] | 345 |     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""" | 
| mbligh | 9deeefa | 2009-05-01 23:11:08 +0000 | [diff] [blame] | 355 |         (options, leftover) = super(host_jobs, self).parse() | 
| mbligh | 6996fe8 | 2008-08-13 00:32:27 +0000 | [diff] [blame] | 356 |         self.max_queries = options.max_query | 
 | 357 |         return (options, leftover) | 
 | 358 |  | 
 | 359 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 360 |     def execute(self): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 361 |         """Execute 'atest host jobs'.""" | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 362 |         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', | 
| mbligh | 6996fe8 | 2008-08-13 00:32:27 +0000 | [diff] [blame] | 377 |                                              host__hostname=host, | 
| mbligh | 1494eca | 2008-08-13 21:24:22 +0000 | [diff] [blame] | 378 |                                              query_limit=self.max_queries, | 
| showard | 6958c72 | 2009-09-23 20:03:16 +0000 | [diff] [blame] | 379 |                                              sort_by=['-job__id']) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 380 |             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): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 392 |         """Print output of 'atest host jobs'. | 
 | 393 |  | 
 | 394 |         @param results: the results to be printed. | 
 | 395 |         """ | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 396 |         for host, jobs in results: | 
 | 397 |             print '-'*5 | 
 | 398 |             print 'Hostname: %s' % host | 
 | 399 |             self.print_table(jobs, keys_header=['job_id', | 
| showard | be0d869 | 2009-08-20 23:42:44 +0000 | [diff] [blame] | 400 |                                                 'job_owner', | 
 | 401 |                                                 'job_name', | 
 | 402 |                                                 'status']) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 403 |  | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 404 | class BaseHostModCreate(host): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 405 |     """The base class for host_mod and host_create""" | 
| Justin Giorgi | 9261f9f | 2016-07-11 17:48:52 -0700 | [diff] [blame] | 406 |     # Matches one attribute=value pair | 
 | 407 |     attribute_regex = r'(?P<attribute>\w+)=(?P<value>.+)?' | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 408 |  | 
 | 409 |     def __init__(self): | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 410 |         """Add the options shared between host mod and host create actions.""" | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 411 |         self.messages = [] | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 412 |         self.host_ids = {} | 
 | 413 |         super(BaseHostModCreate, self).__init__() | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 414 |         self.parser.add_option('-l', '--lock', | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 415 |                                help='Lock hosts.', | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 416 |                                action='store_true') | 
| Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 417 |         self.parser.add_option('-r', '--lock_reason', | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 418 |                                help='Reason for locking hosts.', | 
| Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 419 |                                default='') | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 420 |         self.parser.add_option('-u', '--unlock', | 
 | 421 |                                help='Unlock hosts.', | 
 | 422 |                                action='store_true') | 
| Ningning Xia | 5c0e8f3 | 2018-05-21 16:26:50 -0700 | [diff] [blame] | 423 |  | 
| mbligh | e163b03 | 2008-10-18 14:30:27 +0000 | [diff] [blame] | 424 |         self.parser.add_option('-p', '--protection', type='choice', | 
| mbligh | aed47e8 | 2009-03-17 19:06:18 +0000 | [diff] [blame] | 425 |                                help=('Set the protection level on a host.  ' | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 426 |                                      'Must be one of: %s. %s' % | 
 | 427 |                                      (', '.join('"%s"' % p | 
 | 428 |                                                for p in self.protections), | 
 | 429 |                                       skylab_utils.MSG_INVALID_IN_SKYLAB)), | 
| mbligh | aed47e8 | 2009-03-17 19:06:18 +0000 | [diff] [blame] | 430 |                                choices=self.protections) | 
| Justin Giorgi | 9261f9f | 2016-07-11 17:48:52 -0700 | [diff] [blame] | 431 |         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') | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 439 |         self.parser.add_option('-b', '--labels', | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 440 |                                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).')) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 444 |         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 Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 449 |                                help=('Comma separated list of ACLs. %s' % | 
 | 450 |                                      skylab_utils.MSG_INVALID_IN_SKYLAB)) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 451 |         self.parser.add_option('-A', '--alist', | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 452 |                                help=('File listing the acls. %s' % | 
 | 453 |                                      skylab_utils.MSG_INVALID_IN_SKYLAB), | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 454 |                                type='string', | 
 | 455 |                                metavar='ACL_FLIST') | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 456 |         self.parser.add_option('-t', '--platform', | 
| Ningning Xia | 5c0e8f3 | 2018-05-21 16:26:50 -0700 | [diff] [blame] | 457 |                                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)) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 461 |  | 
 | 462 |  | 
 | 463 |     def parse(self): | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 464 |         """Consume the options common to host create and host mod. | 
 | 465 |         """ | 
| mbligh | 9deeefa | 2009-05-01 23:11:08 +0000 | [diff] [blame] | 466 |         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 Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 473 |         (options, leftover) = super(BaseHostModCreate, self).parse([label_info, | 
| mbligh | 9deeefa | 2009-05-01 23:11:08 +0000 | [diff] [blame] | 474 |                                                               acl_info], | 
 | 475 |                                                              req_items='hosts') | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 476 |  | 
 | 477 |         self._parse_lock_options(options) | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 478 |  | 
| Ningning Xia | 64ced00 | 2018-05-16 17:36:20 -0700 | [diff] [blame] | 479 |         self.label_map = None | 
| Ningning Xia | 5c0e8f3 | 2018-05-21 16:26:50 -0700 | [diff] [blame] | 480 |         if self.allow_skylab and self.skylab: | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 481 |             # TODO(nxia): drop these flags when all hosts are migrated to skylab | 
| Ningning Xia | 64ced00 | 2018-05-16 17:36:20 -0700 | [diff] [blame] | 482 |             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 Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 490 |  | 
| mbligh | aed47e8 | 2009-03-17 19:06:18 +0000 | [diff] [blame] | 491 |         if options.protection: | 
 | 492 |             self.data['protection'] = options.protection | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 493 |             self.messages.append('Protection set to "%s"' % options.protection) | 
 | 494 |  | 
 | 495 |         self.attributes = {} | 
| Justin Giorgi | 9261f9f | 2016-07-11 17:48:52 -0700 | [diff] [blame] | 496 |         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: | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 503 |                     raise topic_common.CliError( | 
 | 504 |                             'Multiple values provided for attribute ' | 
 | 505 |                             '%s.' % m.group('attribute')) | 
| Justin Giorgi | 9261f9f | 2016-07-11 17:48:52 -0700 | [diff] [blame] | 506 |                 self.attributes[m.group('attribute')] = m.group('value') | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 507 |  | 
 | 508 |         self.platform = options.platform | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 509 |         return (options, leftover) | 
 | 510 |  | 
 | 511 |  | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 512 |     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 |  | 
 | 588 | class 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 Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 597 |  | 
 | 598 |     def __init__(self): | 
 | 599 |         """Add the options specific to the mod action""" | 
 | 600 |         super(host_mod, self).__init__() | 
| Ningning Xia | 5c0e8f3 | 2018-05-21 16:26:50 -0700 | [diff] [blame] | 601 |         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 Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 605 |         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 Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 609 |                                help=('Remove all active acls. %s' % | 
 | 610 |                                      skylab_utils.MSG_INVALID_IN_SKYLAB), | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 611 |                                action='store_true') | 
 | 612 |         self.parser.add_option('--remove_labels', | 
 | 613 |                                help='Remove all labels.', | 
 | 614 |                                action='store_true') | 
 | 615 |  | 
| Ningning Xia | 5c0e8f3 | 2018-05-21 16:26:50 -0700 | [diff] [blame] | 616 |         self.add_skylab_options() | 
| Ningning Xia | 1dd82ee | 2018-06-08 11:41:03 -0700 | [diff] [blame] | 617 |         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 Xia | 5c0e8f3 | 2018-05-21 16:26:50 -0700 | [diff] [blame] | 624 |  | 
 | 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 Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 639 |  | 
 | 640 |     def parse(self): | 
 | 641 |         """Consume the specific options""" | 
 | 642 |         (options, leftover) = super(host_mod, self).parse() | 
 | 643 |  | 
| Ningning Xia | 5c0e8f3 | 2018-05-21 16:26:50 -0700 | [diff] [blame] | 644 |         self._parse_unlock_options(options) | 
 | 645 |  | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 646 |         if options.force_modify_locking: | 
 | 647 |              self.data['force_modify_locking'] = True | 
 | 648 |  | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 649 |         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 Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 653 |         self.remove_acls = options.remove_acls | 
 | 654 |         self.remove_labels = options.remove_labels | 
| Ningning Xia | 1dd82ee | 2018-06-08 11:41:03 -0700 | [diff] [blame] | 655 |         self.new_env = options.new_env | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 656 |  | 
 | 657 |         return (options, leftover) | 
 | 658 |  | 
 | 659 |  | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 660 |     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 Xia | 1dd82ee | 2018-06-08 11:41:03 -0700 | [diff] [blame] | 689 |                         label_map=self.label_map, | 
 | 690 |                         new_env=self.new_env) | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 691 |                 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 Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 712 |     def execute(self): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 713 |         """Execute 'atest host mod'.""" | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 714 |         if self.skylab: | 
 | 715 |             return self.execute_skylab() | 
 | 716 |  | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 717 |         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): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 754 |         """Print output of 'atest host mod'. | 
 | 755 |  | 
 | 756 |         @param hosts: the host list to be printed. | 
 | 757 |         """ | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 758 |         for msg in self.messages: | 
 | 759 |             self.print_wrapped(msg, hosts) | 
 | 760 |  | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 761 |         if hosts and self.skylab: | 
| Ningning Xia | c8b430d | 2018-05-17 15:10:25 -0700 | [diff] [blame] | 762 |             print('Modified hosts: %s.' % ', '.join(hosts)) | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 763 |             if self.skylab and not self.dryrun and not self.submit: | 
| Ningning Xia | c8b430d | 2018-05-17 15:10:25 -0700 | [diff] [blame] | 764 |                 print(skylab_utils.get_cl_message(self.change_number)) | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 765 |  | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 766 |  | 
 | 767 | class 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 |  | 
 | 775 | class 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 Giorgi | 5208eaa | 2016-07-02 20:12:12 -0700 | [diff] [blame] | 836 |     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 Prabhu | d252d26 | 2017-05-31 11:46:34 -0700 | [diff] [blame] | 847 |         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 Giorgi | 5208eaa | 2016-07-02 20:12:12 -0700 | [diff] [blame] | 855 |         try: | 
| Allen Li | 2c32d6b | 2017-02-03 15:28:10 -0800 | [diff] [blame] | 856 |             if bin_utils.ping(host, tries=1, deadline=1) == 0: | 
| Justin Giorgi | 5208eaa | 2016-07-02 20:12:12 -0700 | [diff] [blame] | 857 |                 serials = self.attributes.get('serials', '').split(',') | 
| Richard Barnette | 9db8068 | 2018-04-26 00:55:15 +0000 | [diff] [blame] | 858 |                 adb_serial = self.attributes.get('serials') | 
 | 859 |                 host_dut = hosts.create_host(machine, | 
 | 860 |                                              adb_serial=adb_serial) | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 861 |  | 
| Prathmesh Prabhu | d252d26 | 2017-05-31 11:46:34 -0700 | [diff] [blame] | 862 |                 info = HostInfo(host, host_dut.get_platform(), | 
 | 863 |                                 host_dut.get_labels()) | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 864 |                 # 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 Giorgi | 5208eaa | 2016-07-02 20:12:12 -0700 | [diff] [blame] | 868 |             else: | 
 | 869 |                 # Can't ping the host, use default information. | 
| Prathmesh Prabhu | d252d26 | 2017-05-31 11:46:34 -0700 | [diff] [blame] | 870 |                 info = HostInfo(host, None, []) | 
| Justin Giorgi | 5208eaa | 2016-07-02 20:12:12 -0700 | [diff] [blame] | 871 |         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 Prabhu | d252d26 | 2017-05-31 11:46:34 -0700 | [diff] [blame] | 875 |             info = HostInfo(host, None, []) | 
 | 876 |         return info | 
| Justin Giorgi | 5208eaa | 2016-07-02 20:12:12 -0700 | [diff] [blame] | 877 |  | 
 | 878 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 879 |     def _execute_add_one_host(self, host): | 
| mbligh | 719e14a | 2008-12-04 01:17:08 +0000 | [diff] [blame] | 880 |         # Always add the hosts as locked to avoid the host | 
| Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 881 |         # being picked up by the scheduler before it's ACL'ed. | 
| mbligh | 719e14a | 2008-12-04 01:17:08 +0000 | [diff] [blame] | 882 |         self.data['locked'] = True | 
| Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 883 |         if not self.locked: | 
 | 884 |             self.data['lock_reason'] = 'Forced lock on device creation' | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 885 |         self.execute_rpc('add_host', hostname=host, status="Ready", **self.data) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 886 |  | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 887 |         # If there are labels avaliable for host, use them. | 
| Prathmesh Prabhu | d252d26 | 2017-05-31 11:46:34 -0700 | [diff] [blame] | 888 |         info = self._detect_host_info(host) | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 889 |         labels = set(self.labels) | 
| Prathmesh Prabhu | d252d26 | 2017-05-31 11:46:34 -0700 | [diff] [blame] | 890 |         if info.labels: | 
 | 891 |             labels.update(info.labels) | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 892 |  | 
 | 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 Prabhu | d252d26 | 2017-05-31 11:46:34 -0700 | [diff] [blame] | 899 |         platform = self.platform if self.platform else info.platform | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 900 |         if platform: | 
 | 901 |             self._set_platform_label(host, platform) | 
 | 902 |  | 
 | 903 |         if self.attributes: | 
 | 904 |             self._set_attributes(host, self.attributes) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 905 |  | 
 | 906 |  | 
| Justin Giorgi | 5208eaa | 2016-07-02 20:12:12 -0700 | [diff] [blame] | 907 |     def execute(self): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 908 |         """Execute 'atest host create'.""" | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 909 |         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 Luo | c342f9f | 2014-05-19 16:22:03 -0700 | [diff] [blame] | 916 |  | 
 | 917 |         if successful_hosts: | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 918 |             self._set_acls(successful_hosts, self.acls) | 
| Jiaxi Luo | c342f9f | 2014-05-19 16:22:03 -0700 | [diff] [blame] | 919 |  | 
 | 920 |             if not self.locked: | 
 | 921 |                 for host in successful_hosts: | 
| Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 922 |                     self.execute_rpc('modify_host', id=host, locked=False, | 
 | 923 |                                      lock_reason='') | 
| Jiaxi Luo | c342f9f | 2014-05-19 16:22:03 -0700 | [diff] [blame] | 924 |         return successful_hosts | 
 | 925 |  | 
 | 926 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 927 |     def output(self, hosts): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 928 |         """Print output of 'atest host create'. | 
 | 929 |  | 
 | 930 |         @param hosts: the added host list to be printed. | 
 | 931 |         """ | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 932 |         self.print_wrapped('Added host', hosts) | 
 | 933 |  | 
 | 934 |  | 
 | 935 | class host_delete(action_common.atest_delete, host): | 
 | 936 |     """atest host delete [--mlist <mach_file>] <hosts>""" | 
| Ningning Xia | c8b430d | 2018-05-17 15:10:25 -0700 | [diff] [blame] | 937 |  | 
 | 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 Xia | c46bdd1 | 2018-05-29 11:24:14 -0700 | [diff] [blame] | 986 |  | 
 | 987 |  | 
 | 988 | class InvalidHostnameError(Exception): | 
 | 989 |     """Cannot perform actions on the host because of invalid hostname.""" | 
 | 990 |  | 
 | 991 |  | 
 | 992 | def _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 |  | 
 | 1001 | def _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 |  | 
 | 1011 | class 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 Xia | b261b16 | 2018-06-07 17:24:59 -0700 | [diff] [blame] | 1074 |                 host_id = self.host_ids[host] | 
| Ningning Xia | c46bdd1 | 2018-05-29 11:24:14 -0700 | [diff] [blame] | 1075 |                 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 Xia | 423c796 | 2018-06-07 15:27:18 -0700 | [diff] [blame] | 1084 |                     # TODO(crbug.com/850737): delete and abort HQE. | 
| Ningning Xia | c46bdd1 | 2018-05-29 11:24:14 -0700 | [diff] [blame] | 1085 |                     data = {'hostname': new_hostname} | 
| Ningning Xia | b261b16 | 2018-06-07 17:24:59 -0700 | [diff] [blame] | 1086 |                     self.execute_rpc('modify_host', item=host, id=host_id, | 
 | 1087 |                                      **data) | 
| Ningning Xia | c46bdd1 | 2018-05-29 11:24:14 -0700 | [diff] [blame] | 1088 |                 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 Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1105 |  | 
 | 1106 |  | 
 | 1107 | class 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 Xia | 56d6843 | 2018-06-06 17:28:20 -0700 | [diff] [blame] | 1161 |     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 Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1170 |         for hostname in hostnames: | 
 | 1171 |             if hostname.endswith(MIGRATED_HOST_SUFFIX): | 
| Ningning Xia | 56d6843 | 2018-06-06 17:28:20 -0700 | [diff] [blame] | 1172 |                 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 Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1186 |  | 
 | 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 Xia | 56d6843 | 2018-06-06 17:28:20 -0700 | [diff] [blame] | 1199 |         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 Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1204 |  | 
 | 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 Xia | 56d6843 | 2018-06-06 17:28:20 -0700 | [diff] [blame] | 1223 |         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 Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1230 |         return self.execute_skylab_migration(hostnames) | 
 | 1231 |  | 
 | 1232 |  | 
| Xixuan Wu | a8d93c8 | 2018-08-03 16:23:26 -0700 | [diff] [blame] | 1233 |     def assign_duts_to_drone(self, infra, devices, environment): | 
| Ningning Xia | 877817f | 2018-06-08 12:23:48 -0700 | [diff] [blame] | 1234 |         """Assign uids of the devices to a random skylab drone. | 
| Ningning Xia | ffb3c1f | 2018-06-07 18:42:32 -0700 | [diff] [blame] | 1235 |  | 
 | 1236 |         @param infra: An instance of lab_pb2.Infrastructure. | 
| Xixuan Wu | a8d93c8 | 2018-08-03 16:23:26 -0700 | [diff] [blame] | 1237 |         @param devices: A list of device_pb2.Device to be assigned to the drone. | 
 | 1238 |         @param environment: 'staging' or 'prod'. | 
| Ningning Xia | ffb3c1f | 2018-06-07 18:42:32 -0700 | [diff] [blame] | 1239 |         """ | 
 | 1240 |         skylab_drones = skylab_server.get_servers( | 
| Xixuan Wu | a8d93c8 | 2018-08-03 16:23:26 -0700 | [diff] [blame] | 1241 |                 infra, environment, role='skylab_drone', status='primary') | 
| Ningning Xia | ffb3c1f | 2018-06-07 18:42:32 -0700 | [diff] [blame] | 1242 |  | 
 | 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 Wu | dfc2de2 | 2018-08-06 09:29:01 -0700 | [diff] [blame^] | 1249 |         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 Xia | 877817f | 2018-06-08 12:23:48 -0700 | [diff] [blame] | 1253 |  | 
 | 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 Xia | ffb3c1f | 2018-06-07 18:42:32 -0700 | [diff] [blame] | 1266 |  | 
 | 1267 |  | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1268 |     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 Xia | ffb3c1f | 2018-06-07 18:42:32 -0700 | [diff] [blame] | 1287 |         infra = text_manager.load_infrastructure(skylab_data_dir) | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1288 |  | 
 | 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 Xia | 423c796 | 2018-06-07 15:27:18 -0700 | [diff] [blame] | 1311 |                 'Please run this command after the CL is submitted:\n' | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1312 |                 'atest host rename --for-migration %s' % | 
 | 1313 |                 (len(all_devices), ' '.join(device_hostnames))) | 
| Ningning Xia | ffb3c1f | 2018-06-07 18:42:32 -0700 | [diff] [blame] | 1314 |  | 
| Xixuan Wu | a8d93c8 | 2018-08-03 16:23:26 -0700 | [diff] [blame] | 1315 |             self.assign_duts_to_drone(infra, prod_devices, 'prod') | 
 | 1316 |             self.assign_duts_to_drone(infra, staging_devices, 'staging') | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1317 |         else: | 
 | 1318 |             # rollback | 
 | 1319 |             prod_devices = device.move_devices( | 
 | 1320 |                     skylab_lab, prod_lab, 'duts', environment='prod', | 
| Ningning Xia | 56d6843 | 2018-06-06 17:28:20 -0700 | [diff] [blame] | 1321 |                     label_map=label_map, hostnames=hostnames) | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1322 |             staging_devices = device.move_devices( | 
| Ningning Xia | 877817f | 2018-06-08 12:23:48 -0700 | [diff] [blame] | 1323 |                     skylab_lab, staging_lab, 'duts', environment='staging', | 
| Ningning Xia | 56d6843 | 2018-06-06 17:28:20 -0700 | [diff] [blame] | 1324 |                     label_map=label_map, hostnames=hostnames) | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1325 |  | 
 | 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 Xia | 423c796 | 2018-06-07 15:27:18 -0700 | [diff] [blame] | 1333 |                 'Please run this command after the CL is submitted:\n' | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1334 |                 'atest host rename --for-rollback %s' % | 
 | 1335 |                 (len(all_devices), ' '.join(device_hostnames))) | 
 | 1336 |  | 
| Ningning Xia | 877817f | 2018-06-08 12:23:48 -0700 | [diff] [blame] | 1337 |             self.remove_duts_from_drone(infra, all_devices) | 
 | 1338 |  | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1339 |         if all_devices: | 
| Ningning Xia | ffb3c1f | 2018-06-07 18:42:32 -0700 | [diff] [blame] | 1340 |             text_manager.dump_infrastructure(skylab_data_dir, infra) | 
 | 1341 |  | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1342 |             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.') |