Added capability to deploy PostgreSQL servers on Google Cloud. #5750

This commit is contained in:
Yogesh Mahajan 2023-03-13 14:56:16 +05:30 committed by GitHub
parent 6ab43e4582
commit 63c7d14638
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1892 additions and 48 deletions

View File

@ -1,12 +1,12 @@
.. _cloud_aws_rds:
******************************************
`Amazon AWS RDS Cloud Deployment`:index:
`Amazon RDS Cloud Deployment`:index:
******************************************
To deploy a PostgreSQL server on the Amazon AWS cloud, follow the below steps.
To deploy a PostgreSQL server on the Amazon cloud, follow the below steps.
.. image:: images/cloud_aws_provider.png
.. image:: images/cloud_provider_for_postgresql.png
:alt: Cloud Deployment Provider
:align: center

View File

@ -1,16 +1,16 @@
.. _cloud_azure_postgresql:
.. cloud_azure_database:
******************************************
`Azure PostgreSQL Cloud Deployment`:index:
`Azure Database Cloud Deployment`:index:
******************************************
To deploy a PostgreSQL server on the Azure cloud, follow the below steps.
To deploy a PostgreSQL server on the Azure Database, follow the below steps.
.. image:: images/cloud_azure_provider.png
.. image:: images/cloud_provider_for_postgresql.png
:alt: Cloud Deployment
:align: center
Once you launch the tool, select the Azure PostgreSQL option.
Once you launch the tool, select the Azure Database option.
Click on the *Next* button to proceed further.
@ -103,7 +103,7 @@ Click on the next button to proceed.
:align: center
At the end, review the instance details that you provided. Click on Finish
button to deploy the instance on Azure PostgreSQL.
button to deploy the instance on Azure Database.
Once you click on the finish, one background process will start which will
deploy the instance in the cloud and monitor the progress of the deployment.

View File

@ -1,11 +1,12 @@
.. _cloud_deployment:
******************************
`Cloud Deployment`:index:
******************************
*************************************
`PostgreSQL Cloud Deployment`:index:
*************************************
A PostgreSQL server can be deployed on the Amazon AWS and EDB BigAnimal
cloud using this module. In future more cloud options will be available.
A PostgreSQL server can be deployed on the Amazon, EDB BigAnimal, Azure,
Google cloud using this module. In future more cloud provider options will
be available.
To launch the *Cloud Deployment...* tool, right click on the *Server Group* or
*Server* of the tree control, and select *Deploy a Cloud Instance* from the
@ -16,4 +17,5 @@ To launch the *Cloud Deployment...* tool, right click on the *Server Group* or
cloud_aws_rds
cloud_edb_biganimal
cloud_azure_postgresql
cloud_azure_database
cloud_google_cloud_sql

View File

@ -6,7 +6,7 @@
To deploy a PostgreSQL server on the EDB BigAnimal cloud, follow the below steps.
.. image:: images/cloud_biganimal_provider.png
.. image:: images/cloud_provider_for_postgresql.png
:alt: Cloud Deployment Provider
:align: center

View File

@ -0,0 +1,117 @@
.. cloud_google_cloud_sql:
************************************************
`Google Cloud SQL Deployment`:index:
************************************************
To deploy a PostgreSQL server on the Google Cloud SQL, follow the below steps.
.. image:: images/cloud_provider_for_postgresql.png
:alt: Cloud Deployment
:align: center
Once you launch the tool, select the Google Cloud SQL option.
Click on the *Next* button to proceed further.
.. image:: images/cloud_google_credentials.png
:alt: Cloud Deployment
:align: center
In the Credentials dialog, select client secret file to authenticate
using google.You can download a client secret which is json formatted file
from google cloud console once OAuth2 client ID is created.
.. note:: While creating client OAuth client ID, select Desktop App as application type.
Refer `this <https://support.google.com/cloud/answer/6158849?hl=en/>`_ link for creating client secret.
Clicking the *Click here to authenticate yourself to Google*
button, user will be redirected to the Google authentication page in a
new browser tab.
Once authentication is completed, click on the next button to proceed.
.. image:: images/cloud_google_instance.png
:alt: Cloud Deployment
:align: center
Use the fields from the Instance Specification tab to specify the Instance
details.
* Use the *Cluster name* field to add a name for the PostgreSQL
server; the name specified will be displayed in the *Browser* tree control too.
* Select the project from *project* dropdown under which the
PostgreSQL instance will be created.
* Select the location to deploy PostgreSQL instance from *Location*
options.
* Select the availability zone in specified region to deploy PostgreSQL
instance from *Availability zone* options.
* Use *Database version* options to specify PostgreSQL database version.
* Use the *Instance class* field to allocate the computational and
memory capacity required by planned workload of this DB instance.
* Use the *Instance type* field to select the instance type.
* Specify storage type by selecting option from *Storage type*.
* Use the *Storage capacity* option to specify the storage capacity.
.. image:: images/cloud_google_network.png
:alt: Cloud Deployment
:align: center
* Use the *Public IP* field to specify the list of IP address range
for allowed inbound traffic, for example: 127.0.0.1/32. Add multiple
IP addresses/ranges separated with commas.
* Use the *High Availability* option to specify High Availability
option. This option creates a standby in a select Secondary
Availability Zone.
* Select the secondary availability zone for high availability
from *Secondary Availability zone* options.
.. image:: images/cloud_google_database.png
:alt: Cloud Deployment
:align: center
Use the fields from the Database Details tab to specify the PostgreSQL database details.
* Use the drop-down list in the *pgAdmin server group* field to select the parent
node for the server; the server will be displayed in the *Browser* tree
control within the specified group.
* Admin username field will be default to postgres.
server.
* Use the *Password* field to provide a password that will be supplied when
authenticating with the server.
* Use the *Confirm password* field to repeat the password.
Click on the next button to proceed.
.. image:: images/cloud_google_review.png
:alt: Cloud Deployment
:align: center
At the end, review the instance details that you provided. Click on Finish
button to deploy the instance on Azure PostgreSQL.
Once you click on the finish, one background process will start which will
deploy the instance in the cloud and monitor the progress of the deployment.
You can view all the background process with there running status and logs
on the :ref:`Processes <processes>` tab
The Server will be added to the tree with the cloud deployment icon. Once the
deployment is done, the server details will be updated.
.. image:: images/cloud_google_deployment_tree.png
:alt: Cloud Deployment Provider
:align: center

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@ -62,3 +62,5 @@ azure-mgmt-rdbms==10.1.0
azure-mgmt-resource==21.0.0
azure-mgmt-subscription==3.0.0
azure-identity==1.9.0
google-api-python-client==2.*
google-auth-oauthlib==1.0.0

View File

@ -0,0 +1,223 @@
# ##########################################################################
# #
# # pgAdmin 4 - PostgreSQL Tools
# #
# # Copyright (C) 2013 - 2023, The pgAdmin Development Team
# # This software is released under the PostgreSQL Licence
# #
# ##########################################################################
import json
import os
import time
from utils.io import debug, error, output
from utils.misc import get_my_ip, get_random_id
from providers._abstract import AbsProvider
from googleapiclient import discovery
from googleapiclient.errors import HttpError
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
class GoogleProvider(AbsProvider):
def __init__(self):
self._credentials_json = None
self._credentials = None
self._cloud_resource_manager_api_version = 'v1'
self._sqladmin_api_version = 'v1'
self._compute_api_version = 'v1'
self._scopes = ['https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/sqlservice.admin']
self._database_password = None
# Get credentials from environment
if 'GOOGLE_CREDENTIALS' in os.environ:
self._credentials_json = \
json.loads(os.environ['GOOGLE_CREDENTIALS'])
if 'GOOGLE_DATABASE_PASSWORD' in os.environ:
self._database_password = os.environ['GOOGLE_DATABASE_PASSWORD']
def init_args(self, parsers):
""" Create the command line parser for this provider """
self.parser = parsers.add_parser('google',
help='Google Cloud PostgreSQL',
epilog='Credentials are read from '
'the environment.')
# Create the command sub-parser
parsers = self.parser.add_subparsers(help='Google commands',
dest='command')
# Create the create instance command parser
parser_create_instance = parsers.add_parser('create-instance',
help='create a new '
'instance')
parser_create_instance.add_argument('--region', help='name of the '
'Google region')
parser_create_instance.add_argument('--project', required=True,
help='name of the project in which'
' instance to be created')
parser_create_instance.add_argument('--name', required=True,
help='name of the instance')
parser_create_instance.add_argument('--db-password', required=False,
help='password for the database')
parser_create_instance.add_argument('--db-version', required=False,
default='POSTGRES_14',
help='version of PostgreSQL to '
'deploy (default: POSTGRES_14)')
parser_create_instance.add_argument('--instance-type', required=True,
help='machine type for the '
'instance nodes, e.g. '
'db-f1-micro')
parser_create_instance.add_argument('--storage-size', type=int,
required=True,
help='storage size in GB')
parser_create_instance.add_argument('--storage-type', default='PD_SSD',
help='storage type for the data '
'database (default: SSD)')
parser_create_instance.add_argument('--high-availability',
default=False)
parser_create_instance.add_argument('--public-ip', default='127.0.0.1',
help='Public IP '
'(default: 127.0.0.1)')
parser_create_instance.add_argument('--availability-zone',
help='name of the availability '
'zone')
parser_create_instance.add_argument('--secondary-availability-zone',
help='name of the secondary '
'availability zone')
##########################################################################
# Google Helper functions
##########################################################################
def _get_credentials(self, scopes):
self._credentials = Credentials.from_authorized_user_info(
self._credentials_json, scopes)
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
else:
from google_auth_oauthlib.flow import InstalledAppFlow
flow = InstalledAppFlow.from_client_config(self._client_config,
scopes)
self._credentials = flow.run_local_server()
return self._credentials
@staticmethod
def get_authorized_network_list(ip):
authorized_networks = []
ip = ip.split(',')
for i in ip:
authorized_networks.append({
'value': i,
'name': 'pgcloud client {}'.format(i),
'kind': 'sql#aclEntry'
})
return authorized_networks
def _create_google_postgresql_instance(self, args):
credentials = self._get_credentials(self._scopes)
service = discovery.build('sqladmin', 'v1beta4',
credentials=credentials)
high_availability = \
'REGIONAL' if eval(args.high_availability) else 'ZONAL'
db_password = self._database_password \
if self._database_password is not None else args.db_password
ip = args.public_ip if args.public_ip else '{}/32'.format(get_my_ip())
authorized_networks = self.get_authorized_network_list(ip)
database_instance_body = {
'databaseVersion': args.db_version,
'instanceType': 'CLOUD_SQL_INSTANCE',
'project': args.project,
'name': args.name,
'region': args.region,
'gceZone': args.availability_zone,
'secondaryGceZone': args.secondary_availability_zone,
"rootPassword": db_password,
'settings': {
'tier': args.instance_type,
'availabilityType': high_availability,
'dataDiskType': args.storage_type,
'dataDiskSizeGb': args.storage_size,
'ipConfiguration': {
"authorizedNetworks": authorized_networks,
'ipv4Enabled': True
},
}
}
operation = None
try:
debug('Creating Google instance: {}...'.format(args.name))
req = service.instances().insert(project=args.project,
body=database_instance_body)
res = req.execute()
operation = res['name']
except HttpError as err:
if err.status_code == 409:
error('Google SQL instance {} already exists.'.
format(args.name))
else:
error(str(err))
except Exception as e:
error(str(e))
# Wait for completion
instance_provisioning = True
log_msg = 10000
while instance_provisioning:
req = service.operations().get(project=args.project,
operation=operation)
res = req.execute()
status = res['status']
if status != 'PENDING' and status != 'RUNNING':
instance_provisioning = False
else:
time.sleep(5)
log_msg -= 1
if log_msg % 15 == 0:
debug('Creating Google instance: {}...'.format(args.name))
req = service.instances().get(project=args.project, instance=args.name)
res = req.execute()
return res
##########################################################################
# User commands
##########################################################################
def cmd_create_instance(self, args):
""" Create an Google instance"""
instance_data = self._create_google_postgresql_instance(args)
data = {'instance': {
'Hostname': instance_data['ipAddresses'][0]['ipAddress'],
'Port': 5432,
'Database': 'postgres',
'Username': 'postgres',
}}
output(data)
def load():
""" Loads the current provider """
return GoogleProvider()

View File

@ -27,6 +27,7 @@ from pgadmin.misc.cloud.biganimal import deploy_on_biganimal,\
clear_biganimal_session
from pgadmin.misc.cloud.rds import deploy_on_rds, clear_aws_session
from pgadmin.misc.cloud.azure import deploy_on_azure, clear_azure_session
from pgadmin.misc.cloud.google import clear_google_session, deploy_on_google
import config
# set template path for sql scripts
@ -78,6 +79,9 @@ class CloudModule(PgAdminModule):
from .rds import blueprint as module
app.register_blueprint(module)
from .google import blueprint as module
app.register_blueprint(module)
# Create blueprint for CloudModule class
blueprint = CloudModule(
@ -135,6 +139,8 @@ def deploy_on_cloud():
status, p, resp = deploy_on_biganimal(data)
elif data['cloud'] == 'azure':
status, p, resp = deploy_on_azure(data)
elif data['cloud'] == 'google':
status, p, resp = deploy_on_google(data)
else:
status = False
resp = gettext('No cloud implementation.')
@ -214,6 +220,7 @@ def clear_cloud_session(pid=None):
clear_aws_session()
clear_biganimal_session()
clear_azure_session(pid)
clear_google_session()
@blueprint.route(

View File

@ -0,0 +1,532 @@
# ##########################################################################
# #
# # pgAdmin 4 - PostgreSQL Tools
# #
# # Copyright (C) 2013 - 2023, The pgAdmin Development Team
# # This software is released under the PostgreSQL Licence
# #
# ##########################################################################
# Google Cloud Deployment Implementation
import pickle
import json
import os
from pathlib import Path
from oauthlib.oauth2 import AccessDeniedError
from config import root
from pgadmin.utils.csrf import pgCSRFProtect
from pgadmin import make_json_response
from pgadmin.utils.ajax import plain_text_response
from pgadmin.misc.bgprocess import BatchProcess
from pgadmin.misc.cloud.utils import _create_server, CloudProcessDesc
from pgadmin.utils import PgAdminModule
from flask_security import login_required
from flask import session, current_app, request
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_own_stylesheets(self):
"""
Returns:
list: the stylesheets used by this module.
"""
stylesheets = []
return stylesheets
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('/verify_credentials/',
methods=['POST'], endpoint='verify_credentials')
@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 = True
error = None
res_data = {}
if client_secret_path is not None and Path(client_secret_path).exists():
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
auth_url, error_msg = _google.get_auth_url(request.host_url)
if error_msg:
status = False
error = error_msg
else:
res_data = {'auth_url': auth_url}
# save google object
session['google']['client_config'] = client_config
session['google']['google_obj'] = pickle.dumps(_google, -1)
else:
status = False
error = 'Client secret path not found'
return make_json_response(success=status, errormsg=error, data=res_data)
@blueprint.route('/callback',
methods=['GET'], endpoint='callback')
@pgCSRFProtect.exempt
@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')
@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')
@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')
@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')
@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')
@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')
@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

View File

@ -24,10 +24,11 @@ import { PrimaryButton } from '../../../../static/js/components/Buttons';
import {AwsCredentials, AwsInstanceDetails, AwsDatabaseDetails, validateCloudStep1, validateCloudStep2, validateCloudStep3} from './aws';
import {BigAnimalInstance, BigAnimalDatabase, BigAnimalClusterType, getProviderOptions, validateBigAnimal, validateBigAnimalStep2, validateBigAnimalStep3, validateBigAnimalStep4} from './biganimal';
import { isEmptyString } from 'sources/validators';
import { AWSIcon, BigAnimalIcon, AzureIcon } from '../../../../static/js/components/ExternalIcon';
import { AWSIcon, BigAnimalIcon, AzureIcon, GoogleCloudIcon } from '../../../../static/js/components/ExternalIcon';
import {AzureCredentials, AzureInstanceDetails, AzureDatabaseDetails, checkClusternameAvailbility, validateAzureStep2, validateAzureStep3} from './azure';
import { GoogleCredentials, GoogleInstanceDetails, GoogleDatabaseDetails, validateGoogleStep2, validateGoogleStep3 } from './google';
import EventBus from '../../../../static/js/helpers/EventBus';
import { CLOUD_PROVIDERS } from './cloud_constants';
import { CLOUD_PROVIDERS, CLOUD_PROVIDERS_LABELS } from './cloud_constants';
const useStyles = makeStyles(() =>
@ -63,10 +64,8 @@ const useStyles = makeStyles(() =>
export const CloudWizardEventsContext = React.createContext();
export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel}) {
const classes = useStyles();
const eventBus = React.useRef(new EventBus());
let steps = [gettext('Cloud Provider'), gettext('Credentials'), gettext('Cluster Type'),
@ -81,6 +80,7 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel})
const [hostIP, setHostIP] = React.useState('127.0.0.1/32');
const [cloudProvider, setCloudProvider] = React.useState('');
const [verificationIntiated, setVerificationIntiated] = React.useState(false);
const [bigAnimalInstanceData, setBigAnimalInstanceData] = React.useState({});
const [bigAnimalDatabaseData, setBigAnimalDatabaseData] = React.useState({});
const [bigAnimalClusterTypeData, setBigAnimalClusterTypeData] = React.useState({});
@ -90,6 +90,10 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel})
const [azureInstanceData, setAzureInstanceData] = React.useState({});
const [azureDatabaseData, setAzureDatabaseData] = React.useState({});
const [googleCredData, setGoogleCredData] = React.useState({});
const [googleInstanceData, setGoogleInstanceData] = React.useState({});
const [googleDatabaseData, setGoogleDatabaseData] = React.useState({});
const axiosApi = getApiInstance();
const [verificationURI, setVerificationURI] = React.useState('');
@ -128,7 +132,7 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel})
let _url = url_for('cloud.deploy_on_cloud'),
post_data = {};
if (cloudProvider == CLOUD_PROVIDERS.RDS) {
if (cloudProvider == CLOUD_PROVIDERS.AWS) {
post_data = {
gid: nodeInfo.server_group._id,
cloud: cloudProvider,
@ -144,6 +148,14 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel})
instance_details:azureInstanceData,
db_details: azureDatabaseData
};
}else if(cloudProvider == CLOUD_PROVIDERS.GOOGLE){
post_data = {
gid: nodeInfo.server_group._id,
secret: googleCredData,
cloud: cloudProvider,
instance_details:googleInstanceData,
db_details: googleDatabaseData
};
}else {
post_data = {
@ -170,10 +182,10 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel})
setCallRDSAPI(currentStep);
let isError = (cloudProvider == '');
switch(cloudProvider) {
case CLOUD_PROVIDERS.RDS:
case CLOUD_PROVIDERS.AWS:
switch (currentStep) {
case 0:
setCloudSelection(CLOUD_PROVIDERS.RDS);
setCloudSelection(CLOUD_PROVIDERS.AWS);
break;
case 1:
isError = validateCloudStep1(cloudDBCred);
@ -231,13 +243,32 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel})
break;
}
break;
case CLOUD_PROVIDERS.GOOGLE:
switch (currentStep) {
case 0:
setCloudSelection(CLOUD_PROVIDERS.GOOGLE);
break;
case 1:
isError = !verificationIntiated;
break;
case 2:
break;
case 3:
isError = validateGoogleStep2(googleInstanceData);
break;
case 4:
isError = validateGoogleStep3(googleDatabaseData, nodeInfo);
break;
default:
break;
}
}
return isError;
};
const onBeforeBack = (activeStep) => {
return new Promise((resolve)=>{
if(activeStep == 3 && (cloudProvider == CLOUD_PROVIDERS.RDS || cloudProvider == CLOUD_PROVIDERS.AZURE)) {
if(activeStep == 3 && (cloudProvider == CLOUD_PROVIDERS.AWS || cloudProvider == CLOUD_PROVIDERS.AZURE || cloudProvider == CLOUD_PROVIDERS.GOOGLE)) {
resolve(true);
}
resolve();
@ -246,7 +277,7 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel})
const onBeforeNext = (activeStep) => {
return new Promise((resolve, reject)=>{
if(activeStep == 1 && cloudProvider == CLOUD_PROVIDERS.RDS) {
if(activeStep == 1 && cloudProvider == CLOUD_PROVIDERS.AWS) {
setErrMsg([MESSAGE_TYPE.INFO, gettext('Validating credentials...')]);
let _url = url_for('rds.verify_credentials');
const post_data = {
@ -298,6 +329,7 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel})
} else if (cloudProvider == CLOUD_PROVIDERS.AZURE) {
if (activeStep == 1) {
// Skip the current step
setErrMsg(['', '']);
resolve(true);
} else if (activeStep == 2) {
setErrMsg([MESSAGE_TYPE.INFO, gettext('Checking cluster name availability...')]);
@ -316,6 +348,14 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel})
} else {
resolve();
}
}else if (cloudProvider == CLOUD_PROVIDERS.GOOGLE) {
if (activeStep == 1) {
// Skip the current step
setErrMsg(['', '']);
resolve(true);
} else if (activeStep == 2) { resolve(true);} else {
resolve();
}
}
else {
setErrMsg(['', '']);
@ -375,9 +415,11 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel})
setErrMsg([]);
});
let cloud_providers = [{label: gettext('Amazon RDS'), value: CLOUD_PROVIDERS.RDS, icon: <AWSIcon className={classes.icon} />},
{label: gettext('EDB BigAnimal'), value: CLOUD_PROVIDERS.BIGANIMAL, icon: <BigAnimalIcon className={classes.icon} />},
{'label': gettext('Azure PostgreSQL'), value: CLOUD_PROVIDERS.AZURE, icon: <AzureIcon className={classes.icon} /> }];
let cloud_providers = [
{label: gettext(CLOUD_PROVIDERS_LABELS.AWS), value: CLOUD_PROVIDERS.AWS, icon: <AWSIcon className={classes.icon} />},
{label: gettext(CLOUD_PROVIDERS_LABELS.BIGANIMAL), value: CLOUD_PROVIDERS.BIGANIMAL, icon: <BigAnimalIcon className={classes.icon} />},
{label: gettext(CLOUD_PROVIDERS_LABELS.AZURE), value: CLOUD_PROVIDERS.AZURE, icon: <AzureIcon className={classes.icon} /> },
{label: gettext(CLOUD_PROVIDERS_LABELS.GOOGLE), value: CLOUD_PROVIDERS.GOOGLE, icon: <GoogleCloudIcon className={classes.icon} /> }];
return (
<CloudWizardEventsContext.Provider value={eventBus.current}>
@ -393,7 +435,7 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel})
beforeBack={onBeforeBack}>
<WizardStep stepId={0}>
<Box className={classes.messageBox}>
<Box className={classes.messagePadding}>{gettext('Select a cloud provider.')}</Box>
<Box className={classes.messagePadding}>{gettext('Select a cloud provider for PostgreSQL database.')}</Box>
</Box>
<Box className={classes.messageBox}>
<ToggleButtons cloudProvider={cloudProvider} setCloudProvider={setCloudProvider}
@ -416,9 +458,13 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel})
<Box ></Box>
</Box>}
</Box>
{cloudProvider == CLOUD_PROVIDERS.RDS && <AwsCredentials cloudProvider={cloudProvider} nodeInfo={nodeInfo} nodeData={nodeData} setCloudDBCred={setCloudDBCred}/>}
{cloudProvider == CLOUD_PROVIDERS.AWS && <AwsCredentials cloudProvider={cloudProvider} nodeInfo={nodeInfo} nodeData={nodeData} setCloudDBCred={setCloudDBCred}/>}
{ cloudProvider == CLOUD_PROVIDERS.AZURE &&
<Box flexGrow={1}>
{cloudProvider == CLOUD_PROVIDERS.AZURE && <AzureCredentials cloudProvider={cloudProvider} nodeInfo={nodeInfo} nodeData={nodeData} setAzureCredData={setAzureCredData}/>}
<AzureCredentials cloudProvider={cloudProvider} nodeInfo={nodeInfo} nodeData={nodeData} setAzureCredData={setAzureCredData}/>
</Box>}
<Box flexGrow={1}>
{cloudProvider == CLOUD_PROVIDERS.GOOGLE && <GoogleCredentials cloudProvider={cloudProvider} nodeInfo={nodeInfo} nodeData={nodeData} setGoogleCredData={setGoogleCredData}/>}
</Box>
<FormFooterMessage type={errMsg[0]} message={errMsg[1]} onClose={onErrClose} />
</WizardStep>
@ -434,7 +480,7 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel})
<FormFooterMessage type={errMsg[0]} message={errMsg[1]} onClose={onErrClose} />
</WizardStep>
<WizardStep stepId={3} >
{cloudProvider == CLOUD_PROVIDERS.RDS && callRDSAPI == 3 && <AwsInstanceDetails
{cloudProvider == CLOUD_PROVIDERS.AWS && callRDSAPI == 3 && <AwsInstanceDetails
cloudProvider={cloudProvider}
nodeInfo={nodeInfo}
nodeData={nodeData}
@ -456,10 +502,18 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel})
hostIP={hostIP}
azureInstanceData = {azureInstanceData}
/> }
{cloudProvider == CLOUD_PROVIDERS.GOOGLE && callRDSAPI == 3 && <GoogleInstanceDetails
cloudProvider={cloudProvider}
nodeInfo={nodeInfo}
nodeData={nodeData}
setGoogleInstanceData={setGoogleInstanceData}
hostIP={hostIP}
googleInstanceData = {googleInstanceData}
/> }
<FormFooterMessage type={errMsg[0]} message={errMsg[1]} onClose={onErrClose} />
</WizardStep>
<WizardStep stepId={4} >
{cloudProvider == CLOUD_PROVIDERS.RDS && <AwsDatabaseDetails
{cloudProvider == CLOUD_PROVIDERS.AWS && <AwsDatabaseDetails
cloudProvider={cloudProvider}
nodeInfo={nodeInfo}
nodeData={nodeData}
@ -481,11 +535,18 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel})
setAzureDatabaseData={setAzureDatabaseData}
/>
}
{cloudProvider == CLOUD_PROVIDERS.GOOGLE && <GoogleDatabaseDetails
cloudProvider={cloudProvider}
nodeInfo={nodeInfo}
nodeData={nodeData}
setGoogleDatabaseData={setGoogleDatabaseData}
/>
}
</WizardStep>
<WizardStep stepId={5} >
<Box className={classes.boxText}>{gettext('Please review the details before creating the cloud instance.')}</Box>
<Paper variant="outlined" elevation={0} className={classes.summaryContainer}>
{cloudProvider == CLOUD_PROVIDERS.RDS && callRDSAPI == 5 && <FinalSummary
{cloudProvider == CLOUD_PROVIDERS.AWS && callRDSAPI == 5 && <FinalSummary
cloudProvider={cloudProvider}
instanceData={cloudInstanceDetails}
databaseData={cloudDBDetails}
@ -504,6 +565,12 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose, cloudPanel})
databaseData={azureDatabaseData}
/>
}
{cloudProvider == CLOUD_PROVIDERS.GOOGLE && callRDSAPI == 5 && <FinalSummary
cloudProvider={cloudProvider}
instanceData={googleInstanceData}
databaseData={googleDatabaseData}
/>
}
</Paper>
</WizardStep>
</Wizard>

View File

@ -74,7 +74,8 @@ export function AzureCredentials(props) {
.then((res)=>{
if (res.data.success){
clearInterval(interval);
window.open(res.data.data.verification_uri, 'azure_authentication');
let params = 'scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no, width=550,height=650,left=920,top=150';
window.open(res.data.data.verification_uri, 'azure_authentication', params);
resolve(res);
}
})

View File

@ -76,7 +76,7 @@ define('pgadmin.misc.cloud', [
// Register dialog panel
pgBrowser.Node.registerUtilityPanel();
let panel = pgBrowser.Node.addUtilityPanel(920, 650),
let panel = pgBrowser.Node.addUtilityPanel(930, 650),
j = panel.$container.find('.obj_properties').first();
panel.title(gettext('Deploy Cloud Instance'));

View File

@ -20,13 +20,23 @@ import { commonTableStyles } from '../../../../static/js/Theme';
import { Table, TableBody, TableCell, TableHead, TableRow } from '@material-ui/core';
import clsx from 'clsx';
import gettext from 'sources/gettext';
import { getGoogleSummary } from './google';
import { CLOUD_PROVIDERS_LABELS } from './cloud_constants';
const useStyles = makeStyles(() =>
({
toggleButton: {
toggleButtonGroup: {
height: '100px',
flexGrow: '1'
},
toggleButtonMargin:{
marginTop: '0px !important',
padding: '12px'
},
gcpiconpadding:{
paddingLeft: '1.5rem'
}
}),
);
@ -43,13 +53,14 @@ export function ToggleButtons(props) {
color="primary"
value={props.cloudProvider}
onChange={handleCloudProvider}
className={classes.toggleButton}
className={classes.toggleButtonGroup}
orientation="vertical"
exclusive>
{
(props.options||[]).map((option)=>{
return (<ToggleButton value={option.value} key={option.label} aria-label={option.label} component={props.cloudProvider == option.value ? PrimaryButton : DefaultButton}>
return (<ToggleButton value={option.value} key={option.label} aria-label={option.label} className={clsx(classes.toggleButtonMargin, option.label==gettext(CLOUD_PROVIDERS_LABELS.GOOGLE) ? classes.gcpiconpadding : null )} component={props.cloudProvider == option.value ? PrimaryButton : DefaultButton}>
<CheckRoundedIcon style={{visibility: props.cloudProvider == option.value ? 'visible': 'hidden'}}/>&nbsp;
{option.icon}&nbsp;&nbsp;{option.label}
{option.icon}&nbsp;&nbsp;&nbsp;&nbsp;{option.label}
</ToggleButton>);
})
}
@ -74,6 +85,9 @@ export function FinalSummary(props) {
} else if(props.cloudProvider == 'azure') {
summaryHeader.push('Network Connectivity','Availability');
summary = getAzureSummary(props.cloudProvider, props.instanceData, props.databaseData);
}else if(props.cloudProvider == 'google') {
summaryHeader.push('Network Connectivity','Availability');
summary = getGoogleSummary(props.cloudProvider, props.instanceData, props.databaseData);
}else {
summaryHeader.push('Availability');
summary = getAWSSummary(props.cloudProvider, props.instanceData, props.databaseData);

View File

@ -11,5 +11,12 @@ export const CLOUD_PROVIDERS = {
AZURE: 'azure',
BIGANIMAL: 'biganimal',
AWS: 'aws',
RDS: 'rds',
GOOGLE: 'google',
};
export const CLOUD_PROVIDERS_LABELS = {
AZURE: 'Azure Database',
BIGANIMAL: 'EDB BigAnimal',
AWS: 'Amazon RDS',
GOOGLE: 'Google Cloud SQL',
};

View File

@ -0,0 +1,314 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2023, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import {GoogleCredSchema, GoogleClusterSchema, GoogleDatabaseSchema} from './google_schema.ui';
import pgAdmin from 'sources/pgadmin';
import { getNodeAjaxOptions, getNodeListById } from 'pgbrowser/node_ajax';
import SchemaView from '../../../../static/js/SchemaView';
import url_for from 'sources/url_for';
import { isEmptyString } from 'sources/validators';
import PropTypes from 'prop-types';
import getApiInstance from '../../../../static/js/api_instance';
import { CloudWizardEventsContext } from './CloudWizard';
import {MESSAGE_TYPE } from '../../../../static/js/components/FormComponents';
import gettext from 'sources/gettext';
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles(() =>
({
formClass: {
overflow: 'auto',
}
}),
);
export function GoogleCredentials(props) {
const [cloudDBCredInstance, setCloudDBCredInstance] = React.useState();
let _eventBus = React.useContext(CloudWizardEventsContext);
let child = null;
React.useMemo(() => {
const googleCredSchema = new GoogleCredSchema({
authenticateGoogle:(client_secret_file) => {
let loading_icon_url = url_for(
'static', { 'filename': 'img/loading.gif'}
);
const axiosApi = getApiInstance();
_eventBus.fireEvent('SET_ERROR_MESSAGE_FOR_CLOUD_WIZARD', [MESSAGE_TYPE.INFO, 'Google authentication process is in progress..<img src="' + loading_icon_url + '" alt="' + gettext('Loading...') + '">']);
let _url = url_for('google.verify_credentials');
const post_data = {
cloud: 'google',
secret: {'client_secret_file':client_secret_file}
};
return new Promise((resolve, reject)=>{axiosApi.post(_url, post_data)
.then((res) => {
if (res.data && res.data.success == 1 ) {
_eventBus.fireEvent('SET_CRED_VERIFICATION_INITIATED',true);
let params = 'scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no, width=550,height=650,left=600,top=150';
child = window.open(res.data.data.auth_url, 'google_authentication', params);
resolve(true);
}
else if (res.data && res.data.success == 0) {
_eventBus.fireEvent('SET_ERROR_MESSAGE_FOR_CLOUD_WIZARD',[MESSAGE_TYPE.ERROR, res.data.errormsg]);
_eventBus.fireEvent('SET_CRED_VERIFICATION_INITIATED',false);
resolve(false);
}
})
.catch((error) => {
_eventBus.fireEvent('SET_ERROR_MESSAGE_FOR_CLOUD_WIZARD',[MESSAGE_TYPE.ERROR, gettext(`Error while authentication: ${error}`)]);
reject(false);
});
});
},
verification_ack:()=>{
let auth_url = url_for('google.verification_ack');
let countdown = 90;
const axiosApi = getApiInstance();
return new Promise((resolve, reject)=>{
const interval = setInterval(()=>{
axiosApi.get(auth_url)
.then((res)=>{
if (res.data.success && res.data.success == 1 ){
_eventBus.fireEvent('SET_ERROR_MESSAGE_FOR_CLOUD_WIZARD',[MESSAGE_TYPE.SUCCESS, gettext('Authentication completed successfully. Click the Next button to proceed.')]);
clearInterval(interval);
if(child){
// close authentication window
child.close();
}
resolve();
} else if (res.data && res.data.success == 0 && (res.data.errormsg == 'Invalid state parameter.' || res.data.errormsg == 'Access denied.' || res.data.errormsg == 'Authentication is failed.')){
_eventBus.fireEvent('SET_ERROR_MESSAGE_FOR_CLOUD_WIZARD',[MESSAGE_TYPE.ERROR, res.data.errormsg]);
_eventBus.fireEvent('SET_CRED_VERIFICATION_INITIATED',false);
clearInterval(interval);
resolve(false);
} else if (child && child.closed || countdown <= 0) {
_eventBus.fireEvent('SET_ERROR_MESSAGE_FOR_CLOUD_WIZARD',[MESSAGE_TYPE.ERROR, 'Authentication is aborted.']);
_eventBus.fireEvent('SET_CRED_VERIFICATION_INITIATED',false);
clearInterval(interval);
}
})
.catch((error)=>{
clearInterval(interval);
reject(error);
});
countdown = countdown - 1;
}, 1000);
});
}
}, {}, _eventBus);
setCloudDBCredInstance(googleCredSchema);
}, [props.cloudProvider]);
return <SchemaView
formType={'dialog'}
getInitData={() => { /*This is intentional (SonarQube)*/ }}
viewHelperProps={{ mode: 'create' }}
schema={cloudDBCredInstance}
showFooter={false}
isTabView={false}
onDataChange={(isChanged, changedData) => {
props.setGoogleCredData(changedData);
}}
/>;
}
GoogleCredentials.propTypes = {
nodeInfo: PropTypes.object,
nodeData: PropTypes.object,
cloudProvider: PropTypes.string,
setGoogleCredData: PropTypes.func
};
// Google Instance
export function GoogleInstanceDetails(props) {
const [googleInstanceSchema, setGoogleInstanceSchema] = React.useState();
const classes = useStyles();
React.useMemo(() => {
const GoogleClusterSchemaObj = new GoogleClusterSchema({
projects: () => getNodeAjaxOptions('get_projects', {}, {}, {},{
useCache:false,
cacheNode: 'server',
customGenerateUrl: ()=>{
return url_for('google.projects');
}
}),
regions: (project)=>getNodeAjaxOptions('get_regions', pgAdmin.Browser.Nodes['server'], props.nodeInfo, props.nodeData,{
useCache:false,
cacheNode: 'server',
customGenerateUrl: ()=>{
return url_for('google.regions', {'project_id': project});
}
}),
availabilityZones: (region)=>getNodeAjaxOptions('get_availability_zones', pgAdmin.Browser.Nodes['server'], props.nodeInfo, props.nodeData, {
useCache:false,
cacheNode: 'server',
customGenerateUrl: ()=>{
return url_for('google.availability_zones', {'region': region});
}
}),
dbVersions: ()=>getNodeAjaxOptions('get_db_versions', pgAdmin.Browser.Nodes['server'], props.nodeInfo, props.nodeData, {
useCache:false,
cacheNode: 'server',
customGenerateUrl: ()=>{
return url_for('google.database_versions');
}
}),
instanceTypes: (project, region, instanceClass)=>{
if (isEmptyString(project) || isEmptyString(region) || isEmptyString(instanceClass)) return [];
return getNodeAjaxOptions('get_instance_types', pgAdmin.Browser.Nodes['server'], props.nodeInfo, props.nodeData, {
useCache:false,
cacheNode: 'server',
customGenerateUrl: ()=>{
return url_for('google.instance_types', {'project_id':project, 'region': region, 'instance_class': instanceClass});
}
});},
}, {
nodeInfo: props.nodeInfo,
nodeData: props.nodeData,
hostIP: props.hostIP,
...props.googleInstanceData
});
setGoogleInstanceSchema(GoogleClusterSchemaObj);
}, [props.cloudProvider]);
return <SchemaView
formType={'dialog'}
getInitData={() => { /*This is intentional (SonarQube)*/ }}
viewHelperProps={{ mode: 'create' }}
schema={googleInstanceSchema}
showFooter={false}
isTabView={false}
onDataChange={(isChanged, changedData) => {
props.setGoogleInstanceData(changedData);
}}
formClassName={classes.formClass}
/>;
}
GoogleInstanceDetails.propTypes = {
nodeInfo: PropTypes.object,
nodeData: PropTypes.object,
cloudProvider: PropTypes.string,
setGoogleInstanceData: PropTypes.func,
hostIP: PropTypes.string,
subscriptions: PropTypes.array,
googleInstanceData: PropTypes.object
};
// Google Database Details
export function GoogleDatabaseDetails(props) {
const [gooeleDBInstance, setGoogleDBInstance] = React.useState();
const classes = useStyles();
React.useMemo(() => {
const googleDBSchema = new GoogleDatabaseSchema({
server_groups: ()=>getNodeListById(pgAdmin.Browser.Nodes['server_group'], props.nodeInfo, props.nodeData),
},
{
gid: props.nodeInfo['server_group']._id,
}
);
setGoogleDBInstance(googleDBSchema);
}, [props.cloudProvider]);
return <SchemaView
formType={'dialog'}
getInitData={() => { /*This is intentional (SonarQube)*/ }}
viewHelperProps={{ mode: 'create' }}
schema={gooeleDBInstance}
showFooter={false}
isTabView={false}
onDataChange={(isChanged, changedData) => {
props.setGoogleDatabaseData(changedData);
}}
formClassName={classes.formClass}
/>;
}
GoogleDatabaseDetails.propTypes = {
nodeInfo: PropTypes.object,
nodeData: PropTypes.object,
cloudProvider: PropTypes.string,
setGoogleDatabaseData: PropTypes.func,
};
// Validation functions
export function validateGoogleStep2(cloudInstanceDetails) {
let isError = false;
if (isEmptyString(cloudInstanceDetails.name) ||
isEmptyString(cloudInstanceDetails.db_version) || isEmptyString(cloudInstanceDetails.instance_type) ||
isEmptyString(cloudInstanceDetails.region)|| isEmptyString(cloudInstanceDetails.storage_size) || isEmptyString(cloudInstanceDetails.public_ips)) {
isError = true;
}
return isError;
}
export function validateGoogleStep3(cloudDBDetails, nodeInfo) {
let isError = false;
if (isEmptyString(cloudDBDetails.db_username) || isEmptyString(cloudDBDetails.db_password)) {
isError = true;
}
if (cloudDBDetails.db_password != cloudDBDetails.db_confirm_password) {
isError = true;
}
if (isEmptyString(cloudDBDetails.gid)) cloudDBDetails.gid = nodeInfo['server_group']._id;
return isError;
}
// Summary creation
function createData(name, value) {
if (typeof(value) == 'boolean') {
value = (value === true) ? 'True' : 'False';
}
return { name, value };
}
// Summary section
export function getGoogleSummary(cloud, cloudInstanceDetails, cloudDBDetails) {
let dbVersion = cloudInstanceDetails.db_version;
dbVersion = dbVersion.charAt(0) + dbVersion.slice(1,7).toLowerCase() + 'SQL ' + dbVersion.split('_')[1];
let storageType = cloudInstanceDetails.storage_type.split('_')[1];
const rows1 = [
createData(gettext('Cloud'), cloud),
createData(gettext('Instance name'), cloudInstanceDetails.name),
createData(gettext('Project'), cloudInstanceDetails.project),
createData(gettext('Region'), cloudInstanceDetails.region),
createData(gettext('Availability zone'), cloudInstanceDetails.availability_zone),
];
const rows2 = [
createData(gettext('PostgreSQL version'), dbVersion),
createData(gettext('Instance type'), cloudInstanceDetails.instance_type),
];
const rows3 = [
createData(gettext('Storage type'), storageType),
createData(gettext('Allocated storage'), cloudInstanceDetails.storage_size + ' GB'),
];
const rows4 = [
createData(gettext('Username'), cloudDBDetails.db_username),
createData(gettext('Password'), 'xxxxxxx'),
];
const rows5 = [
createData(gettext('Public IP'), cloudInstanceDetails.public_ips),
];
const rows6 = [
createData(gettext('High availability'), cloudInstanceDetails.high_availability),
createData(gettext('Secondary availability zone'), cloudInstanceDetails.secondary_availability_zone),
];
return [rows1, rows2, rows3, rows4, rows5, rows6];
}

View File

@ -0,0 +1,524 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2023, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import { isEmptyString } from 'sources/validators';
class GoogleCredSchema extends BaseUISchema{
constructor(fieldOptions = {}, initValues = {}, eventBus={}) {
super({
oid: null,
client_secret_file: undefined,
...initValues,
});
this.fieldOptions = {
...fieldOptions,
};
this.eventBus = eventBus;
}
get idAttribute() {
return 'oid';
}
get baseFields() {
let obj = this;
return [
{
id: 'client_secret_file',
label: gettext('Client secret file'),
type: 'file',
helpMessage: gettext('Select a client secrets file containing the client ID, client secret, and other OAuth 2.0 parameters for google authentication. Refer <a href="https://support.google.com/cloud/answer/6158849?hl=en#userconsent&zippy=%2Cuser-consent%2Cpublic-and-internal-applications">link</a> for creating client secret.'),
controlProps: {
dialogType: 'select_file',
supportedTypes: ['json'],
dialogTitle: 'Select file',
},
},
{
id: 'auth_btn',
mode: ['create'],
deps: ['client_secret_file'],
type: 'button',
btnName: gettext('Click here to authenticate yourself to Google'),
helpMessage: gettext('After clicking the button above you will be redirected to the Google authentication page in a new browser tab.'),
disabled: (state)=>{
return state.client_secret_file ? false : true;
},
depChange: ()=> {
return {is_authenticating: true};
},
deferredDepChange: (state, source)=>{
return new Promise((resolve, reject)=>{
/* button clicked */
if(source == 'auth_btn') {
obj.fieldOptions.authenticateGoogle(state.client_secret_file)
.then(()=>{
resolve(()=>({
}));
})
.catch((err)=>{
reject(err);
});
}
});
}
},
{
id: 'is_authenticating',
visible: false,
type: '',
deps:['auth_btn'],
deferredDepChange: (state, source)=>{
return new Promise((resolve, reject)=>{
if(source == 'auth_btn' && state.is_authenticating ) {
obj.fieldOptions.verification_ack()
.then(()=>{
resolve();
})
.catch((err)=>{
reject(err);
});
}
});
},
},
];}
}
class GoogleProjectDetailsSchema extends BaseUISchema {
constructor(fieldOptions = {}, initValues = {}) {
super({
oid: undefined,
project: '',
region: '',
availability_zone: '',
...initValues,
});
this.fieldOptions = {
...fieldOptions,
};
this.initValues = initValues;
}
get idAttribute() {
return 'oid';
}
get baseFields() {
return [
{
id: 'project',
label: gettext('Project'),
mode: ['create'],
allowClear: false,
noEmpty: true,
type: () => {
return {
type: 'select',
options: this.fieldOptions.projects
};
},
},
{
id: 'region',
label: gettext('Location'),
mode: ['create'],
deps: ['project'],
noEmpty: true,
type: (state) => {
return {
type: 'select',
options: state.project
? () => this.fieldOptions.regions(state.project)
: [],
optionsReloadBasis: state.project,
allowClear: false,
};
},
},
{
id: 'availability_zone',
label: gettext('Availability zone'),
deps: ['region'],
allowClear: false,
noEmpty: true,
type: (state) => {
return {
type: 'select',
options: state.region
? () => this.fieldOptions.availabilityZones(state.region)
: [],
optionsReloadBasis: state.region,
};
},
}
];
}
}
class GoogleInstanceSchema extends BaseUISchema {
constructor(fieldOptions = {}, initValues = {}) {
super({
db_version: '',
instance_type: '',
storage_size: '',
});
this.fieldOptions = {
...fieldOptions,
};
this.initValues = initValues;
}
get idAttribute() {
return 'oid';
}
get baseFields() {
return [
{
id: 'db_version',
label: gettext('Database version'),
deps: ['availability_zone'],
type: 'select',
noEmpty: true,
options: this.fieldOptions.dbVersions
},
{
id: 'instance_class',
label: gettext('Instance class'),
type: 'select',
noEmpty: true,
options: [
{
label: gettext('Shared core'),
value: 'shared' },
{
label: gettext('Standard'),
value: 'standard',
},
{
label: gettext('High Memory'),
value: 'highmem',
},
],
},
{
id: 'instance_type',
label: gettext('Instance type'),
deps: ['instance_class'],
noEmpty: true,
type: (state) => {
return {
type: 'select',
allowClear: false,
options: state.instance_class
? () => this.fieldOptions.instanceTypes(state.project, state.region, state.instance_class)
: [],
optionsReloadBasis: state.instance_class
};
},
}
];
}
}
class GoogleStorageSchema extends BaseUISchema {
constructor() {
super({
storage_type: 'SSD',
});
}
get baseFields() {
return [
{
id: 'storage_type',
label: gettext('Storage type'),
type: 'select',
mode: ['create'],
noEmpty: true,
options: [
{'label': gettext('SSD'), 'value': 'PD_SSD'},
{'label': gettext('HDD'), 'value': 'PD_HDD'},
],
},
{
id: 'storage_size',
label: gettext('Storage capacity'),
type: 'text',
mode: ['create'],
noEmpty: true,
deps: ['storage_type'],
helpMessage: gettext('Size in GB.'),
}
];
}
validate(data, setErrMsg) {
if (data.storage_size && (data.storage_size < 9 || data.storage_size > 65536)) {
setErrMsg('storage_size', gettext('Please enter value betwwen 10 and 65,536.'));
return true;
}
return false;
}
}
class GoogleNetworkSchema extends BaseUISchema {
constructor() {
super();
}
get baseFields() {
return [
{
id: 'public_ips',
label: gettext('Public IP range'),
type: 'text',
mode: ['create'],
noEmpty: true,
helpMessage: gettext('IP address range for allowed inbound traffic, for example: 127.0.0.1/32. Add multiple IP addresses/ranges separated with commas.'
),
},
];
}
}
class GoogleHighAvailabilitySchema extends BaseUISchema {
constructor(fieldOptions = {}, initValues = {}) {
super({
oid: undefined,
high_availability: false,
...initValues,
});
this.fieldOptions = {
...fieldOptions,
};
this.initValues = initValues;
}
get idAttribute() {
return 'oid';
}
get baseFields() {
return [
{
id: 'high_availability',
label: gettext('High availability?'),
type: 'switch',
helpMessage: gettext(''),
},
{
id: 'secondary_availability_zone',
label: gettext('Secondary availability zone'),
deps: ['high_availability'],
allowClear: false,
disabled:(state)=> {
if (!state.high_availability){
state.secondary_availability_zone = '';
}
return!state.high_availability;},
type: (state) => {
return {
type: 'select',
options: state.region
? () => this.fieldOptions.availabilityZones(state.region)
: [],
optionsReloadBasis: state.region,
};
},
helpMessage: gettext(''),
}
];
}
validate(data, setErrMsg) {
if (data.high_availability && (isEmptyString(data.secondary_availability_zone))) {
setErrMsg('secondary_availability_zone', gettext('Please select Secondary availability zone.'));
return true;
}
return false;
}
}
class GoogleDatabaseSchema extends BaseUISchema {
constructor(fieldOptions = {}, initValues = {}) {
super({
oid: undefined,
gid: undefined,
db_username: 'postgres',
db_password: '',
db_confirm_password: '',
...initValues,
});
this.fieldOptions = {
...fieldOptions,
};
}
get baseFields() {
return [
{
id: 'gid',
label: gettext('pgAdmin server group'),
type: 'select',
options: this.fieldOptions.server_groups,
mode: ['create'],
controlProps: { allowClear: false },
noEmpty: true,
},
{
id: 'db_username',
label: gettext('Admin username'),
type: 'text',
mode: ['create'],
noEmpty: true,
disabled: true,
helpMessage: gettext(
'Admin username for your Google Cloud Sql PostgreSQL instance.'),
},
{
id: 'db_password',
label: gettext('Password'),
type: 'password',
mode: ['create'],
noEmpty: true,
helpMessage: gettext(
'Set a password for the default admin user "postgres".'
),
},
{
id: 'db_confirm_password',
label: gettext('Confirm password'),
type: 'password',
mode: ['create'],
noEmpty: true,
},
];
}
validate(data, setErrMsg) {
if (!isEmptyString(data.db_password) && !isEmptyString(data.db_confirm_password) && data.db_password != data.db_confirm_password) {
setErrMsg('db_confirm_password', gettext('Passwords do not match.'));
return true;
}
return false;
}
}
class GoogleClusterSchema extends BaseUISchema {
constructor(fieldOptions = {}, initValues = {}) {
super({
oid: undefined,
name: '',
// Need to initilize child class init values in parent class itself
public_ips: initValues?.hostIP,
db_instance_class: undefined,
high_availability: false,
...initValues,
});
this.fieldOptions = {
...fieldOptions,
};
this.initValues = initValues;
this.googleProjectDetails = new GoogleProjectDetailsSchema(
{
projects: this.fieldOptions.projects,
regions: this.fieldOptions.regions,
availabilityZones: this.fieldOptions.availabilityZones,
},
{}
);
this.googleInstanceDetails = new GoogleInstanceSchema(
{
dbVersions: this.fieldOptions.dbVersions,
instanceTypes: this.fieldOptions.instanceTypes,
},
{}
);
this.googleStorageDetails = new GoogleStorageSchema(
{},
{}
);
this.googleNetworkDetails = new GoogleNetworkSchema({}, {});
this.googleHighAvailabilityDetails = new GoogleHighAvailabilitySchema(
{
availabilityZones: this.fieldOptions.availabilityZones,
},
{}
);
}
get idAttribute() {
return 'oid';
}
get baseFields() {
return [
{
id: 'name',
label: gettext('Cluster name'),
type: 'text',
mode: ['create'],
noEmpty: true,
},
{
type: 'nested-fieldset',
label: gettext('Project Details'),
mode: ['create'],
schema: this.googleProjectDetails,
},
{
type: 'nested-fieldset',
label: gettext('Version & Instance'),
mode: ['create'],
schema: this.googleInstanceDetails,
},
{
type: 'nested-fieldset',
label: gettext('Storage'),
mode: ['create'],
schema: this.googleStorageDetails,
},
{
type: 'nested-fieldset',
label: gettext('Network Connectivity'),
mode: ['create'],
schema: this.googleNetworkDetails,
},
{
type: 'nested-fieldset',
label: gettext('Availability'),
mode: ['create'],
schema: this.googleHighAvailabilityDetails,
},
];
}
}
export {GoogleCredSchema, GoogleClusterSchema, GoogleDatabaseSchema};

View File

@ -69,9 +69,11 @@ class CloudProcessDesc(IProcessDesc):
if _provider == 'rds':
self.provider = 'Amazon RDS'
elif _provider == 'azure':
self.provider = 'Azure PostgreSQL'
self.provider = 'Azure Database'
elif _provider == 'google':
self.provider = 'Google Cloud SQL'
else:
self.provider = 'EDB Big Animal'
self.provider = 'EDB BigAnimal'
@property
def message(self):

View File

@ -1 +1 @@
<svg height="2359" width="2500" xmlns="http://www.w3.org/2000/svg" viewBox="-0.4500000000000005 0.38 800.8891043012813 754.2299999999999"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="353.1" x2="107.1" y1="56.3" y2="783"><stop offset="0" stop-color="#114a8b"/><stop offset="1" stop-color="#0669bc"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="429.8" x2="372.9" y1="394.9" y2="414.2"><stop offset="0" stop-opacity=".3"/><stop offset=".1" stop-opacity=".2"/><stop offset=".3" stop-opacity=".1"/><stop offset=".6" stop-opacity=".1"/><stop offset="1" stop-opacity="0"/></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="398.4" x2="668.4" y1="35.1" y2="754.4"><stop offset="0" stop-color="#3ccbf4"/><stop offset="1" stop-color="#2892df"/></linearGradient><path d="M266.71.4h236.71L257.69 728.9a37.8 37.8 0 0 1-5.42 10.38c-2.33 3.16-5.14 5.93-8.33 8.22s-6.71 4.07-10.45 5.27-7.64 1.82-11.56 1.82H37.71c-5.98 0-11.88-1.42-17.2-4.16A37.636 37.636 0 0 1 7.1 738.87a37.762 37.762 0 0 1-6.66-16.41c-.89-5.92-.35-11.97 1.56-17.64L230.94 26.07c1.25-3.72 3.08-7.22 5.42-10.38 2.33-3.16 5.15-5.93 8.33-8.22 3.19-2.29 6.71-4.07 10.45-5.27S262.78.38 266.7.38v.01z" fill="url(#a)"/><path d="M703.07 754.59H490.52c-2.37 0-4.74-.22-7.08-.67-2.33-.44-4.62-1.1-6.83-1.97s-4.33-1.95-6.34-3.21a38.188 38.188 0 0 1-5.63-4.34l-241.2-225.26a17.423 17.423 0 0 1-5.1-8.88 17.383 17.383 0 0 1 7.17-18.21c2.89-1.96 6.3-3.01 9.79-3.01h375.36l92.39 265.56z" fill="#0078d4"/><path d="M504.27.4l-165.7 488.69 270.74-.06 92.87 265.56H490.43c-2.19-.02-4.38-.22-6.54-.61s-4.28-.96-6.34-1.72a38.484 38.484 0 0 1-11.36-6.51L303.37 593.79l-45.58 134.42c-1.18 3.36-2.8 6.55-4.82 9.48a40.479 40.479 0 0 1-16.05 13.67 40.03 40.03 0 0 1-10.13 3.23H37.82c-6.04.02-12-1.42-17.37-4.2A37.664 37.664 0 0 1 .43 722a37.77 37.77 0 0 1 1.87-17.79L230.87 26.58c1.19-3.79 2.98-7.36 5.3-10.58 2.31-3.22 5.13-6.06 8.33-8.4s6.76-4.16 10.53-5.38S262.75.38 266.72.4h237.56z" fill="url(#b)"/><path d="M797.99 704.82a37.847 37.847 0 0 1 1.57 17.64 37.867 37.867 0 0 1-6.65 16.41 37.691 37.691 0 0 1-30.61 15.72H498.48c5.98 0 11.88-1.43 17.21-4.16 5.32-2.73 9.92-6.7 13.41-11.56s5.77-10.49 6.66-16.41.35-11.97-1.56-17.64L305.25 26.05a37.713 37.713 0 0 0-13.73-18.58c-3.18-2.29-6.7-4.06-10.43-5.26S273.46.4 269.55.4h263.81c3.92 0 7.81.61 11.55 1.81 3.73 1.2 7.25 2.98 10.44 5.26 3.18 2.29 5.99 5.06 8.32 8.21s4.15 6.65 5.41 10.37l228.95 678.77z" fill="url(#c)"/></svg>
<svg height="2359" width="2500" xmlns="http://www.w3.org/2000/svg" viewBox="-0.4500000000000005 0.38 800.8891043012813 754.2299999999999"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="353.1" x2="107.1" y1="56.3" y2="783"><stop offset="0" stop-color="#114a8b"/><stop offset="1" stop-color="#0669bc"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="429.8" x2="372.9" y1="394.9" y2="414.2"><stop offset="0" stop-opacity=".3"/><stop offset=".1" stop-opacity=".2"/><stop offset=".3" stop-opacity=".1"/><stop offset=".6" stop-opacity=".1"/><stop offset="1" stop-opacity="0"/></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="398.4" x2="668.4" y1="35.1" y2="754.4"><stop offset="0" stop-color="#3ccbf4"/><stop offset="1" stop-color="#2892df"/></linearGradient><path d="M266.71.4h236.71L257.69 728.9a37.8 37.8 0 0 1-5.42 10.38c-2.33 3.16-5.14 5.93-8.33 8.22s-6.71 4.07-10.45 5.27-7.64 1.82-11.56 1.82H37.71c-5.98 0-11.88-1.42-17.2-4.16A37.636 37.636 0 0 1 7.1 738.87a37.762 37.762 0 0 1-6.66-16.41c-.89-5.92-.35-11.97 1.56-17.64L230.94 26.07c1.25-3.72 3.08-7.22 5.42-10.38 2.33-3.16 5.15-5.93 8.33-8.22 3.19-2.29 6.71-4.07 10.45-5.27S262.78.38 266.7.38v.01z" fill="url(#a)"/><path d="M703.07 754.59H490.52c-2.37 0-4.74-.22-7.08-.67-2.33-.44-4.62-1.1-6.83-1.97s-4.33-1.95-6.34-3.21a38.188 38.188 0 0 1-5.63-4.34l-241.2-225.26a17.423 17.423 0 0 1-5.1-8.88 17.383 17.383 0 0 1 7.17-18.21c2.89-1.96 6.3-3.01 9.79-3.01h375.36l92.39 265.56z" fill="#0078d4"/><path d="M504.27.4l-165.7 488.69 270.74-.06 92.87 265.56H490.43c-2.19-.02-4.38-.22-6.54-.61s-4.28-.96-6.34-1.72a38.484 38.484 0 0 1-11.36-6.51L303.37 593.79l-45.58 134.42c-1.18 3.36-2.8 6.55-4.82 9.48a40.479 40.479 0 0 1-16.05 13.67 40.03 40.03 0 0 1-10.13 3.23H37.82c-6.04.02-12-1.42-17.37-4.2A37.664 37.664 0 0 1 .43 722a37.77 37.77 0 0 1 1.87-17.79L230.87 26.58c1.19-3.79 2.98-7.36 5.3-10.58 2.31-3.22 5.13-6.06 8.33-8.4s6.76-4.16 10.53-5.38S262.75.38 266.72.4h237.56z" fill="url(#b)"/><path d="M797.99 704.82a37.847 37.847 0 0 1 1.57 17.64 37.867 37.867 0 0 1-6.65 16.41 37.691 37.691 0 0 1-30.61 15.72H498.48c5.98 0 11.88-1.43 17.21-4.16 5.32-2.73 9.92-6.7 13.41-11.56s5.77-10.49 6.66-16.41.35-11.97-1.56-17.64L305.25 26.05a37.713 37.713 0 0 0-13.73-18.58c-3.18-2.29-6.7-4.06-10.43-5.26S273.46.4 269.55.4h263.81c3.92 0 7.81.61 11.55 1.81 3.73 1.2 7.25 2.98 10.44 5.26 3.18 2.29 5.99 5.06 8.32 8.21s4.15 6.65 5.41 10.37l228.95 678.77z" fill="url(#c)"/></svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg height="1465" width="2500" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 2385.7 1919.9" style="enable-background:new 0 0 0 0;" xml:space="preserve">
<style type="text/css">
.st0{fill:#EA4335;}
.st1{fill:#4285F4;}
.st2{fill:#34A853;}
.st3{fill:#FBBC05;}
</style>
<g>
<path class="st0" d="M1513.8,528.7h72.8l207.4-207.4l10.2-88c-385.9-340.6-975-303.9-1315.6,82C393.9,422.5,325.2,550,287.8,688
c23.1-9.5,48.7-11,72.8-4.4l414.7-68.4c0,0,21.1-34.9,32-32.7c184.5-202.6,495-226.2,708-53.8H1513.8z"/>
<path class="st1" d="M2089.4,688c-47.7-175.5-145.5-333.3-281.6-454l-291,291c122.9,100.4,192.9,251.7,189.9,410.4v51.7
c143.1,0,259,116,259,259c0,143.1-116,259-259,259h-518.1l-51.7,52.4v310.7l51.7,51.7h518.1c297,2.3,560.5-190.2,648.7-473.8
C2443.4,1162.4,2335.4,854.4,2089.4,688L2089.4,688z"/>
<path class="st2" d="M669.8,1917h518.1v-414.7H669.8c-36.9,0-73.4-7.9-107-23.3l-72.8,22.5l-208.8,207.4l-18.2,72.8
C380.1,1870.1,523,1917.6,669.8,1917L669.8,1917z"/>
<path class="st3" d="M669.8,571.6c-287.8,1.7-542.7,186-634.5,458.7c-91.8,272.7-0.3,573.7,227.8,749.1l300.5-300.5
c-130.4-58.9-188.3-212.3-129.4-342.7c58.9-130.4,212.3-188.3,342.7-129.4c57.4,26,103.4,72,129.4,129.4l300.5-300.5
C1078.9,668.6,880.2,570.9,669.8,571.6L669.8,571.6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -20,6 +20,7 @@ import Azure from '../../img/azure.svg?svgr';
import SQLFileSvg from '../../img/sql_file.svg?svgr';
import MagicSvg from '../../img/magic.svg?svgr';
import MsAzure from '../../img/ms_azure.svg?svgr';
import GoogleCloud from '../../img/google-cloud-1.svg?svgr';
export default function ExternalIcon({Icon, ...props}) {
return <Icon className={'MuiSvgIcon-root'} {...props} />;
@ -71,15 +72,18 @@ ExpandDialogIcon.propTypes = {style: PropTypes.object};
export const MinimizeDialogIcon = ({style})=><ExternalIcon Icon={Collapse} style={{height: '1.4rem', ...style}} data-label="MinimizeDialogIcon" />;
MinimizeDialogIcon.propTypes = {style: PropTypes.object};
export const AWSIcon = ({style})=><ExternalIcon Icon={AWS} style={{height: '1.4rem', ...style}} data-label="AWSIcon" />;
export const AWSIcon = ({style})=><ExternalIcon Icon={AWS} style={{height: '2.2rem',width: '3.2rem', ...style}} data-label="AWSIcon" />;
AWSIcon.propTypes = {style: PropTypes.object};
export const BigAnimalIcon = ({style})=><ExternalIcon Icon={BigAnimal} style={{height: '1.4rem', ...style}} data-label="BigAnimalIcon" />;
export const BigAnimalIcon = ({style})=><ExternalIcon Icon={BigAnimal} style={{height: '2.2rem',width: '3.2rem', ...style}} data-label="BigAnimalIcon" />;
BigAnimalIcon.propTypes = {style: PropTypes.object};
export const AzureIcon = ({style})=><ExternalIcon Icon={Azure} style={{height: '1.4rem', ...style}} data-label="AzureIcon" />;
export const AzureIcon = ({style})=><ExternalIcon Icon={Azure} style={{height: '2.2rem', width: '3.2rem', ...style}} data-label="AzureIcon" />;
AzureIcon.propTypes = {style: PropTypes.object};
export const GoogleCloudIcon = ({style})=><ExternalIcon Icon={GoogleCloud} style={{height: '2.2rem', width: '3.2rem', ...style}} data-label="GoogleCloudIcon" />;
GoogleCloudIcon.propTypes = {style: PropTypes.object};
export const SQLFileIcon = ({style})=><ExternalIcon Icon={SQLFileSvg} style={{height: '1rem', ...style}} data-label="SQLFileIcon" />;
SQLFileIcon.propTypes = {style: PropTypes.object};

View File

@ -182,3 +182,8 @@ def service_unavailable(errormsg=_("Service Unavailable"), info='',
result=result,
data=data
)
def plain_text_response(message=''):
response = Response(message, status=200, mimetype="text/plain")
return response