Added capability to deploy PostgreSQL servers on Amazon RDS. Fixes #7177

This commit is contained in:
Khushboo Vashi
2022-02-14 12:13:48 +05:30
committed by Akshay Joshi
parent b89e306df0
commit e61a1045f5
50 changed files with 3393 additions and 42 deletions

View File

@@ -0,0 +1,410 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2022, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""Implements Cloud Deployment"""
import simplejson as json
from flask import Response, url_for, session
from flask import render_template, request, current_app
from flask_babel import gettext
from flask_security import login_required, current_user
from pgadmin.utils import PgAdminModule, html
from pgadmin.utils.ajax import make_json_response,\
internal_server_error, bad_request, success_return
from pgadmin.utils.constants import MIMETYPE_APP_JS
from pgadmin.misc.bgprocess.processes import BatchProcess, IProcessDesc
from pgadmin.model import db, Server, Process
from pgadmin.misc.cloud.utils.rds import RDS, verify_aws_credentials,\
get_aws_db_instances, get_aws_db_versions, clear_aws_session,\
get_aws_regions
from config import root
# set template path for sql scripts
MODULE_NAME = 'cloud'
server_info = {}
class CloudModule(PgAdminModule):
"""
class CloudModule(Object):
It is a wizard which inherits PgAdminModule
class and define methods to load its own
javascript file.
LABEL = gettext('Browser')
"""
def get_own_stylesheets(self):
"""
Returns:
list: the stylesheets used by this module.
"""
stylesheets = []
return stylesheets
def get_own_javascripts(self):
""""
Returns:
list: js files used by this module
"""
scripts = []
scripts.append({
'name': 'pgadmin.misc.cloud',
'path': url_for('cloud.index') + 'cloud',
'when': None
})
scripts.append({
'name': 'pgadmin.browser.wizard',
'path': url_for('browser.static', filename='js/wizard'),
'when': None
})
return scripts
def get_exposed_url_endpoints(self):
"""
Returns:
list: URL endpoints for cloud module
"""
return ['cloud.deploy_on_cloud',
'cloud.get_aws_db_versions',
'cloud.verify_credentials',
'cloud.get_aws_db_instances',
'cloud.update_cloud_server',
'cloud.update_cloud_process',
'cloud.get_aws_regions']
# Create blueprint for CloudModule class
blueprint = CloudModule(
MODULE_NAME, __name__, static_url_path='/misc/cloud')
@blueprint.route("/")
@login_required
def index():
return bad_request(
errormsg=gettext("This URL cannot be called directly.")
)
@blueprint.route("/cloud.js")
@login_required
def script():
"""render own javascript"""
res = Response(response=render_template(
"cloud/js/cloud.js", _=gettext),
status=200,
mimetype=MIMETYPE_APP_JS)
return res
@blueprint.route('/verify_credentials/',
methods=['POST'], endpoint='verify_credentials')
@login_required
def verify_credentials():
"""Verify Credentials."""
data = json.loads(request.data, encoding='utf-8')
status, msg = verify_aws_credentials(data)
if status:
msg = 'verified'
return make_json_response(success=status, info=msg)
@blueprint.route('/get_aws_db_instances/',
methods=['GET'], endpoint='get_aws_db_instances')
@login_required
def get_db_instances():
"""
Fetch AWS DB Instances based on engine version.
"""
# Get Engine Version
eng_version = request.args.get('eng_version')
status, versions = get_aws_db_instances(eng_version)
if not status:
return make_json_response(
status=410,
success=0,
errormsg=versions
)
return make_json_response(data=versions)
@blueprint.route('/get_aws_db_versions/',
methods=['GET', 'POST'], endpoint='get_aws_db_versions')
@login_required
def get_db_versions():
"""GET AWS Database Versions for AWS."""
status, versions = get_aws_db_versions()
if not status:
return make_json_response(
status=410,
success=0,
errormsg=str(versions)
)
return make_json_response(data=versions)
@blueprint.route('/get_aws_regions/',
methods=['GET', 'POST'], endpoint='get_aws_regions')
@login_required
def get_db_versions():
"""GET AWS Regions for AWS."""
status, regions = get_aws_regions()
if not status:
return make_json_response(
status=410,
success=0,
errormsg=str(regions)
)
return make_json_response(data=regions)
@blueprint.route(
'/deploy', methods=['POST'], endpoint='deploy_on_cloud'
)
@login_required
def deploy_on_cloud():
"""Deploy on Cloud"""
data = json.loads(request.data, encoding='utf-8')
from subprocess import Popen, PIPE
_cmd = 'python'
_cmd_script = '{0}/pgacloud/pgacloud.py'.format(root)
args = [_cmd_script,
'--debug',
data['cloud'],
'--region',
str(data['secret']['aws_region']),
'create-instance',
'--name',
data['instance_details']['aws_name'],
'--db-name',
data['db_details']['aws_db_name'],
'--db-username',
data['db_details']['aws_db_username'],
'--db-port',
str(data['db_details']['aws_db_port']),
'--db-version',
str(data['instance_details']['aws_db_version']),
'--instance-type',
data['instance_details']['aws_instance_type'],
'--storage-type',
data['instance_details']['aws_storage_type'],
'--storage-size',
str(data['instance_details']['aws_storage_size']),
'--public-ip',
str(data['instance_details']['aws_public_ip']),
]
if data['instance_details']['aws_storage_type'] == 'io1':
args.append('--storage-iops')
args.append(str(data['instance_details']['aws_storage_IOPS']))
_cmd_msg = '{0} {1} {2}'.format(_cmd, _cmd_script, ' '.join(args))
try:
sid = _create_server({
'gid': data['db_details']['gid'],
'name': data['instance_details']['aws_name'],
'db': data['db_details']['aws_db_name'],
'username': data['db_details']['aws_db_username'],
'port': data['db_details']['aws_db_port'],
'cloud_status': -1
})
p = BatchProcess(
desc=CloudProcessDesc(sid, _cmd_msg, data['cloud'],
data['instance_details']['aws_name']),
cmd=_cmd,
args=args
)
env = dict()
env['AWS_ACCESS_KEY_ID'] = data['secret']['aws_access_key']
env['AWS_SECRET_ACCESS_KEY'] = data['secret']['aws_secret_access_key']
if 'aws_session_token' in data['secret'] and\
data['secret']['aws_session_token'] is not None:
env['AWS_SESSION_TOKEN'] = data['secret']['aws_session_token']
if 'aws_db_password' in data['db_details']:
env['AWS_DATABASE_PASSWORD'] = data[
'db_details']['aws_db_password']
p.set_env_variables(None, env=env)
p.update_server_id(p.id, sid)
p.start()
except Exception as e:
current_app.logger.exception(e)
return make_json_response(
status=410,
success=0,
errormsg=str(e)
)
# Return response
return make_json_response(
success=1,
data={'job_id': 1, 'node': {
'_id': sid,
'_pid': data['db_details']['gid'],
'connected': False,
'_type': 'server',
'icon': 'icon-server-cloud-deploy',
'id': 'server_{}'.format(sid),
'inode': True,
'label': data['instance_details']['aws_name'],
'server_type': 'pg',
'module': 'pgadmin.node.server',
'cloud_status': -1
}}
)
def _create_server(data):
"""Create Server"""
server = Server(
user_id=current_user.id,
servergroup_id=data.get('gid'),
name=data.get('name'),
maintenance_db=data.get('db'),
username=data.get('username'),
ssl_mode='prefer',
cloud_status=data.get('cloud_status')
)
db.session.add(server)
db.session.commit()
return server.id
def update_server(data):
"""Update Server."""
server_data = data
server = Server.query.filter_by(
user_id=current_user.id,
id=server_data['instance']['sid']
).first()
if server is None:
return False, "Could not find the server."
if server_data['instance'] == '' or\
not server_data['instance']['status']:
db.session.delete(server)
else:
server.host = server_data['instance']['Hostname']
server.port = server_data['instance']['Port']
server.cloud_status = 1
try:
db.session.commit()
except Exception as e:
db.session.rollback()
return False, e.message
_server = {
'id': server.id,
'servergroup_id': server.servergroup_id,
'name': server.name,
'cloud_status': server.cloud_status
}
if not server_data['instance']['status']:
_server['status'] = False
else:
_server['status'] = True
clear_aws_session()
return True, _server
@blueprint.route(
'/update_cloud_process/<sid>', methods=['GET'],
endpoint='update_cloud_process'
)
@login_required
def update_cloud_process(sid):
"""Update Cloud Server Process"""
_process = Process.query.filter_by(user_id=current_user.id,
server_id=sid).first()
_process.acknowledge = None
db.session.commit()
return success_return()
@blueprint.route(
'/update_cloud_server', methods=['POST'],
endpoint='update_cloud_server'
)
@login_required
def update_cloud_server():
"""Update Cloud Server."""
server_data = json.loads(request.data, encoding='utf-8')
status, server = update_server(server_data)
if not status:
return make_json_response(
status=410, success=0, errormsg=server
)
return make_json_response(
success=1,
data={'node': {
'sid': server.id,
'gid': server.servergroup_id,
'_type': 'server',
'icon': 'icon-server-not-connected',
'id': 'server_{}'.format(server.id),
'label': server.name
}}
)
class CloudProcessDesc(IProcessDesc):
"""Cloud Server Process Description."""
def __init__(self, _sid, _cmd, _provider, _instance_name):
self.sid = _sid
self.cmd = _cmd
self.instance_name = _instance_name
self.provider = 'Amazon RDS'
if _provider == 'rds':
self.provider = 'Amazon RDS'
elif _provider == 'azure':
self.provider = 'Azure PostgreSQL'
else:
self.provider = 'EDB Big Animal'
@property
def message(self):
return "Deployment on {0} is started for instance {1}.".format(
self.provider, self.instance_name)
def details(self, cmd, args):
res = '<div>' + self.message
res += '</div><div class="py-1">'
res += '<div class="pg-bg-cmd enable-selection p-1">'
res += html.safe_str(self.cmd)
res += '</div></div>'
return res
@property
def type_desc(self):
return "Cloud Deployment"

View File

@@ -0,0 +1,454 @@
/////////////////////////////////////////////////////////////
//
// 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 url_for from 'sources/url_for';
import React from 'react';
import { Box, Table, TableBody, TableCell, TableHead, TableRow, Paper } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import Wizard from '../../../../static/js/helpers/wizard/Wizard';
import WizardStep from '../../../../static/js/helpers/wizard/WizardStep';
import {FormFooterMessage, MESSAGE_TYPE, InputToggle } from '../../../../static/js/components/FormComponents';
import getApiInstance from '../../../../static/js/api_instance';
import SchemaView from '../../../../static/js/SchemaView';
import Alertify from 'pgadmin.alertifyjs';
import PropTypes from 'prop-types';
import {CloudInstanceDetailsSchema, CloudDBCredSchema, DatabaseSchema} from './cloud_db_details_schema.ui';
import { isEmptyString } from 'sources/validators';
import pgAdmin from 'sources/pgadmin';
import { getNodeAjaxOptions, getNodeListById } from 'pgbrowser/node_ajax';
import { commonTableStyles } from '../../../../static/js/Theme';
import clsx from 'clsx';
const useStyles = makeStyles(() =>
({
messageBox: {
marginBottom: '1em',
display: 'flex',
},
messagePadding: {
flex: 2.5
},
toggleButton: {
height: '100px',
},
table: {
marginLeft: '4px',
marginTop: '12px',
},
tableCellHeading: {
fontWeight: 'bold',
paddingLeft: '9px',
},
tableCell: {
padding: '9px',
paddingLeft: '11px',
}
}),
);
export default function CloudWizard({ nodeInfo, nodeData }) {
const classes = useStyles();
const tableClasses = commonTableStyles();
var steps = ['Cloud Provider', 'Credentials', 'Instance Specification', 'Database Details', 'Review'];
const [currentStep, setCurrentStep] = React.useState('');
const [selectionVal, setCloudSelection] = React.useState('');
const [errMsg, setErrMsg] = React.useState('');
const [cloudInstanceDetailsInstance, setCloudInstanceDetailsInstance] = React.useState();
const [cloudDBCredInstance, setCloudDBCredInstance] = React.useState();
const [cloudDBInstance, setCloudDBInstance] = React.useState();
const [cloudInstanceDetails, setCloudInstanceDetails] = React.useState({});
const [cloudDBCred, setCloudDBCred] = React.useState({});
const [cloudDBDetails, setCloudDBDetails] = React.useState({});
const [callRDSAPI, setCallRDSAPI] = React.useState({});
const axiosApi = getApiInstance();
React.useEffect(() => {
if (callRDSAPI == 2) {
const cloudDBInstanceSchema = new CloudInstanceDetailsSchema({
version: ()=>getNodeAjaxOptions('get_aws_db_versions', pgAdmin.Browser.Nodes['server'], nodeInfo, nodeData, {
useCache:false,
cacheNode: 'server',
customGenerateUrl: ()=>{
return url_for('cloud.get_aws_db_versions');
}
}),
getInstances: (engine, reload, options) =>
{
return new Promise((resolve, reject)=>{
const api = getApiInstance();
var _url = url_for('cloud.get_aws_db_instances') ;
if (engine) _url += '?eng_version=' + engine;
if (reload) {
api.get(_url)
.then(res=>{
let data = res.data.data;
resolve(data);
})
.catch((err)=>{
reject(err);
});
} else {
resolve(options);
}
});
},
instance_type: ()=>getNodeAjaxOptions('get_aws_db_instances', pgAdmin.Browser.Nodes['server'], nodeInfo, nodeData, {
useCache:false,
cacheNode: 'server',
customGenerateUrl: ()=>{
return url_for('cloud.get_aws_db_instances');
}
}),
server_groups: ()=>getNodeListById(pgAdmin.Browser.Nodes['server_group'], nodeInfo, nodeData),
}, {
gid: nodeInfo['server_group']._id,
});
setCloudInstanceDetailsInstance(cloudDBInstanceSchema);
}
}, [callRDSAPI]);
React.useEffect(() => {
const cloudDBCredSchema = new CloudDBCredSchema({
regions: ()=>getNodeAjaxOptions('get_aws_regions', pgAdmin.Browser.Nodes['server'], nodeInfo, nodeData, {
useCache:false,
cacheNode: 'server',
customGenerateUrl: ()=>{
return url_for('cloud.get_aws_regions');
}
}),
});
setCloudDBCredInstance(cloudDBCredSchema);
const cloudDBSchema = new DatabaseSchema({
server_groups: ()=>getNodeListById(pgAdmin.Browser.Nodes['server_group'], nodeInfo, nodeData),
},
{
gid: nodeInfo['server_group']._id,
}
);
setCloudDBInstance(cloudDBSchema);
}, []);
const wizardStepChange = (data) => {
setCurrentStep(data.currentStep);
};
const validateCloudStep1 = (cloudDBCred) => {
let isError = false;
if (isEmptyString(cloudDBCred.aws_access_key) || isEmptyString(cloudDBCred.aws_secret_access_key)) {
isError = true;
}
return isError;
};
const validateCloudStep2 = (cloudInstanceDetails) => {
let isError = false;
if (isEmptyString(cloudInstanceDetails.aws_name) ||
isEmptyString(cloudInstanceDetails.aws_db_version) || isEmptyString(cloudInstanceDetails.aws_instance_type) ||
isEmptyString(cloudInstanceDetails.aws_storage_type)|| isEmptyString(cloudInstanceDetails.aws_storage_size)) {
isError = true;
}
if(cloudInstanceDetails.aws_storage_type == 'io1' && isEmptyString(cloudInstanceDetails.aws_storage_IOPS)) {
isError = true;
}
if (isEmptyString(cloudInstanceDetails.aws_public_ip)) cloudInstanceDetails.aws_public_ip = '127.0.0.1/32';
return isError;
};
const validateCloudStep3 = (cloudDBDetails) => {
let isError = false;
if (isEmptyString(cloudDBDetails.aws_db_name) ||
isEmptyString(cloudDBDetails.aws_db_username) || isEmptyString(cloudDBDetails.aws_db_password)) {
isError = true;
}
if (isEmptyString(cloudDBDetails.aws_db_port)) cloudDBDetails.aws_db_port = 5432;
if (isEmptyString(cloudDBDetails.gid)) cloudDBDetails.gid = nodeInfo['server_group']._id;
return isError;
};
const getStorageType = (cloudInstanceDetails) => {
let _storage_type = 'General Purpose SSD (gp2)',
_io1 = undefined;
if(cloudInstanceDetails.aws_storage_type == 'gp2') _storage_type = 'General Purpose SSD (gp2)';
else if(cloudInstanceDetails.aws_storage_type == 'io1') {
_storage_type = 'Provisioned IOPS SSD (io1)';
_io1 = cloudInstanceDetails.aws_storage_IOPS;
}
else if(cloudInstanceDetails.aws_storage_type == 'magnetic') _storage_type = 'Magnetic';
return [_io1, _storage_type];
};
const onSave = () => {
var _url = url_for('cloud.deploy_on_cloud');
const post_data = {
gid: nodeInfo.server_group._id,
cloud: selectionVal,
secret: cloudDBCred,
instance_details:cloudInstanceDetails,
db_details: cloudDBDetails
};
axiosApi.post(_url, post_data)
.then((res) => {
pgAdmin.Browser.Events.trigger('pgadmin:browser:tree:add', res.data.data.node, {'server_group': nodeInfo['server_group']});
pgAdmin.Browser.Events.trigger('pgadmin-bgprocess:created', Alertify.cloudWizardDialog());
Alertify.cloudWizardDialog().close();
})
.catch((error) => {
Alertify.error(gettext(`Error while saving cloud wizard data: ${error.response.data.errormsg}`));
});
};
const disableNextCheck = () => {
setCallRDSAPI(currentStep);
let isError = false;
switch (currentStep) {
case 0:
setCloudSelection('rds');
break;
case 1:
isError = validateCloudStep1(cloudDBCred);
break;
case 2:
isError = validateCloudStep2(cloudInstanceDetails);
break;
case 3:
isError = validateCloudStep3(cloudDBDetails);
break;
default:
break;
}
return isError;
};
const onBeforeNext = (activeStep) => {
return new Promise((resolve, reject)=>{
if(activeStep == 1) {
setErrMsg([MESSAGE_TYPE.INFO, 'Validating credentials...']);
var _url = url_for('cloud.verify_credentials');
const post_data = {
cloud: selectionVal,
secret: cloudDBCred,
};
axiosApi.post(_url, post_data)
.then((res) => {
if(!res.data.success) {
setErrMsg([MESSAGE_TYPE.ERROR, res.data.info]);
reject();
} else {
setErrMsg(['', '']);
resolve();
}
})
.catch(() => {
setErrMsg([MESSAGE_TYPE.ERROR, 'Error while checking cloud credentials']);
reject();
});
} else {
resolve();
}
});
};
const onDialogHelp = () => {
window.open(url_for('help.static', { 'filename': 'cloud_deployment.html' }), 'pgadmin_help');
};
function createData(name, value) {
return { name, value };
}
let cloud = '';
switch (selectionVal) {
case 'rds':
cloud = 'Amazon RDS';
break;
case 'azure':
cloud = 'Azure PostgreSQL';
break;
case 'biganimal':
cloud = 'EDB Big Animal';
break;
}
const rows1 = [
createData('Cloud', cloud),
createData('Instance name', cloudInstanceDetails.aws_name),
createData('Public IP', cloudInstanceDetails.aws_public_ip),
];
const rows2 = [
createData('PostgreSQL version', cloudInstanceDetails.aws_db_version),
createData('Instance type', cloudInstanceDetails.aws_instance_type),
];
let _storage_type = getStorageType(cloudInstanceDetails);
const rows3 = [
createData('Storage type', _storage_type[1]),
createData('Allocated storage', cloudInstanceDetails.aws_storage_size + ' GiB'),
];
if (_storage_type[0] !== undefined) {
rows3.push(createData('Provisioned IOPS', _storage_type[0]));
}
const rows4 = [
createData('Database name', cloudDBDetails.aws_db_name),
createData('Username', cloudDBDetails.aws_db_username),
createData('Password', 'xxxxxxx'),
createData('Port', cloudDBDetails.aws_db_port),
];
const onErrClose = React.useCallback(()=>{
setErrMsg([]);
});
const displayTableRows = (rows) => {
return rows.map((row) => (
<TableRow key={row.name} >
<TableCell scope="row">{row.name}</TableCell>
<TableCell align="right">{row.value}</TableCell>
</TableRow>
));
};
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('Deploy on Amazon RDS cloud.')}</Box>
</Box>
<Box className={classes.messageBox}>
<InputToggle
value='rds'
options={[{'label': gettext('Amazon RDS'), value: 'rds'}]}
className={classes.toggleButton}
onChange={(value) => {
setCloudSelection(value);}
}
>
</InputToggle>
</Box>
<Box className={classes.messageBox}>
<Box className={classes.messagePadding}>{gettext('More cloud providers are coming soon...')}</Box>
</Box>
</WizardStep>
<WizardStep stepId={1} >
{cloudDBCredInstance &&
<SchemaView
formType={'dialog'}
getInitData={() => { /*This is intentional (SonarQube)*/ }}
viewHelperProps={{ mode: 'create' }}
schema={cloudDBCredInstance}
showFooter={false}
isTabView={false}
onDataChange={(isChanged, changedData) => {
setCloudDBCred(changedData);
}}
/>
}
<FormFooterMessage type={errMsg[0]} message={errMsg[1]} onClose={onErrClose} />
</WizardStep>
<WizardStep stepId={2} >
{cloudInstanceDetailsInstance &&
<SchemaView
formType={'dialog'}
getInitData={() => { /*This is intentional (SonarQube)*/ }}
viewHelperProps={{ mode: 'create' }}
schema={cloudInstanceDetailsInstance}
showFooter={false}
isTabView={false}
onDataChange={(isChanged, changedData) => {
setCloudInstanceDetails(changedData);
}}
/>
}
</WizardStep>
<WizardStep stepId={3} >
{cloudDBInstance &&
<SchemaView
formType={'dialog'}
getInitData={() => { /*This is intentional (SonarQube)*/ }}
viewHelperProps={{ mode: 'create' }}
schema={cloudDBInstance}
showFooter={false}
isTabView={false}
onDataChange={(isChanged, changedData) => {
setCloudDBDetails(changedData);
}}
/>
}
</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}>
<Table aria-label="simple table" className={clsx(tableClasses.table)}>
<TableBody>
{displayTableRows(rows1)}
</TableBody>
</Table>
<Table aria-label="simple table" className={clsx(tableClasses.table)}>
<TableHead>
<TableRow>
<TableCell colSpan={2}>{gettext('Version and Instance Details')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{displayTableRows(rows2)}
</TableBody>
</Table>
<Table aria-label="simple table" className={clsx(tableClasses.table)}>
<TableHead>
<TableRow>
<TableCell colSpan={2}>{gettext('Storage Details')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{displayTableRows(rows3)}
</TableBody>
</Table>
<Table aria-label="simple table" className={clsx(tableClasses.table)}>
<TableHead>
<TableRow>
<TableCell colSpan={2}>{gettext('Database Details')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{displayTableRows(rows4)}
</TableBody>
</Table>
</Paper>
</WizardStep>
</Wizard>
</>
);
}
CloudWizard.propTypes = {
nodeInfo: PropTypes.object,
nodeData: PropTypes.object,
};

View File

@@ -0,0 +1,151 @@
/////////////////////////////////////////////////////////////
//
// 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 ReactDOM from 'react-dom';
import Theme from 'sources/Theme';
import CloudWizard from './CloudWizard';
// Cloud Wizard
define('pgadmin.misc.cloud', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'pgadmin.alertifyjs',
'pgadmin.browser',
'pgadmin.browser.wizard',
], function(
gettext, url_for, $, _, Alertify, pgBrowser
) {
// if module is already initialized, refer to that.
if (pgBrowser.Cloud) {
return pgBrowser.Cloud;
}
// Create an Object Cloud of pgBrowser class
pgBrowser.Cloud = {
init: function() {
if (this.initialized)
return;
this.initialized = true;
// Define the nodes on which the menus to be appear
var menus = [{
name: 'register_and_deploy_cloud_instance',
module: this,
applies: ['object', 'context'],
callback: 'start_cloud_wizard',
priority: 15,
label: gettext('Deploy Cloud Instance...'),
icon: 'wcTabIcon icon-server',
enable: true,
data: {action: 'create'},
category: 'register',
node: 'server_group',
}, {
name: 'register_and_deploy_cloud_instance',
module: this,
applies: ['object', 'context'],
callback: 'start_cloud_wizard',
priority: 15,
label: gettext('Deploy Cloud Instance...'),
icon: 'wcTabIcon icon-server',
enable: true,
data: {action: 'create'},
category: 'register',
node: 'server',
}];
pgBrowser.add_menus(menus);
return this;
},
// Callback to draw Wizard Dialog
start_cloud_wizard: function() {
// Declare Wizard dialog
if (!Alertify.cloudWizardDialog) {
Alertify.dialog('cloudWizardDialog', function factory() {
// Generate wizard main container
var $container = $('<div class=\'wizard_dlg\' id=\'cloudWizardDlg\'></div>');
return {
main: function () {
/*This is intentional (SonarQube)*/
},
setup: function () {
return {
// Set options for dialog
options: {
frameless: true,
resizable: true,
autoReset: false,
maximizable: true,
closable: true,
closableByDimmer: false,
modal: true,
pinnable: false,
},
};
},
build: function () {
this.elements.content.appendChild($container.get(0));
Alertify.pgDialogBuild.apply(this);
var t = pgBrowser.tree,
i = t.selected(),
d = this.d = i ? t.itemData(i) : undefined,
info = this.info = pgBrowser.tree.getTreeNodeHierarchy(i);
setTimeout(function () {
if (document.getElementById('cloudWizardDlg')) {
ReactDOM.render(
<Theme>
<CloudWizard nodeInfo={info} nodeData={d} />
</Theme>,
document.getElementById('cloudWizardDlg'));
Alertify.cloudWizardDialog().elements.modal.style.maxHeight=0;
Alertify.cloudWizardDialog().elements.modal.style.maxWidth='none';
Alertify.cloudWizardDialog().elements.modal.style.overflow='visible';
Alertify.cloudWizardDialog().elements.dimmer.style.display='none';
}
}, 500);
},
prepare: function () {
$container.empty().append('<div class=\'cloud_wizard_container\'></div>');
},
hooks: {
// Triggered when the dialog is closed
onclose: function () {
// Clear the view and remove the react component.
return setTimeout((function () {
ReactDOM.unmountComponentAtNode(document.getElementById('cloudWizardDlg'));
return Alertify.cloudWizardDialog().destroy();
}), 500);
},
}
};
});
}
// Call Grant Wizard Dialog and set dimensions for wizard
Alertify.cloudWizardDialog('').set({
onmaximize:function(){
Alertify.cloudWizardDialog().elements.modal.style.maxHeight='initial';
},
onrestore:function(){
Alertify.cloudWizardDialog().elements.modal.style.maxHeight=0;
},
}).resizeTo(920, 620);
},
};
return pgBrowser.Cloud;
});

View File

@@ -0,0 +1,292 @@
/////////////////////////////////////////////////////////////
//
// 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';
class CloudInstanceDetailsSchema extends BaseUISchema {
constructor(fieldOptions = {}, initValues = {}) {
super({
oid: undefined,
aws_name: '',
aws_public_ip: '127.0.0.1/32',
...initValues
});
this.fieldOptions = {
...fieldOptions,
};
this.initValues = initValues;
}
get idAttribute() {
return 'oid';
}
get baseFields() {
return [
{
id: 'aws_name', label: gettext('Instance name'), type: 'text',
mode: ['create'], noEmpty: true,
}, {
id: 'aws_public_ip', label: gettext('Public IP range'), type: 'text',
mode: ['create'],
helpMessage: gettext('IP Address range for permitting the inbound traffic. Ex: 127.0.0.1/32'),
}, {
type: 'nested-fieldset', label: gettext('Version & Instance'),
mode: ['create'],
schema: new InstanceSchema(this.fieldOptions.version,
this.fieldOptions.instance_type,
this.fieldOptions.getInstances),
}, {
type: 'nested-fieldset', label: gettext('Storage'),
mode: ['create'],
schema: new StorageSchema(),
},
];
}
}
class CloudDBCredSchema extends BaseUISchema {
constructor(fieldOptions = {}, initValues = {}) {
super({
oid: null,
aws_region: '',
aws_access_key: '',
aws_secret_access_key: '',
aws_session_token: '',
is_valid_cred: false,
...initValues
});
this.fieldOptions = {
...fieldOptions,
};
}
get idAttribute() {
return 'oid';
}
get baseFields() {
return [
{
id: 'aws_region', label: gettext('Region'),
type: 'select',
options: this.fieldOptions.regions,
controlProps: { allowClear: false },
noEmpty: true,
helpMessage: gettext('The cloud instance will be deployed in the selected region.')
},{
id: 'aws_access_key', label: gettext('AWS access key'), type: 'text',
mode: ['create'], noEmpty: true,
}, {
id: 'aws_secret_access_key', label: gettext('AWS secret access key'), type: 'text',
mode: ['create'], noEmpty: true,
}, {
id: 'aws_session_token', label: gettext('AWS session token'), type: 'multiline',
mode: ['create'], noEmpty: false,
helpMessage: gettext('Temporary AWS session required session token.')
}
];
}
}
class DatabaseSchema extends BaseUISchema {
constructor(fieldOptions = {}, initValues={}) {
super({
oid: undefined,
gid: undefined,
aws_db_name: '',
aws_db_username: '',
aws_db_password: '',
aws_db_confirm_password: '',
aws_db_port: 5432,
...initValues,
});
this.fieldOptions = {
...fieldOptions,
};
}
validate(data, setErrMsg) {
if(!isEmptyString(data.aws_db_password) && !isEmptyString(data.aws_db_confirm_password)
&& data.aws_db_password != data.aws_db_confirm_password) {
setErrMsg('aws_db_confirm_password', gettext('Passwords do not match.'));
return true;
} else {
return false;
}
}
get idAttribute() {
return 'oid';
}
get baseFields() {
return [{
id: 'gid', label: gettext('Server group'), type: 'select',
options: this.fieldOptions.server_groups,
mode: ['create'],
controlProps: { allowClear: false },
noEmpty: true,
}, {
id: 'aws_db_name', label: gettext('Database name'), type: 'text',
mode: ['create'], noEmpty: true,
}, {
id: 'aws_db_username', label: gettext('Username'), type: 'text',
mode: ['create'], noEmpty: true,
}, {
id: 'aws_db_password', label: gettext('Password'), type: 'password',
mode: ['create'], noEmpty: true,
}, {
id: 'aws_db_confirm_password', label: gettext('Confirm password'),
type: 'password',
mode: ['create'], noEmpty: true,
}, {
id: 'aws_db_port', label: gettext('Port'), type: 'text',
mode: ['create'], noEmpty: true,
}];
}
}
export class InstanceSchema extends BaseUISchema {
constructor(versionOpts, instanceOpts, getInstances) {
super({
aws_db_version: '',
aws_db_instance_class: 'm',
aws_instance_type: '',
reload_instances: true,
});
this.versionOpts = versionOpts;
this.instanceOpts = instanceOpts;
this.getInstances = getInstances;
this.instanceData = [];
}
get baseFields() {
return [{
id: 'aws_db_version', label: gettext('Database version'),
type: 'select',
options: this.versionOpts,
controlProps: { allowClear: false },
deps: ['aws_name'],
noEmpty: true,
},{
id: 'aws_db_instance_class', label: gettext('Instance class'),
type: 'toggle',
options: [
{'label': gettext('Standard classes (includes m classes)'), value: 'm'},
{'label': gettext('Memory optimized classes (includes r & x classes)'), value: 'x'},
{'label': gettext('Burstable classes (includes t classes)'), value: 't'},
], noEmpty: true, orientation: 'vertical',
},{
id: 'aws_instance_type', label: gettext('Instance type'),
options: this.instanceOpts, noEmpty: true,
controlProps: { allowClear: false },
deps: ['aws_db_version', 'aws_db_instance_class'],
depChange: (state, source)=> {
if (source[0] == 'aws_db_instance_class') {
state.reload_instances = false;
} else {
state.reload_instances = true;
}
},
type: (state) => {
return {
type: 'select',
options: ()=>this.getInstances(state.aws_db_version,
state.reload_instances, state.instanceData),
optionsLoaded: (options) => { state.instanceData = options; },
optionsReloadBasis: state.aws_db_version + (state.aws_db_instance_class || 'm'),
controlProps: {
allowClear: false,
filter: (options) => {
let pattern = 'db.m';
let pattern_1 = 'db.m';
if (state.aws_db_instance_class) {
pattern = 'db.' + state.aws_db_instance_class;
pattern_1 = 'db.' + state.aws_db_instance_class;
}
if (state.aws_db_instance_class == 'x') {
pattern_1 = 'db.' + 'r';
}
return options.filter((option) => {
return (option.value.includes(pattern) || option.value.includes(pattern_1));
});
},
}
};
},
}];
}
}
export class StorageSchema extends BaseUISchema {
constructor() {
super({
aws_storage_type: 'io1',
aws_storage_size: 100,
aws_storage_IOPS: 3000,
aws_storage_msg: 'Minimum: 20 GiB. Maximum: 16,384 GiB.'
});
}
get baseFields() {
return [
{
id: 'aws_storage_type', label: gettext('Storage type'), type: 'select',
mode: ['create'],
options: [
{'label': gettext('General Purpose SSD (gp2)'), 'value': 'gp2'},
{'label': gettext('Provisioned IOPS SSD (io1)'), 'value': 'io1'},
{'label': gettext('Magnetic'), 'value': 'standard'}
], noEmpty: true,
},{
id: 'aws_storage_size', label: gettext('Allocated storage'), type: 'text',
mode: ['create'], noEmpty: true, deps: ['aws_storage_type'],
depChange: (state)=> {
if(state.aws_storage_type === 'io1') {
state.aws_storage_size = 100;
} else if(state.aws_storage_type === 'gp2') {
state.aws_storage_size = 20;
} else {
state.aws_storage_size = 5;
}
},
helpMessage: gettext('Size in GiB.')
}, {
id: 'aws_storage_IOPS', label: gettext('Provisioned IOPS'), type: 'text',
mode: ['create'],
visible: (state) => {
if(state.aws_storage_type === 'io1') {
state.aws_storage_IOPS = 3000;
return true;
}
return false;
} , deps: ['aws_storage_type']
},
];
}
}
export {
CloudInstanceDetailsSchema,
CloudDBCredSchema,
DatabaseSchema,
};

View File

@@ -0,0 +1,171 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2022, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
# AWS RDS PostgreSQL provider
import boto3
import pickle
from flask import session
from boto3.session import Session
class RDS():
def __init__(self, access_key, secret_key, session_token=None,
default_region='ap-south-1'):
self._clients = {}
self._access_key = access_key
self._secret_key = secret_key
self._session_token = session_token
self._default_region = default_region
##########################################################################
# AWS Helper functions
##########################################################################
def _get_aws_client(self, type):
""" 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=self._default_region)
return self._clients[type]
def get_available_db_version(self, engine='postgres'):
rds = self._get_aws_client('rds')
return rds.describe_db_engine_versions(Engine=engine)
def get_available_db_instance_class(self, engine='postgres',
engine_version='9.6'):
rds = self._get_aws_client('rds')
_instances = rds.describe_orderable_db_instance_options(
Engine=engine,
EngineVersion=engine_version)
_instances_list = _instances['OrderableDBInstanceOptions']
_marker = _instances['Marker'] if 'Marker' in _instances else None
while _marker:
_tmp_instances = rds.describe_orderable_db_instance_options(
Engine=engine,
EngineVersion=engine_version,
Marker=_marker)
_instances_list = [*_instances_list,
*_tmp_instances['OrderableDBInstanceOptions']]
_marker = _tmp_instances['Marker'] if 'Marker'\
in _tmp_instances else None
return _instances_list
def get_db_instance(self, instance_name):
rds = self._get_aws_client('rds')
return rds.describe_db_instances(
DBInstanceIdentifier=instance_name)
def validate_credentials(self):
client = self._get_aws_client('sts')
try:
identity = client.get_caller_identity()
return True, identity
except Exception as e:
return False, str(e)
finally:
self._clients.pop('sts')
def verify_aws_credentials(data):
"""Verify Credentials"""
session_token = data['secret']['aws_session_token'] if\
'aws_session_token' in data['secret'] else None
if 'aws' not in session:
session['aws'] = {}
if 'aws_rds_obj' not in session['aws'] or\
session['aws']['secret'] != data['secret']:
_rds = RDS(
access_key=data['secret']['aws_access_key'],
secret_key=data['secret']['aws_secret_access_key'],
session_token=session_token,
default_region=data['secret']['aws_region'])
status, identity = _rds.validate_credentials()
if status:
session['aws']['secret'] = data['secret']
session['aws']['aws_rds_obj'] = pickle.dumps(_rds, -1)
return status, identity
return True, None
def clear_aws_session():
"""Clear AWS Session"""
if 'aws' in session:
session.pop('aws')
def get_aws_db_instances(eng_version):
"""Get AWS DB Instances"""
if 'aws' not in session:
return False, 'Session has not created yet.'
if not eng_version or eng_version == '' or eng_version == 'undefined':
eng_version = '9.6.1'
rds_obj = pickle.loads(session['aws']['aws_rds_obj'])
res = rds_obj.get_available_db_instance_class(
engine_version=eng_version)
versions_set = set()
versions = []
for value in res:
versions_set.add(value['DBInstanceClass'])
for value in versions_set:
versions.append({
'label': value,
'value': value
})
return True, versions
def get_aws_db_versions():
"""Get AWS DB Versions"""
if 'aws' not in session:
return False, 'Session has not created yet.'
rds_obj = pickle.loads(session['aws']['aws_rds_obj'])
db_versions = rds_obj.get_available_db_version()
res = db_versions['DBEngineVersions']
versions = []
for value in res:
versions.append({
'label': value['DBEngineVersionDescription'],
'value': value['EngineVersion']
})
return True, versions
def get_aws_regions():
"""Get AWS DB Versions"""
clear_aws_session()
_session = Session()
res = _session.get_available_regions('rds')
regions = []
for value in res:
regions.append({
'label': value,
'value': value
})
return True, regions