pgadmin4/web/pgacloud/providers/rds.py
Khushboo Vashi ffc1c6c3b7 Fixed following issues related to cloud deployment:
1) The Mumbai region issue has been resolved
2) Display name of regions has been modified appropriately
3) The password field has been validated the same way as AWS
4) Added support for a list of IP addresses in the public IP address range field.
2022-02-28 18:19:18 +05:30

320 lines
13 KiB
Python

##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2022, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
""" Amazon RDS PostgreSQL provider """
import os
import time
import boto3
from providers._abstract import AbsProvider
from utils.io import debug, error, output
from utils.misc import get_my_ip, get_random_id
DEL_SEC_GROUP_MSG = 'Deleting security group: {}...'
class RdsProvider(AbsProvider):
def __init__(self):
self._clients = {}
self._access_key = None
self._secret_key = None
self._session_token = None
self._database_pass = None
self._default_region = None
# Get the credentials
if 'AWS_ACCESS_KEY_ID' in os.environ:
self._access_key = os.environ['AWS_ACCESS_KEY_ID']
if 'AWS_SECRET_ACCESS_KEY' in os.environ:
self._secret_key = os.environ['AWS_SECRET_ACCESS_KEY']
if 'AWS_SESSION_TOKEN' in os.environ:
self._session_token = os.environ['AWS_SESSION_TOKEN']
if 'AWS_DATABASE_PASSWORD' in os.environ:
self._database_pass = os.environ['AWS_DATABASE_PASSWORD']
def init_args(self, parsers):
""" Create the command line parser for this provider """
self.parser = parsers.add_parser('rds',
help='Amazon AWS RDS PostgreSQL',
epilog='Credentials are read from '
'~/.aws/config by default and '
'can be overridden in the '
'AWS_ACCESS_KEY_ID and '
'AWS_SECRET_ACCESS_KEY '
'environment variables. '
'The default region is read '
'from ~/.aws/config and will '
'fall back to us-east-1 if '
'not present.')
self.parser.add_argument('--region', default=self._default_region,
help='name of the AWS region (default: {})'
.format(self._default_region))
# Create the command sub-parser
parsers = self.parser.add_subparsers(help='RDS 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-name', default='postgres',
help='name of the default '
'database '
'(default: postgres)')
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-port', type=int,
default=5432,
help='port of the database '
'(default: 5432)')
parser_create_instance.add_argument('--db-version',
default='13.3',
help='version of PostgreSQL '
'to deploy (default: 13.3)')
parser_create_instance.add_argument('--instance-type', required=True,
help='machine type for the '
'instance nodes, e.g. '
'db.m3.large')
parser_create_instance.add_argument('--storage-iops', type=int,
default=0,
help='storage IOPs to allocate '
'(default: 0)')
parser_create_instance.add_argument('--storage-size', type=int,
required=True,
help='storage size in GB')
parser_create_instance.add_argument('--storage-type', default='gp2',
help='storage type for the data '
'database (default: gp2)')
parser_create_instance.add_argument('--public-ip', default='127.0.0.1',
help='Public IP '
'(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')
parser_delete_instance.add_argument('--security-group',
help='name of a security group to'
'delete as well')
##########################################################################
# AWS Helper functions
##########################################################################
def _get_aws_client(self, type, args):
""" Create/cache/return an AWS client object """
if type in self._clients:
return self._clients[type]
session = boto3.Session(
aws_access_key_id=self._access_key,
aws_secret_access_key=self._secret_key,
aws_session_token=self._session_token
)
self._clients['type'] = session.client(type, region_name=args.region)
return self._clients['type']
def _create_security_group(self, args):
""" Create a new security group for the instance """
ec2 = self._get_aws_client('ec2', args)
ip = args.public_ip if args.public_ip else get_my_ip()
ip = ip.split(',')
# Deploy the security group
try:
name = 'pgacloud_{}_{}_{}'.format(args.name,
ip[0].replace('.', '-'),
get_random_id())
debug(args, 'Creating security group: {}...'.format(name))
output({'Creating': 'Creating security group: {}...'.format(name)})
response = ec2.create_security_group(
Description='Inbound access for {} to RDS instance {}'.format(
ip[0], args.name),
GroupName=name
)
except Exception as e:
error(args, str(e))
return response['GroupId']
def _add_ingress_rule(self, args, security_group):
""" Add a local -> PostgreSQL ingress rule to a security group """
ec2 = self._get_aws_client('ec2', args)
ip = args.public_ip if args.public_ip else\
'{}/32'.format(get_my_ip())
port = args.db_port or 5432
IpRanges = []
ip = ip.split(',')
for i in ip:
IpRanges.append({
'CidrIp': i,
'Description': 'pgcloud client {}'.format(i)
})
try:
output({'Adding': 'Adding ingress rule for: {}...'.format(ip)})
debug(args,
'Adding ingress rule for: {}...'.format(ip))
ec2.authorize_security_group_ingress(
GroupId=security_group,
IpPermissions=[
{
'FromPort': port,
'ToPort': port,
'IpProtocol': 'tcp',
'IpRanges': IpRanges
},
]
)
except Exception as e:
error(args, e)
def _create_rds_instance(self, args, security_group):
""" Create an RDS instance """
ec2 = self._get_aws_client('ec2', args)
rds = self._get_aws_client('rds', args)
db_password = self._database_pass if self._database_pass is not None\
else args.db_password
try:
debug(args, 'Creating RDS instance: {}...'.format(args.name))
rds.create_db_instance(DBInstanceIdentifier=args.name,
AllocatedStorage=args.storage_size,
DBName=args.db_name,
Engine='postgres',
Port=args.db_port,
EngineVersion=args.db_version,
StorageType=args.storage_type,
StorageEncrypted=True,
Iops=args.storage_iops,
AutoMinorVersionUpgrade=True,
MultiAZ=False,
MasterUsername=args.db_username,
MasterUserPassword=db_password,
DBInstanceClass=args.instance_type,
VpcSecurityGroupIds=[
security_group,
])
except rds.exceptions.DBInstanceAlreadyExistsFault as e:
try:
debug(args, DEL_SEC_GROUP_MSG.format(security_group))
ec2.delete_security_group(GroupId=security_group)
except Exception:
pass
error(args, 'RDS instance {} already exists.'.format(args.name))
except Exception as e:
try:
debug(args, DEL_SEC_GROUP_MSG.format(security_group))
ec2.delete_security_group(GroupId=security_group)
except Exception:
pass
error(args, str(e))
# Wait for completion
running = True
while running:
response = rds.describe_db_instances(
DBInstanceIdentifier=args.name)
db_instance = response['DBInstances'][0]
status = db_instance['DBInstanceStatus']
if status != 'creating' and status != 'backing-up':
running = False
if running:
time.sleep(5)
return response['DBInstances']
def _delete_rds_instance(self, args, name):
""" Delete an RDS instance """
rds = self._get_aws_client('rds', args)
debug(args, 'Deleting RDS instance: {}...'.format(name))
try:
rds.delete_db_instance(
DBInstanceIdentifier=name,
SkipFinalSnapshot=True,
DeleteAutomatedBackups=True
)
except Exception as e:
error(args, str(e))
# Wait for completion
while True:
try:
rds.describe_db_instances(DBInstanceIdentifier=args.name)
except rds.exceptions.DBInstanceNotFoundFault:
return
except Exception as e:
error(args, str(e))
time.sleep(5)
def _delete_security_group(self, args, id):
""" Delete a security group """
ec2 = self._get_aws_client('ec2', args)
debug(args, 'Deleting security group: {}...'.format(id))
try:
ec2.delete_security_group(
GroupId=id
)
except Exception as e:
error(args, str(e))
##########################################################################
# User commands
##########################################################################
def cmd_create_instance(self, args):
""" Create an RDS instance and security group """
security_group = self._create_security_group(args)
self._add_ingress_rule(args, security_group)
instance = self._create_rds_instance(args, security_group)
data = {'instance': {
'Id': instance[0]['DBInstanceIdentifier'],
'Location': instance[0]['AvailabilityZone'],
'SecurityGroupId': security_group,
'Hostname': instance[0]['Endpoint']['Address'],
'Port': instance[0]['Endpoint']['Port'],
'Database': instance[0]['DBName'],
'Username': instance[0]['MasterUsername']
}}
output(data)
def cmd_delete_instance(self, args):
""" Delete an RDS instance and (optionally) a security group """
self._delete_rds_instance(args, args.name)
if args.security_group is not None:
self._delete_security_group(args, args.security_group)
def load():
""" Loads the current provider """
return RdsProvider()