xref: /aosp_15_r20/external/autotest/site_utils/sync_cloudsql_access.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li#!/usr/bin/env python3
2*9c5db199SXin Li
3*9c5db199SXin Li# Copyright 2015 The Chromium OS Authors. All rights reserved.
4*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
5*9c5db199SXin Li# found in the LICENSE file.
6*9c5db199SXin Li
7*9c5db199SXin Li"""Tool to sync lab servers to the "Allowed Networks" of a CloudSQL instance.
8*9c5db199SXin Li
9*9c5db199SXin LiFor a lab server to access CloudSQL instance, the server's IP must be added to
10*9c5db199SXin Lithe "Allowed Networks" list of the CloudSQL instance. This tool is to be used to
11*9c5db199SXin Liread the list of lab servers from server database and update the list of
12*9c5db199SXin Li"Allowed Networks" of a given CloudSQL instance.
13*9c5db199SXin Li
14*9c5db199SXin LiThe tool also reads CLOUD/tko_access_servers from global config to add these
15*9c5db199SXin Liservers to the "Allowed Networks" list of the CloudSQL instance. This allows
16*9c5db199SXin Liservers that do not run Autotest code can access the CloudSQL instance.
17*9c5db199SXin Li
18*9c5db199SXin LiNote that running this tool will overwrite existing IPs in the "Allowed
19*9c5db199SXin LiNetworks" list. Therefore, manually editing that list from CloudSQL console
20*9c5db199SXin Lishould be prohibited. Instead, the servers should be added to
21*9c5db199SXin LiCLOUD/tko_access_servers in shadow_config.ini.
22*9c5db199SXin Li
23*9c5db199SXin Li"""
24*9c5db199SXin Li
25*9c5db199SXin Lifrom __future__ import absolute_import
26*9c5db199SXin Lifrom __future__ import division
27*9c5db199SXin Lifrom __future__ import print_function
28*9c5db199SXin Li
29*9c5db199SXin Liimport argparse
30*9c5db199SXin Liimport socket
31*9c5db199SXin Liimport sys
32*9c5db199SXin Li
33*9c5db199SXin Liimport common
34*9c5db199SXin Lifrom autotest_lib.client.bin import utils
35*9c5db199SXin Lifrom autotest_lib.client.common_lib import error
36*9c5db199SXin Lifrom autotest_lib.client.common_lib import global_config
37*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros import retry
38*9c5db199SXin Lifrom autotest_lib.server import frontend
39*9c5db199SXin Li
40*9c5db199SXin Li
41*9c5db199SXin LiROLES_REQUIRE_TKO_ACCESS = {
42*9c5db199SXin Li        'afe',
43*9c5db199SXin Li        'database',
44*9c5db199SXin Li        'drone',
45*9c5db199SXin Li        'scheduler',
46*9c5db199SXin Li        'sentinel',
47*9c5db199SXin Li        'shard',
48*9c5db199SXin Li        'skylab_drone',
49*9c5db199SXin Li}
50*9c5db199SXin Li
51*9c5db199SXin Li
52*9c5db199SXin Lidef gcloud_login(project):
53*9c5db199SXin Li    """Login to Google Cloud service for gcloud command to run.
54*9c5db199SXin Li
55*9c5db199SXin Li    @param project: Name of the Google Cloud project.
56*9c5db199SXin Li    """
57*9c5db199SXin Li    # Login with user account. If the user hasn't log in yet, the script will
58*9c5db199SXin Li    # print a url and ask for a verification code. User should load the url in
59*9c5db199SXin Li    # browser, and copy the verification code from the web page. When private IP
60*9c5db199SXin Li    # can be supported to be added using non-corp account, the login can be done
61*9c5db199SXin Li    # through service account and key file, e.g.,
62*9c5db199SXin Li    # gcloud auth activate-service-account --key-file ~/key.json
63*9c5db199SXin Li    utils.run('gcloud auth login', stdout_tee=sys.stdout,
64*9c5db199SXin Li              stderr_tee=sys.stderr, stdin=sys.stdin)
65*9c5db199SXin Li
66*9c5db199SXin Li
67*9c5db199SXin Li@retry.retry(error.CmdError, timeout_min=3)
68*9c5db199SXin Lidef _fetch_external_ip(server_name):
69*9c5db199SXin Li    return utils.run('ssh %s curl -s ifconfig.me' % server_name).stdout.rstrip()
70*9c5db199SXin Li
71*9c5db199SXin Li
72*9c5db199SXin Lidef update_allowed_networks(project, instance, afe=None, extra_servers=None,
73*9c5db199SXin Li                            dryrun=False):
74*9c5db199SXin Li    """Update the "Allowed Networks" list of the given CloudSQL instance.
75*9c5db199SXin Li
76*9c5db199SXin Li    @param project: Name of the Google Cloud project.
77*9c5db199SXin Li    @param instance: Name of the CloudSQL instance.
78*9c5db199SXin Li    @param afe: Server of the frontend RPC, default to None to use the server
79*9c5db199SXin Li                specified in global config.
80*9c5db199SXin Li    @param extra_servers: Extra servers to be included in the "Allowed Networks"
81*9c5db199SXin Li                          list. Default is None.
82*9c5db199SXin Li    @param dryrun: Boolean indicating whether this is a dryrun.
83*9c5db199SXin Li    """
84*9c5db199SXin Li    # Get the IP address of all servers need access to CloudSQL instance.
85*9c5db199SXin Li    rpc = frontend.AFE(server=afe)
86*9c5db199SXin Li    servers = [s['hostname'] for s in rpc.run('get_servers')
87*9c5db199SXin Li               if s['status'] != 'repair_required' and
88*9c5db199SXin Li               ROLES_REQUIRE_TKO_ACCESS.intersection(s['roles'])]
89*9c5db199SXin Li    if extra_servers:
90*9c5db199SXin Li        servers.extend(extra_servers.split(','))
91*9c5db199SXin Li    # Extra servers can be listed in CLOUD/tko_access_servers shadow config.
92*9c5db199SXin Li    tko_servers = global_config.global_config.get_config_value(
93*9c5db199SXin Li            'CLOUD', 'tko_access_servers', default='')
94*9c5db199SXin Li    if tko_servers:
95*9c5db199SXin Li        servers.extend(tko_servers.split(','))
96*9c5db199SXin Li    print('Adding servers %s to access list for projects %s' % (servers,
97*9c5db199SXin Li                                                                instance))
98*9c5db199SXin Li    print('Fetching their IP addresses...')
99*9c5db199SXin Li    ips = []
100*9c5db199SXin Li    for name in servers:
101*9c5db199SXin Li        try:
102*9c5db199SXin Li            # collect internal ips
103*9c5db199SXin Li            ips.append(socket.gethostbyname(name))
104*9c5db199SXin Li            # collect external ips
105*9c5db199SXin Li            ips.append(_fetch_external_ip(name))
106*9c5db199SXin Li        except socket.gaierror:
107*9c5db199SXin Li            print('Failed to resolve internal IP address for name %s' % name)
108*9c5db199SXin Li            raise
109*9c5db199SXin Li        except error.TimeoutException:
110*9c5db199SXin Li            print('Failed to resolve external IP address for %s' % name)
111*9c5db199SXin Li            raise
112*9c5db199SXin Li
113*9c5db199SXin Li    print('...Done: %s' % ips)
114*9c5db199SXin Li
115*9c5db199SXin Li    cidr_ips = [str(ip) + '/32' for ip in ips]
116*9c5db199SXin Li
117*9c5db199SXin Li    if dryrun:
118*9c5db199SXin Li        print('This is a dryrun: skip updating glcoud sql allowlists.')
119*9c5db199SXin Li        return
120*9c5db199SXin Li
121*9c5db199SXin Li    login = False
122*9c5db199SXin Li    while True:
123*9c5db199SXin Li        try:
124*9c5db199SXin Li            utils.run('gcloud config set project %s -q' % project)
125*9c5db199SXin Li            cmd = ('gcloud sql instances patch %s --authorized-networks %s '
126*9c5db199SXin Li                   '-q' % (instance, ','.join(cidr_ips)))
127*9c5db199SXin Li            print('Running command to update allowlists: "%s"' % cmd)
128*9c5db199SXin Li            utils.run(cmd, stdout_tee=sys.stdout, stderr_tee=sys.stderr)
129*9c5db199SXin Li            return
130*9c5db199SXin Li        except error.CmdError:
131*9c5db199SXin Li            if login:
132*9c5db199SXin Li                raise
133*9c5db199SXin Li
134*9c5db199SXin Li            # Try to login and retry if the command failed.
135*9c5db199SXin Li            gcloud_login(project)
136*9c5db199SXin Li            login = True
137*9c5db199SXin Li
138*9c5db199SXin Li
139*9c5db199SXin Lidef main():
140*9c5db199SXin Li    """main script."""
141*9c5db199SXin Li    parser = argparse.ArgumentParser()
142*9c5db199SXin Li    parser.add_argument('--project', type=str, dest='project',
143*9c5db199SXin Li                        help='Name of the Google Cloud project.')
144*9c5db199SXin Li    parser.add_argument('--instance', type=str, dest='instance',
145*9c5db199SXin Li                        help='Name of the CloudSQL instance.')
146*9c5db199SXin Li    parser.add_argument('--afe', type=str, dest='afe',
147*9c5db199SXin Li                        help='Name of the RPC server to get server list.',
148*9c5db199SXin Li                        default=None)
149*9c5db199SXin Li    parser.add_argument('--extra_servers', type=str, dest='extra_servers',
150*9c5db199SXin Li                        help=('Extra servers to be included in the "Allowed '
151*9c5db199SXin Li                              'Networks" list separated by comma.'),
152*9c5db199SXin Li                        default=None)
153*9c5db199SXin Li    parser.add_argument('--dryrun', dest='dryrun', action='store_true',
154*9c5db199SXin Li                        default=False,
155*9c5db199SXin Li                        help='Fetch IPs without updating allowlists in gcloud.')
156*9c5db199SXin Li    options = parser.parse_args()
157*9c5db199SXin Li
158*9c5db199SXin Li    update_allowed_networks(options.project, options.instance, options.afe,
159*9c5db199SXin Li                            options.extra_servers, options.dryrun)
160*9c5db199SXin Li
161*9c5db199SXin Li
162*9c5db199SXin Liif __name__ == '__main__':
163*9c5db199SXin Li    main()
164