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

@@ -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'