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