| #!/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:])) |