2022-06-15 00:52:42 -05:00
|
|
|
##########################################################################
|
|
|
|
#
|
|
|
|
# pgAdmin 4 - PostgreSQL Tools
|
|
|
|
#
|
2024-01-01 02:43:48 -06:00
|
|
|
# Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
2022-06-15 00:52:42 -05:00
|
|
|
# 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
|
2022-07-06 01:13:49 -05:00
|
|
|
from azure.identity import AzureCliCredential, DeviceCodeCredential, \
|
2022-06-27 09:06:20 -05:00
|
|
|
AuthenticationRecord
|
2022-06-15 00:52:42 -05:00
|
|
|
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
|
2022-06-28 00:58:55 -05:00
|
|
|
import sys
|
|
|
|
CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
|
|
|
|
root = os.path.dirname(os.path.dirname(CURRENT_PATH))
|
|
|
|
sys.path.insert(0, root)
|
2023-10-31 03:37:34 -05:00
|
|
|
from utils.azure_cache import load_persistent_cache, \
|
2022-06-27 09:06:20 -05:00
|
|
|
TokenCachePersistenceOptions
|
2022-06-15 00:52:42 -05:00
|
|
|
|
|
|
|
|
|
|
|
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
|
2022-07-06 01:13:49 -05:00
|
|
|
self._interactive_browser_credential = False
|
2022-06-15 00:52:42 -05:00
|
|
|
self._available_capabilities = None
|
|
|
|
self._credentials = None
|
|
|
|
self._authentication_record_json = None
|
|
|
|
self._cli_credentials = None
|
2022-07-06 01:13:49 -05:00
|
|
|
self._azure_cred_cache_name = None
|
|
|
|
self._azure_cred_cache_location = None
|
2022-06-15 00:52:42 -05:00
|
|
|
|
|
|
|
# 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:
|
2022-07-06 01:13:49 -05:00
|
|
|
self._interactive_browser_credential = False \
|
2022-06-15 00:52:42 -05:00
|
|
|
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']
|
|
|
|
|
2022-06-27 09:06:20 -05:00
|
|
|
if 'AZURE_CRED_CACHE_NAME' in os.environ:
|
2022-06-28 00:58:55 -05:00
|
|
|
self._azure_cred_cache_name = os.environ['AZURE_CRED_CACHE_NAME']
|
|
|
|
|
|
|
|
if 'AZURE_CRED_CACHE_LOCATION' in os.environ:
|
|
|
|
self._azure_cred_cache_location = \
|
|
|
|
os.environ['AZURE_CRED_CACHE_LOCATION']
|
2022-06-27 09:06:20 -05:00
|
|
|
|
2022-06-15 00:52:42 -05:00
|
|
|
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:
|
2022-07-06 01:13:49 -05:00
|
|
|
if self._interactive_browser_credential:
|
|
|
|
_credentials = self._azure_interactive_auth()
|
2022-06-15 00:52:42 -05:00
|
|
|
else:
|
2022-07-06 01:13:49 -05:00
|
|
|
_credentials = self._azure_cli_auth()
|
2022-06-15 00:52:42 -05:00
|
|
|
except Exception as e:
|
|
|
|
return False, str(e)
|
|
|
|
return True, _credentials
|
|
|
|
|
2022-07-06 01:13:49 -05:00
|
|
|
def _azure_cli_auth(self):
|
|
|
|
if self._cli_credentials is None:
|
|
|
|
self._cli_credentials = AzureCliCredential()
|
|
|
|
return self._cli_credentials
|
|
|
|
|
|
|
|
def _azure_interactive_auth(self):
|
|
|
|
if self._authentication_record_json is None:
|
|
|
|
_interactive_credential = DeviceCodeCredential(
|
2022-06-15 00:52:42 -05:00
|
|
|
tenant_id=self._tenant_id,
|
|
|
|
timeout=180,
|
2022-07-06 01:13:49 -05:00
|
|
|
prompt_callback=None,
|
|
|
|
_cache=load_persistent_cache(TokenCachePersistenceOptions(
|
|
|
|
name=self._azure_cred_cache_name,
|
|
|
|
allow_unencrypted_storage=True,
|
|
|
|
cache_location=self._azure_cred_cache_location)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
_auth_record = _interactive_credential.authenticate()
|
|
|
|
self._authentication_record_json = _auth_record.serialize()
|
2022-06-15 00:52:42 -05:00
|
|
|
else:
|
2022-07-06 01:13:49 -05:00
|
|
|
deserialized_auth_record = AuthenticationRecord.deserialize(
|
|
|
|
self._authentication_record_json)
|
|
|
|
_interactive_credential = DeviceCodeCredential(
|
2022-06-15 00:52:42 -05:00
|
|
|
tenant_id=self._tenant_id,
|
|
|
|
timeout=180,
|
2022-07-06 01:13:49 -05:00
|
|
|
prompt_callback=None,
|
|
|
|
_cache=load_persistent_cache(TokenCachePersistenceOptions(
|
|
|
|
name=self._azure_cred_cache_name,
|
|
|
|
allow_unencrypted_storage=True,
|
|
|
|
cache_location=self._azure_cred_cache_location)
|
|
|
|
),
|
|
|
|
authentication_record=deserialized_auth_record
|
2022-06-23 07:49:32 -05:00
|
|
|
)
|
2022-07-06 01:13:49 -05:00
|
|
|
return _interactive_credential
|
2022-06-15 00:52:42 -05:00
|
|
|
|
|
|
|
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:
|
2024-01-24 07:03:43 -06:00
|
|
|
_, self._credentials = \
|
2022-06-15 00:52:42 -05:00
|
|
|
self._get_azure_credentials()
|
|
|
|
|
|
|
|
if type in self._clients:
|
|
|
|
return self._clients[type]
|
|
|
|
|
2022-06-27 09:06:20 -05:00
|
|
|
self._clients['postgresql'] = PostgreSQLManagementClient(
|
|
|
|
self._credentials, self._subscription_id)
|
|
|
|
self._clients['resource'] = ResourceManagementClient(
|
|
|
|
self._credentials, self._subscription_id)
|
2022-06-15 00:52:42 -05:00
|
|
|
|
|
|
|
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:
|
2022-08-05 08:28:03 -05:00
|
|
|
error(str(e))
|
2022-06-15 00:52:42 -05:00
|
|
|
|
|
|
|
if svr is not None:
|
2022-08-05 08:28:03 -05:00
|
|
|
error('Azure Database for PostgreSQL instance {} already '
|
|
|
|
'exists.'.format(args.name))
|
2022-06-15 00:52:42 -05:00
|
|
|
|
|
|
|
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:
|
2022-08-05 08:28:03 -05:00
|
|
|
error(str(e))
|
2022-06-15 00:52:42 -05:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2022-08-05 08:28:03 -05:00
|
|
|
def _delete_azure_instance(self, args):
|
2022-06-15 00:52:42 -05:00
|
|
|
""" 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:
|
2022-08-05 08:28:03 -05:00
|
|
|
error(str(e))
|
2022-06-15 00:52:42 -05:00
|
|
|
|
|
|
|
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 """
|
2022-08-05 08:28:03 -05:00
|
|
|
self._delete_azure_instance(args)
|
2022-06-15 00:52:42 -05:00
|
|
|
|
|
|
|
|
|
|
|
def load():
|
|
|
|
""" Loads the current provider """
|
|
|
|
return AzureProvider()
|