1# Copyright 2014 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5""" 6The server module contains the objects and methods used to manage servers in 7Autotest. 8 9The valid actions are: 10list: list all servers in the database 11create: create a server 12delete: deletes a server 13modify: modify a server's role or status. 14 15The common options are: 16--role / -r: role that's related to server actions. 17 18See topic_common.py for a High Level Design and Algorithm. 19""" 20 21from __future__ import print_function 22 23import common 24 25from autotest_lib.cli import action_common 26from autotest_lib.cli import skylab_utils 27from autotest_lib.cli import topic_common 28from autotest_lib.client.common_lib import error 29from autotest_lib.client.common_lib import revision_control 30# The django setup is moved here as test_that uses sqlite setup. If this line 31# is in server_manager, test_that unittest will fail. 32from autotest_lib.frontend import setup_django_environment 33 34try: 35 from skylab_inventory import text_manager 36 from skylab_inventory import translation_utils 37 from skylab_inventory.lib import server as skylab_server 38except ImportError: 39 pass 40 41 42ATEST_DISABLE_MSG = ('Updating server_db via atest server command has been ' 43 'disabled. Please use use go/cros-infra-inventory-tool ' 44 'to update it in skylab inventory service.') 45 46 47class server(topic_common.atest): 48 """Server class 49 50 atest server [list|create|delete|modify] <options> 51 """ 52 usage_action = '[list|create|delete|modify]' 53 topic = msg_topic = 'server' 54 msg_items = '<server>' 55 56 def __init__(self, hostname_required=True, allow_multiple_hostname=False): 57 """Add to the parser the options common to all the server actions. 58 59 @param hostname_required: True to require the command has hostname 60 specified. Default is True. 61 """ 62 super(server, self).__init__() 63 64 self.parser.add_option('-r', '--role', 65 help='Name of a role', 66 type='string', 67 default=None, 68 metavar='ROLE') 69 self.parser.add_option('-x', '--action', 70 help=('Set to True to apply actions when role ' 71 'or status is changed, e.g., restart ' 72 'scheduler when a drone is removed. %s' % 73 skylab_utils.MSG_INVALID_IN_SKYLAB), 74 action='store_true', 75 default=False, 76 metavar='ACTION') 77 78 self.add_skylab_options(enforce_skylab=True) 79 80 self.topic_parse_info = topic_common.item_parse_info( 81 attribute_name='hostname', use_leftover=True) 82 83 self.hostname_required = hostname_required 84 self.allow_multiple_hostname = allow_multiple_hostname 85 86 87 def parse(self): 88 """Parse command arguments. 89 """ 90 role_info = topic_common.item_parse_info(attribute_name='role') 91 kwargs = {} 92 if self.hostname_required: 93 kwargs['req_items'] = 'hostname' 94 (options, leftover) = super(server, self).parse([role_info], **kwargs) 95 if options.web_server: 96 self.invalid_syntax('Server actions will access server database ' 97 'defined in your local global config. It does ' 98 'not rely on RPC, no autotest server needs to ' 99 'be specified.') 100 101 # self.hostname is a list. Action on server only needs one hostname at 102 # most. 103 if (not self.hostname and self.hostname_required): 104 self.invalid_syntax('`server` topic requires hostname. ' 105 'Use -h to see available options.') 106 107 if (self.hostname_required and not self.allow_multiple_hostname and 108 len(self.hostname) > 1): 109 self.invalid_syntax('`server` topic can only manipulate 1 server. ' 110 'Use -h to see available options.') 111 112 if self.hostname: 113 if not self.allow_multiple_hostname or not self.skylab: 114 # Only support create multiple servers in skylab. 115 # Override self.hostname with the first hostname in the list. 116 self.hostname = self.hostname[0] 117 118 self.role = options.role 119 120 if self.skylab and self.role: 121 translation_utils.validate_server_role(self.role) 122 123 return (options, leftover) 124 125 126 def output(self, results): 127 """Display output. 128 129 For most actions, the return is a string message, no formating needed. 130 131 @param results: return of the execute call. 132 """ 133 print(results) 134 135 136class server_help(server): 137 """Just here to get the atest logic working. Usage is set by its parent. 138 """ 139 pass 140 141 142class server_list(action_common.atest_list, server): 143 """atest server list [--role <role>]""" 144 145 def __init__(self): 146 """Initializer. 147 """ 148 super(server_list, self).__init__(hostname_required=False) 149 150 self.parser.add_option('-s', '--status', 151 help='Only show servers with given status.', 152 type='string', 153 default=None, 154 metavar='STATUS') 155 self.parser.add_option('--json', 156 help=('Format output as JSON.'), 157 action='store_true', 158 default=False) 159 self.parser.add_option('-N', '--hostnames-only', 160 help=('Only return hostnames.'), 161 action='store_true', 162 default=False) 163 # TODO(crbug.com/850344): support '--table' and '--summary' formats. 164 165 166 def parse(self): 167 """Parse command arguments. 168 """ 169 (options, leftover) = super(server_list, self).parse() 170 self.json = options.json 171 self.status = options.status 172 self.namesonly = options.hostnames_only 173 174 if sum([self.json, self.namesonly]) > 1: 175 self.invalid_syntax('May only specify up to 1 output-format flag.') 176 return (options, leftover) 177 178 179 def execute_skylab(self): 180 """Execute 'atest server list --skylab' 181 182 @return: A list of servers matched the given hostname and role. 183 """ 184 inventory_repo = skylab_utils.InventoryRepo( 185 self.inventory_repo_dir) 186 inventory_repo.initialize() 187 infrastructure = text_manager.load_infrastructure( 188 inventory_repo.get_data_dir()) 189 190 return skylab_server.get_servers( 191 infrastructure, 192 self.environment, 193 hostname=self.hostname, 194 role=self.role, 195 status=self.status) 196 197 198 def execute(self): 199 """Execute the command. 200 201 @return: A list of servers matched given hostname and role. 202 """ 203 if self.skylab: 204 try: 205 return self.execute_skylab() 206 except (skylab_server.SkylabServerActionError, 207 revision_control.GitError, 208 skylab_utils.InventoryRepoDirNotClean) as e: 209 self.failure(e, what_failed='Failed to list servers from skylab' 210 ' inventory.', item=self.hostname, fatal=True) 211 else: 212 return None 213 214 215 def output(self, results): 216 """Display output. 217 218 @param results: return of the execute call, a list of server object that 219 contains server information. 220 """ 221 if results: 222 if self.json: 223 if self.skylab: 224 formatter = skylab_server.format_servers_json 225 else: 226 return None 227 elif self.namesonly: 228 return None 229 else: 230 return None 231 print(formatter(results)) 232 else: 233 self.failure('No server is found.', 234 what_failed='Failed to find servers', 235 item=self.hostname, fatal=True) 236 237 238class server_create(server): 239 """atest server create hostname --role <role> --note <note> 240 """ 241 242 def __init__(self): 243 """Initializer. 244 """ 245 super(server_create, self).__init__(allow_multiple_hostname=True) 246 self.parser.add_option('-n', '--note', 247 help='note of the server', 248 type='string', 249 default=None, 250 metavar='NOTE') 251 252 253 def parse(self): 254 """Parse command arguments. 255 """ 256 (options, leftover) = super(server_create, self).parse() 257 self.note = options.note 258 259 if not self.role: 260 self.invalid_syntax('--role is required to create a server.') 261 262 return (options, leftover) 263 264 265 def execute_skylab(self): 266 """Execute the command for skylab inventory changes.""" 267 inventory_repo = skylab_utils.InventoryRepo( 268 self.inventory_repo_dir) 269 inventory_repo.initialize() 270 data_dir = inventory_repo.get_data_dir() 271 infrastructure = text_manager.load_infrastructure(data_dir) 272 273 new_servers = [] 274 for hostname in self.hostname: 275 new_servers.append(skylab_server.create( 276 infrastructure, 277 hostname, 278 self.environment, 279 role=self.role, 280 note=self.note)) 281 text_manager.dump_infrastructure(data_dir, infrastructure) 282 283 message = skylab_utils.construct_commit_message( 284 'Add new server: %s' % self.hostname) 285 self.change_number = inventory_repo.upload_change( 286 message, draft=self.draft, dryrun=self.dryrun, 287 submit=self.submit) 288 289 return new_servers 290 291 292 def execute(self): 293 """Execute the command. 294 295 @return: A Server object if it is created successfully. 296 """ 297 self.failure(ATEST_DISABLE_MSG, 298 what_failed='Failed to create server', 299 item=self.hostname, 300 fatal=True) 301 302 303 def output(self, results): 304 """Display output. 305 306 @param results: return of the execute call, a server object that 307 contains server information. 308 """ 309 if results: 310 print('Server %s is added.\n' % self.hostname) 311 print(results) 312 313 if self.skylab and not self.dryrun and not self.submit: 314 print(skylab_utils.get_cl_message(self.change_number)) 315 316 317 318class server_delete(server): 319 """atest server delete hostname""" 320 321 def execute_skylab(self): 322 """Execute the command for skylab inventory changes.""" 323 inventory_repo = skylab_utils.InventoryRepo( 324 self.inventory_repo_dir) 325 inventory_repo.initialize() 326 data_dir = inventory_repo.get_data_dir() 327 infrastructure = text_manager.load_infrastructure(data_dir) 328 329 skylab_server.delete(infrastructure, self.hostname, self.environment) 330 text_manager.dump_infrastructure(data_dir, infrastructure) 331 332 message = skylab_utils.construct_commit_message( 333 'Delete server: %s' % self.hostname) 334 self.change_number = inventory_repo.upload_change( 335 message, draft=self.draft, dryrun=self.dryrun, 336 submit=self.submit) 337 338 339 def execute(self): 340 """Execute the command. 341 342 @return: True if server is deleted successfully. 343 """ 344 self.failure(ATEST_DISABLE_MSG, 345 what_failed='Failed to delete server', 346 item=self.hostname, 347 fatal=True) 348 349 350 def output(self, results): 351 """Display output. 352 353 @param results: return of the execute call. 354 """ 355 if results: 356 print('Server %s is deleted.\n' % 357 self.hostname) 358 359 if self.skylab and not self.dryrun and not self.submit: 360 print(skylab_utils.get_cl_message(self.change_number)) 361 362 363 364class server_modify(server): 365 """atest server modify hostname 366 367 modify action can only change one input at a time. Available inputs are: 368 --status: Status of the server. 369 --note: Note of the server. 370 --role: New role to be added to the server. 371 --delete_role: Existing role to be deleted from the server. 372 """ 373 374 def __init__(self): 375 """Initializer. 376 """ 377 super(server_modify, self).__init__() 378 self.parser.add_option('-s', '--status', 379 help='Status of the server', 380 type='string', 381 metavar='STATUS') 382 self.parser.add_option('-n', '--note', 383 help='Note of the server', 384 type='string', 385 default=None, 386 metavar='NOTE') 387 self.parser.add_option('-d', '--delete', 388 help=('Set to True to delete given role.'), 389 action='store_true', 390 default=False, 391 metavar='DELETE') 392 self.parser.add_option('-a', '--attribute', 393 help='Name of the attribute of the server', 394 type='string', 395 default=None, 396 metavar='ATTRIBUTE') 397 self.parser.add_option('-e', '--value', 398 help='Value for the attribute of the server', 399 type='string', 400 default=None, 401 metavar='VALUE') 402 403 404 def parse(self): 405 """Parse command arguments. 406 """ 407 (options, leftover) = super(server_modify, self).parse() 408 self.status = options.status 409 self.note = options.note 410 self.delete = options.delete 411 self.attribute = options.attribute 412 self.value = options.value 413 self.action = options.action 414 415 # modify supports various options. However, it's safer to limit one 416 # option at a time so no complicated role-dependent logic is needed 417 # to handle scenario that both role and status are changed. 418 # self.parser is optparse, which does not have function in argparse like 419 # add_mutually_exclusive_group. That's why the count is used here. 420 flags = [self.status is not None, self.role is not None, 421 self.attribute is not None, self.note is not None] 422 if flags.count(True) != 1: 423 msg = ('Action modify only support one option at a time. You can ' 424 'try one of following 5 options:\n' 425 '1. --status: Change server\'s status.\n' 426 '2. --note: Change server\'s note.\n' 427 '3. --role with optional -d: Add/delete role from server.\n' 428 '4. --attribute --value: Set/change the value of a ' 429 'server\'s attribute.\n' 430 '5. --attribute -d: Delete the attribute from the ' 431 'server.\n' 432 '\nUse option -h to see a complete list of options.') 433 self.invalid_syntax(msg) 434 if (self.status != None or self.note != None) and self.delete: 435 self.invalid_syntax('--delete does not apply to status or note.') 436 if self.attribute != None and not self.delete and self.value == None: 437 self.invalid_syntax('--attribute must be used with option --value ' 438 'or --delete.') 439 440 # TODO(nxia): crbug.com/832964 support --action with --skylab 441 if self.skylab and self.action: 442 self.invalid_syntax('--action is currently not supported with' 443 ' --skylab.') 444 445 return (options, leftover) 446 447 448 def execute_skylab(self): 449 """Execute the command for skylab inventory changes.""" 450 inventory_repo = skylab_utils.InventoryRepo( 451 self.inventory_repo_dir) 452 inventory_repo.initialize() 453 data_dir = inventory_repo.get_data_dir() 454 infrastructure = text_manager.load_infrastructure(data_dir) 455 456 target_server = skylab_server.modify( 457 infrastructure, 458 self.hostname, 459 self.environment, 460 role=self.role, 461 status=self.status, 462 delete_role=self.delete, 463 note=self.note, 464 attribute=self.attribute, 465 value=self.value, 466 delete_attribute=self.delete) 467 text_manager.dump_infrastructure(data_dir, infrastructure) 468 469 status = inventory_repo.git_repo.status() 470 if not status: 471 print('Nothing is changed for server %s.' % self.hostname) 472 return 473 474 message = skylab_utils.construct_commit_message( 475 'Modify server: %s' % self.hostname) 476 self.change_number = inventory_repo.upload_change( 477 message, draft=self.draft, dryrun=self.dryrun, 478 submit=self.submit) 479 480 return target_server 481 482 483 def execute(self): 484 """Execute the command. 485 486 @return: The updated server object if it is modified successfully. 487 """ 488 self.failure(ATEST_DISABLE_MSG, 489 what_failed='Failed to modify server', 490 item=self.hostname, 491 fatal=True) 492 493 494 def output(self, results): 495 """Display output. 496 497 @param results: return of the execute call, which is the updated server 498 object. 499 """ 500 if results: 501 print('Server %s is modified.\n' % self.hostname) 502 print(results) 503 504 if self.skylab and not self.dryrun and not self.submit: 505 print(skylab_utils.get_cl_message(self.change_number)) 506