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