Home | History | Annotate | Download | only in cli
      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