| # 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. |
| """ |
| |
| 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] <options> |
| """ |
| usage_action = '[list|create|delete|modify]' |
| topic = msg_topic = 'server' |
| msg_items = '<server>' |
| |
| 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 <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 <role> --note <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) |