????
Current Path : C:/opt/pgsql/pgAdmin 4/web/pgadmin/misc/cloud/google/ |
Current File : C:/opt/pgsql/pgAdmin 4/web/pgadmin/misc/cloud/google/__init__.py |
# ########################################################################## # # # # pgAdmin 4 - PostgreSQL Tools # # # # Copyright (C) 2013 - 2024, The pgAdmin Development Team # # This software is released under the PostgreSQL Licence # # # ########################################################################## # Google Cloud Deployment Implementation import pickle import json import os from urllib.parse import unquote from config import root from pgadmin.utils.csrf import pgCSRFProtect from pgadmin.utils.ajax import plain_text_response, unauthorized, \ make_json_response, bad_request from pgadmin.misc.bgprocess import BatchProcess from pgadmin.misc.cloud.utils import _create_server, CloudProcessDesc from pgadmin.utils import PgAdminModule, filename_with_file_manager_path from pgadmin.user_login_check import pga_login_required from flask import session, current_app, request from flask_babel import gettext as _ from oauthlib.oauth2 import AccessDeniedError from googleapiclient import discovery from googleapiclient.errors import HttpError from google_auth_oauthlib.flow import InstalledAppFlow from google.auth.transport.requests import Request MODULE_NAME = 'google' os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' # Required for Oauth2 class GooglePostgresqlModule(PgAdminModule): """Cloud module to deploy on Google Cloud""" def get_exposed_url_endpoints(self): return ['google.verify_credentials', 'google.projects', 'google.regions', 'google.database_versions', 'google.instance_types', 'google.availability_zones', 'google.verification_ack', 'google.callback'] blueprint = GooglePostgresqlModule(MODULE_NAME, __name__, static_url_path='/misc/cloud/google') @blueprint.route("/") @pga_login_required def index(): return bad_request(errormsg=_("This URL cannot be called directly.")) @blueprint.route('/verify_credentials/', methods=['POST'], endpoint='verify_credentials') @pga_login_required def verify_credentials(): """ Initiate process of authorisation for google oauth2 """ data = json.loads(request.data) client_secret_path = data['secret']['client_secret_file'] if \ 'client_secret_file' in data['secret'] else None status = False error = None res_data = {} client_secret_path = unquote(client_secret_path) try: client_secret_path = \ filename_with_file_manager_path(client_secret_path) except PermissionError as e: return unauthorized(errormsg=str(e)) except Exception as e: return bad_request(errormsg=str(e)) if client_secret_path and os.path.exists(client_secret_path): with open(client_secret_path, 'r') as json_file: client_config = json.load(json_file) if 'google' not in session: session['google'] = {} if 'google_obj' not in session['google'] or \ session['google']['client_config'] != client_config: _google = Google(client_config) else: _google = pickle.loads(session['google']['google_obj']) # get auth url host_url = request.origin + '/' if request.root_path != '': host_url = host_url + request.root_path + '/' auth_url, error_msg = _google.get_auth_url(host_url) if error_msg: error = error_msg else: status = True res_data = {'auth_url': auth_url} # save google object session['google']['client_config'] = client_config session['google']['google_obj'] = pickle.dumps(_google, -1) else: error = 'Client secret path not found' session.pop('google', None) return make_json_response(success=status, errormsg=error, data=res_data) @blueprint.route('/callback', methods=['GET'], endpoint='callback') @pgCSRFProtect.exempt @pga_login_required def callback(): """ Call back function on google authentication response. :return: """ google_obj = pickle.loads(session['google']['google_obj']) res = google_obj.callback(request) session['google']['google_obj'] = pickle.dumps(google_obj, -1) return plain_text_response(res) @blueprint.route('/verification_ack', methods=['GET'], endpoint='verification_ack') @pga_login_required def verification_ack(): """ Checks for google oauth2 authorisation confirmation :return: """ verified = False if 'google' in session and 'google_obj' in session['google']: google_obj = pickle.loads(session['google']['google_obj']) verified, error = google_obj.verification_ack() session['google']['google_obj'] = pickle.dumps(google_obj, -1) return make_json_response(success=verified, errormsg=error) else: return make_json_response(success=verified, errormsg='Authentication is failed.') @blueprint.route('/projects/', methods=['GET'], endpoint='projects') @pga_login_required def get_projects(): """ Lists the projects for authorized user :return: list of projects """ if 'google' in session and 'google_obj' in session['google']: google_obj = pickle.loads(session['google']['google_obj']) projects_list = google_obj.get_projects() return make_json_response(data=projects_list) @blueprint.route('/regions/<project_id>', methods=['GET'], endpoint='regions') @pga_login_required def get_regions(project_id): """ Lists regions based on project for authorized user :param project_id: google project id :return: google cloud sql region list """ if 'google' in session and 'google_obj' in session['google'] \ and project_id: google_obj = pickle.loads(session['google']['google_obj']) regions_list = google_obj.get_regions(project_id) session['google']['google_obj'] = pickle.dumps(google_obj, -1) return make_json_response(data=regions_list) else: return make_json_response(data=[]) @blueprint.route('/availability_zones/<region>', methods=['GET'], endpoint='availability_zones') @pga_login_required def get_availability_zones(region): """ List availability zones for specified region :param region: google region :return: google cloud sql availability zone list """ if 'google' in session and 'google_obj' in session['google'] and region: google_obj = pickle.loads(session['google']['google_obj']) availability_zone_list = google_obj.get_availability_zones(region) return make_json_response(data=availability_zone_list) else: return make_json_response(data=[]) @blueprint.route('/instance_types/<project_id>/<region>/<instance_class>', methods=['GET'], endpoint='instance_types') @pga_login_required def get_instance_types(project_id, region, instance_class): """ List the instances types for specified google project, region & instance type :param project_id: google project id :param region: google cloud region :param instance_class: google cloud sql instnace class :return: """ if 'google' in session and 'google_obj' in session['google'] and \ project_id and region: google_obj = pickle.loads(session['google']['google_obj']) instance_types_dict = google_obj.get_instance_types( project_id, region) instance_types_list = instance_types_dict.get(instance_class, []) return make_json_response(data=instance_types_list) else: return make_json_response(data=[]) @blueprint.route('/database_versions/', methods=['GET'], endpoint='database_versions') @pga_login_required def get_database_versions(): """ Lists the postgresql database versions. :return: PostgreSQL version list """ if 'google' in session and 'google_obj' in session['google']: google_obj = pickle.loads(session['google']['google_obj']) db_version_list = google_obj.get_database_versions() return make_json_response(data=db_version_list) else: return make_json_response(data=[]) def deploy_on_google(data): """Deploy the Postgres instance on RDS.""" _cmd = 'python' _cmd_script = '{0}/pgacloud/pgacloud.py'.format(root) _label = data['instance_details']['name'] # Supported arguments for google cloud sql deployment args = [_cmd_script, data['cloud'], 'create-instance', '--project', data['instance_details']['project'], '--region', data['instance_details']['region'], '--name', data['instance_details']['name'], '--db-version', data['instance_details']['db_version'], '--instance-type', data['instance_details']['instance_type'], '--storage-type', data['instance_details']['storage_type'], '--storage-size', str(data['instance_details']['storage_size']), '--public-ip', str(data['instance_details']['public_ips']), '--availability-zone', data['instance_details']['availability_zone'], '--high-availability', str(data['instance_details']['high_availability']), '--secondary-availability-zone', data['instance_details']['secondary_availability_zone'], ] _cmd_msg = '{0} {1} {2}'.format(_cmd, _cmd_script, ' '.join(args)) try: sid = _create_server({ 'gid': data['db_details']['gid'], 'name': data['instance_details']['name'], 'db': 'postgres', 'username': 'postgres', 'port': 5432, 'cloud_status': -1 }) p = BatchProcess( desc=CloudProcessDesc(sid, _cmd_msg, data['cloud'], data['instance_details']['name']), cmd=_cmd, args=args ) # Set env variables for background process of deployment env = dict() google_obj = pickle.loads(session['google']['google_obj']) env['GOOGLE_CREDENTIALS'] = json.dumps(google_obj.credentials_json) if 'db_password' in data['db_details']: env['GOOGLE_DATABASE_PASSWORD'] = data['db_details']['db_password'] p.set_env_variables(None, env=env) p.update_server_id(p.id, sid) p.start() return True, p, {'label': _label, 'sid': sid} except Exception as e: current_app.logger.exception(e) return False, None, str(e) def clear_google_session(): """Clear Google Session""" if 'google' in session: session.pop('google') class Google: def __init__(self, client_config=None): # Google cloud sql api versions self._cloud_resource_manager_api_version = 'v1' self._sqladmin_api_version = 'v1' self._compute_api_version = 'v1' # Scope required for google cloud sql deployment self._scopes = ['https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/sqlservice.admin'] # Instance classed self._instance_classes = [{'label': 'Standard', 'value': 'standard'}, {'label': 'High Memory', 'value': 'highmem'}, {'label': 'Shared', 'value': 'shared'}] self._client_config = client_config self._credentials = None self.credentials_json = None self._project_id = None self._regions = [] self._availability_zones = {} self._verification_successful = False self._verification_error = None self._redirect_url = None def get_auth_url(self, host_url): """ Provides google authorisation url :param host_url: Base url for hosting application :return: authorisation url to complete authentication """ auth_url = None error = None # reset below variable to get latest values in fresh # authentication call self._verification_successful = False self._verification_error = None try: self._redirect_url = host_url + 'google/callback' flow = InstalledAppFlow.from_client_config( client_config=self._client_config, scopes=self._scopes, redirect_uri=self._redirect_url) auth_url, state = flow.authorization_url( prompt='select_account', access_type='offline', include_granted_scopes='true') session["state"] = state except Exception as e: error = str(e) self._verification_error = error return auth_url, error def callback(self, flask_request): """ Callback function on completion of google authorisation request :param flask_request: :return: Success or error message """ try: authorization_response = flask_request.url if session['state'] != flask_request.args.get('state', None): self._verification_successful = False, self._verification_error = 'Invalid state parameter' flow = InstalledAppFlow.from_client_config( client_config=self._client_config, scopes=self._scopes, redirect_uri=self._redirect_url) flow.fetch_token(authorization_response=authorization_response) self._credentials = flow.credentials self.credentials_json = \ self._credentials_to_dict(self._credentials) self._verification_successful = True return 'The authentication flow has completed. ' \ 'This window will be closed.' except AccessDeniedError as er: self._verification_successful = False self._verification_error = er.error if self._verification_error == 'access_denied': self._verification_error = 'Access denied.' return self._verification_error @staticmethod def _credentials_to_dict(credentials): return {'token': credentials.token, 'refresh_token': credentials.refresh_token, 'token_uri': credentials.token_uri, 'client_id': credentials.client_id, 'client_secret': credentials.client_secret, 'scopes': credentials.scopes, 'id_token': credentials.id_token} def verification_ack(self): """Check the Verification is done or not.""" return self._verification_successful, self._verification_error def _get_credentials(self, scopes): """ Provides google credentials for google cloud sql api calls :param scopes: Required scope of credentials :return: google credential object """ if not self._credentials or not self._credentials.valid: if self._credentials and self._credentials.expired and \ self._credentials.refresh_token and \ self._credentials.has_scopes(scopes): self._credentials.refresh(Request()) return self._credentials return self._credentials def get_projects(self): """ List the google projects for authorised user :return: """ projects = [] credentials = self._get_credentials(self._scopes) service = discovery.build('cloudresourcemanager', self._cloud_resource_manager_api_version, credentials=credentials) req = service.projects().list() res = req.execute() for project in res.get('projects', []): projects.append({'label': project['projectId'], 'value': project['projectId']}) return projects def get_regions(self, project): """ List regions for specified google cloud project :param project: google cloud project id. :return: """ self._project_id = project credentials = self._get_credentials(self._scopes) service = discovery.build('compute', self._compute_api_version, credentials=credentials) try: req = service.regions().list(project=project) res = req.execute() except HttpError: self._regions = [] return self._regions for item in res.get('items', []): region_name = item['name'] self._regions.append({'label': region_name, 'value': region_name}) region_zones = item.get('zones', []) region_zones = list( map(lambda region: region.split('/')[-1], region_zones)) self._availability_zones[region_name] = region_zones return self._regions def get_availability_zones(self, region): """ List availability zones in given google cloud region :param region: google cloud region :return: """ az_list = [] for az in self._availability_zones.get(region, []): az_list.append({'label': az, 'value': az}) return az_list def get_instance_types(self, project, region): """ Lists google cloud sql instance types. :param project: :param region: :return: """ standard_instances = [] shared_instances = [] high_mem = [] credentials = self._get_credentials(self._scopes) service = discovery.build('sqladmin', self._sqladmin_api_version, credentials=credentials) req = service.tiers().list(project=project) res = req.execute() for item in res.get('items', []): if region in item.get('region', []): if item['tier'].find('standard') != -1: vcpu = item['tier'].split('-')[-1] mem = round(int(item['RAM']) / (1024 * 1024)) label = vcpu + ' vCPU, ' + str(round(mem / 1024)) + ' GB' value = 'db-custom-' + str(vcpu) + '-' + str(mem) standard_instances.append({'label': label, 'value': value}) elif item['tier'].find('highmem') != -1: vcpu = item['tier'].split('-')[-1] mem = round(int(item['RAM']) / (1024 * 1024)) label = vcpu + ' vCPU, ' + str(round(mem / 1024)) + ' GB' value = 'db-custom-' + str(vcpu) + '-' + str(mem) high_mem.append({'label': label, 'value': value}) else: label = '1 vCPU, ' + str( round((int(item['RAM']) / 1073741824), 2)) + ' GB' value = item['tier'] shared_instances.append({'label': label, 'value': value}) instance_types = {'standard': standard_instances, 'highmem': high_mem, 'shared': shared_instances} return instance_types def get_database_versions(self): """ Lists the PostgreSQL database versions :return: """ pg_database_versions = [] database_versions = [] credentials = self._get_credentials(self._scopes) service = discovery.build('sqladmin', self._sqladmin_api_version, credentials=credentials) req = service.flags().list() res = req.execute() for item in res.get('items', []): if item.get('name', '') == 'max_parallel_workers': pg_database_versions = item.get('appliesTo', []) for version in pg_database_versions: label = (version.title().split('_')[0])[0:7] \ + 'SQL ' + version.split('_')[1] database_versions.append({'label': label, 'value': version}) return database_versions