[autotest] Add actions to take when add/remove role from server
This CL adds actions needed to make adding/removing role from server effective.
For example, when server database is enabled and a new drone is added, scheduler
needs to be restarted.
BUG=chromium:424778
CQ-DEPEND=CL:232003
TEST=unittest, setup local server database, manually test follow cases
(CL 232525 is needed for drone test)
add scheduler: Confirm scheduler service is started in the server.
remove scheduler: Confirm scheduler service is stopped in the server.
add drone: Confirm scheduler service is restarted.
remove drone: Confirm scheduler service is restarted
Change-Id: I14d6bb15d68a9b94fa3ab5b0bcc202469b253c89
Reviewed-on: https://chromium-review.googlesource.com/233181
Tested-by: Dan Shi <[email protected]>
Trybot-Ready: Dan Shi <[email protected]>
Reviewed-by: Fang Deng <[email protected]>
Commit-Queue: Dan Shi <[email protected]>
diff --git a/site_utils/server_manager_utils.py b/site_utils/server_manager_utils.py
new file mode 100644
index 0000000..21c1088
--- /dev/null
+++ b/site_utils/server_manager_utils.py
@@ -0,0 +1,271 @@
+# 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.
+
+"""This module provides utility functions to help managing servers in server
+database (defined in global config section AUTOTEST_SERVER_DB).
+
+"""
+
+import subprocess
+import sys
+
+import common
+
+import django.core.exceptions
+from autotest_lib.client.common_lib.global_config import global_config
+from autotest_lib.frontend.server import models as server_models
+from autotest_lib.site_utils.lib import infra
+
+
+class ServerActionError(Exception):
+ """Exception raised when action on server failed.
+ """
+
+
+def use_server_db():
+ """Check if use_server_db is enabled in configuration.
+
+ @return: True if use_server_db is set to True in global config.
+ """
+ return global_config.get_config_value(
+ 'SERVER', 'use_server_db', default=False, type=bool)
+
+
+def warn_missing_role(role, exclude_server):
+ """Post a warning if Autotest instance has no other primary server with
+ given role.
+
+ @param role: Name of the role.
+ @param exclude_server: Server to be excluded from search for role.
+ """
+ servers = server_models.Server.objects.filter(
+ roles__role=role,
+ status=server_models.Server.STATUS.PRIMARY).exclude(
+ hostname=exclude_server.hostname)
+ if not servers:
+ message = ('WARNING! There will be no server with role %s after it\'s '
+ 'removed from server %s. Autotest will not function '
+ 'normally without any server in role %s.' %
+ (role, exclude_server.hostname, role))
+ print >> sys.stderr, message
+
+
+def get_servers(hostname=None, role=None, status=None):
+ """Find servers with given role and status.
+
+ @param hostname: hostname of the server.
+ @param role: Role of server, default to None.
+ @param status: Status of server, default to None.
+
+ @return: A list of server objects with given role and status.
+ """
+ filters = {}
+ if hostname:
+ filters['hostname'] = hostname
+ if role:
+ filters['roles__role'] = role
+ if status:
+ filters['status'] = status
+ return list(server_models.Server.objects.filter(**filters))
+
+
+def get_server_details(servers, table=False, summary=False):
+ """Get a string of given servers' details.
+
+ The method can return a string of server information in 3 different formats:
+ A detail view:
+ Hostname : server2
+ Status : primary
+ Roles : drone
+ Attributes : {'max_processes':300}
+ Date Created : 2014-11-25 12:00:00
+ Date Modified: None
+ Note : Drone in lab1
+ A table view:
+ Hostname | Status | Roles | Date Created | Date Modified | Note
+ server1 | backup | scheduler | 2014-11-25 23:45:19 | |
+ server2 | primary | drone | 2014-11-25 12:00:00 | | Drone
+ A summary view:
+ scheduler : server1(backup), server3(primary),
+ host_scheduler :
+ drone : server2(primary),
+ devserver :
+ database :
+ suite_scheduler:
+ crash_server :
+ No Role :
+
+ The method returns detail view of each server and a summary view by default.
+ If `table` is set to True, only table view will be returned.
+ If `summary` is set to True, only summary view will be returned.
+
+ @param servers: A list of servers to get details.
+ @param table: True to return a table view instead of a detail view,
+ default is set to False.
+ @param summary: True to only show the summary of roles and status of
+ given servers.
+
+ @return: A string of the information of given servers.
+ """
+ # Format string to display a table view.
+ # Hostname, Status, Roles, Date Created, Date Modified, Note
+ TABLEVIEW_FORMAT = ('%(hostname)-30s | %(status)-7s | %(roles)-20s | '
+ '%(date_created)-19s | %(date_modified)-19s | %(note)s')
+
+ result = ''
+ if not table and not summary:
+ for server in servers:
+ result += '\n' + str(server)
+ elif table:
+ result += (TABLEVIEW_FORMAT %
+ {'hostname':'Hostname', 'status':'Status',
+ 'roles':'Roles', 'date_created':'Date Created',
+ 'date_modified':'Date Modified', 'note':'Note'})
+ for server in servers:
+ roles = ','.join(server.get_role_names())
+ result += '\n' + (TABLEVIEW_FORMAT %
+ {'hostname':server.hostname,
+ 'status': server.status or '',
+ 'roles': roles,
+ 'date_created': server.date_created,
+ 'date_modified': server.date_modified or '',
+ 'note': server.note or ''})
+ elif summary:
+ result += 'Roles and status of servers:\n\n'
+ for role, _ in server_models.ServerRole.ROLE.choices():
+ servers_of_role = [s for s in servers if role in
+ [r.role for r in s.roles.all()]]
+ result += '%-15s: ' % role
+ for server in servers_of_role:
+ result += '%s(%s), ' % (server.hostname, server.status)
+ result += '\n'
+ servers_without_role = [s.hostname for s in servers
+ if not s.roles.all()]
+ result += '%-15s: %s' % ('No Role', ', '.join(servers_without_role))
+
+ return result
+
+
+def check_server(hostname, role):
+ """Confirm server with given hostname is ready to be primary of given role.
+
+ If the server is a backup and failed to be verified for the role, remove
+ the role from its roles list. If it has no other role, set its status to
+ repair_required.
+
+ @param hostname: hostname of the server.
+ @param role: Role to be checked.
+ @return: True if server can be verified for the given role, otherwise
+ return False.
+ """
+ # TODO(dshi): Add more logic to confirm server is ready for the role.
+ # For now, the function just checks if server is ssh-able.
+ try:
+ infra.execute_command(hostname, 'true')
+ return True
+ except subprocess.CalledProcessError as e:
+ print >> sys.stderr, ('Failed to check server %s, error: %s' %
+ (hostname, e))
+ return False
+
+
+def verify_server(exist=True):
+ """Decorator to check if server with given hostname exists in the database.
+
+ @param exist: Set to True to confirm server exists in the database, raise
+ exception if not. If it's set to False, raise exception if
+ server exists in database. Default is True.
+
+ @raise ServerActionError: If `exist` is True and server does not exist in
+ the database, or `exist` is False and server exists
+ in the database.
+ """
+ def deco_verify(func):
+ """Wrapper for the decorator.
+
+ @param func: Function to be called.
+ """
+ def func_verify(*args, **kwargs):
+ """Decorator to check if server exists.
+
+ If exist is set to True, raise ServerActionError is server with
+ given hostname is not found in server database.
+ If exist is set to False, raise ServerActionError is server with
+ given hostname is found in server database.
+
+ @param func: function to be called.
+ @param args: arguments for function to be called.
+ @param kwargs: keyword arguments for function to be called.
+ """
+ hostname = kwargs['hostname']
+ try:
+ server = server_models.Server.objects.get(hostname=hostname)
+ except django.core.exceptions.ObjectDoesNotExist:
+ server = None
+
+ if not exist and server:
+ raise ServerActionError('Server %s already exists.' %
+ hostname)
+ if exist and not server:
+ raise ServerActionError('Server %s does not exist in the '
+ 'database.' % hostname)
+ if server:
+ kwargs['server'] = server
+ return func(*args, **kwargs)
+ return func_verify
+ return deco_verify
+
+
+def get_drones():
+ """Get a list of drones in status primary.
+
+ @return: A list of drones in status primary.
+ """
+ servers = get_servers(role=server_models.ServerRole.ROLE.DRONE,
+ status=server_models.Server.STATUS.PRIMARY)
+ return [s.hostname for s in servers]
+
+
+def delete_attribute(server, attribute):
+ """Delete the attribute from the host.
+
+ @param server: An object of server_models.Server.
+ @param attribute: Name of an attribute of the server.
+ """
+ attributes = server.attributes.filter(attribute=attribute)
+ if not attributes:
+ raise ServerActionError('Server %s does not have attribute %s' %
+ (server.hostname, attribute))
+ attributes[0].delete()
+ print 'Attribute %s is deleted from server %s.' % (attribute,
+ server.hostname)
+
+
+def change_attribute(server, attribute, value):
+ """Change the value of an attribute of the server.
+
+ @param server: An object of server_models.Server.
+ @param attribute: Name of an attribute of the server.
+ @param value: Value of the attribute of the server.
+
+ @raise ServerActionError: If the attribute already exists and has the
+ given value.
+ """
+ attributes = server_models.ServerAttribute.objects.filter(
+ server=server, attribute=attribute)
+ if attributes and attributes[0].value == value:
+ raise ServerActionError('Attribute %s for Server %s already has '
+ 'value of %s.' %
+ (attribute, server.hostname, value))
+ if attributes:
+ old_value = attributes[0].value
+ attributes[0].value = value
+ attributes[0].save()
+ print ('Attribute `%s` of server %s is changed from %s to %s.' %
+ (attribute, server.hostname, old_value, value))
+ else:
+ server_models.ServerAttribute.objects.create(
+ server=server, attribute=attribute, value=value)
+ print ('Attribute `%s` of server %s is set to %s.' %
+ (attribute, server.hostname, value))