xref: /aosp_15_r20/external/autotest/site_utils/mysql_bootstrap.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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