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