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