mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2024-11-25 10:10:19 -06:00
344 lines
14 KiB
Python
344 lines
14 KiB
Python
|
##########################################################################
|
||
|
#
|
||
|
# 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()
|