| #!/usr/bin/python3 |
| # Copyright 2014 The ChromiumOS Authors |
| # 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 main 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:])) |