# ########################################################################## # # # # 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' 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/', 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/', 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//', 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']: 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' _high_availability = '1' if data['db_details']['high_availability']\ else '0' _instance_size = data['instance_details']['instance_size'].split('||')[1] 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, '--high-availability', _high_availability ] 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, {'label': _label, 'sid': sid} except Exception as e: current_app.logger.exception(e) return False, str(e)