Revert "Revert "[autotest] Bootstrapping mysql during server bringup.""
This reverts commit bd92448fc67407d67e07d8bbebb120c7016b70a2.
We think the CQ failure was an anomaly caused by pushing to
prod, and not a bug in the CL.
Change-Id: Ic46332d28d21969e6546a5a1ab1c1af8120583b2
Reviewed-on: https://chromium-review.googlesource.com/228362
Reviewed-by: Richard Barnette <[email protected]>
Tested-by: Richard Barnette <[email protected]>
diff --git a/site_utils/mysql_bootstrap.py b/site_utils/mysql_bootstrap.py
new file mode 100755
index 0000000..9383121
--- /dev/null
+++ b/site_utils/mysql_bootstrap.py
@@ -0,0 +1,236 @@
+#!/usr/bin/python
+# Copyright (c) 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.
+
+"""Bootstrap mysql.
+
+The purpose of this module is to grant access to a new-user/host/password
+combination on a remote db server. For example, if we were bootstrapping
+a new autotest master A1 with a remote database server A2, the scheduler
+running on A1 needs to access the database on A2 with the credentials
+specified in the shadow_config of A1 (A1_user, A1_pass). To achieve this
+we ssh into A2 and execute the grant privileges command for (A1_user,
+A1_pass, A1_host). If OTOH the db server is running locally we only need
+to grant permissions for (A1_user, A1_pass, localhost).
+
+The operation to achieve this will look like:
+ ssh/become into A2
+ Execute mysql -u <default_user> -p<default_pass> -e
+ "GRANT privileges on <db> to 'A1_user'@A1 identified by 'A1_pass';"
+
+However this will only grant the right access permissions to A1, so we need
+to repeat for all subsequent db clients we add. This will happen through puppet.
+
+In the case of a vagrant cluster, a remote vm cannot ssh into the db server
+vm with plain old ssh. However, the entire vm cluster is provisioned at the
+same time, so we can grant access to all remote vm clients directly on the
+database server without knowing their ips by using the ip of the gateway.
+This works because the db server vm redirects its database port (3306) to
+a predefined port (defined in the vagrant file, defaults to 8002), and all
+other vms in the cluster can only access it through the vm host identified
+by the gateway.
+
+The operation to achieve this will look like:
+ Provision the vagrant db server
+ Execute mysql -u <default_user> -p<default_pass> -e
+ "GRANT privileges on <db> to 'A1_user'@(gateway address)
+ identified by 'A1_pass';"
+This will grant the right access permissions to all vms running on the
+host machine as long as they use the right port to access the database.
+"""
+
+import argparse
+import logging
+import socket
+import subprocess
+import sys
+
+import common
+
+from autotest_lib.client.common_lib import global_config
+from autotest_lib.client.common_lib import utils
+from autotest_lib.site_utils.lib import infra
+
+
+class MySQLCommandError(Exception):
+ """Generic mysql command execution exception."""
+
+
+class MySQLCommandExecutor(object):
+ """Class to shell out to mysql.
+
+ USE THIS CLASS WITH CARE. It doesn't protect against SQL injection on
+ assumption that anyone with access to our servers can run the same
+ commands directly instead of through this module. Do not expose it
+ through a webserver, it is meant solely as a utility module to allow
+ easy database bootstrapping via puppet.
+ """
+
+ DEFAULT_USER = global_config.global_config.get_config_value(
+ 'AUTOTEST_WEB', 'default_db_user', default='root')
+
+ DEFAULT_PASS = global_config.global_config.get_config_value(
+ 'AUTOTEST_WEB', 'default_db_pass', default='autotest')
+
+
+ @classmethod
+ def mysql_cmd(cls, cmd, user=DEFAULT_USER, password=DEFAULT_PASS,
+ host='localhost', port=3306):
+ """Wrap the given mysql command.
+
+ @param cmd: The mysql command to wrap with the --execute option.
+ @param host: The host against which to run the command.
+ @param user: The user to use in the given command.
+ @param password: The password for the user.
+ @param port: The port mysql server is listening on.
+ """
+ return ('mysql -u %s -p%s --host %s --port %s -e "%s"' %
+ (user, password, host, port, cmd))
+
+
+ @staticmethod
+ def execute(dest_server, full_cmd):
+ """Execute a mysql statement on a remote server by sshing into it.
+
+ @param dest_server: The hostname of the remote mysql server.
+ @param full_cmd: The full mysql command to execute.
+
+ @raises MySQLCommandError: If the full_cmd failed on dest_server.
+ """
+ try:
+ return infra.execute_command(dest_server, full_cmd)
+ except subprocess.CalledProcessError as e:
+ raise MySQLCommandError('Failed to execute %s against %s' %
+ (full_cmd, dest_server))
+
+
+ @classmethod
+ def ping(cls, db_server, user=DEFAULT_USER, password=DEFAULT_PASS,
+ use_ssh=False):
+ """Ping the given db server as 'user' using 'password'.
+
+ @param db_server: The host running the mysql server.
+ @param user: The user to use in the ping.
+ @param password: The password of the user.
+ @param use_ssh: If False, the command is executed on localhost
+ by supplying --host=db_server in the mysql command. Otherwise we
+ ssh/become into the db_server and execute the command with
+ --host=localhost.
+
+ @raises MySQLCommandError: If the ping command fails.
+ """
+ if use_ssh:
+ ssh_dest_server = db_server
+ mysql_cmd_host = 'localhost'
+ else:
+ ssh_dest_server = 'localhost'
+ mysql_cmd_host = db_server
+ ping = cls.mysql_cmd(
+ 'SELECT version();', host=mysql_cmd_host, user=user,
+ password=password)
+ cls.execute(ssh_dest_server, ping)
+
+
+def bootstrap(user, password, source_host, dest_host):
+ """Bootstrap the given user against dest_host.
+
+ Allow a user from source_host to access the db server running on
+ dest_host.
+
+ @param user: The user to bootstrap.
+ @param password: The password for the user.
+ @param source_host: The host from which the new user will access the db.
+ @param dest_host: The hostname of the remote db server.
+
+ @raises MySQLCommandError: If we can't ping the db server using the default
+ user/password specified in the shadow_config under default_db_*, or
+ we can't ping it with the new credentials after bootstrapping.
+ """
+ # Confirm ssh/become access.
+ try:
+ infra.execute_command(dest_host, 'echo "hello"')
+ except subprocess.CalledProcessError as e:
+ logging.error("Cannot become/ssh into dest host. You need to bootstrap "
+ "it using fab -H <hostname> bootstrap from the "
+ "chromeos-admin repo.")
+ return
+ # Confirm the default user has at least database read privileges. Note if
+ # the default user has *only* read privileges everything else will still
+ # fail. This is a remote enough case given our current setup that we can
+ # avoid more complicated checking at this level.
+ MySQLCommandExecutor.ping(dest_host, use_ssh=True)
+
+ # Prepare and execute the grant statement for the new user.
+ creds = {
+ 'new_user': user,
+ 'new_pass': password,
+ 'new_host': source_host,
+ }
+ # TODO(beeps): Restrict these permissions. For now we have a couple of
+ # databases which may/may-not exist on various roles that need refactoring.
+ grant_privileges = (
+ "GRANT ALL PRIVILEGES ON *.* to '%(new_user)s'@'%(new_host)s' "
+ "IDENTIFIED BY '%(new_pass)s'; FLUSH PRIVILEGES;")
+ MySQLCommandExecutor.execute(
+ dest_host, MySQLCommandExecutor.mysql_cmd(grant_privileges % creds))
+
+ # Confirm the new user can ping the remote database server from localhost.
+ MySQLCommandExecutor.ping(
+ dest_host, user=user, password=password, use_ssh=False)
+
+
+def get_gateway():
+ """Return the address of the default gateway.
+
+ @raises: subprocess.CalledProcessError: If the address of the gateway
+ cannot be determined via netstat.
+ """
+ cmd = 'netstat -rn | grep "^0.0.0.0 " | cut -d " " -f10 | head -1'
+ try:
+ return infra.execute_command('localhost', cmd).rstrip('\n')
+ except subprocess.CalledProcessError as e:
+ logging.error('Unable to get gateway: %s', e)
+ raise
+
+
+def _parse_args(args):
+ parser = argparse.ArgumentParser(description='A script to bootstrap mysql '
+ 'with credentials from the shadow_config.')
+ parser.add_argument(
+ '--enable_gateway', action='store_true', dest='enable_gateway',
+ default=False, help='Enable gateway access for vagrant testing.')
+ return parser.parse_args(args)
+
+
+def main(argv):
+ """Main bootstrapper method.
+
+ Grants permissions to the appropriate user on localhost, then enables the
+ access through the gateway if --enable_gateway is specified.
+ """
+ args = _parse_args(argv)
+ dest_host = global_config.global_config.get_config_value(
+ 'AUTOTEST_WEB', 'host')
+ user = global_config.global_config.get_config_value(
+ 'AUTOTEST_WEB', 'user')
+ password = global_config.global_config.get_config_value(
+ 'AUTOTEST_WEB', 'password')
+
+ # For access via localhost, one needs to specify localhost as the hostname.
+ # Neither the ip or the actual hostname of localhost will suffice in
+ # mysql version 5.5, without complications.
+ local_hostname = ('localhost' if utils.is_localhost(dest_host)
+ else socket.gethostname())
+ logging.info('Bootstrapping user %s on host %s against db server %s',
+ user, local_hostname, dest_host)
+ bootstrap(user, password, local_hostname, dest_host)
+
+ if args.enable_gateway:
+ gateway = get_gateway()
+ logging.info('Enabling access through gateway %s', gateway)
+ bootstrap(user, password, gateway, dest_host)
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))