| 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 | 
| Gregory Nisbet | 99b6194 | 2019-07-08 09:43:06 -0700 | [diff] [blame] | 23 | import json | 
| Ningning Xia | ffb3c1f | 2018-06-07 18:42:32 -0700 | [diff] [blame] | 24 | import random | 
| Simran Basi | 0739d68 | 2015-02-25 16:22:56 -0800 | [diff] [blame] | 25 | import re | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 26 | import socket | 
| Gregory Nisbet | 93fa5b5 | 2019-07-19 14:56:35 -0700 | [diff] [blame] | 27 | import time | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 28 |  | 
| Gregory Nisbet | c4d63ca | 2019-08-08 10:46:40 -0700 | [diff] [blame] | 29 | from autotest_lib.cli import action_common, rpc, topic_common, skylab_utils, skylab_migration | 
| Gregory Nisbet | 93fa5b5 | 2019-07-19 14:56:35 -0700 | [diff] [blame] | 30 | from autotest_lib.cli import fair_partition | 
| Xixuan Wu | 06f7e23 | 2019-07-12 23:33:28 +0000 | [diff] [blame] | 31 | from autotest_lib.client.bin import utils as bin_utils | 
| Gregory Nisbet | 93fa5b5 | 2019-07-19 14:56:35 -0700 | [diff] [blame] | 32 | from autotest_lib.cli.skylab_json_utils import process_labels | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 33 | from autotest_lib.client.common_lib import error, host_protections | 
| Justin Giorgi | 5208eaa | 2016-07-02 20:12:12 -0700 | [diff] [blame] | 34 | from autotest_lib.server import frontend, hosts | 
| Prathmesh Prabhu | d252d26 | 2017-05-31 11:46:34 -0700 | [diff] [blame] | 35 | from autotest_lib.server.hosts import host_info | 
| Gregory Nisbet | 93fa5b5 | 2019-07-19 14:56:35 -0700 | [diff] [blame] | 36 | from autotest_lib.server.lib.status_history import HostJobHistory | 
|  | 37 | from autotest_lib.server.lib.status_history import UNUSED, WORKING | 
|  | 38 | from autotest_lib.server.lib.status_history import BROKEN, UNKNOWN | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 39 |  | 
|  | 40 |  | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 41 | try: | 
|  | 42 | from skylab_inventory import text_manager | 
|  | 43 | from skylab_inventory.lib import device | 
| Ningning Xia | ffb3c1f | 2018-06-07 18:42:32 -0700 | [diff] [blame] | 44 | from skylab_inventory.lib import server as skylab_server | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 45 | except ImportError: | 
|  | 46 | pass | 
|  | 47 |  | 
|  | 48 |  | 
| Ningning Xia | c46bdd1 | 2018-05-29 11:24:14 -0700 | [diff] [blame] | 49 | MIGRATED_HOST_SUFFIX = '-migrated-do-not-use' | 
|  | 50 |  | 
|  | 51 |  | 
| Gregory Nisbet | 99b6194 | 2019-07-08 09:43:06 -0700 | [diff] [blame] | 52 | ID_AUTOGEN_MESSAGE = ("[IGNORED]. Do not edit (crbug.com/950553). ID is " | 
|  | 53 | "auto-generated.") | 
|  | 54 |  | 
|  | 55 |  | 
|  | 56 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 57 | class host(topic_common.atest): | 
|  | 58 | """Host class | 
| Gregory Nisbet | c4d63ca | 2019-08-08 10:46:40 -0700 | [diff] [blame] | 59 | atest host [create|delete|list|stat|mod|jobs|rename|migrate|skylab_migrate|statjson] <options>""" | 
|  | 60 | usage_action = '[create|delete|list|stat|mod|jobs|rename|migrate|skylab_migrate|statjson]' | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 61 | topic = msg_topic = 'host' | 
|  | 62 | msg_items = '<hosts>' | 
|  | 63 |  | 
| mbligh | aed47e8 | 2009-03-17 19:06:18 +0000 | [diff] [blame] | 64 | protections = host_protections.Protection.names | 
|  | 65 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 66 |  | 
|  | 67 | def __init__(self): | 
|  | 68 | """Add to the parser the options common to all the | 
|  | 69 | host actions""" | 
|  | 70 | super(host, self).__init__() | 
|  | 71 |  | 
|  | 72 | self.parser.add_option('-M', '--mlist', | 
|  | 73 | help='File listing the machines', | 
|  | 74 | type='string', | 
|  | 75 | default=None, | 
|  | 76 | metavar='MACHINE_FLIST') | 
|  | 77 |  | 
| mbligh | 9deeefa | 2009-05-01 23:11:08 +0000 | [diff] [blame] | 78 | self.topic_parse_info = topic_common.item_parse_info( | 
|  | 79 | attribute_name='hosts', | 
|  | 80 | filename_option='mlist', | 
|  | 81 | use_leftover=True) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 82 |  | 
|  | 83 |  | 
|  | 84 | def _parse_lock_options(self, options): | 
|  | 85 | if options.lock and options.unlock: | 
|  | 86 | self.invalid_syntax('Only specify one of ' | 
|  | 87 | '--lock and --unlock.') | 
|  | 88 |  | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 89 | self.lock = options.lock | 
|  | 90 | self.unlock = options.unlock | 
|  | 91 | self.lock_reason = options.lock_reason | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 92 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 93 | if options.lock: | 
|  | 94 | self.data['locked'] = True | 
|  | 95 | self.messages.append('Locked host') | 
|  | 96 | elif options.unlock: | 
|  | 97 | self.data['locked'] = False | 
| Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 98 | self.data['lock_reason'] = '' | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 99 | self.messages.append('Unlocked host') | 
|  | 100 |  | 
| Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 101 | if options.lock and options.lock_reason: | 
|  | 102 | self.data['lock_reason'] = options.lock_reason | 
|  | 103 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 104 |  | 
|  | 105 | def _cleanup_labels(self, labels, platform=None): | 
|  | 106 | """Removes the platform label from the overall labels""" | 
|  | 107 | if platform: | 
|  | 108 | return [label for label in labels | 
|  | 109 | if label != platform] | 
|  | 110 | else: | 
|  | 111 | try: | 
|  | 112 | return [label for label in labels | 
|  | 113 | if not label['platform']] | 
|  | 114 | except TypeError: | 
|  | 115 | # This is a hack - the server will soon | 
|  | 116 | # do this, so all this code should be removed. | 
|  | 117 | return labels | 
|  | 118 |  | 
|  | 119 |  | 
|  | 120 | def get_items(self): | 
|  | 121 | return self.hosts | 
|  | 122 |  | 
|  | 123 |  | 
|  | 124 | class host_help(host): | 
|  | 125 | """Just here to get the atest logic working. | 
|  | 126 | Usage is set by its parent""" | 
|  | 127 | pass | 
|  | 128 |  | 
|  | 129 |  | 
|  | 130 | class host_list(action_common.atest_list, host): | 
|  | 131 | """atest host list [--mlist <file>|<hosts>] [--label <label>] | 
| mbligh | 536a524 | 2008-10-18 14:35:54 +0000 | [diff] [blame] | 132 | [--status <status1,status2>] [--acl <ACL>] [--user <user>]""" | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 133 |  | 
|  | 134 | def __init__(self): | 
|  | 135 | super(host_list, self).__init__() | 
|  | 136 |  | 
|  | 137 | self.parser.add_option('-b', '--label', | 
| mbligh | 6444c6b | 2008-10-27 20:55:13 +0000 | [diff] [blame] | 138 | default='', | 
|  | 139 | help='Only list hosts with all these labels ' | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 140 | '(comma separated). When --skylab is provided, ' | 
|  | 141 | 'a label must be in the format of ' | 
|  | 142 | 'label-key:label-value (e.g., board:lumpy).') | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 143 | self.parser.add_option('-s', '--status', | 
| mbligh | 6444c6b | 2008-10-27 20:55:13 +0000 | [diff] [blame] | 144 | default='', | 
|  | 145 | help='Only list hosts with any of these ' | 
|  | 146 | 'statuses (comma separated)') | 
| mbligh | 536a524 | 2008-10-18 14:35:54 +0000 | [diff] [blame] | 147 | self.parser.add_option('-a', '--acl', | 
| mbligh | 6444c6b | 2008-10-27 20:55:13 +0000 | [diff] [blame] | 148 | default='', | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 149 | help=('Only list hosts within this ACL. %s' % | 
|  | 150 | skylab_utils.MSG_INVALID_IN_SKYLAB)) | 
| mbligh | 536a524 | 2008-10-18 14:35:54 +0000 | [diff] [blame] | 151 | self.parser.add_option('-u', '--user', | 
| mbligh | 6444c6b | 2008-10-27 20:55:13 +0000 | [diff] [blame] | 152 | default='', | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 153 | help=('Only list hosts available to this user. ' | 
|  | 154 | '%s' % skylab_utils.MSG_INVALID_IN_SKYLAB)) | 
| mbligh | 70b9bf4 | 2008-11-18 15:00:46 +0000 | [diff] [blame] | 155 | self.parser.add_option('-N', '--hostnames-only', help='Only return ' | 
|  | 156 | 'hostnames for the machines queried.', | 
|  | 157 | action='store_true') | 
| mbligh | 91e0efd | 2009-02-26 01:02:16 +0000 | [diff] [blame] | 158 | self.parser.add_option('--locked', | 
|  | 159 | default=False, | 
|  | 160 | help='Only list locked hosts', | 
|  | 161 | action='store_true') | 
|  | 162 | self.parser.add_option('--unlocked', | 
|  | 163 | default=False, | 
|  | 164 | help='Only list unlocked hosts', | 
|  | 165 | action='store_true') | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 166 | self.parser.add_option('--full-output', | 
|  | 167 | default=False, | 
|  | 168 | help=('Print out the full content of the hosts. ' | 
|  | 169 | 'Only supported with --skylab.'), | 
|  | 170 | action='store_true', | 
|  | 171 | dest='full_output') | 
| mbligh | 91e0efd | 2009-02-26 01:02:16 +0000 | [diff] [blame] | 172 |  | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 173 | self.add_skylab_options() | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 174 |  | 
|  | 175 |  | 
|  | 176 | def parse(self): | 
|  | 177 | """Consume the specific options""" | 
| jamesren | c286316 | 2010-07-12 21:20:51 +0000 | [diff] [blame] | 178 | label_info = topic_common.item_parse_info(attribute_name='labels', | 
|  | 179 | inline_option='label') | 
|  | 180 |  | 
|  | 181 | (options, leftover) = super(host_list, self).parse([label_info]) | 
|  | 182 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 183 | self.status = options.status | 
| mbligh | 536a524 | 2008-10-18 14:35:54 +0000 | [diff] [blame] | 184 | self.acl = options.acl | 
|  | 185 | self.user = options.user | 
| mbligh | 70b9bf4 | 2008-11-18 15:00:46 +0000 | [diff] [blame] | 186 | self.hostnames_only = options.hostnames_only | 
| mbligh | 91e0efd | 2009-02-26 01:02:16 +0000 | [diff] [blame] | 187 |  | 
|  | 188 | if options.locked and options.unlocked: | 
|  | 189 | self.invalid_syntax('--locked and --unlocked are ' | 
|  | 190 | 'mutually exclusive') | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 191 |  | 
| mbligh | 91e0efd | 2009-02-26 01:02:16 +0000 | [diff] [blame] | 192 | self.locked = options.locked | 
|  | 193 | self.unlocked = options.unlocked | 
| Ningning Xia | 64ced00 | 2018-05-16 17:36:20 -0700 | [diff] [blame] | 194 | self.label_map = None | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 195 |  | 
|  | 196 | if self.skylab: | 
|  | 197 | if options.user or options.acl or options.status: | 
|  | 198 | self.invalid_syntax('--user, --acl or --status is not ' | 
|  | 199 | 'supported with --skylab.') | 
|  | 200 | self.full_output = options.full_output | 
|  | 201 | if self.full_output and self.hostnames_only: | 
|  | 202 | self.invalid_syntax('--full-output is conflicted with ' | 
|  | 203 | '--hostnames-only.') | 
| Ningning Xia | 64ced00 | 2018-05-16 17:36:20 -0700 | [diff] [blame] | 204 |  | 
|  | 205 | if self.labels: | 
|  | 206 | self.label_map = device.convert_to_label_map(self.labels) | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 207 | else: | 
|  | 208 | if options.full_output: | 
|  | 209 | self.invalid_syntax('--full_output is only supported with ' | 
|  | 210 | '--skylab.') | 
|  | 211 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 212 | return (options, leftover) | 
|  | 213 |  | 
|  | 214 |  | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 215 | def execute_skylab(self): | 
|  | 216 | """Execute 'atest host list' with --skylab.""" | 
|  | 217 | inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir) | 
|  | 218 | inventory_repo.initialize() | 
|  | 219 | lab = text_manager.load_lab(inventory_repo.get_data_dir()) | 
|  | 220 |  | 
|  | 221 | # TODO(nxia): support filtering on run-time labels and status. | 
|  | 222 | return device.get_devices( | 
|  | 223 | lab, | 
|  | 224 | 'duts', | 
|  | 225 | self.environment, | 
| Ningning Xia | 64ced00 | 2018-05-16 17:36:20 -0700 | [diff] [blame] | 226 | label_map=self.label_map, | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 227 | hostnames=self.hosts, | 
|  | 228 | locked=self.locked, | 
|  | 229 | unlocked=self.unlocked) | 
|  | 230 |  | 
|  | 231 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 232 | def execute(self): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 233 | """Execute 'atest host list'.""" | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 234 | if self.skylab: | 
|  | 235 | return self.execute_skylab() | 
|  | 236 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 237 | filters = {} | 
|  | 238 | check_results = {} | 
|  | 239 | if self.hosts: | 
|  | 240 | filters['hostname__in'] = self.hosts | 
|  | 241 | check_results['hostname__in'] = 'hostname' | 
| mbligh | 6444c6b | 2008-10-27 20:55:13 +0000 | [diff] [blame] | 242 |  | 
|  | 243 | if self.labels: | 
| jamesren | c286316 | 2010-07-12 21:20:51 +0000 | [diff] [blame] | 244 | if len(self.labels) == 1: | 
| mbligh | f703fb4 | 2009-01-30 00:35:05 +0000 | [diff] [blame] | 245 | # This is needed for labels with wildcards (x86*) | 
| jamesren | c286316 | 2010-07-12 21:20:51 +0000 | [diff] [blame] | 246 | filters['labels__name__in'] = self.labels | 
| mbligh | f703fb4 | 2009-01-30 00:35:05 +0000 | [diff] [blame] | 247 | check_results['labels__name__in'] = None | 
|  | 248 | else: | 
| jamesren | c286316 | 2010-07-12 21:20:51 +0000 | [diff] [blame] | 249 | filters['multiple_labels'] = self.labels | 
| mbligh | f703fb4 | 2009-01-30 00:35:05 +0000 | [diff] [blame] | 250 | check_results['multiple_labels'] = None | 
| mbligh | 6444c6b | 2008-10-27 20:55:13 +0000 | [diff] [blame] | 251 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 252 | if self.status: | 
| mbligh | 6444c6b | 2008-10-27 20:55:13 +0000 | [diff] [blame] | 253 | statuses = self.status.split(',') | 
|  | 254 | statuses = [status.strip() for status in statuses | 
|  | 255 | if status.strip()] | 
|  | 256 |  | 
|  | 257 | filters['status__in'] = statuses | 
| mbligh | cd8eb97 | 2008-08-25 19:20:39 +0000 | [diff] [blame] | 258 | check_results['status__in'] = None | 
| mbligh | 6444c6b | 2008-10-27 20:55:13 +0000 | [diff] [blame] | 259 |  | 
| mbligh | 536a524 | 2008-10-18 14:35:54 +0000 | [diff] [blame] | 260 | if self.acl: | 
| showard | d9ac445 | 2009-02-07 02:04:37 +0000 | [diff] [blame] | 261 | filters['aclgroup__name'] = self.acl | 
|  | 262 | check_results['aclgroup__name'] = None | 
| mbligh | 536a524 | 2008-10-18 14:35:54 +0000 | [diff] [blame] | 263 | if self.user: | 
| showard | d9ac445 | 2009-02-07 02:04:37 +0000 | [diff] [blame] | 264 | filters['aclgroup__users__login'] = self.user | 
|  | 265 | check_results['aclgroup__users__login'] = None | 
| mbligh | 91e0efd | 2009-02-26 01:02:16 +0000 | [diff] [blame] | 266 |  | 
|  | 267 | if self.locked or self.unlocked: | 
|  | 268 | filters['locked'] = self.locked | 
|  | 269 | check_results['locked'] = None | 
|  | 270 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 271 | return super(host_list, self).execute(op='get_hosts', | 
|  | 272 | filters=filters, | 
|  | 273 | check_results=check_results) | 
|  | 274 |  | 
|  | 275 |  | 
|  | 276 | def output(self, results): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 277 | """Print output of 'atest host list'. | 
|  | 278 |  | 
|  | 279 | @param results: the results to be printed. | 
|  | 280 | """ | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 281 | if results and not self.skylab: | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 282 | # Remove the platform from the labels. | 
|  | 283 | for result in results: | 
|  | 284 | result['labels'] = self._cleanup_labels(result['labels'], | 
|  | 285 | result['platform']) | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 286 | if self.skylab and self.full_output: | 
|  | 287 | print results | 
|  | 288 | return | 
|  | 289 |  | 
|  | 290 | if self.skylab: | 
|  | 291 | results = device.convert_to_autotest_hosts(results) | 
|  | 292 |  | 
| mbligh | 70b9bf4 | 2008-11-18 15:00:46 +0000 | [diff] [blame] | 293 | if self.hostnames_only: | 
| mbligh | df75f8b | 2008-11-18 19:07:42 +0000 | [diff] [blame] | 294 | self.print_list(results, key='hostname') | 
| mbligh | 70b9bf4 | 2008-11-18 15:00:46 +0000 | [diff] [blame] | 295 | else: | 
| Ningning Xia | ea02ab1 | 2018-05-11 15:08:57 -0700 | [diff] [blame] | 296 | keys = ['hostname', 'status', 'shard', 'locked', 'lock_reason', | 
|  | 297 | 'locked_by', 'platform', 'labels'] | 
| Prashanth Balasubramanian | a504856 | 2014-12-12 10:14:11 -0800 | [diff] [blame] | 298 | super(host_list, self).output(results, keys=keys) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 299 |  | 
|  | 300 |  | 
|  | 301 | class host_stat(host): | 
|  | 302 | """atest host stat --mlist <file>|<hosts>""" | 
|  | 303 | usage_action = 'stat' | 
|  | 304 |  | 
|  | 305 | def execute(self): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 306 | """Execute 'atest host stat'.""" | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 307 | results = [] | 
|  | 308 | # Convert wildcards into real host stats. | 
|  | 309 | existing_hosts = [] | 
|  | 310 | for host in self.hosts: | 
|  | 311 | if host.endswith('*'): | 
|  | 312 | stats = self.execute_rpc('get_hosts', | 
|  | 313 | hostname__startswith=host.rstrip('*')) | 
|  | 314 | if len(stats) == 0: | 
|  | 315 | self.failure('No hosts matching %s' % host, item=host, | 
|  | 316 | what_failed='Failed to stat') | 
|  | 317 | continue | 
|  | 318 | else: | 
|  | 319 | stats = self.execute_rpc('get_hosts', hostname=host) | 
|  | 320 | if len(stats) == 0: | 
|  | 321 | self.failure('Unknown host %s' % host, item=host, | 
|  | 322 | what_failed='Failed to stat') | 
|  | 323 | continue | 
|  | 324 | existing_hosts.extend(stats) | 
|  | 325 |  | 
|  | 326 | for stat in existing_hosts: | 
|  | 327 | host = stat['hostname'] | 
|  | 328 | # The host exists, these should succeed | 
|  | 329 | acls = self.execute_rpc('get_acl_groups', hosts__hostname=host) | 
|  | 330 |  | 
|  | 331 | labels = self.execute_rpc('get_labels', host__hostname=host) | 
| Simran Basi | 0739d68 | 2015-02-25 16:22:56 -0800 | [diff] [blame] | 332 | results.append([[stat], acls, labels, stat['attributes']]) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 333 | return results | 
|  | 334 |  | 
|  | 335 |  | 
|  | 336 | def output(self, results): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 337 | """Print output of 'atest host stat'. | 
|  | 338 |  | 
|  | 339 | @param results: the results to be printed. | 
|  | 340 | """ | 
| Simran Basi | 0739d68 | 2015-02-25 16:22:56 -0800 | [diff] [blame] | 341 | for stats, acls, labels, attributes in results: | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 342 | print '-'*5 | 
|  | 343 | self.print_fields(stats, | 
| Aviv Keshet | a40d127 | 2017-11-16 00:56:35 -0800 | [diff] [blame] | 344 | keys=['hostname', 'id', 'platform', | 
| mbligh | e163b03 | 2008-10-18 14:30:27 +0000 | [diff] [blame] | 345 | 'status', 'locked', 'locked_by', | 
| Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 346 | 'lock_time', 'lock_reason', 'protection',]) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 347 | self.print_by_ids(acls, 'ACLs', line_before=True) | 
|  | 348 | labels = self._cleanup_labels(labels) | 
|  | 349 | self.print_by_ids(labels, 'Labels', line_before=True) | 
| Simran Basi | 0739d68 | 2015-02-25 16:22:56 -0800 | [diff] [blame] | 350 | self.print_dict(attributes, 'Host Attributes', line_before=True) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 351 |  | 
|  | 352 |  | 
| Gregory Nisbet | 93fa5b5 | 2019-07-19 14:56:35 -0700 | [diff] [blame] | 353 | class host_get_migration_plan(host_stat): | 
|  | 354 | """atest host get_migration_plan --mlist <file>|<hosts>""" | 
|  | 355 | usage_action = "get_migration_plan" | 
|  | 356 |  | 
|  | 357 | def __init__(self): | 
|  | 358 | super(host_get_migration_plan, self).__init__() | 
|  | 359 | self.parser.add_option("--ratio", default=0.5, type=float, dest="ratio") | 
|  | 360 | self.add_skylab_options() | 
|  | 361 |  | 
|  | 362 | def parse(self): | 
|  | 363 | (options, leftover) = super(host_get_migration_plan, self).parse() | 
|  | 364 | self.ratio = options.ratio | 
|  | 365 | return (options, leftover) | 
|  | 366 |  | 
|  | 367 | def execute(self): | 
|  | 368 | afe = frontend.AFE() | 
|  | 369 | results = super(host_get_migration_plan, self).execute() | 
|  | 370 | working = [] | 
|  | 371 | non_working = [] | 
|  | 372 | for stats, _, _, _ in results: | 
|  | 373 | assert len(stats) == 1 | 
|  | 374 | stats = stats[0] | 
|  | 375 | hostname = stats["hostname"] | 
|  | 376 | now = time.time() | 
|  | 377 | history = HostJobHistory.get_host_history( | 
|  | 378 | afe=afe, | 
|  | 379 | hostname=hostname, | 
|  | 380 | start_time=now, | 
|  | 381 | end_time=now - 24 * 60 * 60, | 
|  | 382 | ) | 
|  | 383 | dut_status, _ = history.last_diagnosis() | 
|  | 384 | if dut_status in [UNUSED, WORKING]: | 
|  | 385 | working.append(hostname) | 
|  | 386 | elif dut_status == BROKEN: | 
|  | 387 | non_working.append(hostname) | 
|  | 388 | elif dut_status == UNKNOWN: | 
|  | 389 | # if it's unknown, randomly assign it to working or | 
|  | 390 | # nonworking, since we don't know. | 
|  | 391 | # The two choices aren't actually equiprobable, but it | 
|  | 392 | # should be fine. | 
|  | 393 | random.choice([working, non_working]).append(hostname) | 
|  | 394 | else: | 
|  | 395 | raise ValueError("unknown status %s" % dut_status) | 
|  | 396 | working_transfer, working_retain = fair_partition.partition(working, self.ratio) | 
|  | 397 | non_working_transfer, non_working_retain = \ | 
|  | 398 | fair_partition.partition(non_working, self.ratio) | 
|  | 399 | return { | 
|  | 400 | "transfer": working_transfer + non_working_transfer, | 
|  | 401 | "retain": working_retain + non_working_retain, | 
|  | 402 | } | 
|  | 403 |  | 
|  | 404 | def output(self, results): | 
|  | 405 | print json.dumps(results, indent=4, sort_keys=True) | 
|  | 406 |  | 
| Gregory Nisbet | 99b6194 | 2019-07-08 09:43:06 -0700 | [diff] [blame] | 407 |  | 
|  | 408 | class host_statjson(host_stat): | 
|  | 409 | """atest host statjson --mlist <file>|<hosts> | 
|  | 410 |  | 
|  | 411 | exposes the same information that 'atest host stat' does, but in the json | 
|  | 412 | format that 'skylab add-dut' expects | 
|  | 413 | """ | 
|  | 414 |  | 
|  | 415 | usage_action = "statjson" | 
|  | 416 |  | 
|  | 417 |  | 
|  | 418 | def output(self, results): | 
|  | 419 | """Print output of 'atest host stat-skylab-json'""" | 
|  | 420 | for row in results: | 
|  | 421 | stats, acls, labels, attributes = row | 
|  | 422 | # TODO(gregorynisbet): under what circumstances is stats | 
|  | 423 | #    not a list of length 1? | 
|  | 424 | assert len(stats) == 1 | 
|  | 425 | stats_map = stats[0] | 
| Gregory Nisbet | ce77cef | 2019-07-29 12:27:21 -0700 | [diff] [blame] | 426 |  | 
|  | 427 | # Stripping the MIGRATED_HOST_SUFFIX makes it possible to | 
|  | 428 | # migrate a DUT from autotest to skylab even after its hostname | 
|  | 429 | # has been changed. | 
|  | 430 | # This enables the steps (renaming the host, | 
|  | 431 | # copying the inventory information to skylab) to be doable in | 
|  | 432 | # either order. | 
| Gregory Nisbet | 48f1eea | 2019-08-05 16:18:56 -0700 | [diff] [blame] | 433 | hostname = _remove_hostname_suffix_if_present( | 
|  | 434 | stats_map["hostname"], | 
|  | 435 | MIGRATED_HOST_SUFFIX | 
|  | 436 | ) | 
|  | 437 |  | 
| Gregory Nisbet | 99b6194 | 2019-07-08 09:43:06 -0700 | [diff] [blame] | 438 | labels = self._cleanup_labels(labels) | 
|  | 439 | attrs = [{"key": k, "value": v} for k, v in attributes.iteritems()] | 
|  | 440 | out_labels = process_labels(labels, platform=stats_map["platform"]) | 
|  | 441 | skylab_json = { | 
|  | 442 | "common": { | 
|  | 443 | "attributes": attrs, | 
| Gregory Nisbet | 23a06b9 | 2019-07-11 16:44:43 -0700 | [diff] [blame] | 444 | "environment": "ENVIRONMENT_PROD", | 
| Gregory Nisbet | ce77cef | 2019-07-29 12:27:21 -0700 | [diff] [blame] | 445 | "hostname": hostname, | 
| Gregory Nisbet | 99b6194 | 2019-07-08 09:43:06 -0700 | [diff] [blame] | 446 | "id": ID_AUTOGEN_MESSAGE, | 
|  | 447 | "labels": out_labels, | 
| Gregory Nisbet | cc0941f | 2019-08-29 13:56:05 -0700 | [diff] [blame^] | 448 | "serialNumber": attributes.get("serial_number", None), | 
| Gregory Nisbet | 99b6194 | 2019-07-08 09:43:06 -0700 | [diff] [blame] | 449 | } | 
|  | 450 | } | 
|  | 451 | print json.dumps(skylab_json, indent=4, sort_keys=True) | 
|  | 452 |  | 
|  | 453 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 454 | class host_jobs(host): | 
| mbligh | 1494eca | 2008-08-13 21:24:22 +0000 | [diff] [blame] | 455 | """atest host jobs [--max-query] --mlist <file>|<hosts>""" | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 456 | usage_action = 'jobs' | 
|  | 457 |  | 
| mbligh | 6996fe8 | 2008-08-13 00:32:27 +0000 | [diff] [blame] | 458 | def __init__(self): | 
|  | 459 | super(host_jobs, self).__init__() | 
|  | 460 | self.parser.add_option('-q', '--max-query', | 
|  | 461 | help='Limits the number of results ' | 
|  | 462 | '(20 by default)', | 
|  | 463 | type='int', default=20) | 
|  | 464 |  | 
|  | 465 |  | 
|  | 466 | def parse(self): | 
|  | 467 | """Consume the specific options""" | 
| mbligh | 9deeefa | 2009-05-01 23:11:08 +0000 | [diff] [blame] | 468 | (options, leftover) = super(host_jobs, self).parse() | 
| mbligh | 6996fe8 | 2008-08-13 00:32:27 +0000 | [diff] [blame] | 469 | self.max_queries = options.max_query | 
|  | 470 | return (options, leftover) | 
|  | 471 |  | 
|  | 472 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 473 | def execute(self): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 474 | """Execute 'atest host jobs'.""" | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 475 | results = [] | 
|  | 476 | real_hosts = [] | 
|  | 477 | for host in self.hosts: | 
|  | 478 | if host.endswith('*'): | 
|  | 479 | stats = self.execute_rpc('get_hosts', | 
|  | 480 | hostname__startswith=host.rstrip('*')) | 
|  | 481 | if len(stats) == 0: | 
|  | 482 | self.failure('No host matching %s' % host, item=host, | 
|  | 483 | what_failed='Failed to stat') | 
|  | 484 | [real_hosts.append(stat['hostname']) for stat in stats] | 
|  | 485 | else: | 
|  | 486 | real_hosts.append(host) | 
|  | 487 |  | 
|  | 488 | for host in real_hosts: | 
|  | 489 | queue_entries = self.execute_rpc('get_host_queue_entries', | 
| mbligh | 6996fe8 | 2008-08-13 00:32:27 +0000 | [diff] [blame] | 490 | host__hostname=host, | 
| mbligh | 1494eca | 2008-08-13 21:24:22 +0000 | [diff] [blame] | 491 | query_limit=self.max_queries, | 
| showard | 6958c72 | 2009-09-23 20:03:16 +0000 | [diff] [blame] | 492 | sort_by=['-job__id']) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 493 | jobs = [] | 
|  | 494 | for entry in queue_entries: | 
|  | 495 | job = {'job_id': entry['job']['id'], | 
|  | 496 | 'job_owner': entry['job']['owner'], | 
|  | 497 | 'job_name': entry['job']['name'], | 
|  | 498 | 'status': entry['status']} | 
|  | 499 | jobs.append(job) | 
|  | 500 | results.append((host, jobs)) | 
|  | 501 | return results | 
|  | 502 |  | 
|  | 503 |  | 
|  | 504 | def output(self, results): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 505 | """Print output of 'atest host jobs'. | 
|  | 506 |  | 
|  | 507 | @param results: the results to be printed. | 
|  | 508 | """ | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 509 | for host, jobs in results: | 
|  | 510 | print '-'*5 | 
|  | 511 | print 'Hostname: %s' % host | 
|  | 512 | self.print_table(jobs, keys_header=['job_id', | 
| showard | be0d869 | 2009-08-20 23:42:44 +0000 | [diff] [blame] | 513 | 'job_owner', | 
|  | 514 | 'job_name', | 
|  | 515 | 'status']) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 516 |  | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 517 | class BaseHostModCreate(host): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 518 | """The base class for host_mod and host_create""" | 
| Justin Giorgi | 9261f9f | 2016-07-11 17:48:52 -0700 | [diff] [blame] | 519 | # Matches one attribute=value pair | 
|  | 520 | attribute_regex = r'(?P<attribute>\w+)=(?P<value>.+)?' | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 521 |  | 
|  | 522 | def __init__(self): | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 523 | """Add the options shared between host mod and host create actions.""" | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 524 | self.messages = [] | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 525 | self.host_ids = {} | 
|  | 526 | super(BaseHostModCreate, self).__init__() | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 527 | self.parser.add_option('-l', '--lock', | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 528 | help='Lock hosts.', | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 529 | action='store_true') | 
| Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 530 | self.parser.add_option('-r', '--lock_reason', | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 531 | help='Reason for locking hosts.', | 
| Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 532 | default='') | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 533 | self.parser.add_option('-u', '--unlock', | 
|  | 534 | help='Unlock hosts.', | 
|  | 535 | action='store_true') | 
| Ningning Xia | 5c0e8f3 | 2018-05-21 16:26:50 -0700 | [diff] [blame] | 536 |  | 
| mbligh | e163b03 | 2008-10-18 14:30:27 +0000 | [diff] [blame] | 537 | self.parser.add_option('-p', '--protection', type='choice', | 
| mbligh | aed47e8 | 2009-03-17 19:06:18 +0000 | [diff] [blame] | 538 | help=('Set the protection level on a host.  ' | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 539 | 'Must be one of: %s. %s' % | 
|  | 540 | (', '.join('"%s"' % p | 
|  | 541 | for p in self.protections), | 
|  | 542 | skylab_utils.MSG_INVALID_IN_SKYLAB)), | 
| mbligh | aed47e8 | 2009-03-17 19:06:18 +0000 | [diff] [blame] | 543 | choices=self.protections) | 
| Justin Giorgi | 9261f9f | 2016-07-11 17:48:52 -0700 | [diff] [blame] | 544 | self._attributes = [] | 
|  | 545 | self.parser.add_option('--attribute', '-i', | 
|  | 546 | help=('Host attribute to add or change. Format ' | 
|  | 547 | 'is <attribute>=<value>. Multiple ' | 
|  | 548 | 'attributes can be set by passing the ' | 
|  | 549 | 'argument multiple times. Attributes can ' | 
|  | 550 | 'be unset by providing an empty value.'), | 
|  | 551 | action='append') | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 552 | self.parser.add_option('-b', '--labels', | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 553 | help=('Comma separated list of labels. ' | 
|  | 554 | 'When --skylab is provided, a label must ' | 
|  | 555 | 'be in the format of label-key:label-value' | 
|  | 556 | ' (e.g., board:lumpy).')) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 557 | self.parser.add_option('-B', '--blist', | 
|  | 558 | help='File listing the labels', | 
|  | 559 | type='string', | 
|  | 560 | metavar='LABEL_FLIST') | 
|  | 561 | self.parser.add_option('-a', '--acls', | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 562 | help=('Comma separated list of ACLs. %s' % | 
|  | 563 | skylab_utils.MSG_INVALID_IN_SKYLAB)) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 564 | self.parser.add_option('-A', '--alist', | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 565 | help=('File listing the acls. %s' % | 
|  | 566 | skylab_utils.MSG_INVALID_IN_SKYLAB), | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 567 | type='string', | 
|  | 568 | metavar='ACL_FLIST') | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 569 | self.parser.add_option('-t', '--platform', | 
| Ningning Xia | 5c0e8f3 | 2018-05-21 16:26:50 -0700 | [diff] [blame] | 570 | help=('Sets the platform label. %s Please set ' | 
|  | 571 | 'platform in labels (e.g., -b ' | 
|  | 572 | 'platform:platform_name) with --skylab.' % | 
|  | 573 | skylab_utils.MSG_INVALID_IN_SKYLAB)) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 574 |  | 
|  | 575 |  | 
|  | 576 | def parse(self): | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 577 | """Consume the options common to host create and host mod. | 
|  | 578 | """ | 
| mbligh | 9deeefa | 2009-05-01 23:11:08 +0000 | [diff] [blame] | 579 | label_info = topic_common.item_parse_info(attribute_name='labels', | 
|  | 580 | inline_option='labels', | 
|  | 581 | filename_option='blist') | 
|  | 582 | acl_info = topic_common.item_parse_info(attribute_name='acls', | 
|  | 583 | inline_option='acls', | 
|  | 584 | filename_option='alist') | 
|  | 585 |  | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 586 | (options, leftover) = super(BaseHostModCreate, self).parse([label_info, | 
| mbligh | 9deeefa | 2009-05-01 23:11:08 +0000 | [diff] [blame] | 587 | acl_info], | 
|  | 588 | req_items='hosts') | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 589 |  | 
|  | 590 | self._parse_lock_options(options) | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 591 |  | 
| Ningning Xia | 64ced00 | 2018-05-16 17:36:20 -0700 | [diff] [blame] | 592 | self.label_map = None | 
| Ningning Xia | 5c0e8f3 | 2018-05-21 16:26:50 -0700 | [diff] [blame] | 593 | if self.allow_skylab and self.skylab: | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 594 | # TODO(nxia): drop these flags when all hosts are migrated to skylab | 
| Ningning Xia | 64ced00 | 2018-05-16 17:36:20 -0700 | [diff] [blame] | 595 | if (options.protection or options.acls or options.alist or | 
|  | 596 | options.platform): | 
|  | 597 | self.invalid_syntax( | 
|  | 598 | '--protection, --acls, --alist or --platform is not ' | 
|  | 599 | 'supported with --skylab.') | 
|  | 600 |  | 
|  | 601 | if self.labels: | 
|  | 602 | self.label_map = device.convert_to_label_map(self.labels) | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 603 |  | 
| mbligh | aed47e8 | 2009-03-17 19:06:18 +0000 | [diff] [blame] | 604 | if options.protection: | 
|  | 605 | self.data['protection'] = options.protection | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 606 | self.messages.append('Protection set to "%s"' % options.protection) | 
|  | 607 |  | 
|  | 608 | self.attributes = {} | 
| Justin Giorgi | 9261f9f | 2016-07-11 17:48:52 -0700 | [diff] [blame] | 609 | if options.attribute: | 
|  | 610 | for pair in options.attribute: | 
|  | 611 | m = re.match(self.attribute_regex, pair) | 
|  | 612 | if not m: | 
|  | 613 | raise topic_common.CliError('Attribute must be in key=value ' | 
|  | 614 | 'syntax.') | 
|  | 615 | elif m.group('attribute') in self.attributes: | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 616 | raise topic_common.CliError( | 
|  | 617 | 'Multiple values provided for attribute ' | 
|  | 618 | '%s.' % m.group('attribute')) | 
| Justin Giorgi | 9261f9f | 2016-07-11 17:48:52 -0700 | [diff] [blame] | 619 | self.attributes[m.group('attribute')] = m.group('value') | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 620 |  | 
|  | 621 | self.platform = options.platform | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 622 | return (options, leftover) | 
|  | 623 |  | 
|  | 624 |  | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 625 | def _set_acls(self, hosts, acls): | 
|  | 626 | """Add hosts to acls (and remove from all other acls). | 
|  | 627 |  | 
|  | 628 | @param hosts: list of hostnames | 
|  | 629 | @param acls: list of acl names | 
|  | 630 | """ | 
|  | 631 | # Remove from all ACLs except 'Everyone' and ACLs in list | 
|  | 632 | # Skip hosts that don't exist | 
|  | 633 | for host in hosts: | 
|  | 634 | if host not in self.host_ids: | 
|  | 635 | continue | 
|  | 636 | host_id = self.host_ids[host] | 
|  | 637 | for a in self.execute_rpc('get_acl_groups', hosts=host_id): | 
|  | 638 | if a['name'] not in self.acls and a['id'] != 1: | 
|  | 639 | self.execute_rpc('acl_group_remove_hosts', id=a['id'], | 
|  | 640 | hosts=self.hosts) | 
|  | 641 |  | 
|  | 642 | # Add hosts to the ACLs | 
|  | 643 | self.check_and_create_items('get_acl_groups', 'add_acl_group', | 
|  | 644 | self.acls) | 
|  | 645 | for a in acls: | 
|  | 646 | self.execute_rpc('acl_group_add_hosts', id=a, hosts=hosts) | 
|  | 647 |  | 
|  | 648 |  | 
|  | 649 | def _remove_labels(self, host, condition): | 
|  | 650 | """Remove all labels from host that meet condition(label). | 
|  | 651 |  | 
|  | 652 | @param host: hostname | 
|  | 653 | @param condition: callable that returns bool when given a label | 
|  | 654 | """ | 
|  | 655 | if host in self.host_ids: | 
|  | 656 | host_id = self.host_ids[host] | 
|  | 657 | labels_to_remove = [] | 
|  | 658 | for l in self.execute_rpc('get_labels', host=host_id): | 
|  | 659 | if condition(l): | 
|  | 660 | labels_to_remove.append(l['id']) | 
|  | 661 | if labels_to_remove: | 
|  | 662 | self.execute_rpc('host_remove_labels', id=host_id, | 
|  | 663 | labels=labels_to_remove) | 
|  | 664 |  | 
|  | 665 |  | 
|  | 666 | def _set_labels(self, host, labels): | 
|  | 667 | """Apply labels to host (and remove all other labels). | 
|  | 668 |  | 
|  | 669 | @param host: hostname | 
|  | 670 | @param labels: list of label names | 
|  | 671 | """ | 
|  | 672 | condition = lambda l: l['name'] not in labels and not l['platform'] | 
|  | 673 | self._remove_labels(host, condition) | 
|  | 674 | self.check_and_create_items('get_labels', 'add_label', labels) | 
|  | 675 | self.execute_rpc('host_add_labels', id=host, labels=labels) | 
|  | 676 |  | 
|  | 677 |  | 
|  | 678 | def _set_platform_label(self, host, platform_label): | 
|  | 679 | """Apply the platform label to host (and remove existing). | 
|  | 680 |  | 
|  | 681 | @param host: hostname | 
|  | 682 | @param platform_label: platform label's name | 
|  | 683 | """ | 
|  | 684 | self._remove_labels(host, lambda l: l['platform']) | 
|  | 685 | self.check_and_create_items('get_labels', 'add_label', [platform_label], | 
|  | 686 | platform=True) | 
|  | 687 | self.execute_rpc('host_add_labels', id=host, labels=[platform_label]) | 
|  | 688 |  | 
|  | 689 |  | 
|  | 690 | def _set_attributes(self, host, attributes): | 
|  | 691 | """Set attributes on host. | 
|  | 692 |  | 
|  | 693 | @param host: hostname | 
|  | 694 | @param attributes: attribute dictionary | 
|  | 695 | """ | 
|  | 696 | for attr, value in self.attributes.iteritems(): | 
|  | 697 | self.execute_rpc('set_host_attribute', attribute=attr, | 
|  | 698 | value=value, hostname=host) | 
|  | 699 |  | 
|  | 700 |  | 
|  | 701 | class host_mod(BaseHostModCreate): | 
|  | 702 | """atest host mod [--lock|--unlock --force_modify_locking | 
|  | 703 | --platform <arch> | 
|  | 704 | --labels <labels>|--blist <label_file> | 
|  | 705 | --acls <acls>|--alist <acl_file> | 
|  | 706 | --protection <protection_type> | 
|  | 707 | --attributes <attr>=<value>;<attr>=<value> | 
|  | 708 | --mlist <mach_file>] <hosts>""" | 
|  | 709 | usage_action = 'mod' | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 710 |  | 
|  | 711 | def __init__(self): | 
|  | 712 | """Add the options specific to the mod action""" | 
|  | 713 | super(host_mod, self).__init__() | 
| Ningning Xia | 5c0e8f3 | 2018-05-21 16:26:50 -0700 | [diff] [blame] | 714 | self.parser.add_option('--unlock-lock-id', | 
|  | 715 | help=('Unlock the lock with the lock-id. %s' % | 
|  | 716 | skylab_utils.MSG_ONLY_VALID_IN_SKYLAB), | 
|  | 717 | default=None) | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 718 | self.parser.add_option('-f', '--force_modify_locking', | 
|  | 719 | help='Forcefully lock\unlock a host', | 
|  | 720 | action='store_true') | 
|  | 721 | self.parser.add_option('--remove_acls', | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 722 | help=('Remove all active acls. %s' % | 
|  | 723 | skylab_utils.MSG_INVALID_IN_SKYLAB), | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 724 | action='store_true') | 
|  | 725 | self.parser.add_option('--remove_labels', | 
|  | 726 | help='Remove all labels.', | 
|  | 727 | action='store_true') | 
|  | 728 |  | 
| Ningning Xia | 5c0e8f3 | 2018-05-21 16:26:50 -0700 | [diff] [blame] | 729 | self.add_skylab_options() | 
| Ningning Xia | 1dd82ee | 2018-06-08 11:41:03 -0700 | [diff] [blame] | 730 | self.parser.add_option('--new-env', | 
|  | 731 | dest='new_env', | 
|  | 732 | choices=['staging', 'prod'], | 
|  | 733 | help=('The new environment ("staging" or ' | 
|  | 734 | '"prod") of the hosts. %s' % | 
|  | 735 | skylab_utils.MSG_ONLY_VALID_IN_SKYLAB), | 
|  | 736 | default=None) | 
| Ningning Xia | 5c0e8f3 | 2018-05-21 16:26:50 -0700 | [diff] [blame] | 737 |  | 
|  | 738 |  | 
|  | 739 | def _parse_unlock_options(self, options): | 
|  | 740 | """Parse unlock related options.""" | 
|  | 741 | if self.skylab and options.unlock and options.unlock_lock_id is None: | 
|  | 742 | self.invalid_syntax('Must provide --unlock-lock-id with "--skylab ' | 
|  | 743 | '--unlock".') | 
|  | 744 |  | 
|  | 745 | if (not (self.skylab and options.unlock) and | 
|  | 746 | options.unlock_lock_id is not None): | 
|  | 747 | self.invalid_syntax('--unlock-lock-id is only valid with ' | 
|  | 748 | '"--skylab --unlock".') | 
|  | 749 |  | 
|  | 750 | self.unlock_lock_id = options.unlock_lock_id | 
|  | 751 |  | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 752 |  | 
|  | 753 | def parse(self): | 
|  | 754 | """Consume the specific options""" | 
|  | 755 | (options, leftover) = super(host_mod, self).parse() | 
|  | 756 |  | 
| Ningning Xia | 5c0e8f3 | 2018-05-21 16:26:50 -0700 | [diff] [blame] | 757 | self._parse_unlock_options(options) | 
|  | 758 |  | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 759 | if options.force_modify_locking: | 
|  | 760 | self.data['force_modify_locking'] = True | 
|  | 761 |  | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 762 | if self.skylab and options.remove_acls: | 
|  | 763 | # TODO(nxia): drop the flag when all hosts are migrated to skylab | 
|  | 764 | self.invalid_syntax('--remove_acls is not supported with --skylab.') | 
|  | 765 |  | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 766 | self.remove_acls = options.remove_acls | 
|  | 767 | self.remove_labels = options.remove_labels | 
| Ningning Xia | 1dd82ee | 2018-06-08 11:41:03 -0700 | [diff] [blame] | 768 | self.new_env = options.new_env | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 769 |  | 
|  | 770 | return (options, leftover) | 
|  | 771 |  | 
|  | 772 |  | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 773 | def execute_skylab(self): | 
|  | 774 | """Execute atest host mod with --skylab. | 
|  | 775 |  | 
|  | 776 | @return A list of hostnames which have been successfully modified. | 
|  | 777 | """ | 
|  | 778 | inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir) | 
|  | 779 | inventory_repo.initialize() | 
|  | 780 | data_dir = inventory_repo.get_data_dir() | 
|  | 781 | lab = text_manager.load_lab(data_dir) | 
|  | 782 |  | 
|  | 783 | locked_by = None | 
|  | 784 | if self.lock: | 
|  | 785 | locked_by = inventory_repo.git_repo.config('user.email') | 
|  | 786 |  | 
|  | 787 | successes = [] | 
|  | 788 | for hostname in self.hosts: | 
|  | 789 | try: | 
|  | 790 | device.modify( | 
|  | 791 | lab, | 
|  | 792 | 'duts', | 
|  | 793 | hostname, | 
|  | 794 | self.environment, | 
|  | 795 | lock=self.lock, | 
|  | 796 | locked_by=locked_by, | 
|  | 797 | lock_reason = self.lock_reason, | 
|  | 798 | unlock=self.unlock, | 
|  | 799 | unlock_lock_id=self.unlock_lock_id, | 
|  | 800 | attributes=self.attributes, | 
|  | 801 | remove_labels=self.remove_labels, | 
| Ningning Xia | 1dd82ee | 2018-06-08 11:41:03 -0700 | [diff] [blame] | 802 | label_map=self.label_map, | 
|  | 803 | new_env=self.new_env) | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 804 | successes.append(hostname) | 
|  | 805 | except device.SkylabDeviceActionError as e: | 
|  | 806 | print('Cannot modify host %s: %s' % (hostname, e)) | 
|  | 807 |  | 
|  | 808 | if successes: | 
|  | 809 | text_manager.dump_lab(data_dir, lab) | 
|  | 810 |  | 
|  | 811 | status = inventory_repo.git_repo.status() | 
|  | 812 | if not status: | 
|  | 813 | print('Nothing is changed for hosts %s.' % successes) | 
|  | 814 | return [] | 
|  | 815 |  | 
|  | 816 | message = skylab_utils.construct_commit_message( | 
|  | 817 | 'Modify %d hosts.\n\n%s' % (len(successes), successes)) | 
|  | 818 | self.change_number = inventory_repo.upload_change( | 
|  | 819 | message, draft=self.draft, dryrun=self.dryrun, | 
|  | 820 | submit=self.submit) | 
|  | 821 |  | 
|  | 822 | return successes | 
|  | 823 |  | 
|  | 824 |  | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 825 | def execute(self): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 826 | """Execute 'atest host mod'.""" | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 827 | if self.skylab: | 
|  | 828 | return self.execute_skylab() | 
|  | 829 |  | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 830 | successes = [] | 
|  | 831 | for host in self.execute_rpc('get_hosts', hostname__in=self.hosts): | 
|  | 832 | self.host_ids[host['hostname']] = host['id'] | 
|  | 833 | for host in self.hosts: | 
|  | 834 | if host not in self.host_ids: | 
|  | 835 | self.failure('Cannot modify non-existant host %s.' % host) | 
|  | 836 | continue | 
|  | 837 | host_id = self.host_ids[host] | 
|  | 838 |  | 
|  | 839 | try: | 
|  | 840 | if self.data: | 
|  | 841 | self.execute_rpc('modify_host', item=host, | 
|  | 842 | id=host, **self.data) | 
|  | 843 |  | 
|  | 844 | if self.attributes: | 
|  | 845 | self._set_attributes(host, self.attributes) | 
|  | 846 |  | 
|  | 847 | if self.labels or self.remove_labels: | 
|  | 848 | self._set_labels(host, self.labels) | 
|  | 849 |  | 
|  | 850 | if self.platform: | 
|  | 851 | self._set_platform_label(host, self.platform) | 
|  | 852 |  | 
|  | 853 | # TODO: Make the AFE return True or False, | 
|  | 854 | # especially for lock | 
|  | 855 | successes.append(host) | 
|  | 856 | except topic_common.CliError, full_error: | 
|  | 857 | # Already logged by execute_rpc() | 
|  | 858 | pass | 
|  | 859 |  | 
|  | 860 | if self.acls or self.remove_acls: | 
|  | 861 | self._set_acls(self.hosts, self.acls) | 
|  | 862 |  | 
|  | 863 | return successes | 
|  | 864 |  | 
|  | 865 |  | 
|  | 866 | def output(self, hosts): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 867 | """Print output of 'atest host mod'. | 
|  | 868 |  | 
|  | 869 | @param hosts: the host list to be printed. | 
|  | 870 | """ | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 871 | for msg in self.messages: | 
|  | 872 | self.print_wrapped(msg, hosts) | 
|  | 873 |  | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 874 | if hosts and self.skylab: | 
| Ningning Xia | c8b430d | 2018-05-17 15:10:25 -0700 | [diff] [blame] | 875 | print('Modified hosts: %s.' % ', '.join(hosts)) | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 876 | if self.skylab and not self.dryrun and not self.submit: | 
| Ningning Xia | c8b430d | 2018-05-17 15:10:25 -0700 | [diff] [blame] | 877 | print(skylab_utils.get_cl_message(self.change_number)) | 
| Ningning Xia | ef35cb5 | 2018-05-04 17:58:20 -0700 | [diff] [blame] | 878 |  | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 879 |  | 
|  | 880 | class HostInfo(object): | 
|  | 881 | """Store host information so we don't have to keep looking it up.""" | 
|  | 882 | def __init__(self, hostname, platform, labels): | 
|  | 883 | self.hostname = hostname | 
|  | 884 | self.platform = platform | 
|  | 885 | self.labels = labels | 
|  | 886 |  | 
|  | 887 |  | 
|  | 888 | class host_create(BaseHostModCreate): | 
|  | 889 | """atest host create [--lock|--unlock --platform <arch> | 
|  | 890 | --labels <labels>|--blist <label_file> | 
|  | 891 | --acls <acls>|--alist <acl_file> | 
|  | 892 | --protection <protection_type> | 
|  | 893 | --attributes <attr>=<value>;<attr>=<value> | 
|  | 894 | --mlist <mach_file>] <hosts>""" | 
|  | 895 | usage_action = 'create' | 
|  | 896 |  | 
|  | 897 | def parse(self): | 
|  | 898 | """Option logic specific to create action. | 
|  | 899 | """ | 
|  | 900 | (options, leftovers) = super(host_create, self).parse() | 
|  | 901 | self.locked = options.lock | 
|  | 902 | if 'serials' in self.attributes: | 
|  | 903 | if len(self.hosts) > 1: | 
|  | 904 | raise topic_common.CliError('Can not specify serials with ' | 
|  | 905 | 'multiple hosts.') | 
|  | 906 |  | 
|  | 907 |  | 
|  | 908 | @classmethod | 
|  | 909 | def construct_without_parse( | 
|  | 910 | cls, web_server, hosts, platform=None, | 
|  | 911 | locked=False, lock_reason='', labels=[], acls=[], | 
|  | 912 | protection=host_protections.Protection.NO_PROTECTION): | 
|  | 913 | """Construct a host_create object and fill in data from args. | 
|  | 914 |  | 
|  | 915 | Do not need to call parse after the construction. | 
|  | 916 |  | 
|  | 917 | Return an object of site_host_create ready to execute. | 
|  | 918 |  | 
|  | 919 | @param web_server: A string specifies the autotest webserver url. | 
|  | 920 | It is needed to setup comm to make rpc. | 
|  | 921 | @param hosts: A list of hostnames as strings. | 
|  | 922 | @param platform: A string or None. | 
|  | 923 | @param locked: A boolean. | 
|  | 924 | @param lock_reason: A string. | 
|  | 925 | @param labels: A list of labels as strings. | 
|  | 926 | @param acls: A list of acls as strings. | 
|  | 927 | @param protection: An enum defined in host_protections. | 
|  | 928 | """ | 
|  | 929 | obj = cls() | 
|  | 930 | obj.web_server = web_server | 
|  | 931 | try: | 
|  | 932 | # Setup stuff needed for afe comm. | 
|  | 933 | obj.afe = rpc.afe_comm(web_server) | 
|  | 934 | except rpc.AuthError, s: | 
|  | 935 | obj.failure(str(s), fatal=True) | 
|  | 936 | obj.hosts = hosts | 
|  | 937 | obj.platform = platform | 
|  | 938 | obj.locked = locked | 
|  | 939 | if locked and lock_reason.strip(): | 
|  | 940 | obj.data['lock_reason'] = lock_reason.strip() | 
|  | 941 | obj.labels = labels | 
|  | 942 | obj.acls = acls | 
|  | 943 | if protection: | 
|  | 944 | obj.data['protection'] = protection | 
|  | 945 | obj.attributes = {} | 
|  | 946 | return obj | 
|  | 947 |  | 
|  | 948 |  | 
| Justin Giorgi | 5208eaa | 2016-07-02 20:12:12 -0700 | [diff] [blame] | 949 | def _detect_host_info(self, host): | 
|  | 950 | """Detect platform and labels from the host. | 
|  | 951 |  | 
|  | 952 | @param host: hostname | 
|  | 953 |  | 
|  | 954 | @return: HostInfo object | 
|  | 955 | """ | 
|  | 956 | # Mock an afe_host object so that the host is constructed as if the | 
|  | 957 | # data was already in afe | 
|  | 958 | data = {'attributes': self.attributes, 'labels': self.labels} | 
|  | 959 | afe_host = frontend.Host(None, data) | 
| Prathmesh Prabhu | d252d26 | 2017-05-31 11:46:34 -0700 | [diff] [blame] | 960 | store = host_info.InMemoryHostInfoStore( | 
|  | 961 | host_info.HostInfo(labels=self.labels, | 
|  | 962 | attributes=self.attributes)) | 
|  | 963 | machine = { | 
|  | 964 | 'hostname': host, | 
|  | 965 | 'afe_host': afe_host, | 
|  | 966 | 'host_info_store': store | 
|  | 967 | } | 
| Justin Giorgi | 5208eaa | 2016-07-02 20:12:12 -0700 | [diff] [blame] | 968 | try: | 
| Allen Li | 2c32d6b | 2017-02-03 15:28:10 -0800 | [diff] [blame] | 969 | if bin_utils.ping(host, tries=1, deadline=1) == 0: | 
| Justin Giorgi | 5208eaa | 2016-07-02 20:12:12 -0700 | [diff] [blame] | 970 | serials = self.attributes.get('serials', '').split(',') | 
| Richard Barnette | 9db8068 | 2018-04-26 00:55:15 +0000 | [diff] [blame] | 971 | adb_serial = self.attributes.get('serials') | 
|  | 972 | host_dut = hosts.create_host(machine, | 
|  | 973 | adb_serial=adb_serial) | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 974 |  | 
| Prathmesh Prabhu | d252d26 | 2017-05-31 11:46:34 -0700 | [diff] [blame] | 975 | info = HostInfo(host, host_dut.get_platform(), | 
|  | 976 | host_dut.get_labels()) | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 977 | # Clean host to make sure nothing left after calling it, | 
|  | 978 | # e.g. tunnels. | 
|  | 979 | if hasattr(host_dut, 'close'): | 
|  | 980 | host_dut.close() | 
| Justin Giorgi | 5208eaa | 2016-07-02 20:12:12 -0700 | [diff] [blame] | 981 | else: | 
|  | 982 | # Can't ping the host, use default information. | 
| Prathmesh Prabhu | d252d26 | 2017-05-31 11:46:34 -0700 | [diff] [blame] | 983 | info = HostInfo(host, None, []) | 
| Justin Giorgi | 5208eaa | 2016-07-02 20:12:12 -0700 | [diff] [blame] | 984 | except (socket.gaierror, error.AutoservRunError, | 
|  | 985 | error.AutoservSSHTimeout): | 
|  | 986 | # We may be adding a host that does not exist yet or we can't | 
|  | 987 | # reach due to hostname/address issues or if the host is down. | 
| Prathmesh Prabhu | d252d26 | 2017-05-31 11:46:34 -0700 | [diff] [blame] | 988 | info = HostInfo(host, None, []) | 
|  | 989 | return info | 
| Justin Giorgi | 5208eaa | 2016-07-02 20:12:12 -0700 | [diff] [blame] | 990 |  | 
|  | 991 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 992 | def _execute_add_one_host(self, host): | 
| mbligh | 719e14a | 2008-12-04 01:17:08 +0000 | [diff] [blame] | 993 | # Always add the hosts as locked to avoid the host | 
| Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 994 | # being picked up by the scheduler before it's ACL'ed. | 
| mbligh | 719e14a | 2008-12-04 01:17:08 +0000 | [diff] [blame] | 995 | self.data['locked'] = True | 
| Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 996 | if not self.locked: | 
|  | 997 | self.data['lock_reason'] = 'Forced lock on device creation' | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 998 | self.execute_rpc('add_host', hostname=host, status="Ready", **self.data) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 999 |  | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 1000 | # If there are labels avaliable for host, use them. | 
| Prathmesh Prabhu | d252d26 | 2017-05-31 11:46:34 -0700 | [diff] [blame] | 1001 | info = self._detect_host_info(host) | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 1002 | labels = set(self.labels) | 
| Prathmesh Prabhu | d252d26 | 2017-05-31 11:46:34 -0700 | [diff] [blame] | 1003 | if info.labels: | 
|  | 1004 | labels.update(info.labels) | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 1005 |  | 
|  | 1006 | if labels: | 
|  | 1007 | self._set_labels(host, list(labels)) | 
|  | 1008 |  | 
|  | 1009 | # Now add the platform label. | 
|  | 1010 | # If a platform was not provided and we were able to retrieve it | 
|  | 1011 | # from the host, use the retrieved platform. | 
| Prathmesh Prabhu | d252d26 | 2017-05-31 11:46:34 -0700 | [diff] [blame] | 1012 | platform = self.platform if self.platform else info.platform | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 1013 | if platform: | 
|  | 1014 | self._set_platform_label(host, platform) | 
|  | 1015 |  | 
|  | 1016 | if self.attributes: | 
|  | 1017 | self._set_attributes(host, self.attributes) | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 1018 |  | 
|  | 1019 |  | 
| Justin Giorgi | 5208eaa | 2016-07-02 20:12:12 -0700 | [diff] [blame] | 1020 | def execute(self): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 1021 | """Execute 'atest host create'.""" | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 1022 | successful_hosts = [] | 
|  | 1023 | for host in self.hosts: | 
|  | 1024 | try: | 
|  | 1025 | self._execute_add_one_host(host) | 
|  | 1026 | successful_hosts.append(host) | 
|  | 1027 | except topic_common.CliError: | 
|  | 1028 | pass | 
| Jiaxi Luo | c342f9f | 2014-05-19 16:22:03 -0700 | [diff] [blame] | 1029 |  | 
|  | 1030 | if successful_hosts: | 
| Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 1031 | self._set_acls(successful_hosts, self.acls) | 
| Jiaxi Luo | c342f9f | 2014-05-19 16:22:03 -0700 | [diff] [blame] | 1032 |  | 
|  | 1033 | if not self.locked: | 
|  | 1034 | for host in successful_hosts: | 
| Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 1035 | self.execute_rpc('modify_host', id=host, locked=False, | 
|  | 1036 | lock_reason='') | 
| Jiaxi Luo | c342f9f | 2014-05-19 16:22:03 -0700 | [diff] [blame] | 1037 | return successful_hosts | 
|  | 1038 |  | 
|  | 1039 |  | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 1040 | def output(self, hosts): | 
| xixuan | d6011f1 | 2016-12-08 15:01:58 -0800 | [diff] [blame] | 1041 | """Print output of 'atest host create'. | 
|  | 1042 |  | 
|  | 1043 | @param hosts: the added host list to be printed. | 
|  | 1044 | """ | 
| mbligh | be630eb | 2008-08-01 16:41:48 +0000 | [diff] [blame] | 1045 | self.print_wrapped('Added host', hosts) | 
|  | 1046 |  | 
|  | 1047 |  | 
|  | 1048 | class host_delete(action_common.atest_delete, host): | 
|  | 1049 | """atest host delete [--mlist <mach_file>] <hosts>""" | 
| Ningning Xia | c8b430d | 2018-05-17 15:10:25 -0700 | [diff] [blame] | 1050 |  | 
|  | 1051 | def __init__(self): | 
|  | 1052 | super(host_delete, self).__init__() | 
|  | 1053 |  | 
|  | 1054 | self.add_skylab_options() | 
|  | 1055 |  | 
|  | 1056 |  | 
|  | 1057 | def execute_skylab(self): | 
|  | 1058 | """Execute 'atest host delete' with '--skylab'. | 
|  | 1059 |  | 
|  | 1060 | @return A list of hostnames which have been successfully deleted. | 
|  | 1061 | """ | 
|  | 1062 | inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir) | 
|  | 1063 | inventory_repo.initialize() | 
|  | 1064 | data_dir = inventory_repo.get_data_dir() | 
|  | 1065 | lab = text_manager.load_lab(data_dir) | 
|  | 1066 |  | 
|  | 1067 | successes = [] | 
|  | 1068 | for hostname in self.hosts: | 
|  | 1069 | try: | 
|  | 1070 | device.delete( | 
|  | 1071 | lab, | 
|  | 1072 | 'duts', | 
|  | 1073 | hostname, | 
|  | 1074 | self.environment) | 
|  | 1075 | successes.append(hostname) | 
|  | 1076 | except device.SkylabDeviceActionError as e: | 
|  | 1077 | print('Cannot delete host %s: %s' % (hostname, e)) | 
|  | 1078 |  | 
|  | 1079 | if successes: | 
|  | 1080 | text_manager.dump_lab(data_dir, lab) | 
|  | 1081 | message = skylab_utils.construct_commit_message( | 
|  | 1082 | 'Delete %d hosts.\n\n%s' % (len(successes), successes)) | 
|  | 1083 | self.change_number = inventory_repo.upload_change( | 
|  | 1084 | message, draft=self.draft, dryrun=self.dryrun, | 
|  | 1085 | submit=self.submit) | 
|  | 1086 |  | 
|  | 1087 | return successes | 
|  | 1088 |  | 
|  | 1089 |  | 
|  | 1090 | def execute(self): | 
|  | 1091 | """Execute 'atest host delete'. | 
|  | 1092 |  | 
|  | 1093 | @return A list of hostnames which have been successfully deleted. | 
|  | 1094 | """ | 
|  | 1095 | if self.skylab: | 
|  | 1096 | return self.execute_skylab() | 
|  | 1097 |  | 
|  | 1098 | return super(host_delete, self).execute() | 
| Ningning Xia | c46bdd1 | 2018-05-29 11:24:14 -0700 | [diff] [blame] | 1099 |  | 
|  | 1100 |  | 
|  | 1101 | class InvalidHostnameError(Exception): | 
|  | 1102 | """Cannot perform actions on the host because of invalid hostname.""" | 
|  | 1103 |  | 
|  | 1104 |  | 
|  | 1105 | def _add_hostname_suffix(hostname, suffix): | 
|  | 1106 | """Add the suffix to the hostname.""" | 
|  | 1107 | if hostname.endswith(suffix): | 
|  | 1108 | raise InvalidHostnameError( | 
|  | 1109 | 'Cannot add "%s" as it already contains the suffix.' % suffix) | 
|  | 1110 |  | 
|  | 1111 | return hostname + suffix | 
|  | 1112 |  | 
|  | 1113 |  | 
| Gregory Nisbet | 48f1eea | 2019-08-05 16:18:56 -0700 | [diff] [blame] | 1114 | def _remove_hostname_suffix_if_present(hostname, suffix): | 
| Ningning Xia | c46bdd1 | 2018-05-29 11:24:14 -0700 | [diff] [blame] | 1115 | """Remove the suffix from the hostname.""" | 
| Gregory Nisbet | 48f1eea | 2019-08-05 16:18:56 -0700 | [diff] [blame] | 1116 | if hostname.endswith(suffix): | 
|  | 1117 | return hostname[:len(hostname) - len(suffix)] | 
|  | 1118 | else: | 
|  | 1119 | return hostname | 
| Ningning Xia | c46bdd1 | 2018-05-29 11:24:14 -0700 | [diff] [blame] | 1120 |  | 
|  | 1121 |  | 
|  | 1122 | class host_rename(host): | 
|  | 1123 | """Host rename is only for migrating hosts between skylab and AFE DB.""" | 
|  | 1124 |  | 
|  | 1125 | usage_action = 'rename' | 
|  | 1126 |  | 
|  | 1127 | def __init__(self): | 
|  | 1128 | """Add the options specific to the rename action.""" | 
|  | 1129 | super(host_rename, self).__init__() | 
|  | 1130 |  | 
|  | 1131 | self.parser.add_option('--for-migration', | 
|  | 1132 | help=('Rename hostnames for migration. Rename ' | 
|  | 1133 | 'each "hostname" to "hostname%s". ' | 
|  | 1134 | 'The original "hostname" must not contain ' | 
|  | 1135 | 'suffix.' % MIGRATED_HOST_SUFFIX), | 
|  | 1136 | action='store_true', | 
|  | 1137 | default=False) | 
|  | 1138 | self.parser.add_option('--for-rollback', | 
|  | 1139 | help=('Rename hostnames for migration rollback. ' | 
|  | 1140 | 'Rename each "hostname%s" to its original ' | 
|  | 1141 | '"hostname".' % MIGRATED_HOST_SUFFIX), | 
|  | 1142 | action='store_true', | 
|  | 1143 | default=False) | 
|  | 1144 | self.parser.add_option('--dryrun', | 
|  | 1145 | help='Execute the action as a dryrun.', | 
|  | 1146 | action='store_true', | 
|  | 1147 | default=False) | 
| Gregory Nisbet | ee457f6 | 2019-07-08 15:47:26 -0700 | [diff] [blame] | 1148 | self.parser.add_option('--non-interactive', | 
|  | 1149 | help='run non-interactively', | 
| Gregory Nisbet | 9744ea9 | 2019-07-02 12:36:59 -0700 | [diff] [blame] | 1150 | action='store_true', | 
|  | 1151 | default=False) | 
| Ningning Xia | c46bdd1 | 2018-05-29 11:24:14 -0700 | [diff] [blame] | 1152 |  | 
|  | 1153 |  | 
|  | 1154 | def parse(self): | 
|  | 1155 | """Consume the options common to host rename.""" | 
|  | 1156 | (options, leftovers) = super(host_rename, self).parse() | 
|  | 1157 | self.for_migration = options.for_migration | 
|  | 1158 | self.for_rollback = options.for_rollback | 
|  | 1159 | self.dryrun = options.dryrun | 
| Gregory Nisbet | ee457f6 | 2019-07-08 15:47:26 -0700 | [diff] [blame] | 1160 | self.interactive = not options.non_interactive | 
| Ningning Xia | c46bdd1 | 2018-05-29 11:24:14 -0700 | [diff] [blame] | 1161 | self.host_ids = {} | 
|  | 1162 |  | 
|  | 1163 | if not (self.for_migration ^ self.for_rollback): | 
|  | 1164 | self.invalid_syntax('--for-migration and --for-rollback are ' | 
|  | 1165 | 'exclusive, and one of them must be enabled.') | 
|  | 1166 |  | 
|  | 1167 | if not self.hosts: | 
|  | 1168 | self.invalid_syntax('Must provide hostname(s).') | 
|  | 1169 |  | 
|  | 1170 | if self.dryrun: | 
|  | 1171 | print('This will be a dryrun and will not rename hostnames.') | 
|  | 1172 |  | 
|  | 1173 | return (options, leftovers) | 
|  | 1174 |  | 
|  | 1175 |  | 
|  | 1176 | def execute(self): | 
|  | 1177 | """Execute 'atest host rename'.""" | 
| Gregory Nisbet | ee457f6 | 2019-07-08 15:47:26 -0700 | [diff] [blame] | 1178 | if self.interactive: | 
|  | 1179 | if self.prompt_confirmation(): | 
|  | 1180 | pass | 
|  | 1181 | else: | 
|  | 1182 | return | 
| Ningning Xia | c46bdd1 | 2018-05-29 11:24:14 -0700 | [diff] [blame] | 1183 |  | 
|  | 1184 | successes = [] | 
|  | 1185 | for host in self.execute_rpc('get_hosts', hostname__in=self.hosts): | 
|  | 1186 | self.host_ids[host['hostname']] = host['id'] | 
|  | 1187 | for host in self.hosts: | 
|  | 1188 | if host not in self.host_ids: | 
|  | 1189 | self.failure('Cannot rename non-existant host %s.' % host, | 
|  | 1190 | item=host, what_failed='Failed to rename') | 
|  | 1191 | continue | 
|  | 1192 | try: | 
| Ningning Xia | b261b16 | 2018-06-07 17:24:59 -0700 | [diff] [blame] | 1193 | host_id = self.host_ids[host] | 
| Ningning Xia | c46bdd1 | 2018-05-29 11:24:14 -0700 | [diff] [blame] | 1194 | if self.for_migration: | 
|  | 1195 | new_hostname = _add_hostname_suffix( | 
|  | 1196 | host, MIGRATED_HOST_SUFFIX) | 
|  | 1197 | else: | 
|  | 1198 | #for_rollback | 
|  | 1199 | new_hostname = _remove_hostname_suffix( | 
|  | 1200 | host, MIGRATED_HOST_SUFFIX) | 
|  | 1201 |  | 
|  | 1202 | if not self.dryrun: | 
| Ningning Xia | 423c796 | 2018-06-07 15:27:18 -0700 | [diff] [blame] | 1203 | # TODO(crbug.com/850737): delete and abort HQE. | 
| Ningning Xia | c46bdd1 | 2018-05-29 11:24:14 -0700 | [diff] [blame] | 1204 | data = {'hostname': new_hostname} | 
| Ningning Xia | b261b16 | 2018-06-07 17:24:59 -0700 | [diff] [blame] | 1205 | self.execute_rpc('modify_host', item=host, id=host_id, | 
|  | 1206 | **data) | 
| Ningning Xia | c46bdd1 | 2018-05-29 11:24:14 -0700 | [diff] [blame] | 1207 | successes.append((host, new_hostname)) | 
|  | 1208 | except InvalidHostnameError as e: | 
|  | 1209 | self.failure('Cannot rename host %s: %s' % (host, e), item=host, | 
|  | 1210 | what_failed='Failed to rename') | 
|  | 1211 | except topic_common.CliError, full_error: | 
|  | 1212 | # Already logged by execute_rpc() | 
|  | 1213 | pass | 
|  | 1214 |  | 
|  | 1215 | return successes | 
|  | 1216 |  | 
|  | 1217 |  | 
|  | 1218 | def output(self, results): | 
|  | 1219 | """Print output of 'atest host rename'.""" | 
|  | 1220 | if results: | 
|  | 1221 | print('Successfully renamed:') | 
|  | 1222 | for old_hostname, new_hostname in results: | 
|  | 1223 | print('%s to %s' % (old_hostname, new_hostname)) | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1224 |  | 
|  | 1225 |  | 
|  | 1226 | class host_migrate(action_common.atest_list, host): | 
|  | 1227 | """'atest host migrate' to migrate or rollback hosts.""" | 
|  | 1228 |  | 
|  | 1229 | usage_action = 'migrate' | 
|  | 1230 |  | 
|  | 1231 | def __init__(self): | 
|  | 1232 | super(host_migrate, self).__init__() | 
|  | 1233 |  | 
|  | 1234 | self.parser.add_option('--migration', | 
|  | 1235 | dest='migration', | 
|  | 1236 | help='Migrate the hosts to skylab.', | 
|  | 1237 | action='store_true', | 
|  | 1238 | default=False) | 
|  | 1239 | self.parser.add_option('--rollback', | 
|  | 1240 | dest='rollback', | 
|  | 1241 | help='Rollback the hosts migrated to skylab.', | 
|  | 1242 | action='store_true', | 
|  | 1243 | default=False) | 
|  | 1244 | self.parser.add_option('--model', | 
|  | 1245 | help='Model of the hosts to migrate.', | 
|  | 1246 | dest='model', | 
|  | 1247 | default=None) | 
| Xixuan Wu | 84621e5 | 2018-08-28 14:29:53 -0700 | [diff] [blame] | 1248 | self.parser.add_option('--board', | 
|  | 1249 | help='Board of the hosts to migrate.', | 
|  | 1250 | dest='board', | 
|  | 1251 | default=None) | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1252 | self.parser.add_option('--pool', | 
|  | 1253 | help=('Pool of the hosts to migrate. Must ' | 
|  | 1254 | 'specify --model for the pool.'), | 
|  | 1255 | dest='pool', | 
|  | 1256 | default=None) | 
|  | 1257 |  | 
|  | 1258 | self.add_skylab_options(enforce_skylab=True) | 
|  | 1259 |  | 
|  | 1260 |  | 
|  | 1261 | def parse(self): | 
|  | 1262 | """Consume the specific options""" | 
|  | 1263 | (options, leftover) = super(host_migrate, self).parse() | 
|  | 1264 |  | 
|  | 1265 | self.migration = options.migration | 
|  | 1266 | self.rollback = options.rollback | 
|  | 1267 | self.model = options.model | 
|  | 1268 | self.pool = options.pool | 
| Xixuan Wu | 84621e5 | 2018-08-28 14:29:53 -0700 | [diff] [blame] | 1269 | self.board = options.board | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1270 | self.host_ids = {} | 
|  | 1271 |  | 
|  | 1272 | if not (self.migration ^ self.rollback): | 
|  | 1273 | self.invalid_syntax('--migration and --rollback are exclusive, ' | 
|  | 1274 | 'and one of them must be enabled.') | 
|  | 1275 |  | 
| Xixuan Wu | 84621e5 | 2018-08-28 14:29:53 -0700 | [diff] [blame] | 1276 | if self.pool is not None and (self.model is None and | 
|  | 1277 | self.board is None): | 
|  | 1278 | self.invalid_syntax('Must provide --model or --board with --pool.') | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1279 |  | 
| Xixuan Wu | 84621e5 | 2018-08-28 14:29:53 -0700 | [diff] [blame] | 1280 | if not self.hosts and not (self.model or self.board): | 
|  | 1281 | self.invalid_syntax('Must provide hosts or --model or --board.') | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1282 |  | 
|  | 1283 | return (options, leftover) | 
|  | 1284 |  | 
|  | 1285 |  | 
| Ningning Xia | 56d6843 | 2018-06-06 17:28:20 -0700 | [diff] [blame] | 1286 | def _remove_invalid_hostnames(self, hostnames, log_failure=False): | 
|  | 1287 | """Remove hostnames with MIGRATED_HOST_SUFFIX. | 
|  | 1288 |  | 
|  | 1289 | @param hostnames: A list of hostnames. | 
|  | 1290 | @param log_failure: Bool indicating whether to log invalid hostsnames. | 
|  | 1291 |  | 
|  | 1292 | @return A list of valid hostnames. | 
|  | 1293 | """ | 
|  | 1294 | invalid_hostnames = set() | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1295 | for hostname in hostnames: | 
|  | 1296 | if hostname.endswith(MIGRATED_HOST_SUFFIX): | 
| Ningning Xia | 56d6843 | 2018-06-06 17:28:20 -0700 | [diff] [blame] | 1297 | if log_failure: | 
|  | 1298 | self.failure('Cannot migrate host with suffix "%s" %s.' % | 
|  | 1299 | (MIGRATED_HOST_SUFFIX, hostname), | 
|  | 1300 | item=hostname, what_failed='Failed to rename') | 
|  | 1301 | invalid_hostnames.add(hostname) | 
|  | 1302 |  | 
|  | 1303 | hostnames = list(set(hostnames) - invalid_hostnames) | 
|  | 1304 |  | 
|  | 1305 | return hostnames | 
|  | 1306 |  | 
|  | 1307 |  | 
|  | 1308 | def execute(self): | 
|  | 1309 | """Execute 'atest host migrate'.""" | 
|  | 1310 | hostnames = self._remove_invalid_hostnames(self.hosts, log_failure=True) | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1311 |  | 
|  | 1312 | filters = {} | 
|  | 1313 | check_results = {} | 
|  | 1314 | if hostnames: | 
|  | 1315 | check_results['hostname__in'] = 'hostname' | 
|  | 1316 | if self.migration: | 
|  | 1317 | filters['hostname__in'] = hostnames | 
|  | 1318 | else: | 
|  | 1319 | # rollback | 
|  | 1320 | hostnames_with_suffix = [ | 
|  | 1321 | _add_hostname_suffix(h, MIGRATED_HOST_SUFFIX) | 
|  | 1322 | for h in hostnames] | 
|  | 1323 | filters['hostname__in'] = hostnames_with_suffix | 
| Ningning Xia | 56d6843 | 2018-06-06 17:28:20 -0700 | [diff] [blame] | 1324 | else: | 
|  | 1325 | # TODO(nxia): add exclude_filter {'hostname__endswith': | 
|  | 1326 | # MIGRATED_HOST_SUFFIX} for --migration | 
|  | 1327 | if self.rollback: | 
|  | 1328 | filters['hostname__endswith'] = MIGRATED_HOST_SUFFIX | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1329 |  | 
|  | 1330 | labels = [] | 
|  | 1331 | if self.model: | 
|  | 1332 | labels.append('model:%s' % self.model) | 
|  | 1333 | if self.pool: | 
|  | 1334 | labels.append('pool:%s' % self.pool) | 
| Xixuan Wu | 84621e5 | 2018-08-28 14:29:53 -0700 | [diff] [blame] | 1335 | if self.board: | 
|  | 1336 | labels.append('board:%s' % self.board) | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1337 |  | 
|  | 1338 | if labels: | 
|  | 1339 | if len(labels) == 1: | 
|  | 1340 | filters['labels__name__in'] = labels | 
|  | 1341 | check_results['labels__name__in'] = None | 
|  | 1342 | else: | 
|  | 1343 | filters['multiple_labels'] = labels | 
|  | 1344 | check_results['multiple_labels'] = None | 
|  | 1345 |  | 
|  | 1346 | results = super(host_migrate, self).execute( | 
|  | 1347 | op='get_hosts', filters=filters, check_results=check_results) | 
|  | 1348 | hostnames = [h['hostname'] for h in results] | 
|  | 1349 |  | 
| Ningning Xia | 56d6843 | 2018-06-06 17:28:20 -0700 | [diff] [blame] | 1350 | if self.migration: | 
|  | 1351 | hostnames = self._remove_invalid_hostnames(hostnames) | 
|  | 1352 | else: | 
|  | 1353 | # rollback | 
|  | 1354 | hostnames = [_remove_hostname_suffix(h, MIGRATED_HOST_SUFFIX) | 
|  | 1355 | for h in hostnames] | 
|  | 1356 |  | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1357 | return self.execute_skylab_migration(hostnames) | 
|  | 1358 |  | 
|  | 1359 |  | 
| Xixuan Wu | a8d93c8 | 2018-08-03 16:23:26 -0700 | [diff] [blame] | 1360 | def assign_duts_to_drone(self, infra, devices, environment): | 
| Ningning Xia | 877817f | 2018-06-08 12:23:48 -0700 | [diff] [blame] | 1361 | """Assign uids of the devices to a random skylab drone. | 
| Ningning Xia | ffb3c1f | 2018-06-07 18:42:32 -0700 | [diff] [blame] | 1362 |  | 
|  | 1363 | @param infra: An instance of lab_pb2.Infrastructure. | 
| Xixuan Wu | a8d93c8 | 2018-08-03 16:23:26 -0700 | [diff] [blame] | 1364 | @param devices: A list of device_pb2.Device to be assigned to the drone. | 
|  | 1365 | @param environment: 'staging' or 'prod'. | 
| Ningning Xia | ffb3c1f | 2018-06-07 18:42:32 -0700 | [diff] [blame] | 1366 | """ | 
|  | 1367 | skylab_drones = skylab_server.get_servers( | 
| Xixuan Wu | a8d93c8 | 2018-08-03 16:23:26 -0700 | [diff] [blame] | 1368 | infra, environment, role='skylab_drone', status='primary') | 
| Ningning Xia | ffb3c1f | 2018-06-07 18:42:32 -0700 | [diff] [blame] | 1369 |  | 
|  | 1370 | if len(skylab_drones) == 0: | 
|  | 1371 | raise device.SkylabDeviceActionError( | 
|  | 1372 | 'No skylab drone is found in primary status and staging ' | 
|  | 1373 | 'environment. Please confirm there is at least one valid skylab' | 
|  | 1374 | ' drone added in skylab inventory.') | 
|  | 1375 |  | 
| Xixuan Wu | dfc2de2 | 2018-08-06 09:29:01 -0700 | [diff] [blame] | 1376 | for device in devices: | 
|  | 1377 | # Randomly distribute each device to a skylab_drone. | 
|  | 1378 | skylab_drone = random.choice(skylab_drones) | 
|  | 1379 | skylab_server.add_dut_uids(skylab_drone, [device]) | 
| Ningning Xia | 877817f | 2018-06-08 12:23:48 -0700 | [diff] [blame] | 1380 |  | 
|  | 1381 |  | 
|  | 1382 | def remove_duts_from_drone(self, infra, devices): | 
|  | 1383 | """Remove uids of the devices from their skylab drones. | 
|  | 1384 |  | 
|  | 1385 | @param infra: An instance of lab_pb2.Infrastructure. | 
|  | 1386 | @devices: A list of device_pb2.Device to be remove from the drone. | 
|  | 1387 | """ | 
|  | 1388 | skylab_drones = skylab_server.get_servers( | 
|  | 1389 | infra, 'staging', role='skylab_drone', status='primary') | 
|  | 1390 |  | 
|  | 1391 | for skylab_drone in skylab_drones: | 
|  | 1392 | skylab_server.remove_dut_uids(skylab_drone, devices) | 
| Ningning Xia | ffb3c1f | 2018-06-07 18:42:32 -0700 | [diff] [blame] | 1393 |  | 
|  | 1394 |  | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1395 | def execute_skylab_migration(self, hostnames): | 
|  | 1396 | """Execute migration in skylab_inventory. | 
|  | 1397 |  | 
|  | 1398 | @param hostnames: A list of hostnames to migrate. | 
|  | 1399 | @return If there're hosts to migrate, return a list of the hostnames and | 
|  | 1400 | a message instructing actions after the migration; else return | 
|  | 1401 | None. | 
|  | 1402 | """ | 
|  | 1403 | if not hostnames: | 
|  | 1404 | return | 
|  | 1405 |  | 
|  | 1406 | inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir) | 
|  | 1407 | inventory_repo.initialize() | 
|  | 1408 |  | 
|  | 1409 | subdirs = ['skylab', 'prod', 'staging'] | 
|  | 1410 | data_dirs = skylab_data_dir, prod_data_dir, staging_data_dir = [ | 
|  | 1411 | inventory_repo.get_data_dir(data_subdir=d) for d in subdirs] | 
|  | 1412 | skylab_lab, prod_lab, staging_lab = [ | 
|  | 1413 | text_manager.load_lab(d) for d in data_dirs] | 
| Ningning Xia | ffb3c1f | 2018-06-07 18:42:32 -0700 | [diff] [blame] | 1414 | infra = text_manager.load_infrastructure(skylab_data_dir) | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1415 |  | 
|  | 1416 | label_map = None | 
|  | 1417 | labels = [] | 
| Xixuan Wu | 46f8e20 | 2018-12-13 14:40:58 -0800 | [diff] [blame] | 1418 | if self.board: | 
|  | 1419 | labels.append('board:%s' % self.board) | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1420 | if self.model: | 
| Xixuan Wu | 46f8e20 | 2018-12-13 14:40:58 -0800 | [diff] [blame] | 1421 | labels.append('model:%s' % self.model) | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1422 | if self.pool: | 
|  | 1423 | labels.append('critical_pool:%s' % self.pool) | 
|  | 1424 | if labels: | 
|  | 1425 | label_map = device.convert_to_label_map(labels) | 
|  | 1426 |  | 
|  | 1427 | if self.migration: | 
|  | 1428 | prod_devices = device.move_devices( | 
|  | 1429 | prod_lab, skylab_lab, 'duts', label_map=label_map, | 
|  | 1430 | hostnames=hostnames) | 
|  | 1431 | staging_devices = device.move_devices( | 
|  | 1432 | staging_lab, skylab_lab, 'duts', label_map=label_map, | 
|  | 1433 | hostnames=hostnames) | 
|  | 1434 |  | 
|  | 1435 | all_devices = prod_devices + staging_devices | 
|  | 1436 | # Hostnames in afe_hosts tabel. | 
|  | 1437 | device_hostnames = [str(d.common.hostname) for d in all_devices] | 
|  | 1438 | message = ( | 
|  | 1439 | 'Migration: move %s hosts into skylab_inventory.\n\n' | 
| Ningning Xia | 423c796 | 2018-06-07 15:27:18 -0700 | [diff] [blame] | 1440 | 'Please run this command after the CL is submitted:\n' | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1441 | 'atest host rename --for-migration %s' % | 
|  | 1442 | (len(all_devices), ' '.join(device_hostnames))) | 
| Ningning Xia | ffb3c1f | 2018-06-07 18:42:32 -0700 | [diff] [blame] | 1443 |  | 
| Xixuan Wu | a8d93c8 | 2018-08-03 16:23:26 -0700 | [diff] [blame] | 1444 | self.assign_duts_to_drone(infra, prod_devices, 'prod') | 
|  | 1445 | self.assign_duts_to_drone(infra, staging_devices, 'staging') | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1446 | else: | 
|  | 1447 | # rollback | 
|  | 1448 | prod_devices = device.move_devices( | 
|  | 1449 | skylab_lab, prod_lab, 'duts', environment='prod', | 
| Ningning Xia | 56d6843 | 2018-06-06 17:28:20 -0700 | [diff] [blame] | 1450 | label_map=label_map, hostnames=hostnames) | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1451 | staging_devices = device.move_devices( | 
| Ningning Xia | 877817f | 2018-06-08 12:23:48 -0700 | [diff] [blame] | 1452 | skylab_lab, staging_lab, 'duts', environment='staging', | 
| Ningning Xia | 56d6843 | 2018-06-06 17:28:20 -0700 | [diff] [blame] | 1453 | label_map=label_map, hostnames=hostnames) | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1454 |  | 
|  | 1455 | all_devices = prod_devices + staging_devices | 
|  | 1456 | # Hostnames in afe_hosts tabel. | 
|  | 1457 | device_hostnames = [_add_hostname_suffix(str(d.common.hostname), | 
|  | 1458 | MIGRATED_HOST_SUFFIX) | 
|  | 1459 | for d in all_devices] | 
|  | 1460 | message = ( | 
|  | 1461 | 'Rollback: remove %s hosts from skylab_inventory.\n\n' | 
| Ningning Xia | 423c796 | 2018-06-07 15:27:18 -0700 | [diff] [blame] | 1462 | 'Please run this command after the CL is submitted:\n' | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1463 | 'atest host rename --for-rollback %s' % | 
|  | 1464 | (len(all_devices), ' '.join(device_hostnames))) | 
|  | 1465 |  | 
| Ningning Xia | 877817f | 2018-06-08 12:23:48 -0700 | [diff] [blame] | 1466 | self.remove_duts_from_drone(infra, all_devices) | 
|  | 1467 |  | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1468 | if all_devices: | 
| Ningning Xia | ffb3c1f | 2018-06-07 18:42:32 -0700 | [diff] [blame] | 1469 | text_manager.dump_infrastructure(skylab_data_dir, infra) | 
|  | 1470 |  | 
| Ningning Xia | 9df3d15 | 2018-05-23 17:15:14 -0700 | [diff] [blame] | 1471 | if prod_devices: | 
|  | 1472 | text_manager.dump_lab(prod_data_dir, prod_lab) | 
|  | 1473 |  | 
|  | 1474 | if staging_devices: | 
|  | 1475 | text_manager.dump_lab(staging_data_dir, staging_lab) | 
|  | 1476 |  | 
|  | 1477 | text_manager.dump_lab(skylab_data_dir, skylab_lab) | 
|  | 1478 |  | 
|  | 1479 | self.change_number = inventory_repo.upload_change( | 
|  | 1480 | message, draft=self.draft, dryrun=self.dryrun, | 
|  | 1481 | submit=self.submit) | 
|  | 1482 |  | 
|  | 1483 | return all_devices, message | 
|  | 1484 |  | 
|  | 1485 |  | 
|  | 1486 | def output(self, result): | 
|  | 1487 | """Print output of 'atest host list'. | 
|  | 1488 |  | 
|  | 1489 | @param result: the result to be printed. | 
|  | 1490 | """ | 
|  | 1491 | if result: | 
|  | 1492 | devices, message = result | 
|  | 1493 |  | 
|  | 1494 | if devices: | 
|  | 1495 | hostnames = [h.common.hostname for h in devices] | 
|  | 1496 | if self.migration: | 
|  | 1497 | print('Migrating hosts: %s' % ','.join(hostnames)) | 
|  | 1498 | else: | 
|  | 1499 | # rollback | 
|  | 1500 | print('Rolling back hosts: %s' % ','.join(hostnames)) | 
|  | 1501 |  | 
|  | 1502 | if not self.dryrun: | 
|  | 1503 | if not self.submit: | 
|  | 1504 | print(skylab_utils.get_cl_message(self.change_number)) | 
|  | 1505 | else: | 
|  | 1506 | # Print the instruction command for renaming hosts. | 
|  | 1507 | print('%s' % message) | 
|  | 1508 | else: | 
|  | 1509 | print('No hosts were migrated.') | 
| Gregory Nisbet | c4d63ca | 2019-08-08 10:46:40 -0700 | [diff] [blame] | 1510 |  | 
|  | 1511 |  | 
|  | 1512 | class host_skylab_migrate(action_common.atest_list, host): | 
|  | 1513 | usage_action = 'skylab_migrate' | 
|  | 1514 |  | 
|  | 1515 | def __init__(self): | 
|  | 1516 | super(host_skylab_migrate, self).__init__() | 
|  | 1517 | self.parser.add_option('--dry-run', | 
|  | 1518 | help='Dry run. Show only candidate hosts.', | 
|  | 1519 | action='store_true', | 
|  | 1520 | dest='dry_run') | 
|  | 1521 | self.parser.add_option('--ratio', | 
|  | 1522 | help='ratio of hosts to migrate as number from 0 to 1.', | 
|  | 1523 | type=float, | 
|  | 1524 | dest='ratio', | 
|  | 1525 | default=1) | 
|  | 1526 | self.parser.add_option('--bug-number', | 
|  | 1527 | help='bug number for tracking purposes.', | 
|  | 1528 | dest='bug_number', | 
|  | 1529 | default=None) | 
|  | 1530 | self.parser.add_option('--board', | 
|  | 1531 | help='Board of the hosts to migrate', | 
|  | 1532 | dest='board', | 
|  | 1533 | default=None) | 
|  | 1534 | self.parser.add_option('--model', | 
|  | 1535 | help='Model of the hosts to migrate', | 
|  | 1536 | dest='model', | 
|  | 1537 | default=None) | 
|  | 1538 | self.parser.add_option('--pool', | 
|  | 1539 | help='Pool of the hosts to migrate', | 
|  | 1540 | dest='pool', | 
|  | 1541 | default=None) | 
|  | 1542 |  | 
|  | 1543 | def parse(self): | 
|  | 1544 | (options, leftover) = super(host_skylab_migrate, self).parse() | 
|  | 1545 | self.dry_run = options.dry_run | 
|  | 1546 | self.ratio = options.ratio | 
|  | 1547 | self.bug_number = options.bug_number | 
|  | 1548 | self.model = options.model | 
|  | 1549 | self.pool = options.pool | 
|  | 1550 | self.board = options.board | 
|  | 1551 | self._reason = "migration to skylab: %s" % self.bug_number | 
|  | 1552 | return (options, leftover) | 
|  | 1553 |  | 
|  | 1554 |  | 
|  | 1555 | def _host_skylab_migrate_get_hostnames(self, model=None, pool=None, board=None): | 
|  | 1556 | """ | 
|  | 1557 | @params : in 'model', 'pool', 'board' | 
|  | 1558 |  | 
|  | 1559 | """ | 
|  | 1560 | # TODO(gregorynisbet) | 
|  | 1561 | # this just gets all the hostnames, it doesn't filter by | 
|  | 1562 | # presence or absence of migrated-do-not-use. | 
|  | 1563 | labels = [] | 
|  | 1564 | for key, value in {'model': model, 'board': board, 'pool': pool}: | 
|  | 1565 | if value: | 
|  | 1566 | labels.append(key + ":" + value) | 
|  | 1567 | filters = {} | 
|  | 1568 | check_results = {} | 
|  | 1569 | # Copy the filter and check_results initialization logic from | 
|  | 1570 | # the 'execute' method of the class 'host_migrate'. | 
|  | 1571 | if not labels: | 
|  | 1572 | return [] | 
|  | 1573 | elif len(labels) == 1: | 
|  | 1574 | filters['labels__name__in'] = labels | 
|  | 1575 | check_results['labels__name__in'] = None | 
|  | 1576 | elif len(labels) > 1: | 
|  | 1577 | filters['multiple_labels'] = labels | 
|  | 1578 | check_results['multiple_labels'] = None | 
|  | 1579 | else: | 
|  | 1580 | assert False | 
|  | 1581 |  | 
|  | 1582 | results = super(host_skylab_migrate, self).execute( | 
|  | 1583 | op='get_hosts', filters=filters, check_results=check_results) | 
|  | 1584 | return [result['hostname'] for result in results] | 
|  | 1585 |  | 
|  | 1586 |  | 
|  | 1587 | def _validate_one_hostname_source(self): | 
|  | 1588 | """Validate that hostname source is explicit hostnames or valid query. | 
|  | 1589 |  | 
|  | 1590 | Hostnames must either be provided explicitly or be the result of a | 
|  | 1591 | query defined by 'model', 'board', and 'pool'. | 
|  | 1592 |  | 
|  | 1593 | @returns : whether the hostnames come from exactly one valid source. | 
|  | 1594 | """ | 
|  | 1595 | has_criteria = any([(self.model and self.board), self.board, self.pool]) | 
|  | 1596 | has_command_line_hosts = bool(self.hosts) | 
|  | 1597 | if has_criteria != has_command_line_hosts: | 
|  | 1598 | # all good, one data source | 
|  | 1599 | return True | 
|  | 1600 | if has_criteria and has_command_line_hosts: | 
|  | 1601 | self.failure( | 
|  | 1602 | '--model/host/board and explicit hostnames are alternatives. Provide exactly one.', | 
|  | 1603 | item='cli', | 
|  | 1604 | what_failed='user') | 
|  | 1605 | return False | 
|  | 1606 | self.failure( | 
|  | 1607 | 'no explicit hosts and no criteria provided.', | 
|  | 1608 | item='cli', | 
|  | 1609 | what_failed='user') | 
|  | 1610 | return False | 
|  | 1611 |  | 
|  | 1612 |  | 
|  | 1613 | def execute(self): | 
|  | 1614 | if not self._validate_one_hostname_source(): | 
|  | 1615 | return None | 
|  | 1616 | if self.hosts: | 
|  | 1617 | hostnames = self.hosts | 
|  | 1618 | else: | 
|  | 1619 | hostnames = self.__get_hostnames( | 
|  | 1620 | model=self.model, | 
|  | 1621 | board=self.board, | 
|  | 1622 | pool=self.pool, | 
|  | 1623 | ) | 
|  | 1624 | if self.dry_run: | 
|  | 1625 | return hostnames | 
|  | 1626 | if not hostnames: | 
|  | 1627 | return {'error': 'no hosts to migrate'} | 
|  | 1628 | res = skylab_migration.migrate( | 
|  | 1629 | ratio=self.ratio, | 
|  | 1630 | reason=self._reason, | 
|  | 1631 | hostnames=hostnames, | 
|  | 1632 | max_duration=10 * 60, | 
|  | 1633 | interval_len=2, | 
|  | 1634 | min_ready_intervals=10, | 
|  | 1635 | immediately=True, | 
|  | 1636 | ) | 
|  | 1637 | return res | 
|  | 1638 |  | 
|  | 1639 |  | 
|  | 1640 | def output(self, result): | 
|  | 1641 | if result is not None: | 
|  | 1642 | print json.dumps(result, indent=4, sort_keys=True) |