You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
237 lines
9.4 KiB
237 lines
9.4 KiB
#!/usr/bin/python2
|
|
# 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:]))
|