Added capability to deploy PostgreSQL servers on Microsoft Azure. Fixes #7178
120
docs/en_US/cloud_azure_postgresql.rst
Normal 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
|
|
@ -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
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 130 KiB |
BIN
docs/en_US/images/cloud_azure_bg_process_watcher.png
Normal file
After Width: | Height: | Size: 227 KiB |
BIN
docs/en_US/images/cloud_azure_credentials.png
Normal file
After Width: | Height: | Size: 228 KiB |
BIN
docs/en_US/images/cloud_azure_database.png
Normal file
After Width: | Height: | Size: 237 KiB |
BIN
docs/en_US/images/cloud_azure_instance.png
Normal file
After Width: | Height: | Size: 236 KiB |
BIN
docs/en_US/images/cloud_azure_network.png
Normal file
After Width: | Height: | Size: 271 KiB |
BIN
docs/en_US/images/cloud_azure_provider.png
Normal file
After Width: | Height: | Size: 137 KiB |
BIN
docs/en_US/images/cloud_azure_review.png
Normal file
After Width: | Height: | Size: 232 KiB |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 127 KiB |
|
@ -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?'.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
343
web/pgacloud/providers/azure.py
Normal 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()
|
|
@ -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(
|
||||
|
|
693
web/pgadmin/misc/cloud/azure/__init__.py
Normal 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')
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
||||
|
|
298
web/pgadmin/misc/cloud/static/js/azure.js
Normal 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];
|
||||
}
|
683
web/pgadmin/misc/cloud/static/js/azure_schema.ui.js
Normal 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 };
|
|
@ -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'));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
1
web/pgadmin/static/img/azure.svg
Normal 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 |
|
@ -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 = [
|
||||
|
|
|
@ -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};
|
|
@ -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
|
||||
};
|
||||
|
|