Home | History | Annotate | Download | only in site_utils
      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 """This module provides utility functions to help managing servers in server
      6 database (defined in global config section AUTOTEST_SERVER_DB).
      7 
      8 """
      9 
     10 import collections
     11 import json
     12 import socket
     13 import subprocess
     14 import sys
     15 
     16 import common
     17 
     18 import django.core.exceptions
     19 from autotest_lib.client.common_lib import utils
     20 from autotest_lib.client.common_lib.global_config import global_config
     21 from autotest_lib.frontend.server import models as server_models
     22 from autotest_lib.site_utils.lib import infra
     23 
     24 
     25 class ServerActionError(Exception):
     26     """Exception raised when action on server failed.
     27     """
     28 
     29 
     30 def use_server_db():
     31     """Check if use_server_db is enabled in configuration.
     32 
     33     @return: True if use_server_db is set to True in global config.
     34     """
     35     return global_config.get_config_value(
     36             'SERVER', 'use_server_db', default=False, type=bool)
     37 
     38 
     39 def warn_missing_role(role, exclude_server):
     40     """Post a warning if Autotest instance has no other primary server with
     41     given role.
     42 
     43     @param role: Name of the role.
     44     @param exclude_server: Server to be excluded from search for role.
     45     """
     46     servers = server_models.Server.objects.filter(
     47             roles__role=role,
     48             status=server_models.Server.STATUS.PRIMARY).exclude(
     49                     hostname=exclude_server.hostname)
     50     if not servers:
     51         message = ('WARNING! There will be no server with role %s after it\'s '
     52                    'removed from server %s. Autotest will not function '
     53                    'normally without any server in role %s.' %
     54                    (role, exclude_server.hostname, role))
     55         print >> sys.stderr, message
     56 
     57 
     58 def get_servers(hostname=None, role=None, status=None):
     59     """Find servers with given role and status.
     60 
     61     @param hostname: hostname of the server.
     62     @param role: Role of server, default to None.
     63     @param status: Status of server, default to None.
     64 
     65     @return: A list of server objects with given role and status.
     66     """
     67     filters = {}
     68     if hostname:
     69         filters['hostname'] = hostname
     70     if role:
     71         filters['roles__role'] = role
     72     if status:
     73         filters['status'] = status
     74     return list(server_models.Server.objects.filter(**filters))
     75 
     76 
     77 def format_servers(servers):
     78     """Format servers for printing.
     79 
     80     Example output:
     81 
     82         Hostname     : server2
     83         Status       : primary
     84         Roles        : drone
     85         Attributes   : {'max_processes':300}
     86         Date Created : 2014-11-25 12:00:00
     87         Date Modified: None
     88         Note         : Drone in lab1
     89 
     90     @param servers: Sequence of Server instances.
     91     @returns: Formatted output as string.
     92     """
     93     return '\n'.join(str(server) for server in servers)
     94 
     95 
     96 def format_servers_json(servers):
     97     """Format servers for printing as JSON.
     98 
     99     Example output:
    100 
    101         Hostname     : server2
    102         Status       : primary
    103         Roles        : drone
    104         Attributes   : {'max_processes':300}
    105         Date Created : 2014-11-25 12:00:00
    106         Date Modified: None
    107         Note         : Drone in lab1
    108 
    109     @param servers: Sequence of Server instances.
    110     @returns: String.
    111     """
    112     server_dicts = []
    113     for server in servers:
    114         if server.date_modified is None:
    115             date_modified = None
    116         else:
    117             date_modified = str(server.date_modified)
    118         server_dicts.append({'hostname': server.hostname,
    119                              'status': server.status,
    120                              'roles': server.get_role_names(),
    121                              'date_created': str(server.date_created),
    122                              'date_modified': date_modified,
    123                              'note': server.note})
    124     return json.dumps(server_dicts)
    125 
    126 
    127 _SERVER_TABLE_FORMAT = ('%(hostname)-30s | %(status)-7s | %(roles)-20s |'
    128                         ' %(date_created)-19s | %(date_modified)-19s |'
    129                         ' %(note)s')
    130 
    131 
    132 def format_servers_table(servers):
    133     """format servers for printing as a table.
    134 
    135     Example output:
    136 
    137         Hostname | Status  | Roles     | Date Created    | Date Modified | Note
    138         server1  | backup  | scheduler | 2014-11-25 23:45:19 |           |
    139         server2  | primary | drone     | 2014-11-25 12:00:00 |           | Drone
    140 
    141     @param servers: Sequence of Server instances.
    142     @returns: Formatted output as string.
    143     """
    144     result_lines = [(_SERVER_TABLE_FORMAT %
    145                      {'hostname': 'Hostname',
    146                       'status': 'Status',
    147                       'roles': 'Roles',
    148                       'date_created': 'Date Created',
    149                       'date_modified': 'Date Modified',
    150                       'note': 'Note'})]
    151     for server in servers:
    152         roles = ','.join(server.get_role_names())
    153         result_lines.append(_SERVER_TABLE_FORMAT %
    154                             {'hostname':server.hostname,
    155                              'status': server.status or '',
    156                              'roles': roles,
    157                              'date_created': server.date_created,
    158                              'date_modified': server.date_modified or '',
    159                              'note': server.note or ''})
    160     return '\n'.join(result_lines)
    161 
    162 
    163 def format_servers_summary(servers):
    164     """format servers for printing a summary.
    165 
    166     Example output:
    167 
    168         scheduler      : server1(backup), server3(primary),
    169         host_scheduler :
    170         drone          : server2(primary),
    171         devserver      :
    172         database       :
    173         suite_scheduler:
    174         crash_server   :
    175         No Role        :
    176 
    177     @param servers: Sequence of Server instances.
    178     @returns: Formatted output as string.
    179     """
    180     servers_by_role = _get_servers_by_role(servers)
    181     servers_with_roles = {server for role_servers in servers_by_role.itervalues()
    182                           for server in role_servers}
    183     servers_without_roles = [server for server in servers
    184                              if server not in servers_with_roles]
    185     result_lines = ['Roles and status of servers:', '']
    186     for role, role_servers in servers_by_role.iteritems():
    187         result_lines.append(_format_role_servers_summary(role, role_servers))
    188     if servers_without_roles:
    189         result_lines.append(
    190                 _format_role_servers_summary('No Role', servers_without_roles))
    191     return '\n'.join(result_lines)
    192 
    193 
    194 def format_servers_nameonly(servers):
    195     """format servers for printing names only
    196 
    197     @param servers: Sequence of Server instances.
    198     @returns: Formatted output as string.
    199     """
    200     return '\n'.join(s.hostname for s in servers)
    201 
    202 
    203 def _get_servers_by_role(servers):
    204     """Return a mapping from roles to servers.
    205 
    206     @param servers: Iterable of servers.
    207     @returns: Mapping of role strings to lists of servers.
    208     """
    209     roles = [role for role, _ in server_models.ServerRole.ROLE.choices()]
    210     servers_by_role = collections.defaultdict(list)
    211     for server in servers:
    212         for role in server.get_role_names():
    213             servers_by_role[role].append(server)
    214     return servers_by_role
    215 
    216 
    217 def _format_role_servers_summary(role, servers):
    218     """Format one line of servers for a role in a server list summary.
    219 
    220     @param role: Role string.
    221     @param servers: Iterable of Server instances.
    222     @returns: String.
    223     """
    224     servers_part = ', '.join(
    225             '%s(%s)' % (server.hostname, server.status)
    226             for server in servers)
    227     return '%-15s: %s' % (role, servers_part)
    228 
    229 
    230 def check_server(hostname, role):
    231     """Confirm server with given hostname is ready to be primary of given role.
    232 
    233     If the server is a backup and failed to be verified for the role, remove
    234     the role from its roles list. If it has no other role, set its status to
    235     repair_required.
    236 
    237     @param hostname: hostname of the server.
    238     @param role: Role to be checked.
    239     @return: True if server can be verified for the given role, otherwise
    240              return False.
    241     """
    242     # TODO(dshi): Add more logic to confirm server is ready for the role.
    243     # For now, the function just checks if server is ssh-able.
    244     try:
    245         infra.execute_command(hostname, 'true')
    246         return True
    247     except subprocess.CalledProcessError as e:
    248         print >> sys.stderr, ('Failed to check server %s, error: %s' %
    249                               (hostname, e))
    250         return False
    251 
    252 
    253 def verify_server(exist=True):
    254     """Decorator to check if server with given hostname exists in the database.
    255 
    256     @param exist: Set to True to confirm server exists in the database, raise
    257                   exception if not. If it's set to False, raise exception if
    258                   server exists in database. Default is True.
    259 
    260     @raise ServerActionError: If `exist` is True and server does not exist in
    261                               the database, or `exist` is False and server exists
    262                               in the database.
    263     """
    264     def deco_verify(func):
    265         """Wrapper for the decorator.
    266 
    267         @param func: Function to be called.
    268         """
    269         def func_verify(*args, **kwargs):
    270             """Decorator to check if server exists.
    271 
    272             If exist is set to True, raise ServerActionError is server with
    273             given hostname is not found in server database.
    274             If exist is set to False, raise ServerActionError is server with
    275             given hostname is found in server database.
    276 
    277             @param func: function to be called.
    278             @param args: arguments for function to be called.
    279             @param kwargs: keyword arguments for function to be called.
    280             """
    281             hostname = kwargs['hostname']
    282             try:
    283                 server = server_models.Server.objects.get(hostname=hostname)
    284             except django.core.exceptions.ObjectDoesNotExist:
    285                 server = None
    286 
    287             if not exist and server:
    288                 raise ServerActionError('Server %s already exists.' %
    289                                         hostname)
    290             if exist and not server:
    291                 raise ServerActionError('Server %s does not exist in the '
    292                                         'database.' % hostname)
    293             if server:
    294                 kwargs['server'] = server
    295             return func(*args, **kwargs)
    296         return func_verify
    297     return deco_verify
    298 
    299 
    300 def get_drones():
    301     """Get a list of drones in status primary.
    302 
    303     @return: A list of drones in status primary.
    304     """
    305     servers = get_servers(role=server_models.ServerRole.ROLE.DRONE,
    306                           status=server_models.Server.STATUS.PRIMARY)
    307     return [s.hostname for s in servers]
    308 
    309 
    310 def delete_attribute(server, attribute):
    311     """Delete the attribute from the host.
    312 
    313     @param server: An object of server_models.Server.
    314     @param attribute: Name of an attribute of the server.
    315     """
    316     attributes = server.attributes.filter(attribute=attribute)
    317     if not attributes:
    318         raise ServerActionError('Server %s does not have attribute %s' %
    319                                 (server.hostname, attribute))
    320     attributes[0].delete()
    321     print 'Attribute %s is deleted from server %s.' % (attribute,
    322                                                        server.hostname)
    323 
    324 
    325 def change_attribute(server, attribute, value):
    326     """Change the value of an attribute of the server.
    327 
    328     @param server: An object of server_models.Server.
    329     @param attribute: Name of an attribute of the server.
    330     @param value: Value of the attribute of the server.
    331 
    332     @raise ServerActionError: If the attribute already exists and has the
    333                               given value.
    334     """
    335     attributes = server_models.ServerAttribute.objects.filter(
    336             server=server, attribute=attribute)
    337     if attributes and attributes[0].value == value:
    338         raise ServerActionError('Attribute %s for Server %s already has '
    339                                 'value of %s.' %
    340                                 (attribute, server.hostname, value))
    341     if attributes:
    342         old_value = attributes[0].value
    343         attributes[0].value = value
    344         attributes[0].save()
    345         print ('Attribute `%s` of server %s is changed from %s to %s.' %
    346                      (attribute, server.hostname, old_value, value))
    347     else:
    348         server_models.ServerAttribute.objects.create(
    349                 server=server, attribute=attribute, value=value)
    350         print ('Attribute `%s` of server %s is set to %s.' %
    351                (attribute, server.hostname, value))
    352 
    353 
    354 def get_shards():
    355     """Get a list of shards in status primary.
    356 
    357     @return: A list of shards in status primary.
    358     """
    359     servers = get_servers(role=server_models.ServerRole.ROLE.SHARD,
    360                           status=server_models.Server.STATUS.PRIMARY)
    361     return [s.hostname for s in servers]
    362 
    363 
    364 def confirm_server_has_role(hostname, role):
    365     """Confirm a given server has the given role, and its status is primary.
    366 
    367     @param hostname: hostname of the server.
    368     @param role: Name of the role to be checked.
    369     @raise ServerActionError: If localhost does not have given role or it's
    370                               not in primary status.
    371     """
    372     if hostname.lower() in ['localhost', '127.0.0.1']:
    373         hostname = socket.gethostname()
    374     hostname = utils.normalize_hostname(hostname)
    375 
    376     servers = get_servers(role=role, status=server_models.Server.STATUS.PRIMARY)
    377     for server in servers:
    378         if hostname == utils.normalize_hostname(server.hostname):
    379             return True
    380     raise ServerActionError('Server %s does not have role of %s running in '
    381                             'status primary.' % (hostname, role))
    382