# Copyright 2014 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """ The server module contains the objects and methods used to manage servers in Autotest. The valid actions are: list: list all servers in the database create: create a server delete: deletes a server modify: modify a server's role or status. The common options are: --role / -r: role that's related to server actions. See topic_common.py for a High Level Design and Algorithm. """ from __future__ import print_function import common from autotest_lib.cli import action_common from autotest_lib.cli import skylab_utils from autotest_lib.cli import topic_common from autotest_lib.client.common_lib import error from autotest_lib.client.common_lib import global_config from autotest_lib.client.common_lib import revision_control # The django setup is moved here as test_that uses sqlite setup. If this line # is in server_manager, test_that unittest will fail. from autotest_lib.frontend import setup_django_environment from autotest_lib.site_utils import server_manager from autotest_lib.site_utils import server_manager_utils from chromite.lib import gob_util try: from skylab_inventory import text_manager from skylab_inventory import translation_utils from skylab_inventory.lib import server as skylab_server except ImportError: pass RESPECT_SKYLAB_SERVERDB = global_config.global_config.get_config_value( 'SKYLAB', 'respect_skylab_serverdb', type=bool, default=False) ATEST_DISABLE_MSG = ('Updating server_db via atest server command has been ' 'disabled. Please use use go/cros-infra-inventory-tool ' 'to update it in skylab inventory service.') class server(topic_common.atest): """Server class atest server [list|create|delete|modify] """ usage_action = '[list|create|delete|modify]' topic = msg_topic = 'server' msg_items = '' def __init__(self, hostname_required=True, allow_multiple_hostname=False): """Add to the parser the options common to all the server actions. @param hostname_required: True to require the command has hostname specified. Default is True. """ super(server, self).__init__() self.parser.add_option('-r', '--role', help='Name of a role', type='string', default=None, metavar='ROLE') self.parser.add_option('-x', '--action', help=('Set to True to apply actions when role ' 'or status is changed, e.g., restart ' 'scheduler when a drone is removed. %s' % skylab_utils.MSG_INVALID_IN_SKYLAB), action='store_true', default=False, metavar='ACTION') self.add_skylab_options(enforce_skylab=True) self.topic_parse_info = topic_common.item_parse_info( attribute_name='hostname', use_leftover=True) self.hostname_required = hostname_required self.allow_multiple_hostname = allow_multiple_hostname def parse(self): """Parse command arguments. """ role_info = topic_common.item_parse_info(attribute_name='role') kwargs = {} if self.hostname_required: kwargs['req_items'] = 'hostname' (options, leftover) = super(server, self).parse([role_info], **kwargs) if options.web_server: self.invalid_syntax('Server actions will access server database ' 'defined in your local global config. It does ' 'not rely on RPC, no autotest server needs to ' 'be specified.') # self.hostname is a list. Action on server only needs one hostname at # most. if (not self.hostname and self.hostname_required): self.invalid_syntax('`server` topic requires hostname. ' 'Use -h to see available options.') if (self.hostname_required and not self.allow_multiple_hostname and len(self.hostname) > 1): self.invalid_syntax('`server` topic can only manipulate 1 server. ' 'Use -h to see available options.') if self.hostname: if not self.allow_multiple_hostname or not self.skylab: # Only support create multiple servers in skylab. # Override self.hostname with the first hostname in the list. self.hostname = self.hostname[0] self.role = options.role if self.skylab and self.role: translation_utils.validate_server_role(self.role) return (options, leftover) def output(self, results): """Display output. For most actions, the return is a string message, no formating needed. @param results: return of the execute call. """ print(results) class server_help(server): """Just here to get the atest logic working. Usage is set by its parent. """ pass class server_list(action_common.atest_list, server): """atest server list [--role ]""" def __init__(self): """Initializer. """ super(server_list, self).__init__(hostname_required=False) self.parser.add_option('-s', '--status', help='Only show servers with given status.', type='string', default=None, metavar='STATUS') self.parser.add_option('--json', help=('Format output as JSON.'), action='store_true', default=False) self.parser.add_option('-N', '--hostnames-only', help=('Only return hostnames.'), action='store_true', default=False) # TODO(crbug.com/850344): support '--table' and '--summary' formats. def parse(self): """Parse command arguments. """ (options, leftover) = super(server_list, self).parse() self.json = options.json self.status = options.status self.namesonly = options.hostnames_only if sum([self.json, self.namesonly]) > 1: self.invalid_syntax('May only specify up to 1 output-format flag.') return (options, leftover) def execute_skylab(self): """Execute 'atest server list --skylab' @return: A list of servers matched the given hostname and role. """ inventory_repo = skylab_utils.InventoryRepo( self.inventory_repo_dir) inventory_repo.initialize() infrastructure = text_manager.load_infrastructure( inventory_repo.get_data_dir()) return skylab_server.get_servers( infrastructure, self.environment, hostname=self.hostname, role=self.role, status=self.status) def execute(self): """Execute the command. @return: A list of servers matched given hostname and role. """ if self.skylab: try: return self.execute_skylab() except (skylab_server.SkylabServerActionError, revision_control.GitError, skylab_utils.InventoryRepoDirNotClean) as e: self.failure(e, what_failed='Failed to list servers from skylab' ' inventory.', item=self.hostname, fatal=True) else: try: return server_manager_utils.get_servers( hostname=self.hostname, role=self.role, status=self.status) except (server_manager_utils.ServerActionError, error.InvalidDataError) as e: self.failure(e, what_failed='Failed to find servers', item=self.hostname, fatal=True) def output(self, results): """Display output. @param results: return of the execute call, a list of server object that contains server information. """ if results: if self.json: if self.skylab: formatter = skylab_server.format_servers_json else: formatter = server_manager_utils.format_servers_json elif self.namesonly: formatter = server_manager_utils.format_servers_nameonly else: formatter = server_manager_utils.format_servers print(formatter(results)) else: self.failure('No server is found.', what_failed='Failed to find servers', item=self.hostname, fatal=True) class server_create(server): """atest server create hostname --role --note """ def __init__(self): """Initializer. """ super(server_create, self).__init__(allow_multiple_hostname=True) self.parser.add_option('-n', '--note', help='note of the server', type='string', default=None, metavar='NOTE') def parse(self): """Parse command arguments. """ (options, leftover) = super(server_create, self).parse() self.note = options.note if not self.role: self.invalid_syntax('--role is required to create a server.') return (options, leftover) def execute_skylab(self): """Execute the command for skylab inventory changes.""" inventory_repo = skylab_utils.InventoryRepo( self.inventory_repo_dir) inventory_repo.initialize() data_dir = inventory_repo.get_data_dir() infrastructure = text_manager.load_infrastructure(data_dir) new_servers = [] for hostname in self.hostname: new_servers.append(skylab_server.create( infrastructure, hostname, self.environment, role=self.role, note=self.note)) text_manager.dump_infrastructure(data_dir, infrastructure) message = skylab_utils.construct_commit_message( 'Add new server: %s' % self.hostname) self.change_number = inventory_repo.upload_change( message, draft=self.draft, dryrun=self.dryrun, submit=self.submit) return new_servers def execute(self): """Execute the command. @return: A Server object if it is created successfully. """ if RESPECT_SKYLAB_SERVERDB: self.failure(ATEST_DISABLE_MSG, what_failed='Failed to create server', item=self.hostname, fatal=True) if self.skylab: try: return self.execute_skylab() except (skylab_server.SkylabServerActionError, revision_control.GitError, gob_util.GOBError, skylab_utils.InventoryRepoDirNotClean) as e: self.failure(e, what_failed='Failed to create server in skylab ' 'inventory.', item=self.hostname, fatal=True) else: try: return server_manager.create( hostname=self.hostname, role=self.role, note=self.note) except (server_manager_utils.ServerActionError, error.InvalidDataError) as e: self.failure(e, what_failed='Failed to create server', item=self.hostname, fatal=True) def output(self, results): """Display output. @param results: return of the execute call, a server object that contains server information. """ if results: print('Server %s is added.\n' % self.hostname) print(results) if self.skylab and not self.dryrun and not self.submit: print(skylab_utils.get_cl_message(self.change_number)) class server_delete(server): """atest server delete hostname""" def execute_skylab(self): """Execute the command for skylab inventory changes.""" inventory_repo = skylab_utils.InventoryRepo( self.inventory_repo_dir) inventory_repo.initialize() data_dir = inventory_repo.get_data_dir() infrastructure = text_manager.load_infrastructure(data_dir) skylab_server.delete(infrastructure, self.hostname, self.environment) text_manager.dump_infrastructure(data_dir, infrastructure) message = skylab_utils.construct_commit_message( 'Delete server: %s' % self.hostname) self.change_number = inventory_repo.upload_change( message, draft=self.draft, dryrun=self.dryrun, submit=self.submit) def execute(self): """Execute the command. @return: True if server is deleted successfully. """ if RESPECT_SKYLAB_SERVERDB: self.failure(ATEST_DISABLE_MSG, what_failed='Failed to delete server', item=self.hostname, fatal=True) if self.skylab: try: self.execute_skylab() return True except (skylab_server.SkylabServerActionError, revision_control.GitError, gob_util.GOBError, skylab_utils.InventoryRepoDirNotClean) as e: self.failure(e, what_failed='Failed to delete server from ' 'skylab inventory.', item=self.hostname, fatal=True) else: try: server_manager.delete(hostname=self.hostname) return True except (server_manager_utils.ServerActionError, error.InvalidDataError) as e: self.failure(e, what_failed='Failed to delete server', item=self.hostname, fatal=True) def output(self, results): """Display output. @param results: return of the execute call. """ if results: print('Server %s is deleted.\n' % self.hostname) if self.skylab and not self.dryrun and not self.submit: print(skylab_utils.get_cl_message(self.change_number)) class server_modify(server): """atest server modify hostname modify action can only change one input at a time. Available inputs are: --status: Status of the server. --note: Note of the server. --role: New role to be added to the server. --delete_role: Existing role to be deleted from the server. """ def __init__(self): """Initializer. """ super(server_modify, self).__init__() self.parser.add_option('-s', '--status', help='Status of the server', type='string', metavar='STATUS') self.parser.add_option('-n', '--note', help='Note of the server', type='string', default=None, metavar='NOTE') self.parser.add_option('-d', '--delete', help=('Set to True to delete given role.'), action='store_true', default=False, metavar='DELETE') self.parser.add_option('-a', '--attribute', help='Name of the attribute of the server', type='string', default=None, metavar='ATTRIBUTE') self.parser.add_option('-e', '--value', help='Value for the attribute of the server', type='string', default=None, metavar='VALUE') def parse(self): """Parse command arguments. """ (options, leftover) = super(server_modify, self).parse() self.status = options.status self.note = options.note self.delete = options.delete self.attribute = options.attribute self.value = options.value self.action = options.action # modify supports various options. However, it's safer to limit one # option at a time so no complicated role-dependent logic is needed # to handle scenario that both role and status are changed. # self.parser is optparse, which does not have function in argparse like # add_mutually_exclusive_group. That's why the count is used here. flags = [self.status is not None, self.role is not None, self.attribute is not None, self.note is not None] if flags.count(True) != 1: msg = ('Action modify only support one option at a time. You can ' 'try one of following 5 options:\n' '1. --status: Change server\'s status.\n' '2. --note: Change server\'s note.\n' '3. --role with optional -d: Add/delete role from server.\n' '4. --attribute --value: Set/change the value of a ' 'server\'s attribute.\n' '5. --attribute -d: Delete the attribute from the ' 'server.\n' '\nUse option -h to see a complete list of options.') self.invalid_syntax(msg) if (self.status != None or self.note != None) and self.delete: self.invalid_syntax('--delete does not apply to status or note.') if self.attribute != None and not self.delete and self.value == None: self.invalid_syntax('--attribute must be used with option --value ' 'or --delete.') # TODO(nxia): crbug.com/832964 support --action with --skylab if self.skylab and self.action: self.invalid_syntax('--action is currently not supported with' ' --skylab.') return (options, leftover) def execute_skylab(self): """Execute the command for skylab inventory changes.""" inventory_repo = skylab_utils.InventoryRepo( self.inventory_repo_dir) inventory_repo.initialize() data_dir = inventory_repo.get_data_dir() infrastructure = text_manager.load_infrastructure(data_dir) target_server = skylab_server.modify( infrastructure, self.hostname, self.environment, role=self.role, status=self.status, delete_role=self.delete, note=self.note, attribute=self.attribute, value=self.value, delete_attribute=self.delete) text_manager.dump_infrastructure(data_dir, infrastructure) status = inventory_repo.git_repo.status() if not status: print('Nothing is changed for server %s.' % self.hostname) return message = skylab_utils.construct_commit_message( 'Modify server: %s' % self.hostname) self.change_number = inventory_repo.upload_change( message, draft=self.draft, dryrun=self.dryrun, submit=self.submit) return target_server def execute(self): """Execute the command. @return: The updated server object if it is modified successfully. """ if RESPECT_SKYLAB_SERVERDB: self.failure(ATEST_DISABLE_MSG, what_failed='Failed to modify server', item=self.hostname, fatal=True) if self.skylab: try: return self.execute_skylab() except (skylab_server.SkylabServerActionError, revision_control.GitError, gob_util.GOBError, skylab_utils.InventoryRepoDirNotClean) as e: self.failure(e, what_failed='Failed to modify server in skylab' ' inventory.', item=self.hostname, fatal=True) else: try: return server_manager.modify( hostname=self.hostname, role=self.role, status=self.status, delete=self.delete, note=self.note, attribute=self.attribute, value=self.value, action=self.action) except (server_manager_utils.ServerActionError, error.InvalidDataError) as e: self.failure(e, what_failed='Failed to modify server', item=self.hostname, fatal=True) def output(self, results): """Display output. @param results: return of the execute call, which is the updated server object. """ if results: print('Server %s is modified.\n' % self.hostname) print(results) if self.skylab and not self.dryrun and not self.submit: print(skylab_utils.get_cl_message(self.change_number))