Added capability to deploy PostgreSQL servers on Microsoft Azure. Fixes #7178

This commit is contained in:
Yogesh Mahajan 2022-06-15 11:22:42 +05:30 committed by Akshay Joshi
parent 99c7a50fd6
commit 7e1e068370
26 changed files with 2388 additions and 104 deletions

View File

@ -0,0 +1,120 @@
.. _cloud_azure_postgresql:
******************************************
`Azure PostgreSQL Cloud Deployment`:index:
******************************************
To deploy a PostgreSQL server on the Azure cloud, follow the below steps.
.. image:: images/cloud_azure_provider.png
:alt: Cloud Deployment
:align: center
Once you launch the tool, select the Azure PostgreSQL option.
Click on the *Next* button to proceed further.
.. image:: images/cloud_azure_credentials.png
:alt: Cloud Deployment
:align: center
In the step-2:Credentials, select authentication method either interactive
browser or Azure CLI. Azure CLI will use the currently logged in identity
through the Azure CLI on the local machine. Interactive Browser will
open a browser window to authenticate a user interactively.
Use the *Azure tenant* id to speicify Azure tenant ID against which user
is aunthenticated.
Clicking the *Click here to authenticate yourself to Microsoft Azure*
button, user will be redirected to the Microsoft Azure authentication page in a
new browser tab if the Interactive Browser option is selected.
Azure CLI authentication can be used only in Desktop mode.
Once authentication is comepleted, click on the next button to proceed.
.. image:: images/cloud_azure_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 a subscription from the *Subscription* options which are populated based
on user access levels in Azure portal.
* Select the resource group from *Resource Group* dropdown under which the
PostgreSQL instance will be created.
* Select the location to deploy PostgreSQL instance from *Location*
options.
* Select the availablity zone in specified region to deploy PostgreSQL
instance from *Availability zone* options.
* Use *Database version* options to speicify PostgreSQL database vetsion.
* Use the *Instance class* field to allocate the computational, network, and
memory capacity required by planned workload of this DB instance.
* Use the *Instance type* field to select the instance type.
* Use the *Storage size* option to specify the storage capacity.
.. image:: images/cloud_azure_network.png
:alt: Cloud Deployment
:align: center
* Use the *Public IP* field to specify the List of IP Addresses or range of
IP Addresses (start IP Address - end IP address) from which inbound traffic
should be accepted. Add multiple IP addresses/ranges separated with commas,
for example: "192.168.0.50, 192.168.0.100 - 192.168.0.200"
* User *Zone redundant high availability* option to specify High Availability
option. Zone redundant high availability deploys a standby replica in a
different zone.
The Burstable instance type does not support high availability.
.. image:: images/cloud_azure_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.
* Use the *Admin username* field to add the database name for the PostgreSQL
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_azure_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.
.. image:: images/cloud_azure_bg_process_watcher.png
:alt: Cloud Deployment
:align: center
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_deployment_tree.png
:alt: Cloud Deployment Provider
:align: center

View File

@ -15,4 +15,5 @@ To launch the *Cloud Deployment...* tool, right click on the *Server Group* or
:maxdepth: 2
cloud_aws_rds
cloud_edb_biganimal
cloud_edb_biganimal
cloud_azure_postgresql

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 127 KiB

View File

@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o
New features
************
| `Issue #7178 <https://redmine.postgresql.org/issues/7178>`_ - Added capability to deploy PostgreSQL servers on Microsoft Azure.
| `Issue #7332 <https://redmine.postgresql.org/issues/7332>`_ - Added support for passing password using Docker Secret to Docker images.
| `Issue #7351 <https://redmine.postgresql.org/issues/7351>`_ - Added the option 'Show template databases?' to display template databases regardless of the setting of 'Show system objects?'.

View File

@ -50,3 +50,7 @@ boto3==1.20.*
botocore==1.23.*
urllib3==1.26.*
Werkzeug==2.0.3
azure-mgmt-rdbms==10.1.0
azure-mgmt-resource==21.0.0
azure-mgmt-subscription==3.0.0
azure-identity==1.9.0

View File

@ -0,0 +1,343 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2022, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
""" Azure PostgreSQL provider """
from azure.mgmt.rdbms.postgresql_flexibleservers import \
PostgreSQLManagementClient
from azure.mgmt.rdbms.postgresql_flexibleservers.models import Sku, SkuTier, \
CreateMode, Storage, Server, FirewallRule, HighAvailability
from azure.identity import AzureCliCredential, InteractiveBrowserCredential, \
AuthenticationRecord, TokenCachePersistenceOptions
from azure.mgmt.resource import ResourceManagementClient
from azure.core.exceptions import ResourceNotFoundError
from providers._abstract import AbsProvider
import os
from utils.io import debug, error, output
from utils.misc import get_my_ip, get_random_id
class AzureProvider(AbsProvider):
def __init__(self):
self._clients = {}
self._tenant_id = None
self._client_id = None
self._client_secret = None
self._subscription_id = None
self._default_region = None
self._use_interactive_browser_credential = False
self._available_capabilities = None
self._credentials = None
self._authentication_record_json = None
self._cli_credentials = None
# Get the credentials
if 'AUTHENTICATION_RECORD_JSON' in os.environ:
self._authentication_record_json = os.environ[
'AUTHENTICATION_RECORD_JSON']
if 'AZURE_SUBSCRIPTION_ID' in os.environ:
self._subscription_id = os.environ['AZURE_SUBSCRIPTION_ID']
if 'AZURE_TENANT_ID' in os.environ:
self._tenant_id = os.environ['AZURE_TENANT_ID']
if 'AUTH_TYPE' in os.environ:
self._use_interactive_browser_credential = False \
if os.environ['AUTH_TYPE'] == 'azure_cli_credential' else True
if 'AZURE_DATABASE_PASSWORD' in os.environ:
self._database_pass = os.environ['AZURE_DATABASE_PASSWORD']
def init_args(self, parsers):
""" Create the command line parser for this provider """
self.parser = parsers. \
add_parser('azure',
help='Azure Database for PostgreSQL',
epilog='Credentials are read from '
'the environment, '
'specifically, the '
'AZURE_SUBSCRIPTION_ID, '
'AZURE_TENANT_ID, '
'AZURE_CLIENT_ID and '
'AZURE_CLIENT_SECRET '
'variables. '
'See https://docs.microsoft'
'.com/en-us/azure/developer'
'/python/configure-local'
'-development-environment?tabs=cmd '
'for more information.')
self.parser.add_argument('--region', default=self._default_region,
help='name of the Azure location (default: '
'{})'.format(self._default_region))
self.parser.add_argument('--resource-group', required=True,
help='name of the Azure resource group')
# Create the command sub-parser
parsers = self.parser.add_subparsers(help='Azure 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('--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-username',
default='postgres',
help='user name for the database '
'(default: postgres)')
parser_create_instance.add_argument('--db-major-version',
default='11',
help='version of PostgreSQL '
'to deploy (default: 11)')
parser_create_instance.add_argument('--instance-type', required=True,
help='machine type for the '
'instance nodes, e.g. '
'GP_Gen5_8')
parser_create_instance.add_argument('--instance_tier_type',
required=True,
help='machine type for the '
'instance nodes, e.g. '
'GP_Gen5_8')
parser_create_instance.add_argument('--storage-size', type=int,
required=True,
help='storage size in GB')
parser_create_instance.add_argument('--availability-zone',
required=False,
help='Availability zone')
parser_create_instance.add_argument('--high-availability',
required=False,
help='High Availability')
parser_create_instance.add_argument('--public-ips',
default='127.0.0.1',
help='Public IPs '
'(default: 127.0.0.1)')
# Create the delete instance command parser
parser_delete_instance = parsers.add_parser('delete-instance',
help='delete an instance')
parser_delete_instance.add_argument('--name', required=True,
help='name of the instance')
##########################################################################
# Azure Helper functions
##########################################################################
def _get_azure_credentials(self):
try:
if self._use_interactive_browser_credential:
if self._authentication_record_json is None:
_credentials = self._azure_interactive_browser_credential()
_auth_record_ = _credentials.authenticate()
self._authentication_record_json = \
_auth_record_.serialize()
else:
deserialized_auth_record = AuthenticationRecord.\
deserialize(self._authentication_record_json)
_credentials = \
self._azure_interactive_browser_credential(
deserialized_auth_record)
else:
if self._cli_credentials is None:
self._cli_credentials = AzureCliCredential()
_credentials = self._cli_credentials
except Exception as e:
return False, str(e)
return True, _credentials
def _azure_interactive_browser_credential(
self, deserialized_auth_record=None):
if deserialized_auth_record:
_credential = InteractiveBrowserCredential(
tenant_id=self._tenant_id,
timeout=180,
cache_persistence_options=TokenCachePersistenceOptions(),
authentication_record=deserialized_auth_record)
else:
_credential = InteractiveBrowserCredential(
tenant_id=self._tenant_id,
timeout=180,
cache_persistence_options=TokenCachePersistenceOptions())
return _credential
def _get_azure_client(self, type):
""" Create/cache/return an Azure client object """
# Acquire a credential object using CLI-based authentication.
if self._credentials is None:
status, self._credentials = \
self._get_azure_credentials()
if type in self._clients:
return self._clients[type]
if type == 'postgresql':
client = PostgreSQLManagementClient(self._credentials,
self._subscription_id)
elif type == 'resource':
client = ResourceManagementClient(self._credentials,
self._subscription_id)
self._clients[type] = client
return self._clients[type]
def _create_resource_group(self, args):
""" Create the Resource Group if it doesn't exist """
resource_client = self._get_azure_client('resource')
group_list = resource_client.resource_groups.list()
for group in list(group_list):
if group.name == args.resource_group:
debug('Resource group already exist with name: {}...'.format(
args.resource_group))
return group.__dict__
debug(
'Creating resource group with name: {}...'.format(
args.resource_group))
result = resource_client.resource_groups.create_or_update(
args.resource_group,
{"location": args.region})
return result.__dict__
def _create_azure_instance(self, args):
""" Create an Azure instance """
# Obtain the management client object
postgresql_client = self._get_azure_client('postgresql')
# Check if the server already exists
svr = None
try:
svr = postgresql_client.servers.get(args.resource_group, args.name)
except ResourceNotFoundError:
pass
except Exception as e:
error(args, e)
if svr is not None:
error(args, 'Azure Database for PostgreSQL instance {} already '
'exists.'.format(args.name))
db_password = self._database_pass if self._database_pass is not None \
else args.db_password
# Provision the server and wait for the result
debug('Creating Azure instance: {}...'.format(args.name))
try:
poller = postgresql_client.servers.begin_create(
resource_group_name=args.resource_group,
server_name=args.name,
parameters=Server(
sku=Sku(name=args.instance_type,
tier=SkuTier(args.instance_tier_type)
),
high_availability=HighAvailability(
mode=args.high_availability),
administrator_login=args.db_username,
administrator_login_password=db_password,
version=args.db_major_version,
storage=Storage(
storage_size_gb=args.storage_size
),
location=args.region,
create_mode=CreateMode("Default")
)
)
except Exception as e:
error(e)
server = poller.result()
return server.__dict__
def _create_firewall_rule(self, args):
""" Create a firewall rule on an instance """
firewall_rules = []
postgresql_client = self._get_azure_client('postgresql')
ip = args.public_ips if args.public_ips else get_my_ip()
ip_list = ip.split(',')
for ip in ip_list:
ip = ip.strip()
if '-' in ip:
start_ip = ip.split('-')[0]
end_ip = ip.split('-')[1]
else:
start_ip = ip
end_ip = ip
name = 'pgacloud_{}_{}_{}'.format(args.name,
ip.replace('.', '-'),
get_random_id())
# Provision the rule and wait for completion
debug('Adding ingress rule for: {0} - {1} ...'.format(start_ip,
end_ip))
poller = postgresql_client.firewall_rules.begin_create_or_update(
resource_group_name=args.resource_group,
server_name=args.name,
firewall_rule_name=name,
parameters=FirewallRule(start_ip_address=start_ip,
end_ip_address=end_ip)
)
firewall_rule = poller.result()
firewall_rules.append(firewall_rule.__dict__)
return firewall_rules
def _delete_azure_instance(self, args, name):
""" Delete an Azure instance """
# Obtain the management client object
postgresql_client = self._get_azure_client('postgresql')
# Delete the server and wait for the result
debug('Deleting Azure instance: {}...'.format(args.name))
try:
poller = postgresql_client.servers.begin_delete(
args.resource_group,
args.name
)
except Exception as e:
error(args, e)
poller.result()
##########################################################################
# User commands
##########################################################################
def cmd_create_instance(self, args):
""" Deploy an Azure instance and firewall rule """
rg = self._create_resource_group(args)
instance = self._create_azure_instance(args)
self._create_firewall_rule(args)
data = {'instance': {
'Id': instance['id'],
'ResourceGroupId': rg['name'],
'Location': instance['location'],
'Hostname': instance['fully_qualified_domain_name'],
'Port': 5432,
'Database': "postgres",
'Username': instance['administrator_login']
}}
output(data)
def cmd_delete_instance(self, args):
""" Delete an Azure instance """
self._delete_azure_instance(args, args.name)
def load():
""" Loads the current provider """
return AzureProvider()

View File

@ -26,6 +26,7 @@ from pgadmin.misc.cloud.utils import get_my_ip
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
# set template path for sql scripts
MODULE_NAME = 'cloud'
@ -75,7 +76,8 @@ class CloudModule(PgAdminModule):
return ['cloud.deploy_on_cloud',
'cloud.update_cloud_server',
'cloud.update_cloud_process',
'cloud.get_host_ip']
'cloud.get_host_ip',
'cloud.clear_cloud_session']
# Create blueprint for CloudModule class
@ -102,6 +104,15 @@ def script():
return res
@blueprint.route('/clear_cloud_session/',
methods=['POST'], endpoint='clear_cloud_session')
@login_required
def clear_session():
"""Get host IP Address"""
clear_cloud_session()
return make_json_response(success=1)
@blueprint.route('/get_host_ip/',
methods=['GET'], endpoint='get_host_ip')
@login_required
@ -123,6 +134,8 @@ def deploy_on_cloud():
status, resp = deploy_on_rds(data)
elif data['cloud'] == 'biganimal':
status, resp = deploy_on_biganimal(data)
elif data['cloud'] == 'azure':
status, resp = deploy_on_azure(data)
else:
status = False
resp = gettext('No cloud implementation.')
@ -188,7 +201,7 @@ def update_server(data):
_server['status'] = False
else:
_server['status'] = True
clear_cloud_session()
clear_cloud_session()
return True, _server
@ -197,6 +210,7 @@ def clear_cloud_session():
"""Clear cloud sessions."""
clear_aws_session()
clear_biganimal_session()
clear_azure_session()
@blueprint.route(

View File

@ -0,0 +1,693 @@
# ##########################################################################
# #
# # pgAdmin 4 - PostgreSQL Tools
# #
# # Copyright (C) 2013 - 2022, The pgAdmin Development Team
# # This software is released under the PostgreSQL Licence
# #
# ##########################################################################
# Azure implementation
from pgadmin.misc.cloud.utils import _create_server, CloudProcessDesc
from pgadmin.misc.bgprocess.processes import BatchProcess
from pgadmin import make_json_response
from pgadmin.utils import PgAdminModule
from flask_security import login_required
import simplejson as json
from flask import session, current_app, request
from config import root
from azure.mgmt.rdbms.postgresql_flexibleservers import \
PostgreSQLManagementClient
from azure.identity import AzureCliCredential, InteractiveBrowserCredential, \
TokenCachePersistenceOptions, AuthenticationRecord
from azure.mgmt.resource import ResourceManagementClient
from azure.mgmt.subscription import SubscriptionClient
from azure.mgmt.rdbms.postgresql_flexibleservers.models import \
NameAvailabilityRequest
MODULE_NAME = 'azure'
class AzurePostgresqlModule(PgAdminModule):
"""Cloud module to deploy on Azure Postgresql"""
def get_own_stylesheets(self):
"""
Returns:
list: the stylesheets used by this module.
"""
stylesheets = []
return stylesheets
def get_exposed_url_endpoints(self):
return ['azure.verify_credentials',
'azure.check_cluster_name_availability',
'azure.subscriptions',
'azure.resource_groups',
'azure.regions',
'azure.zone_redundant_ha_supported',
'azure.db_versions',
'azure.instance_types',
'azure.availability_zones',
'azure.storage_types']
blueprint = AzurePostgresqlModule(MODULE_NAME, __name__,
static_url_path='/misc/cloud/azure')
@blueprint.route('/verify_credentials/',
methods=['POST'], endpoint='verify_credentials')
@login_required
def verify_credentials():
"""Verify Credentials."""
data = json.loads(request.data, encoding='utf-8')
session_token = data['secret']['session_token'] if \
'session_token' in data['secret'] else None
tenant_id = data['secret']['azure_tenant_id'] if \
'azure_tenant_id' in data['secret'] else None
interactive_browser_credential = False if \
data['secret']['auth_type'] == 'azure_cli_credential' else True
if 'azure' not in session:
session['azure'] = {}
error = ''
status = True
if 'azure_obj' not in session['azure'] or \
session['azure']['auth_type'] != data['secret']['auth_type'] or \
session['azure']['azure_tenant_id'] != tenant_id:
if 'azure_obj' in session['azure']:
del session['azure']['azure_obj']
azure = Azure(
interactive_browser_credential=interactive_browser_credential,
tenant_id=tenant_id,
session_token=session_token)
status, error = azure.validate_azure_credentials()
if status:
session['azure']['azure_obj'] = azure
session['azure']['auth_type'] = data['secret']['auth_type']
session['azure']['azure_tenant_id'] = tenant_id
if not status and 'double check your tenant name' in error:
error = 'Authentication failed.Please double check tenant id.'
return make_json_response(success=status, errormsg=error)
@blueprint.route('/check_cluster_name_availability/',
methods=['GET'], endpoint='check_cluster_name_availability')
@login_required
def check_cluster_name_availability():
"""Check Server Name availability."""
data = request.args
azure = session['azure']['azure_obj']
server_name_available, error = \
azure.check_cluster_name_availability(data['name'])
if server_name_available:
return make_json_response(success=server_name_available,
errormsg=error)
else:
return make_json_response(
status=410,
success=0,
errormsg=error)
@blueprint.route('/subscriptions/',
methods=['GET'], endpoint='subscriptions')
@login_required
def get_azure_subscriptions():
"""
List subscriptions.
:return:
"""
azure = session['azure']['azure_obj']
subscriptions_list = azure.list_subscriptions()
return make_json_response(data=subscriptions_list)
@blueprint.route('/resource_groups/<subscription_id>',
methods=['GET'], endpoint='resource_groups')
@login_required
def get_azure_resource_groups(subscription_id):
"""
Fetch resource groups based on subscription.
"""
if not subscription_id:
return make_json_response(data=[])
azure = session['azure']['azure_obj']
resource_groups_list = azure.list_resource_groups(subscription_id)
return make_json_response(data=resource_groups_list)
@blueprint.route('/regions/<subscription_id>',
methods=['GET'], endpoint='regions')
@login_required
def get_azure_regions(subscription_id):
"""List Regions for Azure."""
if not subscription_id:
return make_json_response(data=[])
azure = session['azure']['azure_obj']
regions_list = azure.list_regions(subscription_id)
session['azure']['azure_obj'] = azure
return make_json_response(data=regions_list)
@blueprint.route('/zone_redundant_ha_supported/<region_name>',
methods=['GET'], endpoint='zone_redundant_ha_supported')
@login_required
def is_ha_supported(region_name):
"""Check high availability support in given region."""
azure = session['azure']['azure_obj']
is_zone_redundant_ha_supported = \
azure.is_zone_redundant_ha_supported(region_name)
return make_json_response(data={'is_zone_redundant_ha_supported':
is_zone_redundant_ha_supported})
@blueprint.route('/availability_zones/<region_name>',
methods=['GET'], endpoint='availability_zones')
@login_required
def get_azure_availability_zones(region_name):
"""List availability zones in given region."""
if not region_name:
return make_json_response(data=[])
azure = session['azure']['azure_obj']
availability_zones = azure.list_azure_availability_zones(region_name)
session['azure']['azure_obj'] = azure
return make_json_response(data=availability_zones)
@blueprint.route('/db_versions/<availability_zone>',
methods=['GET'], endpoint='db_versions')
@login_required
def get_azure_postgresql_server_versions(availability_zone):
"""Get azure postgres database versions."""
if not availability_zone:
return make_json_response(data=[])
azure = session['azure']['azure_obj']
azure_postgresql_server_versions = \
azure.list_azure_postgresql_server_versions(availability_zone)
session['azure']['azure_obj'] = azure
return make_json_response(data=azure_postgresql_server_versions)
@blueprint.route('/instance_types/<availability_zone>/<db_version>',
methods=['GET'], endpoint='instance_types')
@login_required
def get_azure_instance_types(availability_zone, db_version):
"""Get instance types for Azure."""
if not db_version:
return make_json_response(data=[])
azure = session['azure']['azure_obj']
instance_types = azure.list_compute_types(availability_zone, db_version)
return make_json_response(data=instance_types)
@blueprint.route('/storage_types/<availability_zone>/<db_version>',
methods=['GET'], endpoint='storage_types')
@login_required
def list_azure_storage_types(availability_zone, db_version):
"""Get the storage types supported."""
if not db_version:
return make_json_response(data=[])
azure = session['azure']['azure_obj']
storage_types = azure.list_storage_types(availability_zone, db_version)
return make_json_response(data=storage_types)
@blueprint.route('/clear_session',
methods=['GET'], endpoint='clear_session')
@login_required
def clear_session():
clear_azure_session()
return make_json_response(success=1)
class Azure:
def __init__(self, interactive_browser_credential, tenant_id=None,
session_token=None, region='eastus'):
self._clients = {}
self._tenant_id = tenant_id
self._session_token = session_token
self._use_interactive_browser_credential = \
interactive_browser_credential
self.authentication_record_json = None
self._cli_credentials = None
self._credentials = None
self._region = region
self.subscription_id = None
self._availability_zone = None
self._available_capabilities_list = []
##########################################################################
# Azure Helper functions
##########################################################################
def validate_azure_credentials(self):
"""
Validates azure credentials
:return: True if valid credentials else false
"""
status, identity = self._get_azure_credentials()
error = ''
if not status:
error = identity
return status, error
def _get_azure_credentials(self):
"""
Gets azure credentials depending on
self._use_interactive_browser_credential
:return:
"""
try:
if self._use_interactive_browser_credential:
if self.authentication_record_json is None:
_credentials = self._azure_interactive_browser_credential()
_auth_record_ = _credentials.authenticate()
self.authentication_record_json = _auth_record_.serialize()
else:
deserialized_auth_record = AuthenticationRecord. \
deserialize(self.authentication_record_json)
_credentials = \
self._azure_interactive_browser_credential(
deserialized_auth_record)
else:
if self._cli_credentials is None:
self._cli_credentials = AzureCliCredential()
self.list_subscriptions()
_credentials = self._cli_credentials
except Exception as e:
return False, str(e)
return True, _credentials
def _azure_interactive_browser_credential(
self, deserialized_auth_record=None):
if deserialized_auth_record:
_credential = InteractiveBrowserCredential(
tenant_id=self._tenant_id,
timeout=180,
cache_persistence_options=TokenCachePersistenceOptions(),
authentication_record=deserialized_auth_record)
else:
_credential = InteractiveBrowserCredential(
tenant_id=self._tenant_id,
timeout=180,
cache_persistence_options=TokenCachePersistenceOptions())
return _credential
def _get_azure_client(self, type):
""" Create/cache/return an Azure client object """
if type in self._clients:
return self._clients[type]
status, _credentials = self._get_azure_credentials()
if type == 'postgresql':
client = PostgreSQLManagementClient(_credentials,
self.subscription_id)
elif type == 'resource':
client = ResourceManagementClient(_credentials,
self.subscription_id)
elif type == 'subscription':
client = SubscriptionClient(_credentials)
self._clients[type] = client
return self._clients[type]
def check_cluster_name_availability(self, cluster_name):
"""
Checks whether given server name is available or not
:param cluster_name
"""
postgresql_client = self._get_azure_client('postgresql')
res = postgresql_client.check_name_availability.execute(
NameAvailabilityRequest(
name=cluster_name,
type='Microsoft.DBforPostgreSQL/flexibleServers'))
res = res.__dict__
return res['name_available'], res['message']
def list_subscriptions(self):
"""
List subscriptions
:return:
"""
subscription_client = self._get_azure_client('subscription')
sub_list = subscription_client.subscriptions.list()
subscriptions_list = []
for group in list(sub_list):
subscriptions_list.append(
{'subscription_id': group.subscription_id,
'subscription_name': group.display_name})
return subscriptions_list
def list_resource_groups(self, subscription_id):
"""
List the resource groups
:param subscription_id:
:return:
"""
self.subscription_id = subscription_id
resource_client = self._get_azure_client('resource')
group_list = resource_client.resource_groups.list()
resource_groups_list = []
for group in list(group_list):
resource_groups_list.append(
{'label': group.name,
'value': group.name,
'region': group.location})
return resource_groups_list
def list_regions(self, subscription_id):
"""
List regions depending on subscription id
:param subscription_id:
:return:
"""
self.subscription_id = subscription_id
subscription_client = self._get_azure_client('subscription')
locations = subscription_client.subscriptions.list_locations(
subscription_id=self.subscription_id)
locations_list = []
for location in locations:
locations_list.append(
{'label': location.display_name, 'value': location.name})
return locations_list
def is_zone_redundant_ha_supported(self, region):
if self._region == region and \
len(self._available_capabilities_list) > 1:
return self._available_capabilities_list[0][
'zone_redundant_ha_supported']
else:
self._available_capabilities_list = \
self._get_available_capabilities_list(region)
return self._available_capabilities_list[0][
'zone_redundant_ha_supported']
def list_azure_availability_zones(self, region):
"""
List availability zones in the region
:param region:
:return:
"""
self._region = region
self._available_capabilities_list = \
self._get_available_capabilities_list(region)
availability_zones_list = []
for capability in self._available_capabilities_list:
zone = str(capability['zone'])
if capability['zone'] == 'none':
availability_zones_list.append({'label': 'No Preference',
'value': zone})
else:
availability_zones_list.append({'label': zone,
'value': zone})
return availability_zones_list
def list_azure_postgresql_server_versions(self, availability_zone):
"""
:param availability_zone:
:return: List of postgresql version available in specified availability
zone.
"""
self._availability_zone = availability_zone
server_versions_list = []
for capability in self._available_capabilities_list:
if str(capability['zone']) == availability_zone:
for supported_server_version in \
capability['supported_server_versions']:
server_version = supported_server_version['server_version']
server_versions_list.append({'label': str(server_version),
'value': server_version})
return server_versions_list
def list_compute_types(self, availability_zone, server_version):
"""
:param availability_zone:
:param server_version:
:return: list of compute classes based on specified availability
zone & server version.
"""
compute_types_list = []
for capability in self._available_capabilities_list:
if str(capability['zone']) == availability_zone:
for supported_server_version in \
capability['supported_server_versions']:
if supported_server_version['server_version'] == \
server_version:
compute_types = \
supported_server_version['compute_types']
for value in compute_types:
compute_types_list.append(
{'label': value['display_name'],
'value': value['name'],
'type': value['type']})
return compute_types_list
def list_storage_types(self, availability_zone, server_version):
"""
:param availability_zone:
:param server_version:
:return: list of storages classes based on specified availability
"""
storage_types_list = []
for capability in self._available_capabilities_list:
if str(capability['zone']) == availability_zone:
for supported_server_version in \
capability['supported_server_versions']:
if supported_server_version['server_version'] == \
server_version:
storage_types = \
supported_server_version['storage_types']
for value in storage_types:
storage_types_list.append({
'label': str(value['storage_size_gb']) +
' GiB',
'value': value['storage_size_gb'],
'type': value['type']})
return storage_types_list
def _get_available_capabilities_list(self, region):
"""
list capabilities & serialize them to normal list-dict format
:param region:
:return:
"""
available_capabilities = \
self._get_available_capabilities_object(region)
return self.\
_serialize_available_capabilities_list(available_capabilities)
def _get_available_capabilities_object(self, region):
"""
:param region:
:return: azure capabilities object
"""
postgresql_client = self._get_azure_client('postgresql')
return postgresql_client.location_based_capabilities.execute(
location_name=region)
@staticmethod
def _serialize_available_capabilities_list(available_capabilities):
"""
:param available_capabilities:
:return: serialized available capabilities list
"""
available_capabilities_list = []
for capability in available_capabilities:
supported_server_version_dict = {}
storage_types = []
for supported_flexible_server_edition in \
capability.supported_flexible_server_editions:
compute_type = supported_flexible_server_edition.name
storage_types = Azure. \
_get_storage_types(compute_type,
supported_flexible_server_edition,
storage_types)
supported_server_version_dict = Azure. \
_get_compute_types(compute_type,
supported_flexible_server_edition,
supported_server_version_dict,
storage_types)
supported_server_version_list = []
for key, value in supported_server_version_dict.items():
supported_server_version_list.append(
{'server_version': key,
'compute_types': value['compute_types'],
'storage_types': value['storage_types']})
available_capabilities_list.append(
{'zone': capability.zone,
'zone_redundant_ha_supported':
capability.zone_redundant_ha_supported,
'supported_server_versions':
supported_server_version_list})
return available_capabilities_list
@staticmethod
def _get_storage_types(compute_type, supported_flexible_server_edition,
storage_types):
for supported_storage_edition in \
supported_flexible_server_edition.supported_storage_editions:
for supported_storage_mb in \
supported_storage_edition.supported_storage_mb:
supported_storage_mb_dict = supported_storage_mb.__dict__
storage_types.append({'type': compute_type,
'storage_size_gb':
int(supported_storage_mb_dict[
'storage_size_mb'] / 1024)})
return storage_types
@staticmethod
def _get_compute_types(compute_type, supported_flexible_server_edition,
supported_server_version_dict, storage_types):
for supported_server_version in \
supported_flexible_server_edition.supported_server_versions:
if not supported_server_version.name.isnumeric():
continue
if supported_server_version.name not in \
supported_server_version_dict:
supported_server_version_dict[
supported_server_version.name] = {}
compute_types_list = []
for supported_vcore in supported_server_version.supported_vcores:
vcore_dict = supported_vcore.__dict__
compute_types_list.append(
{'type': compute_type,
'name': vcore_dict['name'],
'supportedIOPS': vcore_dict['additional_properties'][
'supportedIOPS'],
'display_name': vcore_dict['name'] + ' (' +
str(vcore_dict['v_cores']) + ' vCores, ' +
str(int(vcore_dict['supported_memory_per_vcore_mb'] /
1024 * vcore_dict['v_cores'])) + 'GiB memory, ' +
str(vcore_dict['additional_properties']
['supportedIOPS']) +
' max iops)'
})
if 'compute_types' not in supported_server_version_dict[
supported_server_version.name]:
supported_server_version_dict[supported_server_version.name][
'compute_types'] = compute_types_list
else:
supported_server_version_dict[supported_server_version.name][
'compute_types'] = \
supported_server_version_dict[
supported_server_version.name]['compute_types'] + (
compute_types_list)
supported_server_version_dict[supported_server_version.name][
'storage_types'] = storage_types
return supported_server_version_dict
def deploy_on_azure(data):
"""Deploy the Postgres instance on Azure."""
_cmd = 'python'
_cmd_script = '{0}/pgacloud/pgacloud.py'.format(root)
_label = data['instance_details']['name']
if 'high_availability' in data['instance_details']:
if data['instance_details']['high_availability']:
data['instance_details']['high_availability'] = "ZoneRedundant"
else:
data['instance_details']['high_availability'] = "Disabled"
args = [_cmd_script,
'azure',
'--region',
str(data['instance_details']['region']),
'--resource-group',
data['instance_details']['resource_group'],
'create-instance',
'--name',
data['instance_details']['name'],
'--db-username',
data['db_details']['db_username'],
'--db-major-version',
str(data['instance_details']['db_version']),
'--instance_tier_type',
data['instance_details']['db_instance_class'],
'--instance-type',
data['instance_details']['instance_type'],
'--storage-size',
str(data['instance_details']['storage_size']),
'--public-ips',
str(data['instance_details']['public_ips']),
'--availability-zone',
str(data['instance_details']['availability_zone']),
'--high-availability',
data['instance_details']['high_availability']
]
_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': data['db_details']['db_username'],
'port': 5432,
'cloud_status': -1
})
p = BatchProcess(
desc=CloudProcessDesc(sid, _cmd_msg, data['cloud'],
data['instance_details']['name']),
cmd=_cmd,
args=args
)
env = dict()
azure = session['azure']['azure_obj']
env['AZURE_SUBSCRIPTION_ID'] = azure.subscription_id
env['AUTH_TYPE'] = data['secret']['auth_type']
if azure.authentication_record_json is not None:
env['AUTHENTICATION_RECORD_JSON'] = \
azure.authentication_record_json
env['AZURE_TENANT_ID'] = data['secret']['azure_tenant_id']
if 'db_password' in data['db_details']:
env['AZURE_DATABASE_PASSWORD'] = data[
'db_details']['db_password']
p.set_env_variables(None, env=env)
p.update_server_id(p.id, sid)
p.start()
del session['azure']['azure_obj']
return True, {'label': _label, 'sid': sid}
except Exception as e:
current_app.logger.exception(e)
return False, str(e)
def clear_azure_session():
"""Clear session data."""
if 'azure' in session:
session.pop('azure')

View File

@ -21,12 +21,12 @@ import PropTypes from 'prop-types';
import pgAdmin from 'sources/pgadmin';
import {ToggleButtons, FinalSummary} from './cloud_components';
import { PrimaryButton } from '../../../../static/js/components/Buttons';
import {AwsCredentials, AwsInstanceDetails, AwsDatabaseDetails, validateCloudStep1,
validateCloudStep2, validateCloudStep3} from './aws';
import {BigAnimalInstance, BigAnimalDatabase, validateBigAnimal,
validateBigAnimalStep2, validateBigAnimalStep3} from './biganimal';
import {AwsCredentials, AwsInstanceDetails, AwsDatabaseDetails, validateCloudStep1, validateCloudStep2, validateCloudStep3} from './aws';
import {BigAnimalInstance, BigAnimalDatabase, validateBigAnimal,validateBigAnimalStep2, validateBigAnimalStep3} from './biganimal';
import { isEmptyString } from 'sources/validators';
import { AWSIcon, BigAnimalIcon } from '../../../../static/js/components/ExternalIcon';
import { AWSIcon, BigAnimalIcon, AzureIcon } from '../../../../static/js/components/ExternalIcon';
import {AzureCredentials, AzureInstanceDetails, AzureDatabaseDetails, checkClusternameAvailbility, validateAzureStep2, validateAzureStep3} from './azure';
import EventBus from '../../../../static/js/helpers/EventBus';
const useStyles = makeStyles(() =>
({
@ -53,12 +53,20 @@ const useStyles = makeStyles(() =>
boxText: {
paddingBottom: '5px'
},
authButton: {
marginLeft: '12em'
}
}),
);
export const CloudWizardEventsContext = React.createContext();
export default function CloudWizard({ nodeInfo, nodeData }) {
const classes = useStyles();
const eventBus = React.useRef(new EventBus());
var steps = [gettext('Cloud Provider'), gettext('Credentials'),
gettext('Instance Specification'), gettext('Database Details'), gettext('Review')];
const [currentStep, setCurrentStep] = React.useState('');
@ -74,13 +82,27 @@ export default function CloudWizard({ nodeInfo, nodeData }) {
const [bigAnimalInstanceData, setBigAnimalInstanceData] = React.useState({});
const [bigAnimalDatabaseData, setBigAnimalDatabaseData] = React.useState({});
const [azureCredData, setAzureCredData] = React.useState({});
const [azureInstanceData, setAzureInstanceData] = React.useState({});
const [azureDatabaseData, setAzureDatabaseData] = React.useState({});
const axiosApi = getApiInstance();
const [verificationURI, setVerificationURI] = React.useState('');
const [verificationCode, setVerificationCode] = React.useState('');
React.useEffect(()=>{
eventBus.current.registerListener('SET_ERROR_MESSAGE_FOR_CLOUD_WIZARD', (msg) => {
setErrMsg(msg);
});
}, []);
React.useEffect(()=>{
eventBus.current.registerListener('SET_CRED_VERIFICATION_INITIATED', (initiated) => {
setVerificationIntiated(initiated);
});
}, []);
React.useEffect(() => {
let _url = url_for('cloud.get_host_ip') ;
axiosApi.get(_url)
@ -110,7 +132,16 @@ export default function CloudWizard({ nodeInfo, nodeData }) {
instance_details:cloudInstanceDetails,
db_details: cloudDBDetails
};
} else {
} else if(cloudProvider == 'azure'){
post_data = {
gid: nodeInfo.server_group._id,
secret: azureCredData,
cloud: cloudProvider,
instance_details:azureInstanceData,
db_details: azureDatabaseData
};
}else {
post_data = {
gid: nodeInfo.server_group._id,
cloud: cloudProvider,
@ -170,6 +201,24 @@ export default function CloudWizard({ nodeInfo, nodeData }) {
break;
}
break;
case 'azure':
switch (currentStep) {
case 0:
setCloudSelection('azure');
break;
case 1:
isError = !verificationIntiated;
break;
case 2:
isError = validateAzureStep2(azureInstanceData);
break;
case 3:
isError = validateAzureStep3(azureDatabaseData, nodeInfo);
break;
default:
break;
}
break;
}
return isError;
};
@ -211,8 +260,23 @@ export default function CloudWizard({ nodeInfo, nodeData }) {
setErrMsg([MESSAGE_TYPE.ERROR, gettext(error)]);
reject();
});
} else if(activeStep == 2 && cloudProvider == 'azure'){
setErrMsg([MESSAGE_TYPE.INFO, 'Checking cluster name availability...']);
checkClusternameAvailbility(azureInstanceData.name)
.then((res)=>{
if (res.data && res.data.success == 0 ) {
setErrMsg([MESSAGE_TYPE.ERROR, gettext('Specified cluster name is already used.')]);
}else{
setErrMsg(['', '']);
}
resolve();
}).catch((error)=>{
setErrMsg([MESSAGE_TYPE.ERROR, gettext(error)]);
reject();
});
}
else {
setErrMsg(['', '']);
resolve();
}
});
@ -262,96 +326,120 @@ export default function CloudWizard({ nodeInfo, nodeData }) {
});
return (
<>
<Wizard
title={gettext('Deploy Cloud Instance')}
stepList={steps}
disableNextStep={disableNextCheck}
onStepChange={wizardStepChange}
onSave={onSave}
onHelp={onDialogHelp}
beforeNext={onBeforeNext}>
<WizardStep stepId={0}>
<Box className={classes.messageBox}>
<Box className={classes.messagePadding}>{gettext('Select any option to deploy on cloud.')}</Box>
</Box>
<Box className={classes.messageBox}>
<ToggleButtons cloudProvider={cloudProvider} setCloudProvider={setCloudProvider}
options={[{label: 'Amazon RDS', value: 'rds', icon: <AWSIcon className={classes.icon} />}, {label: 'EDB BigAnimal', value: 'biganimal', icon: <BigAnimalIcon className={classes.icon} />}]}
></ToggleButtons>
</Box>
<Box className={classes.messageBox}>
<Box className={classes.messagePadding}>{gettext('More cloud providers are coming soon...')}</Box>
</Box>
<FormFooterMessage type={errMsg[0]} message={errMsg[1]} onClose={onErrClose} />
</WizardStep>
<WizardStep stepId={1} >
<Box className={classes.buttonMarginEDB}>
{cloudProvider == 'biganimal' && <Box className={classes.messageBox}>
<Box>{gettext('The verification code to authenticate the pgAdmin to EDB BigAnimal is: ')} <strong>{verificationCode}</strong>
<br/>{gettext('By clicking the below button, you will be redirected to the EDB BigAnimal authentication page in a new tab.')}
</Box>
</Box>}
{cloudProvider == 'biganimal' && <PrimaryButton onClick={authenticateBigAnimal} disabled={verificationIntiated ? true: false}>
{gettext('Click here to authenticate yourself to EDB BigAnimal')}
</PrimaryButton>}
{cloudProvider == 'biganimal' && <Box className={classes.messageBox}>
<Box ></Box>
</Box>}
</Box>
{cloudProvider == 'rds' && <AwsCredentials cloudProvider={cloudProvider} nodeInfo={nodeInfo} nodeData={nodeData} setCloudDBCred={setCloudDBCred}/>}
<FormFooterMessage type={errMsg[0]} message={errMsg[1]} onClose={onErrClose} />
</WizardStep>
<WizardStep stepId={2} >
{cloudProvider == 'rds' && callRDSAPI == 2 && <AwsInstanceDetails
cloudProvider={cloudProvider}
nodeInfo={nodeInfo}
nodeData={nodeData}
setCloudInstanceDetails={setCloudInstanceDetails}
hostIP={hostIP} /> }
{cloudProvider == 'biganimal' && callRDSAPI == 2 && <BigAnimalInstance
cloudProvider={cloudProvider}
nodeInfo={nodeInfo}
nodeData={nodeData}
setBigAnimalInstanceData={setBigAnimalInstanceData}
hostIP={hostIP}
/> }
</WizardStep>
<WizardStep stepId={3} >
{cloudProvider == 'rds' && <AwsDatabaseDetails
cloudProvider={cloudProvider}
nodeInfo={nodeInfo}
nodeData={nodeData}
setCloudDBDetails={setCloudDBDetails}
/>
}
{cloudProvider == 'biganimal' && callRDSAPI == 3 && <BigAnimalDatabase
cloudProvider={cloudProvider}
nodeInfo={nodeInfo}
nodeData={nodeData}
setBigAnimalDatabaseData={setBigAnimalDatabaseData}
/>
}
</WizardStep>
<WizardStep stepId={4} >
<Box className={classes.boxText}>{gettext('Please review the details before creating the cloud instance.')}</Box>
<Paper variant="outlined" elevation={0} className={classes.summaryContainer}>
{cloudProvider == 'rds' && callRDSAPI == 4 && <FinalSummary
<CloudWizardEventsContext.Provider value={eventBus.current}>
<>
<Wizard
title={gettext('Deploy Cloud Instance')}
stepList={steps}
disableNextStep={disableNextCheck}
onStepChange={wizardStepChange}
onSave={onSave}
onHelp={onDialogHelp}
beforeNext={onBeforeNext}>
<WizardStep stepId={0}>
<Box className={classes.messageBox}>
<Box className={classes.messagePadding}>{gettext('Select a cloud provider.')}</Box>
</Box>
<Box className={classes.messageBox}>
<ToggleButtons cloudProvider={cloudProvider} setCloudProvider={setCloudProvider}
options={[{label: 'Amazon RDS', value: 'rds', icon: <AWSIcon className={classes.icon} />}, {label: 'EDB BigAnimal', value: 'biganimal', icon: <BigAnimalIcon className={classes.icon} />}, {'label': 'Azure PostgreSQL', value: 'azure', icon: <AzureIcon className={classes.icon} /> }]}
></ToggleButtons>
</Box>
<FormFooterMessage type={errMsg[0]} message={errMsg[1]} onClose={onErrClose} />
</WizardStep>
<WizardStep stepId={1} >
<Box className={classes.buttonMarginEDB}>
{cloudProvider == 'biganimal' && <Box className={classes.messageBox}>
<Box>{gettext('The verification code to authenticate the pgAdmin to EDB BigAnimal is: ')} <strong>{verificationCode}</strong>
<br/>{gettext('By clicking the below button, you will be redirected to the EDB BigAnimal authentication page in a new tab.')}
</Box>
</Box>}
{cloudProvider == 'biganimal' && <PrimaryButton onClick={authenticateBigAnimal} disabled={verificationIntiated ? true: false}>
{gettext('Click here to authenticate yourself to EDB BigAnimal')}
</PrimaryButton>}
{cloudProvider == 'biganimal' && <Box className={classes.messageBox}>
<Box ></Box>
</Box>}
</Box>
{cloudProvider == 'rds' && <AwsCredentials cloudProvider={cloudProvider} nodeInfo={nodeInfo} nodeData={nodeData} setCloudDBCred={setCloudDBCred}/>}
<Box>
{cloudProvider == 'azure' && <AzureCredentials cloudProvider={cloudProvider} nodeInfo={nodeInfo} nodeData={nodeData} setAzureCredData={setAzureCredData}/>}
</Box>
<FormFooterMessage type={errMsg[0]} message={errMsg[1]} onClose={onErrClose} />
</WizardStep>
<WizardStep stepId={2} >
{cloudProvider == 'rds' && callRDSAPI == 2 && <AwsInstanceDetails
cloudProvider={cloudProvider}
instanceData={cloudInstanceDetails}
databaseData={cloudDBDetails}
nodeInfo={nodeInfo}
nodeData={nodeData}
setCloudInstanceDetails={setCloudInstanceDetails}
hostIP={hostIP} /> }
{cloudProvider == 'biganimal' && callRDSAPI == 2 && <BigAnimalInstance
cloudProvider={cloudProvider}
nodeInfo={nodeInfo}
nodeData={nodeData}
setBigAnimalInstanceData={setBigAnimalInstanceData}
hostIP={hostIP}
/> }
{cloudProvider == 'azure' && callRDSAPI == 2 && <AzureInstanceDetails
cloudProvider={cloudProvider}
nodeInfo={nodeInfo}
nodeData={nodeData}
setAzureInstanceData={setAzureInstanceData}
hostIP={hostIP}
azureInstanceData = {azureInstanceData}
/> }
<FormFooterMessage type={errMsg[0]} message={errMsg[1]} onClose={onErrClose} />
</WizardStep>
<WizardStep stepId={3} >
{cloudProvider == 'rds' && <AwsDatabaseDetails
cloudProvider={cloudProvider}
nodeInfo={nodeInfo}
nodeData={nodeData}
setCloudDBDetails={setCloudDBDetails}
/>
}
{cloudProvider == 'biganimal' && callRDSAPI == 4 && <FinalSummary
{cloudProvider == 'biganimal' && callRDSAPI == 3 && <BigAnimalDatabase
cloudProvider={cloudProvider}
instanceData={bigAnimalInstanceData}
databaseData={bigAnimalDatabaseData}
nodeInfo={nodeInfo}
nodeData={nodeData}
setBigAnimalDatabaseData={setBigAnimalDatabaseData}
/>
}
</Paper>
</WizardStep>
</Wizard>
</>
{cloudProvider == 'azure' && <AzureDatabaseDetails
cloudProvider={cloudProvider}
nodeInfo={nodeInfo}
nodeData={nodeData}
setAzureDatabaseData={setAzureDatabaseData}
/>
}
</WizardStep>
<WizardStep stepId={4} >
<Box className={classes.boxText}>{gettext('Please review the details before creating the cloud instance.')}</Box>
<Paper variant="outlined" elevation={0} className={classes.summaryContainer}>
{cloudProvider == 'rds' && callRDSAPI == 4 && <FinalSummary
cloudProvider={cloudProvider}
instanceData={cloudInstanceDetails}
databaseData={cloudDBDetails}
/>
}
{cloudProvider == 'biganimal' && callRDSAPI == 4 && <FinalSummary
cloudProvider={cloudProvider}
instanceData={bigAnimalInstanceData}
databaseData={bigAnimalDatabaseData}
/>
}
{cloudProvider == 'azure' && callRDSAPI == 4 && <FinalSummary
cloudProvider={cloudProvider}
instanceData={azureInstanceData}
databaseData={azureDatabaseData}
/>
}
</Paper>
</WizardStep>
</Wizard>
</>
</CloudWizardEventsContext.Provider>
);
}
@ -359,5 +447,3 @@ CloudWizard.propTypes = {
nodeInfo: PropTypes.object,
nodeData: PropTypes.object,
};

View File

@ -0,0 +1,298 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import {AzureCredSchema, AzureClusterSchema, AzureDatabaseSchema} from './azure_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';
// Azure credentials
export function AzureCredentials(props) {
const [cloudDBCredInstance, setCloudDBCredInstance] = React.useState();
var _eventBus = React.useContext(CloudWizardEventsContext);
React.useMemo(() => {
const azureCloudDBCredSchema = new AzureCredSchema({
authenticateAzure:(auth_type, azure_tenant_id) => {
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, 'Microsoft Azure authentication process is in progress..<img src="' + loading_icon_url + '" alt="' + gettext('Loading...') + '">']);
let _url = url_for('azure.verify_credentials');
const post_data = {
cloud: 'azure',
secret: {'auth_type':auth_type, 'azure_tenant_id':azure_tenant_id}
};
return new Promise((resolve, reject)=>{axiosApi.post(_url, post_data)
.then((res) => {
if (res.data && 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.')]);
_eventBus.fireEvent('SET_CRED_VERIFICATION_INITIATED',true);
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 verification Microsoft Azure: ${error.response.data.errormsg}`)]);
reject(false);
});});
}
});
setCloudDBCredInstance(azureCloudDBCredSchema);
}, [props.cloudProvider]);
return <SchemaView
formType={'dialog'}
getInitData={() => { /*This is intentional (SonarQube)*/ }}
viewHelperProps={{ mode: 'create' }}
schema={cloudDBCredInstance}
showFooter={false}
isTabView={false}
onDataChange={(isChanged, changedData) => {
props.setAzureCredData(changedData);
}}
/>;
}
AzureCredentials.propTypes = {
nodeInfo: PropTypes.object,
nodeData: PropTypes.object,
cloudProvider: PropTypes.string,
setAzureCredData: PropTypes.func
};
// Azure Instance
export function AzureInstanceDetails(props) {
const [azureInstanceSchema, setAzureInstanceSchema] = React.useState();
React.useMemo(() => {
const AzureSchema = new AzureClusterSchema({
subscriptions: () => getNodeAjaxOptions('get_subscriptions', {}, {}, {},{
useCache:false,
cacheNode: 'server',
customGenerateUrl: ()=>{
return url_for('azure.subscriptions');
}
}),
resourceGroups: (subscription)=>getNodeAjaxOptions('ge_resource_groups', pgAdmin.Browser.Nodes['server'], props.nodeInfo, props.nodeData, {
useCache:false,
cacheNode: 'server',
customGenerateUrl: ()=>{
return url_for('azure.resource_groups', {'subscription_id': subscription});
}
}),
regions: (subscription)=>getNodeAjaxOptions('get_regions', pgAdmin.Browser.Nodes['server'], props.nodeInfo, props.nodeData,{
useCache:false,
cacheNode: 'server',
customGenerateUrl: ()=>{
return url_for('azure.regions', {'subscription_id': subscription});
}
}),
availabilityZones: (region)=>getNodeAjaxOptions('get_availability_zones', pgAdmin.Browser.Nodes['server'], props.nodeInfo, props.nodeData, {
useCache:false,
cacheNode: 'server',
customGenerateUrl: ()=>{
return url_for('azure.availability_zones', {'region_name': region});
}
}),
versionOptions: (availabilityZone)=>getNodeAjaxOptions('get_db_versions', pgAdmin.Browser.Nodes['server'], props.nodeInfo, props.nodeData, {
useCache:false,
cacheNode: 'server',
customGenerateUrl: ()=>{
return url_for('azure.db_versions', {'availability_zone': availabilityZone});
}
}),
instanceOptions: (dbVersion, availabilityZone)=>{
if (isEmptyString(dbVersion) || isEmptyString(availabilityZone) ) return [];
return getNodeAjaxOptions('get_instance_types', pgAdmin.Browser.Nodes['server'], props.nodeInfo, props.nodeData, {
useCache:false,
cacheNode: 'server',
customGenerateUrl: ()=>{
return url_for('azure.instance_types', {'availability_zone':availabilityZone, 'db_version': dbVersion});
}
});},
storageOptions: (dbVersion, availabilityZone)=>{
if (isEmptyString(dbVersion) || isEmptyString(availabilityZone) ) return [];
return getNodeAjaxOptions('get_instance_types', pgAdmin.Browser.Nodes['server'], props.nodeInfo, props.nodeData, {
useCache:false,
cacheNode: 'server',
customGenerateUrl: ()=>{
return url_for('azure.storage_types', {'availability_zone':availabilityZone, 'db_version': dbVersion});
}
});
},
zoneRedundantHaSupported: (region)=>getNodeAjaxOptions('is_zone_redundant_ha_supported', pgAdmin.Browser.Nodes['server'], props.nodeInfo, props.nodeData,{
useCache:false,
cacheNode: 'server',
customGenerateUrl: ()=>{
return url_for('azure.zone_redundant_ha_supported', {'region_name': region});
}
}),
}, {
nodeInfo: props.nodeInfo,
nodeData: props.nodeData,
hostIP: props.hostIP,
...props.azureInstanceData
});
setAzureInstanceSchema(AzureSchema);
}, [props.cloudProvider]);
return <SchemaView
formType={'dialog'}
getInitData={() => { /*This is intentional (SonarQube)*/ }}
viewHelperProps={{ mode: 'create' }}
schema={azureInstanceSchema}
showFooter={false}
isTabView={false}
onDataChange={(isChanged, changedData) => {
props.setAzureInstanceData(changedData);
}}
/>;
}
AzureInstanceDetails.propTypes = {
nodeInfo: PropTypes.object,
nodeData: PropTypes.object,
cloudProvider: PropTypes.string,
setAzureInstanceData: PropTypes.func,
hostIP: PropTypes.string,
subscriptions: PropTypes.array,
azureInstanceData: PropTypes.object
};
// Azure Database Details
export function AzureDatabaseDetails(props) {
const [azureDBInstance, setAzureDBInstance] = React.useState();
React.useMemo(() => {
const azureDBSchema = new AzureDatabaseSchema({
server_groups: ()=>getNodeListById(pgAdmin.Browser.Nodes['server_group'], props.nodeInfo, props.nodeData),
},
{
gid: props.nodeInfo['server_group']._id,
}
);
setAzureDBInstance(azureDBSchema);
}, [props.cloudProvider]);
return <SchemaView
formType={'dialog'}
getInitData={() => { /*This is intentional (SonarQube)*/ }}
viewHelperProps={{ mode: 'create' }}
schema={azureDBInstance}
showFooter={false}
isTabView={false}
onDataChange={(isChanged, changedData) => {
props.setAzureDatabaseData(changedData);
}}
/>;
}
AzureDatabaseDetails.propTypes = {
nodeInfo: PropTypes.object,
nodeData: PropTypes.object,
cloudProvider: PropTypes.string,
setAzureDatabaseData: PropTypes.func,
};
// Validation functions
export function validateAzureStep2(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 validateAzureStep3(cloudDBDetails, nodeInfo) {
let isError = false;
if (isEmptyString(cloudDBDetails.db_username) || isEmptyString(cloudDBDetails.db_password)) {
isError = true;
}
if (isEmptyString(cloudDBDetails.gid)) cloudDBDetails.gid = nodeInfo['server_group']._id;
return isError;
}
// Check cluster name avaiablity
export function checkClusternameAvailbility(clusterName){
return new Promise((resolve, reject)=>{
let _url = url_for('azure.check_cluster_name_availability');
const axiosApi = getApiInstance();
axiosApi.get(_url, {
params: {
'name': clusterName,
}
}).then((res)=>{
if (res.data) {
resolve(res.data);
}
}).catch((error) => {
reject(gettext(`Error while checking server name availability with Microsoft Azure: ${error.response.data.errormsg}`));
});
});
}
// Summary creation
function createData(name, value) {
if (typeof(value) == 'boolean') {
value = (value === true) ? 'True' : 'False';
}
return { name, value };
}
// Summary section
export function getAzureSummary(cloud, cloudInstanceDetails, cloudDBDetails) {
const rows1 = [
createData('Cloud', cloud),
createData('Subscription', cloudInstanceDetails.subscription),
createData('Resource group', cloudInstanceDetails.resource_group),
createData('Region', cloudInstanceDetails.region),
createData('Availability zone', cloudInstanceDetails.availability_zone),
];
const rows2 = [
createData('PostgreSQL version', cloudInstanceDetails.db_version),
createData('Instance type', cloudInstanceDetails.instance_type),
];
const rows3 = [
createData('Allocated storage', cloudInstanceDetails.storage_size + ' GiB'),
];
const rows4 = [
createData('Username', cloudDBDetails.db_username),
createData('Password', 'xxxxxxx'),
];
const rows5 = [
createData('Public IP', cloudInstanceDetails.public_ips),
];
const rows6 = [
createData('High availability', cloudInstanceDetails.high_availability),
];
return [rows1, rows2, rows3, rows4, rows5, rows6];
}

View File

@ -0,0 +1,683 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, 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';
import { CloudWizardEventsContext } from './CloudWizard';
import React from 'react';
import pgAdmin from 'sources/pgadmin';
class AzureCredSchema extends BaseUISchema {
constructor(fieldOptions = {}, initValues = {}) {
super({
oid: null,
auth_type: 'interactive_browser_credential',
azure_tenant_id: '',
azure_subscription_id: '',
...initValues,
});
this.fieldOptions = {
...fieldOptions,
};
this.eventBus = React.useContext(CloudWizardEventsContext);
}
get idAttribute() {
return 'oid';
}
validate(state, setErrMsg) {
let isError = false;
if (state.auth_type == 'interactive_browser_credential' && state.azure_tenant_id == '') {
isError = true;
setErrMsg(
'azure_tenant_id',
gettext('Azure Tenant Id is required for Azure interactive authentication.')
);
}
return isError;
}
get baseFields() {
let obj = this;
return [
{
id: 'auth_type',
label: gettext('Authenticate via'),
type: 'toggle',
mode: ['create'],
noEmpty: true,
options: [
{
label: gettext('Interactive Browser'),
value: 'interactive_browser_credential',
},
{
label: gettext('Azure CLI'),
value: 'azure_cli_credential',
},
],
disabled: pgAdmin.server_mode == 'True' ? true : false,
helpMessage: gettext(
'Azure CLI will use the currently logged in identity through the Azure CLI on the local machine. Interactive Browser will open a browser window to authenticate a user interactively.'
),
},
{
id: 'azure_tenant_id',
label: gettext('Azure tenant id'),
type: 'text',
mode: ['create'],
deps: ['auth_type'],
helpMessage: gettext(
'Enter the Azure tenant ID against which the user is authenticated.'
),
disabled: (state) => {
return state.auth_type == 'interactive_browser_credential'
? false
: true;
},
depChange: (state) => {
if (state.auth_type == 'azure_cli_credential') {
state.azure_tenant_id = '';
}
this.eventBus.fireEvent('SET_CRED_VERIFICATION_INITIATED', false);
this.eventBus.fireEvent('SET_ERROR_MESSAGE_FOR_CLOUD_WIZARD',['', '']);
},
},
{
id: 'auth_btn',
mode: ['create'],
deps: ['auth_type', 'azure_tenant_id'],
btnName: gettext('Click here to authenticate yourself to Microsoft Azure'),
type: (state) => {
return {
type: 'button',
onClick: () => {
obj.fieldOptions.authenticateAzure(state.auth_type, state.azure_tenant_id).then((res)=>{state._disabled_auth_btn= res;});
},
};
},
helpMessage: gettext(
'After clicking the button above you will be redirected to the Microsoft Azure authentication page in a new browser tab if the Interactive Browser option is selected.'
),
depChange: (state, source)=> {
if(source[0] == 'auth_type' || source[0] == 'azure_tenant_id'){
state._disabled_auth_btn = false;
}
},
disabled: (state)=> {
if(state.auth_type == 'interactive_browser_credential' && state.azure_tenant_id == ''){
return true;
}
return state._disabled_auth_btn;
},
},
];
}
}
class AzureProjectDetailsSchema extends BaseUISchema {
constructor(fieldOptions = {}, initValues = {}) {
super({
oid: undefined,
subscription: '',
new_resource_group: false,
resource_group: '',
regions: '',
availability_zones: '',
high_availability: false,
...initValues,
});
this.fieldOptions = {
...fieldOptions,
};
this.initValues = initValues;
}
get idAttribute() {
return 'oid';
}
get baseFields() {
return [
{
id: 'subscription',
label: gettext('Subscription'),
mode: ['create'],
type: () => {
return {
type: 'select',
options: this.fieldOptions.subscriptions,
controlProps: {
allowClear: false,
filter: (options) => {
if (options.length == 0) return;
let _options = [];
options.forEach((option) => {
_options.push({
label:
option.subscription_name + ' | ' + option.subscription_id,
value: option.subscription_id,
});
});
return _options;
},
},
};
},
},
{
id: 'resource_group',
label: gettext('Resource group'),
mode: ['create'],
deps: ['subscription'],
type: (state) => {
return {
type: 'select',
options: state.subscription
? () => this.fieldOptions.resourceGroups(state.subscription)
: [],
optionsReloadBasis: state.subscription,
controlProps: {
creatable: true,
allowClear: false,
},
};
},
},
{
id: 'region',
label: gettext('Location'),
mode: ['create'],
deps: ['subscription'],
type: (state) => {
return {
type: 'select',
options: state.subscription
? () => this.fieldOptions.regions(state.subscription)
: [],
optionsReloadBasis: state.subscription,
allowClear: false,
};
},
},
{
id: 'availability_zone',
label: gettext('Availability zone'),
deps: ['region'],
allowClear: false,
type: (state) => {
return {
type: 'select',
options: state.region
? () => this.fieldOptions.availabilityZones(state.region)
: [],
optionsReloadBasis: state.region,
};
},
},
];
}
}
class AzureInstanceSchema 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: (state) => {
return {
type: 'select',
options: state.availability_zone
? () => this.fieldOptions.versionOptions(state.availability_zone)
: [],
optionsReloadBasis: state.availability_zone,
};
},
},
{
id: 'db_instance_class',
label: gettext('Instance class'),
type: 'select',
options: [
{
label: gettext('Burstable (1-2 vCores) '),
value: 'Burstable' },
{
label: gettext('General Purpose (2-64 vCores)'),
value: 'GeneralPurpose',
},
{
label: gettext('Memory Optimized (2-64 vCores)'),
value: 'MemoryOptimized',
},
],
},
{
id: 'instance_type',
label: gettext('Instance type'),
deps: ['db_version', 'db_instance_class'],
depChange: (state, source)=>{
if(source[0] == 'db_instance_class'){
state.instance_type = undefined;
}
},
type: (state) => {
return {
type: 'select',
options: () => this.fieldOptions.instanceOptions(state.db_version,state.availability_zone),
optionsReloadBasis: state.db_version + state.db_instance_class,
controlProps: {
allowClear: false,
filter: (options) => {
if (options.length == 0 || state.db_instance_class === undefined)
return;
let _options = [];
options.forEach((option) => {
if (option.type == state.db_instance_class) {
_options.push({
label: option.label,
value: option.value,
});
}
});
return _options;
},
},
};
},
},
{
id: 'storage_size',
label: gettext('Storage Size'),
deps: ['db_version', 'db_instance_class'],
type: (state) => {
return {
type: 'select',
options: () => this.fieldOptions.storageOptions(state.db_version, state.availability_zone),
optionsReloadBasis: state.db_version + (state.db_instance_class || 'Burstable'),
controlProps: {
allowClear: false,
filter: (opts) => {
if (opts.length == 0 || state.db_instance_class === undefined)
return;
let _options = [];
opts.forEach((opt) => {
if (opt.type == state.db_instance_class) {
_options.push({
label: opt.label,
value: opt.value,
});
}
});
return _options;
},
},
};
},
},
];
}
}
class AzureDatabaseSchema extends BaseUISchema {
constructor(fieldOptions = {}, initValues = {}) {
super({
oid: undefined,
gid: undefined,
db_username: '',
db_password: '',
db_confirm_password: '',
...initValues,
});
this.fieldOptions = {
...fieldOptions,
};
}
validateDbUserName(data, setErrMsg) {
if (data.db_username.length < 1 && data.db_username.length > 63 && !/^[A-Za-z0-9]*$/.test(data.db_username)) {
setErrMsg(
'db_username',
gettext('Admin username must be more than 1 character & less than 63 and must only contains characters and numbers.')
);
return true;
}
if (
['azure_superuser', 'azure_pg_admin', 'admin', 'administrator', 'root', 'guest', 'public'].includes(data.db_username) ||
data.db_username.startsWith('pg_')) {
setErrMsg('db_username', gettext('Specified Admin username is not allowed'));
return true;
}
return false;
}
validateDbPassword(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;
}
if (!isEmptyString(data.db_confirm_password) && !/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[#$@!%&*?])[A-Za-z\d#$@!%&*?]{8,128}$/.test(data.db_confirm_password)) {
setErrMsg(
'db_confirm_password',
gettext(
'The password must be 8-128 characters long and must contain characters from three of the following categories - English uppercase letters, English lowercase letters, numbers (0-9), and non-alphanumeric characters (!, $, #, %, etc.)'
)
);
return true;
}
return false;
}
validate(data, setErrMsg) {
if (this.validateDbUserName(data, setErrMsg) || this.validateDbPassword(data, setErrMsg)) {
return true;
}
return false;
}
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,
helpMessage: gettext(
'The admin username must be 1-63 characters long and can only contain character, numbers and the underscore character. The username cannot be "azure_superuser", "azure_pg_admin", "admin", "administrator", "root", "guest", "public", or start with "pg_".'
),
},
{
id: 'db_password',
label: gettext('Password'),
type: 'password',
mode: ['create'],
noEmpty: true,
helpMessage: gettext(
'The password must be 8-128 characters long and must contain characters from three of the following categories - English uppercase letters, English lowercase letters, numbers (0-9), and non-alphanumeric characters (!, $, #, %, etc.), and cannot contain all or part of the login name'
),
},
{
id: 'db_confirm_password',
label: gettext('Confirm password'),
type: 'password',
mode: ['create'],
noEmpty: true,
},
];
}
}
class AzureNetworkSchema extends BaseUISchema {
constructor() {
super();
}
get baseFields() {
return [
{
id: 'public_ips',
label: gettext('Public IP range'),
type: 'text',
mode: ['create'],
helpMessage: gettext(
'List of IP Addresses or range of IP Addresses (start IP Address - end IP address) from which inbound traffic should be accepted. Add multiple IP addresses/ranges separated with commas, for example: "192.168.0.50, 192.168.0.100 - 192.168.0.200"'
),
},
];
}
}
class AzureHighAvailabilitySchema 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('Zone redundant high availability'),
type: 'switch',
mode: ['create'],
deps: ['region', 'db_instance_class'],
depChange: (state, source, topState, actionObj) => {
state._is_zone_redundant_ha_supported = false;
if (state.region != actionObj.oldState.region) {
state.high_availability = false;
this.fieldOptions
.zoneRedundantHaSupported(state.region)
.then((res) => {
state._is_zone_redundant_ha_supported = res.is_zone_redundant_ha_supported;
});
}
if (state.db_instance_class != 'Burstable') {
state._is_zone_redundant_ha_supported = true;
}
},
disabled: (state) => {
if (isEmptyString(state.region) || state.db_instance_class == 'Burstable') {
state.high_availability = false;
return true;
} else {
return !state._is_zone_redundant_ha_supported;
}
},
helpMessage: gettext(
'Zone redundant high availability deploys a standby replica in a different zone. The Burstable instance type does not support high availability.'
),
},
];
}
}
class AzureClusterSchema extends BaseUISchema {
constructor(fieldOptions = {}, initValues = {}) {
super({
oid: undefined,
name: '',
// Need to initilize child class init values in parent class itself
public_ips: initValues?.hostIP.split('/')[0],
db_instance_class: 'Burstable',
...initValues,
});
this.fieldOptions = {
...fieldOptions,
};
this.initValues = initValues;
this.azureProjectDetails = new AzureProjectDetailsSchema(
{
subscriptions: this.fieldOptions.subscriptions,
resourceGroups: this.fieldOptions.resourceGroups,
regions: this.fieldOptions.regions,
availabilityZones: this.fieldOptions.availabilityZones,
},
{}
);
this.azureInstanceDetails = new AzureInstanceSchema(
{
versionOptions: this.fieldOptions.versionOptions,
instanceOptions: this.fieldOptions.instanceOptions,
storageOptions: this.fieldOptions.storageOptions,
},
{}
);
this.azureNetworkSchema = new AzureNetworkSchema({}, {});
this.azureHighAvailabilitySchema = new AzureHighAvailabilitySchema(
{
zoneRedundantHaSupported: this.fieldOptions.zoneRedundantHaSupported,
},
{}
);
}
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.azureProjectDetails,
},
{
type: 'nested-fieldset',
label: gettext('Version & Instance'),
mode: ['create'],
schema: this.azureInstanceDetails,
},
{
type: 'nested-fieldset',
label: gettext('Network Connectivity'),
mode: ['create'],
schema: this.azureNetworkSchema,
},
{
type: 'nested-fieldset',
label: gettext('Availability'),
mode: ['create'],
schema: this.azureHighAvailabilitySchema,
},
];
}
validateProjectDetails(data, setErr){
if(isEmptyString(data.subscription)){
setErr('subscription',gettext('Subscription cannot be empty.'));
return true;
}
if(isEmptyString(data.resource_group)){
setErr('resource_group',gettext('Resource group cannot be empty.'));
return true;
}
if(isEmptyString(data.region)){
setErr('region',gettext('Location cannot be empty.'));
return true;
}
}
validateInstanceDetails(data, setErr){
if(isEmptyString(data.availability_zone)){
setErr('availability_zone',gettext('Availability zone cannot be empty.'));
return true;
}
if(isEmptyString(data.db_version)){
setErr('db_version',gettext('Database version cannot be empty.'));
return true;
}
if(isEmptyString(data.db_instance_class)){
setErr('db_instance_class',gettext('Instance class cannot be empty.'));
return true;
}
}
validateNetworkDetails(data, setErr){
if(isEmptyString(data.instance_type)){
setErr('instance_type',gettext('Instance type cannot be empty.'));
return true;
}
if(isEmptyString(data.storage_size)){
setErr('storage_size',gettext('Storage size cannot be empty.'));
return true;
}
if(isEmptyString(data.public_ips)){
setErr('public_ips',gettext('Public IP range cannot be empty.'));
return true;
}
}
validate(data, setErr) {
if ( !isEmptyString(data.name) && (!/^[a-z0-9\-]*$/.test(data.name) || data.name.length < 3)) {
setErr('name',gettext('Name must be more than 2 characters or more & must only contain lowercase letters, numbers, and hyphens'));
return true;
}
if(this.validateProjectDetails(data, setErr) || this.validateInstanceDetails(data, setErr) || this.validateNetworkDetails(data, setErr)){
return true;
}
return false;
}
}
export { AzureCredSchema, AzureClusterSchema, AzureDatabaseSchema };

View File

@ -10,6 +10,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import Theme from 'sources/Theme';
import CloudWizard from './CloudWizard';
import getApiInstance from '../../../../static/js/api_instance';
// Cloud Wizard
@ -124,6 +125,15 @@ define('pgadmin.misc.cloud', [
hooks: {
// Triggered when the dialog is closed
onclose: function () {
if(event.target instanceof Object){
const axiosApi = getApiInstance();
let _url = url_for('cloud.clear_cloud_session');
axiosApi.post(_url)
.then(() => {})
.catch((error) => {
Alertify.error(gettext(`Error while clearing cloud wizard data: ${error.response.data.errormsg}`));
});
}
// Clear the view and remove the react component.
return setTimeout((function () {
ReactDOM.unmountComponentAtNode(document.getElementById('cloudWizardDlg'));

View File

@ -14,6 +14,7 @@ import { DefaultButton, PrimaryButton } from '../../../../static/js/components/B
import { makeStyles } from '@material-ui/core/styles';
import PropTypes from 'prop-types';
import { getAWSSummary } from './aws';
import {getAzureSummary} from './azure';
import { getBigAnimalSummary } from './biganimal';
import { commonTableStyles } from '../../../../static/js/Theme';
import { Table, TableBody, TableCell, TableHead, TableRow } from '@material-ui/core';
@ -70,7 +71,10 @@ export function FinalSummary(props) {
if (props.cloudProvider == 'biganimal') {
summary = getBigAnimalSummary(props.cloudProvider, props.instanceData, props.databaseData);
summaryHeader[1] = 'Version Details';
} else {
} else if(props.cloudProvider == 'azure'){
summaryHeader.push('Network Connectivity','Availability');
summary = getAzureSummary(props.cloudProvider, props.instanceData, props.databaseData);
}else {
summary = getAWSSummary(props.cloudProvider, props.instanceData, props.databaseData);
}

View File

@ -20,10 +20,10 @@ def get_my_ip():
""" Return the public IP of this host """
http = urllib3.PoolManager()
try:
external_ip = http.request('GET', 'http://ident.me').data
external_ip = http.request('GET', 'http://ifconfig.me/ip').data
except Exception:
try:
external_ip = http.request('GET', 'http://ifconfig.me/ip').data
external_ip = http.request('GET', 'http://ident.me').data
except Exception:
external_ip = '127.0.0.1'

View File

@ -0,0 +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>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -12,7 +12,7 @@ import _ from 'lodash';
import {
FormInputText, FormInputSelect, FormInputSwitch, FormInputCheckbox, FormInputColor,
FormInputFileSelect, FormInputToggle, InputSwitch, FormInputSQL, InputSQL, FormNote, FormInputDateTimePicker, PlainString,
InputSelect, InputText, InputCheckbox, InputDateTimePicker, InputFileSelect, FormInputKeyboardShortcut, FormInputQueryThreshold, FormInputSelectThemes, InputRadio
InputSelect, InputText, InputCheckbox, InputDateTimePicker, InputFileSelect, FormInputKeyboardShortcut, FormInputQueryThreshold, FormInputSelectThemes, InputRadio, FormButton
} from '../components/FormComponents';
import Privilege from '../components/Privilege';
import { evalFunc } from 'sources/utils';
@ -21,7 +21,7 @@ import CustomPropTypes from '../custom_prop_types';
import { SelectRefresh } from '../components/SelectRefresh';
/* Control mapping for form view */
function MappedFormControlBase({ type, value, id, onChange, className, visible, inputRef, noLabel, ...props }) {
function MappedFormControlBase({ type, value, id, onChange, className, visible, inputRef, noLabel, onClick, ...props }) {
const name = id;
const onTextChange = useCallback((e) => {
let val = e;
@ -86,6 +86,8 @@ function MappedFormControlBase({ type, value, id, onChange, className, visible,
return <FormInputQueryThreshold name={name} value={value} onChange={onTextChange} {...props}/>;
case 'theme':
return <FormInputSelectThemes name={name} value={value} onChange={onTextChange} {...props}/>;
case 'button':
return <FormButton name={name} value={value} className={className} onClick={onClick} {...props} />;
default:
return <PlainString value={value} {...props} />;
}
@ -103,7 +105,8 @@ MappedFormControlBase.propTypes = {
]),
visible: PropTypes.bool,
inputRef: CustomPropTypes.ref,
noLabel: PropTypes.bool
noLabel: PropTypes.bool,
onClick: PropTypes.func
};
/* Control mapping for grid cell view */
@ -197,11 +200,11 @@ const ALLOWED_PROPS_FIELD_COMMON = [
'mode', 'value', 'readonly', 'disabled', 'hasError', 'id',
'label', 'options', 'optionsLoaded', 'controlProps', 'schema', 'inputRef',
'visible', 'autoFocus', 'helpMessage', 'className', 'optionsReloadBasis',
'orientation', 'isvalidate', 'fields', 'radioType', 'hideBrowseButton'
'orientation', 'isvalidate', 'fields', 'radioType', 'hideBrowseButton', 'btnName'
];
const ALLOWED_PROPS_FIELD_FORM = [
'type', 'onChange', 'state', 'noLabel', 'text',
'type', 'onChange', 'state', 'noLabel', 'text','onClick'
];
const ALLOWED_PROPS_FIELD_CELL = [

View File

@ -16,6 +16,7 @@ import Expand from '../../img/fonticon/open_in_full.svg?svgr';
import Collapse from '../../img/fonticon/close_fullscreen.svg?svgr';
import AWS from '../../img/aws.svg?svgr';
import BigAnimal from '../../img/biganimal.svg?svgr';
import Azure from '../../img/azure.svg?svgr';
export default function ExternalIcon({Icon, ...props}) {
return <Icon className={'MuiSvgIcon-root'} {...props} />;
@ -72,3 +73,6 @@ AWSIcon.propTypes = {style: PropTypes.object};
export const BigAnimalIcon = ({style})=><ExternalIcon Icon={BigAnimal} style={{height: '1.4rem', ...style}} data-label="BigAnimalIcon" />;
BigAnimalIcon.propTypes = {style: PropTypes.object};
export const AzureIcon = ({style})=><ExternalIcon Icon={Azure} style={{height: '1.4rem', ...style}} data-label="AzureIcon" />;
AzureIcon.propTypes = {style: PropTypes.object};

View File

@ -1289,3 +1289,22 @@ NotifierMessage.propTypes = {
closable: PropTypes.bool,
onClose: PropTypes.func,
};
export function FormButton({required, label,
className, helpMessage, onClick, disabled, ...props }) {
return (
<FormInput required={required} label={label} className={className} helpMessage={helpMessage}>
<PrimaryButton onClick={onClick} disabled={disabled} >{gettext(props.btnName)}</PrimaryButton>
</FormInput>
);
}
FormButton.propTypes = {
required: PropTypes.bool,
label: PropTypes.string,
className: CustomPropTypes.className,
helpMessage: PropTypes.string,
onClick: PropTypes.func,
disabled: PropTypes.bool,
btnName: PropTypes.string
};