pgadmin4/web/pgadmin/misc/cloud/biganimal/__init__.py
Khushboo Vashi ae5254e8a9 Fixed an issue where the cloud deployment wizard creates the cluster with the
High Availability even if that option is not selected. Fixes #7608
2022-08-14 07:14:10 +05:30

450 lines
16 KiB
Python

# ##########################################################################
# #
# # pgAdmin 4 - PostgreSQL Tools
# #
# # Copyright (C) 2013 - 2022, The pgAdmin Development Team
# # This software is released under the PostgreSQL Licence
# #
# ##########################################################################
# EDB BigAnimal Cloud Deployment Implementation
import requests
import json
import pickle
from flask_babel import gettext
import simplejson as json
from flask import session, current_app
from flask_security import login_required
from werkzeug.datastructures import Headers
from pgadmin.utils import PgAdminModule
from pgadmin.misc.cloud.utils import _create_server, CloudProcessDesc
from pgadmin.misc.bgprocess.processes import BatchProcess
from pgadmin.utils.ajax import make_json_response,\
internal_server_error, bad_request, success_return
from config import root
from pgadmin.utils.constants import MIMETYPE_APP_JSON
MODULE_NAME = 'biganimal'
SINGLE_CLUSTER_ARCH = 'single'
HA_CLUSTER_ARCH = 'ha' # High Availability
EHA_CLUSTER_ARCH = 'eha' # Extreme High Availability
class BigAnimalModule(PgAdminModule):
"""Cloud module to deploy on EDB BigAnimal"""
def get_own_stylesheets(self):
"""
Returns:
list: the stylesheets used by this module.
"""
stylesheets = []
return stylesheets
def get_exposed_url_endpoints(self):
return ['biganimal.verification',
'biganimal.verification_ack',
'biganimal.regions',
'biganimal.db_types',
'biganimal.db_versions',
'biganimal.instance_types',
'biganimal.volume_types',
'biganimal.volume_properties']
blueprint = BigAnimalModule(MODULE_NAME, __name__,
static_url_path='/misc/cloud/biganimal')
@blueprint.route('/verification_ack/',
methods=['GET'], endpoint='verification_ack')
@login_required
def biganimal_verification_ack():
"""Check the Verification is done or not."""
biganimal_obj = pickle.loads(session['biganimal']['provider_obj'])
status, error = biganimal_obj.polling_for_token()
session['biganimal']['provider_obj'] = pickle.dumps(biganimal_obj, -1)
return make_json_response(success=status,
errormsg=error)
@blueprint.route('/verification/',
methods=['GET'], endpoint='verification')
@login_required
def verification():
"""Verify Credentials."""
biganimal = BigAnimalProvider()
verification_uri = biganimal.get_device_code()
session['biganimal'] = {}
session['biganimal']['provider_obj'] = pickle.dumps(biganimal, -1)
return make_json_response(data=verification_uri)
@blueprint.route('/regions/',
methods=['GET'], endpoint='regions')
@login_required
def biganimal_regions():
"""Get Regions."""
biganimal_obj = pickle.loads(session['biganimal']['provider_obj'])
status, regions = biganimal_obj.get_regions()
return make_json_response(data=regions)
@blueprint.route('/db_types/',
methods=['GET'], endpoint='db_types')
@login_required
def biganimal_db_types():
"""Get Database Types."""
biganimal_obj = pickle.loads(session['biganimal']['provider_obj'])
pg_types = biganimal_obj.get_postgres_types()
return make_json_response(data=pg_types)
@blueprint.route('/db_versions/',
methods=['GET'], endpoint='db_versions')
@login_required
def biganimal_db_versions():
"""Get Database Version."""
biganimal_obj = pickle.loads(session['biganimal']['provider_obj'])
pg_versions = biganimal_obj.get_postgres_versions()
return make_json_response(data=pg_versions)
@blueprint.route('/instance_types/<region_id>',
methods=['GET'], endpoint='instance_types')
@login_required
def biganimal_instance_types(region_id):
"""Get Instance Types."""
if not region_id:
return make_json_response(data=[])
biganimal_obj = pickle.loads(session['biganimal']['provider_obj'])
biganimal_instances = biganimal_obj.get_instance_types(region_id)
return make_json_response(data=biganimal_instances)
@blueprint.route('/volume_types/<region_id>',
methods=['GET'], endpoint='volume_types')
@login_required
def biganimal_volume_types(region_id):
"""Get Volume Types."""
if not region_id:
return make_json_response(data=[])
biganimal_obj = pickle.loads(session['biganimal']['provider_obj'])
biganimal_volumes = biganimal_obj.get_volume_types(region_id)
return make_json_response(data=biganimal_volumes)
@blueprint.route('/volume_properties/<region_id>/<volume_type>',
methods=['GET'], endpoint='volume_properties')
@login_required
def biganimal_volume_properties(region_id, volume_type):
"""Get Volume Properties."""
if not region_id:
return make_json_response(data=[])
biganimal_obj = pickle.loads(session['biganimal']['provider_obj'])
biganimal_volume_properties = biganimal_obj.get_volume_properties(
region_id,
volume_type)
return make_json_response(data=biganimal_volume_properties)
class BigAnimalProvider():
"""BigAnimal provider class"""
BASE_URL = 'https://portal.biganimal.com/api/v1'
def __init__(self):
self.provider = {}
self.device_code = {}
self.token = {}
self.raw_access_token = None
self.access_token = None
self.token_error = {}
self.token_status = -1
self.get_auth_provider()
def _get_headers(self):
return {
'content-type': MIMETYPE_APP_JSON,
'Authorization': 'Bearer {0}'.format(self.access_token)
}
def get_auth_provider(self):
"""Get Authentication Provider Relevant Information."""
provider_resp = requests.get("{0}/{1}".format(self.BASE_URL,
'auth/provider'))
if provider_resp.status_code == 200 and provider_resp.content:
self.provider = json.loads(provider_resp.content)
def get_device_code(self):
"""Get device code"""
_url = "{0}/{1}".format(self.provider['issuerUri'],
'oauth/device/code')
_headers = {"content-type": "application/x-www-form-urlencoded"}
_data = {
'client_id': self.provider['clientId'],
'audience': self.provider['audience'],
'scope': self.provider['scope']
}
device_resp = requests.post(_url,
headers=_headers,
data=_data)
if device_resp.status_code == 200 and device_resp.content:
self.device_code = json.loads(device_resp.content)
return self.device_code['verification_uri_complete']
def polling_for_token(self):
# Polling for the Token
_url = "{0}/{1}".format(self.provider['issuerUri'], 'oauth/token')
_headers = {"content-type": "application/x-www-form-urlencoded"}
_data = {
'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
'device_code': self.device_code['device_code'],
'client_id': self.provider['clientId']
}
token_resp = requests.post(_url,
headers=_headers,
data=_data)
if token_resp.status_code == 200:
self.token = json.loads(token_resp.content)
self.raw_access_token = self.token['access_token']
self.token_error['error'] = None
self.token_status = 1
status, msg = self.exchange_token()
if status and not self._check_admin_permission():
return False, gettext('forbidden')
return status, msg
elif token_resp.status_code == 403:
self.token_error = json.loads(token_resp.content)
if self.token_error['error'] == 'authorization_pending' or\
self.token_error['error'] == 'access_denied':
self.token_status = 0
return False, self.token_error['error']
return False, None
def exchange_token(self):
_url = "{0}/{1}".format(self.BASE_URL, 'auth/token')
_headers = {"content-type": "application/json"}
_data = {'token': self.raw_access_token}
token_resp = requests.post(_url,
headers=_headers,
data=json.dumps(_data))
final_token = json.loads(token_resp.content)
if token_resp.status_code == 200:
self.access_token = final_token['token']
return True, None
else:
return False, self.token_error['error']
def _check_admin_permission(self):
"""
Check wehether the user has valid role or not.
There is no direct way to do this, so just checking the create cluster
permission.
"""
_url = "{0}/{1}".format(
self.BASE_URL,
'admin/permissions')
resp = requests.get(_url, headers=self._get_headers())
if resp.status_code != 200:
return False
if resp.status_code == 200 and resp.content:
content = json.loads(resp.content)
if 'permissionsList' in content and 'create:clusters' in content[
'permissionsList']:
return True
return False
def get_regions(self):
"""Get regions"""
_url = "{0}/{1}".format(
self.BASE_URL,
'cloud-providers/azure/regions')
regions = []
resp = requests.get(_url, headers=self._get_headers())
if resp.status_code == 200 and resp.content:
regions_resp = json.loads(resp.content)
for value in regions_resp['regionsList']:
regions.append({
'label': value['regionName'],
'value': value['regionId']
})
return True, regions
elif resp.content:
regions_resp = json.loads(resp.content)
return False, regions_resp['error']['message']
else:
return False, gettext('Error retrieving regions.')
def get_postgres_types(self):
"""Get Postgres Types."""
_url = "{0}/{1}".format(
self.BASE_URL,
'postgres-types')
pg_types = []
resp = requests.get(_url, headers=self._get_headers())
if resp.status_code == 200 and resp.content:
pg_types_resp = json.loads(resp.content)
for value in pg_types_resp['pgTypesList']:
# Extreme HA is in Beta, so avoid it
if len(value['supportedClusterArchitectureIds']) != 1:
pg_types.append({
'label': value['name'],
'value': value['id']
})
return pg_types
def get_postgres_versions(self):
"""Get Postgres Versions."""
_url = "{0}/{1}".format(
self.BASE_URL,
'postgres-versions')
pg_versions = []
resp = requests.get(_url, headers=self._get_headers())
if resp.status_code == 200 and resp.content:
pg_versions_resp = json.loads(resp.content)
for value in pg_versions_resp['pgVersionsList']:
pg_versions.append({
'label': value['versionName'],
'value': value['versionId']
})
return pg_versions
def get_instance_types(self, region_id):
"""GEt Instance Types."""
_url = "{0}/{1}".format(
self.BASE_URL,
'cloud-providers/azure/regions/'
'{0}/instance-types'.format(region_id))
resp = requests.get(_url, headers=self._get_headers())
if resp.status_code == 200 and resp.content:
pg_types = json.loads(resp.content)
return pg_types['instanceTypesList']
return []
def get_volume_types(self, region_id):
"""Get Volume Types."""
_url = "{0}/{1}".format(
self.BASE_URL,
'cloud-providers/azure/regions/{0}/volume-types'.format(region_id))
volume_types = []
resp = requests.get(_url, headers=self._get_headers())
if resp.status_code == 200 and resp.content:
volume_resp = json.loads(resp.content)
for value in volume_resp['volumeTypesList']:
volume_types.append({
'label': value['displayName'],
'value': value['id']
})
return volume_types
def get_volume_properties(self, region_id, volume_type):
"""Get Volume Properties."""
_url = "{0}/{1}".format(
self.BASE_URL,
'cloud-providers/azure/regions/{0}/volume-types'
'/{1}/volume-properties'.format(region_id, volume_type))
volume_properties = []
resp = requests.get(_url, headers=self._get_headers())
if resp.status_code == 200 and resp.content:
volume_prop = json.loads(resp.content)
for value in volume_prop['volumePropertiesList']:
volume_properties.append({
'label': value['value'],
'value': value['id']
})
return volume_properties
def clear_biganimal_session():
"""Clear session data."""
if 'biganimal' in session:
session.pop('biganimal')
def deploy_on_biganimal(data):
"""Deploy Postgres instance on BigAnimal"""
_cmd = 'python'
_cmd_script = '{0}/pgacloud/pgacloud.py'.format(root)
_label = data['instance_details']['name']
_private_network = '1' if str(data['instance_details']['cloud_type']
) == 'private' else '0'
_instance_size = data['instance_details']['instance_size'].split('||')[1]
cluster_arch = SINGLE_CLUSTER_ARCH
nodes = 1
if data['db_details']['high_availability']:
cluster_arch = HA_CLUSTER_ARCH
nodes = int(data['db_details']['replicas']) + nodes
args = [_cmd_script,
data['cloud'],
'create-instance',
'--name',
data['instance_details']['name'],
'--region',
str(data['instance_details']['region']),
'--db-type',
str(data['db_details']['database_type']),
'--db-version',
str(data['db_details']['postgres_version']),
'--volume-type',
str(data['instance_details']['volume_type']),
'--volume-properties',
str(data['instance_details']['volume_properties']),
'--instance-type',
str(_instance_size),
'--private-network',
_private_network,
'--cluster-arch',
cluster_arch,
'--nodes',
str(nodes)
]
if 'biganimal_public_ip' in data['instance_details']:
args.append('--public-ip')
args.append(str(data['instance_details']['biganimal_public_ip']))
_cmd_msg = '{0} {1} {2}'.format(_cmd, _cmd_script, ' '.join(args))
try:
sid = _create_server({
'gid': data['db_details']['gid'],
'name': data['instance_details']['name'],
'db': 'edb_admin',
'username': 'edb_admin',
'port': 5432,
'cloud_status': -1
})
p = BatchProcess(
desc=CloudProcessDesc(sid, _cmd_msg,
data['cloud'],
data['instance_details']['name']
),
cmd=_cmd,
args=args
)
env = dict()
biganimal_obj = pickle.loads(session['biganimal']['provider_obj'])
env['BIGANIMAL_ACCESS_KEY'] = biganimal_obj.access_token
if 'password' in data['db_details']:
env['BIGANIMAL_DATABASE_PASSWORD'] = data[
'db_details']['password']
p.set_env_variables(None, env=env)
p.update_server_id(p.id, sid)
p.start()
return True, p, {'label': _label, 'sid': sid}
except Exception as e:
current_app.logger.exception(e)
return False, None, str(e)