2022-02-14 00:43:48 -06:00
|
|
|
##########################################################################
|
|
|
|
#
|
|
|
|
# pgAdmin 4 - PostgreSQL Tools
|
|
|
|
#
|
2024-01-01 02:43:48 -06:00
|
|
|
# Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
2022-02-14 00:43:48 -06:00
|
|
|
# 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 """
|
2023-04-28 04:46:22 -05:00
|
|
|
self.parser = parsers.add_parser('aws',
|
2023-03-23 01:21:21 -05:00
|
|
|
help='Amazon RDS PostgreSQL',
|
2022-02-14 00:43:48 -06:00
|
|
|
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)')
|
2022-09-22 00:56:05 -05:00
|
|
|
parser_create_instance.add_argument('--high-availability',
|
|
|
|
default=False)
|
2022-02-14 00:43:48 -06:00
|
|
|
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,
|
2022-02-18 03:07:05 -06:00
|
|
|
aws_secret_access_key=self._secret_key,
|
|
|
|
aws_session_token=self._session_token
|
2022-02-14 00:43:48 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
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()
|
2022-02-28 06:49:18 -06:00
|
|
|
ip = ip.split(',')
|
2022-02-14 00:43:48 -06:00
|
|
|
|
|
|
|
# Deploy the security group
|
|
|
|
try:
|
|
|
|
name = 'pgacloud_{}_{}_{}'.format(args.name,
|
2022-02-28 06:49:18 -06:00
|
|
|
ip[0].replace('.', '-'),
|
2022-02-14 00:43:48 -06:00
|
|
|
get_random_id())
|
2022-04-26 06:11:10 -05:00
|
|
|
debug('Creating security group: {}...'.format(name))
|
2022-02-14 00:43:48 -06:00
|
|
|
output({'Creating': 'Creating security group: {}...'.format(name)})
|
|
|
|
response = ec2.create_security_group(
|
|
|
|
Description='Inbound access for {} to RDS instance {}'.format(
|
2022-02-28 06:49:18 -06:00
|
|
|
ip[0], args.name),
|
2022-02-14 00:43:48 -06:00
|
|
|
GroupName=name
|
|
|
|
)
|
|
|
|
except Exception as e:
|
2022-04-26 06:11:10 -05:00
|
|
|
error(str(e))
|
2022-02-14 00:43:48 -06:00
|
|
|
|
|
|
|
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)
|
2022-02-28 06:49:18 -06:00
|
|
|
ip = args.public_ip if args.public_ip else\
|
|
|
|
'{}/32'.format(get_my_ip())
|
2022-02-14 00:43:48 -06:00
|
|
|
port = args.db_port or 5432
|
2022-08-26 04:07:59 -05:00
|
|
|
ip_ranges = []
|
2022-02-28 06:49:18 -06:00
|
|
|
|
|
|
|
ip = ip.split(',')
|
|
|
|
for i in ip:
|
2022-08-26 04:07:59 -05:00
|
|
|
ip_ranges.append({
|
2022-02-28 06:49:18 -06:00
|
|
|
'CidrIp': i,
|
|
|
|
'Description': 'pgcloud client {}'.format(i)
|
|
|
|
})
|
2022-02-14 00:43:48 -06:00
|
|
|
try:
|
|
|
|
output({'Adding': 'Adding ingress rule for: {}...'.format(ip)})
|
2022-04-26 06:11:10 -05:00
|
|
|
debug('Adding ingress rule for: {}...'.format(ip))
|
2022-02-14 00:43:48 -06:00
|
|
|
ec2.authorize_security_group_ingress(
|
|
|
|
GroupId=security_group,
|
|
|
|
IpPermissions=[
|
|
|
|
{
|
|
|
|
'FromPort': port,
|
|
|
|
'ToPort': port,
|
|
|
|
'IpProtocol': 'tcp',
|
2022-08-26 04:07:59 -05:00
|
|
|
'IpRanges': ip_ranges
|
2022-02-14 00:43:48 -06:00
|
|
|
},
|
|
|
|
]
|
|
|
|
)
|
|
|
|
except Exception as e:
|
2022-04-26 06:11:10 -05:00
|
|
|
error(e)
|
2022-02-14 00:43:48 -06:00
|
|
|
|
|
|
|
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:
|
2022-04-26 06:11:10 -05:00
|
|
|
debug('Creating RDS instance: {}...'.format(args.name))
|
2022-02-14 00:43:48 -06:00
|
|
|
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,
|
|
|
|
AutoMinorVersionUpgrade=True,
|
2022-09-22 00:56:05 -05:00
|
|
|
MultiAZ=bool(args.high_availability),
|
2022-02-14 00:43:48 -06:00
|
|
|
MasterUsername=args.db_username,
|
|
|
|
MasterUserPassword=db_password,
|
|
|
|
DBInstanceClass=args.instance_type,
|
|
|
|
VpcSecurityGroupIds=[
|
|
|
|
security_group,
|
2023-07-10 00:52:24 -05:00
|
|
|
], **({"Iops": args.storage_iops}
|
|
|
|
if args.storage_iops else {}))
|
2022-02-14 00:43:48 -06:00
|
|
|
|
|
|
|
except rds.exceptions.DBInstanceAlreadyExistsFault as e:
|
|
|
|
try:
|
2022-04-26 06:11:10 -05:00
|
|
|
debug(DEL_SEC_GROUP_MSG.format(security_group))
|
2022-02-14 00:43:48 -06:00
|
|
|
ec2.delete_security_group(GroupId=security_group)
|
|
|
|
except Exception:
|
|
|
|
pass
|
2022-04-26 06:11:10 -05:00
|
|
|
error('RDS instance {} already exists.'.format(args.name))
|
2022-02-14 00:43:48 -06:00
|
|
|
except Exception as e:
|
|
|
|
try:
|
2022-04-26 06:11:10 -05:00
|
|
|
debug(DEL_SEC_GROUP_MSG.format(security_group))
|
2022-02-14 00:43:48 -06:00
|
|
|
ec2.delete_security_group(GroupId=security_group)
|
|
|
|
except Exception:
|
|
|
|
pass
|
2022-04-26 06:11:10 -05:00
|
|
|
error(str(e))
|
2022-02-14 00:43:48 -06:00
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
2022-04-26 06:11:10 -05:00
|
|
|
debug('Deleting RDS instance: {}...'.format(name))
|
2022-02-14 00:43:48 -06:00
|
|
|
try:
|
|
|
|
rds.delete_db_instance(
|
|
|
|
DBInstanceIdentifier=name,
|
|
|
|
SkipFinalSnapshot=True,
|
|
|
|
DeleteAutomatedBackups=True
|
|
|
|
)
|
|
|
|
except Exception as e:
|
2022-04-26 06:11:10 -05:00
|
|
|
error(str(e))
|
2022-02-14 00:43:48 -06:00
|
|
|
|
|
|
|
# Wait for completion
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
rds.describe_db_instances(DBInstanceIdentifier=args.name)
|
|
|
|
except rds.exceptions.DBInstanceNotFoundFault:
|
|
|
|
return
|
|
|
|
except Exception as e:
|
2022-04-26 06:11:10 -05:00
|
|
|
error(str(e))
|
2022-02-14 00:43:48 -06:00
|
|
|
|
|
|
|
time.sleep(5)
|
|
|
|
|
|
|
|
def _delete_security_group(self, args, id):
|
|
|
|
""" Delete a security group """
|
|
|
|
ec2 = self._get_aws_client('ec2', args)
|
|
|
|
|
2022-04-26 06:11:10 -05:00
|
|
|
debug('Deleting security group: {}...'.format(id))
|
2022-02-14 00:43:48 -06:00
|
|
|
try:
|
|
|
|
ec2.delete_security_group(
|
|
|
|
GroupId=id
|
|
|
|
)
|
|
|
|
except Exception as e:
|
2022-04-26 06:11:10 -05:00
|
|
|
error(str(e))
|
2022-02-14 00:43:48 -06:00
|
|
|
|
|
|
|
##########################################################################
|
|
|
|
# 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()
|