1*9c5db199SXin Li#!/usr/bin/python3 2*9c5db199SXin Li# Copyright (c) 2014 The Chromium OS Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li 6*9c5db199SXin Li"""Bootstrap mysql. 7*9c5db199SXin Li 8*9c5db199SXin LiThe purpose of this module is to grant access to a new-user/host/password 9*9c5db199SXin Licombination on a remote db server. For example, if we were bootstrapping 10*9c5db199SXin Lia new autotest main A1 with a remote database server A2, the scheduler 11*9c5db199SXin Lirunning on A1 needs to access the database on A2 with the credentials 12*9c5db199SXin Lispecified in the shadow_config of A1 (A1_user, A1_pass). To achieve this 13*9c5db199SXin Liwe ssh into A2 and execute the grant privileges command for (A1_user, 14*9c5db199SXin LiA1_pass, A1_host). If OTOH the db server is running locally we only need 15*9c5db199SXin Lito grant permissions for (A1_user, A1_pass, localhost). 16*9c5db199SXin Li 17*9c5db199SXin LiThe operation to achieve this will look like: 18*9c5db199SXin Li ssh/become into A2 19*9c5db199SXin Li Execute mysql -u <default_user> -p<default_pass> -e 20*9c5db199SXin Li "GRANT privileges on <db> to 'A1_user'@A1 identified by 'A1_pass';" 21*9c5db199SXin Li 22*9c5db199SXin LiHowever this will only grant the right access permissions to A1, so we need 23*9c5db199SXin Lito repeat for all subsequent db clients we add. This will happen through puppet. 24*9c5db199SXin Li 25*9c5db199SXin LiIn the case of a vagrant cluster, a remote vm cannot ssh into the db server 26*9c5db199SXin Livm with plain old ssh. However, the entire vm cluster is provisioned at the 27*9c5db199SXin Lisame time, so we can grant access to all remote vm clients directly on the 28*9c5db199SXin Lidatabase server without knowing their ips by using the ip of the gateway. 29*9c5db199SXin LiThis works because the db server vm redirects its database port (3306) to 30*9c5db199SXin Lia predefined port (defined in the vagrant file, defaults to 8002), and all 31*9c5db199SXin Liother vms in the cluster can only access it through the vm host identified 32*9c5db199SXin Liby the gateway. 33*9c5db199SXin Li 34*9c5db199SXin LiThe operation to achieve this will look like: 35*9c5db199SXin Li Provision the vagrant db server 36*9c5db199SXin Li Execute mysql -u <default_user> -p<default_pass> -e 37*9c5db199SXin Li "GRANT privileges on <db> to 'A1_user'@(gateway address) 38*9c5db199SXin Li identified by 'A1_pass';" 39*9c5db199SXin LiThis will grant the right access permissions to all vms running on the 40*9c5db199SXin Lihost machine as long as they use the right port to access the database. 41*9c5db199SXin Li""" 42*9c5db199SXin Li 43*9c5db199SXin Liimport argparse 44*9c5db199SXin Liimport logging 45*9c5db199SXin Liimport socket 46*9c5db199SXin Liimport subprocess 47*9c5db199SXin Liimport sys 48*9c5db199SXin Li 49*9c5db199SXin Liimport common 50*9c5db199SXin Li 51*9c5db199SXin Lifrom autotest_lib.client.common_lib import global_config 52*9c5db199SXin Lifrom autotest_lib.client.common_lib import utils 53*9c5db199SXin Lifrom autotest_lib.site_utils.lib import infra 54*9c5db199SXin Li 55*9c5db199SXin Li 56*9c5db199SXin Liclass MySQLCommandError(Exception): 57*9c5db199SXin Li """Generic mysql command execution exception.""" 58*9c5db199SXin Li 59*9c5db199SXin Li 60*9c5db199SXin Liclass MySQLCommandExecutor(object): 61*9c5db199SXin Li """Class to shell out to mysql. 62*9c5db199SXin Li 63*9c5db199SXin Li USE THIS CLASS WITH CARE. It doesn't protect against SQL injection on 64*9c5db199SXin Li assumption that anyone with access to our servers can run the same 65*9c5db199SXin Li commands directly instead of through this module. Do not expose it 66*9c5db199SXin Li through a webserver, it is meant solely as a utility module to allow 67*9c5db199SXin Li easy database bootstrapping via puppet. 68*9c5db199SXin Li """ 69*9c5db199SXin Li 70*9c5db199SXin Li DEFAULT_USER = global_config.global_config.get_config_value( 71*9c5db199SXin Li 'AUTOTEST_WEB', 'default_db_user', default='root') 72*9c5db199SXin Li 73*9c5db199SXin Li DEFAULT_PASS = global_config.global_config.get_config_value( 74*9c5db199SXin Li 'AUTOTEST_WEB', 'default_db_pass', default='autotest') 75*9c5db199SXin Li 76*9c5db199SXin Li 77*9c5db199SXin Li @classmethod 78*9c5db199SXin Li def mysql_cmd(cls, cmd, user=DEFAULT_USER, password=DEFAULT_PASS, 79*9c5db199SXin Li host='localhost', port=3306): 80*9c5db199SXin Li """Wrap the given mysql command. 81*9c5db199SXin Li 82*9c5db199SXin Li @param cmd: The mysql command to wrap with the --execute option. 83*9c5db199SXin Li @param host: The host against which to run the command. 84*9c5db199SXin Li @param user: The user to use in the given command. 85*9c5db199SXin Li @param password: The password for the user. 86*9c5db199SXin Li @param port: The port mysql server is listening on. 87*9c5db199SXin Li """ 88*9c5db199SXin Li return ('mysql -u %s -p%s --host %s --port %s -e "%s"' % 89*9c5db199SXin Li (user, password, host, port, cmd)) 90*9c5db199SXin Li 91*9c5db199SXin Li 92*9c5db199SXin Li @staticmethod 93*9c5db199SXin Li def execute(dest_server, full_cmd): 94*9c5db199SXin Li """Execute a mysql statement on a remote server by sshing into it. 95*9c5db199SXin Li 96*9c5db199SXin Li @param dest_server: The hostname of the remote mysql server. 97*9c5db199SXin Li @param full_cmd: The full mysql command to execute. 98*9c5db199SXin Li 99*9c5db199SXin Li @raises MySQLCommandError: If the full_cmd failed on dest_server. 100*9c5db199SXin Li """ 101*9c5db199SXin Li try: 102*9c5db199SXin Li return infra.execute_command(dest_server, full_cmd) 103*9c5db199SXin Li except subprocess.CalledProcessError as e: 104*9c5db199SXin Li raise MySQLCommandError('Failed to execute %s against %s' % 105*9c5db199SXin Li (full_cmd, dest_server)) 106*9c5db199SXin Li 107*9c5db199SXin Li 108*9c5db199SXin Li @classmethod 109*9c5db199SXin Li def ping(cls, db_server, user=DEFAULT_USER, password=DEFAULT_PASS, 110*9c5db199SXin Li use_ssh=False): 111*9c5db199SXin Li """Ping the given db server as 'user' using 'password'. 112*9c5db199SXin Li 113*9c5db199SXin Li @param db_server: The host running the mysql server. 114*9c5db199SXin Li @param user: The user to use in the ping. 115*9c5db199SXin Li @param password: The password of the user. 116*9c5db199SXin Li @param use_ssh: If False, the command is executed on localhost 117*9c5db199SXin Li by supplying --host=db_server in the mysql command. Otherwise we 118*9c5db199SXin Li ssh/become into the db_server and execute the command with 119*9c5db199SXin Li --host=localhost. 120*9c5db199SXin Li 121*9c5db199SXin Li @raises MySQLCommandError: If the ping command fails. 122*9c5db199SXin Li """ 123*9c5db199SXin Li if use_ssh: 124*9c5db199SXin Li ssh_dest_server = db_server 125*9c5db199SXin Li mysql_cmd_host = 'localhost' 126*9c5db199SXin Li else: 127*9c5db199SXin Li ssh_dest_server = 'localhost' 128*9c5db199SXin Li mysql_cmd_host = db_server 129*9c5db199SXin Li ping = cls.mysql_cmd( 130*9c5db199SXin Li 'SELECT version();', host=mysql_cmd_host, user=user, 131*9c5db199SXin Li password=password) 132*9c5db199SXin Li cls.execute(ssh_dest_server, ping) 133*9c5db199SXin Li 134*9c5db199SXin Li 135*9c5db199SXin Lidef bootstrap(user, password, source_host, dest_host): 136*9c5db199SXin Li """Bootstrap the given user against dest_host. 137*9c5db199SXin Li 138*9c5db199SXin Li Allow a user from source_host to access the db server running on 139*9c5db199SXin Li dest_host. 140*9c5db199SXin Li 141*9c5db199SXin Li @param user: The user to bootstrap. 142*9c5db199SXin Li @param password: The password for the user. 143*9c5db199SXin Li @param source_host: The host from which the new user will access the db. 144*9c5db199SXin Li @param dest_host: The hostname of the remote db server. 145*9c5db199SXin Li 146*9c5db199SXin Li @raises MySQLCommandError: If we can't ping the db server using the default 147*9c5db199SXin Li user/password specified in the shadow_config under default_db_*, or 148*9c5db199SXin Li we can't ping it with the new credentials after bootstrapping. 149*9c5db199SXin Li """ 150*9c5db199SXin Li # Confirm ssh/become access. 151*9c5db199SXin Li try: 152*9c5db199SXin Li infra.execute_command(dest_host, 'echo "hello"') 153*9c5db199SXin Li except subprocess.CalledProcessError as e: 154*9c5db199SXin Li logging.error("Cannot become/ssh into dest host. You need to bootstrap " 155*9c5db199SXin Li "it using fab -H <hostname> bootstrap from the " 156*9c5db199SXin Li "chromeos-admin repo.") 157*9c5db199SXin Li return 158*9c5db199SXin Li # Confirm the default user has at least database read privileges. Note if 159*9c5db199SXin Li # the default user has *only* read privileges everything else will still 160*9c5db199SXin Li # fail. This is a remote enough case given our current setup that we can 161*9c5db199SXin Li # avoid more complicated checking at this level. 162*9c5db199SXin Li MySQLCommandExecutor.ping(dest_host, use_ssh=True) 163*9c5db199SXin Li 164*9c5db199SXin Li # Prepare and execute the grant statement for the new user. 165*9c5db199SXin Li creds = { 166*9c5db199SXin Li 'new_user': user, 167*9c5db199SXin Li 'new_pass': password, 168*9c5db199SXin Li 'new_host': source_host, 169*9c5db199SXin Li } 170*9c5db199SXin Li # TODO(beeps): Restrict these permissions. For now we have a couple of 171*9c5db199SXin Li # databases which may/may-not exist on various roles that need refactoring. 172*9c5db199SXin Li grant_privileges = ( 173*9c5db199SXin Li "GRANT ALL PRIVILEGES ON *.* to '%(new_user)s'@'%(new_host)s' " 174*9c5db199SXin Li "IDENTIFIED BY '%(new_pass)s'; FLUSH PRIVILEGES;") 175*9c5db199SXin Li MySQLCommandExecutor.execute( 176*9c5db199SXin Li dest_host, MySQLCommandExecutor.mysql_cmd(grant_privileges % creds)) 177*9c5db199SXin Li 178*9c5db199SXin Li # Confirm the new user can ping the remote database server from localhost. 179*9c5db199SXin Li MySQLCommandExecutor.ping( 180*9c5db199SXin Li dest_host, user=user, password=password, use_ssh=False) 181*9c5db199SXin Li 182*9c5db199SXin Li 183*9c5db199SXin Lidef get_gateway(): 184*9c5db199SXin Li """Return the address of the default gateway. 185*9c5db199SXin Li 186*9c5db199SXin Li @raises: subprocess.CalledProcessError: If the address of the gateway 187*9c5db199SXin Li cannot be determined via netstat. 188*9c5db199SXin Li """ 189*9c5db199SXin Li cmd = 'netstat -rn | grep "^0.0.0.0 " | cut -d " " -f10 | head -1' 190*9c5db199SXin Li try: 191*9c5db199SXin Li return infra.execute_command('localhost', cmd).rstrip('\n') 192*9c5db199SXin Li except subprocess.CalledProcessError as e: 193*9c5db199SXin Li logging.error('Unable to get gateway: %s', e) 194*9c5db199SXin Li raise 195*9c5db199SXin Li 196*9c5db199SXin Li 197*9c5db199SXin Lidef _parse_args(args): 198*9c5db199SXin Li parser = argparse.ArgumentParser(description='A script to bootstrap mysql ' 199*9c5db199SXin Li 'with credentials from the shadow_config.') 200*9c5db199SXin Li parser.add_argument( 201*9c5db199SXin Li '--enable_gateway', action='store_true', dest='enable_gateway', 202*9c5db199SXin Li default=False, help='Enable gateway access for vagrant testing.') 203*9c5db199SXin Li return parser.parse_args(args) 204*9c5db199SXin Li 205*9c5db199SXin Li 206*9c5db199SXin Lidef main(argv): 207*9c5db199SXin Li """Main bootstrapper method. 208*9c5db199SXin Li 209*9c5db199SXin Li Grants permissions to the appropriate user on localhost, then enables the 210*9c5db199SXin Li access through the gateway if --enable_gateway is specified. 211*9c5db199SXin Li """ 212*9c5db199SXin Li args = _parse_args(argv) 213*9c5db199SXin Li dest_host = global_config.global_config.get_config_value( 214*9c5db199SXin Li 'AUTOTEST_WEB', 'host') 215*9c5db199SXin Li user = global_config.global_config.get_config_value( 216*9c5db199SXin Li 'AUTOTEST_WEB', 'user') 217*9c5db199SXin Li password = global_config.global_config.get_config_value( 218*9c5db199SXin Li 'AUTOTEST_WEB', 'password') 219*9c5db199SXin Li 220*9c5db199SXin Li # For access via localhost, one needs to specify localhost as the hostname. 221*9c5db199SXin Li # Neither the ip or the actual hostname of localhost will suffice in 222*9c5db199SXin Li # mysql version 5.5, without complications. 223*9c5db199SXin Li local_hostname = ('localhost' if utils.is_localhost(dest_host) 224*9c5db199SXin Li else socket.gethostname()) 225*9c5db199SXin Li logging.info('Bootstrapping user %s on host %s against db server %s', 226*9c5db199SXin Li user, local_hostname, dest_host) 227*9c5db199SXin Li bootstrap(user, password, local_hostname, dest_host) 228*9c5db199SXin Li 229*9c5db199SXin Li if args.enable_gateway: 230*9c5db199SXin Li gateway = get_gateway() 231*9c5db199SXin Li logging.info('Enabling access through gateway %s', gateway) 232*9c5db199SXin Li bootstrap(user, password, gateway, dest_host) 233*9c5db199SXin Li 234*9c5db199SXin Li 235*9c5db199SXin Liif __name__ == '__main__': 236*9c5db199SXin Li sys.exit(main(sys.argv[1:])) 237