This is an automated email from the ASF dual-hosted git repository. yasith pushed a commit to branch feat/sdk-facade-migration in repository https://gitbox.apache.org/repos/asf/airavata-portals.git
commit 26b8f879d8d8126dc7fa27d3f90b647668858e01 Author: yasithdev <[email protected]> AuthorDate: Wed Apr 8 00:06:52 2026 -0500 refactor: replace Thrift infrastructure with AiravataClient facade - Remove all Thrift pool classes, transport helpers, and connection pool instantiations from utils.py; replace with a single create_airavata_client() factory using the airavata_sdk.AiravataClient facade - Rewrite middleware.py: AiravataClientMiddleware now creates/closes an AiravataClient per request; remove profile_service_client middleware entirely (facade wraps all profile services) - Inline airavata-django-portal-commons dynamic_apps module into django_airavata.dynamic_apps to eliminate the external dependency - Update auth/middleware.py: replace Thrift camelCase calls (getGatewayGroups, getAllGroupsUserBelongs) with facade snake_case (iam.get_gateway_groups, sharing.get_all_groups_user_belongs); remove request.profile_service usage - Update auth/utils.py: remove AuthzToken Thrift import; return plain dicts instead (SDK handles auth via gRPC metadata) - Update context_processors.py: getAllNotifications -> research.get_all_notifications - Update requirements.txt: drop thrift, thrift_connector, airavata-django-portal-sdk, airavata-django-portal-commons; pin airavata-python-sdk==3.0.0 --- .../django_airavata/apps/auth/middleware.py | 18 +- .../django_airavata/apps/auth/utils.py | 24 +- .../django_airavata/context_processors.py | 4 +- .../django_airavata/dynamic_apps/__init__.py | 53 +++ .../dynamic_apps/context_processors.py | 130 ++++++++ .../django_airavata/dynamic_apps/urls.py | 12 + .../django_airavata/middleware.py | 48 ++- airavata-django-portal/django_airavata/settings.py | 19 +- airavata-django-portal/django_airavata/urls.py | 5 +- airavata-django-portal/django_airavata/utils.py | 357 +-------------------- airavata-django-portal/requirements.txt | 6 +- 11 files changed, 258 insertions(+), 418 deletions(-) diff --git a/airavata-django-portal/django_airavata/apps/auth/middleware.py b/airavata-django-portal/django_airavata/apps/auth/middleware.py index 53569325a..6c9a1f3fd 100644 --- a/airavata-django-portal/django_airavata/apps/auth/middleware.py +++ b/airavata-django-portal/django_airavata/apps/auth/middleware.py @@ -34,13 +34,12 @@ def authz_token_middleware(get_response): def set_admin_group_attributes(request, gateway_groups=None): """Set is_gateway_admin and is_read_only_gateway_admin request attrs.""" if gateway_groups is None: - gateway_groups = request.airavata_client.getGatewayGroups(request.authz_token) - gateway_groups = copy.deepcopy(gateway_groups.__dict__) - admins_group_id = gateway_groups['adminsGroupId'] - read_only_admins_group_id = gateway_groups['readOnlyAdminsGroupId'] - group_manager_client = request.profile_service['group_manager'] - group_memberships = group_manager_client.getAllGroupsUserBelongs( - request.authz_token, request.user.username + "@" + settings.GATEWAY_ID) + gateway_groups = request.airavata_client.iam.get_gateway_groups() + admins_group_id = gateway_groups.get('adminsGroupId') if isinstance(gateway_groups, dict) else gateway_groups.admins_group_id + read_only_admins_group_id = gateway_groups.get('readOnlyAdminsGroupId') if isinstance(gateway_groups, dict) else gateway_groups.read_only_admins_group_id + airavata_internal_user_id = request.user.username + "@" + settings.GATEWAY_ID + group_memberships = request.airavata_client.sharing.get_all_groups_user_belongs( + airavata_internal_user_id) group_ids = [group.id for group in group_memberships] request.is_gateway_admin = admins_group_id in group_ids request.is_read_only_gateway_admin = read_only_admins_group_id in group_ids @@ -63,9 +62,8 @@ def gateway_groups_middleware(get_response): # Load the GatewayGroups and check if user is in the Admins and/or # Read Only Admins groups if not request.session.get('GATEWAY_GROUPS'): - gateway_groups = request.airavata_client.getGatewayGroups( - request.authz_token) - gateway_groups_dict = copy.deepcopy(gateway_groups.__dict__) + gateway_groups = request.airavata_client.iam.get_gateway_groups() + gateway_groups_dict = copy.deepcopy(gateway_groups.__dict__) if hasattr(gateway_groups, '__dict__') else dict(gateway_groups) request.session['GATEWAY_GROUPS'] = gateway_groups_dict set_admin_group_attributes(request, gateway_groups=request.session.get("GATEWAY_GROUPS")) # Gateway Admins are made 'superuser' in Django so they can edit diff --git a/airavata-django-portal/django_airavata/apps/auth/utils.py b/airavata-django-portal/django_airavata/apps/auth/utils.py index cfa53c042..15a1b224b 100644 --- a/airavata-django-portal/django_airavata/apps/auth/utils.py +++ b/airavata-django-portal/django_airavata/apps/auth/utils.py @@ -2,7 +2,6 @@ import time -from airavata.model.security.ttypes import AuthzToken from django.conf import settings from django.contrib.auth import authenticate from django.core.mail import EmailMessage @@ -15,7 +14,13 @@ from . import models def get_authz_token(request, user=None, access_token=None): - """Construct AuthzToken instance from session; refresh token if needed.""" + """Get an access token dict from session; refresh token if needed. + + Returns a dict with 'accessToken', 'gatewayID', and 'userName' for + backwards compatibility with code that reads these fields. The SDK + handles auth via gRPC metadata, so this is mainly used for checking + whether the user is still authenticated. + """ if access_token is not None: return _create_authz_token(request, user=user, access_token=access_token) elif is_request_access_token(request): @@ -48,10 +53,10 @@ def get_service_account_authz_token(): verify=verify) access_token = token.get('access_token') - return AuthzToken( - accessToken=access_token, - # This is a service account, so leaving out userName for now - claimsMap={'gatewayID': settings.GATEWAY_ID}) + return { + 'accessToken': access_token, + 'claimsMap': {'gatewayID': settings.GATEWAY_ID}, + } def _create_authz_token(request, user=None, access_token=None): @@ -61,9 +66,10 @@ def _create_authz_token(request, user=None, access_token=None): user = request.user username = user.username gateway_id = settings.GATEWAY_ID - return AuthzToken(accessToken=access_token, - claimsMap={'gatewayID': gateway_id, - 'userName': username}) + return { + 'accessToken': access_token, + 'claimsMap': {'gatewayID': gateway_id, 'userName': username}, + } def _get_access_token_source(request): diff --git a/airavata-django-portal/django_airavata/context_processors.py b/airavata-django-portal/django_airavata/context_processors.py index 43cbfcf95..b33bfaffe 100644 --- a/airavata-django-portal/django_airavata/context_processors.py +++ b/airavata-django-portal/django_airavata/context_processors.py @@ -19,8 +19,8 @@ def get_notifications(request): if request.user.is_authenticated and hasattr(request, 'airavata_client'): unread_notifications = 0 try: - notifications = request.airavata_client.getAllNotifications( - request.authz_token, settings.GATEWAY_ID) + notifications = request.airavata_client.research.get_all_notifications( + settings.GATEWAY_ID) except Exception: logger.warning("Failed to load notifications") notifications = [] diff --git a/airavata-django-portal/django_airavata/dynamic_apps/__init__.py b/airavata-django-portal/django_airavata/dynamic_apps/__init__.py new file mode 100644 index 000000000..1ba67e3c8 --- /dev/null +++ b/airavata-django-portal/django_airavata/dynamic_apps/__init__.py @@ -0,0 +1,53 @@ +import logging +from importlib import import_module +from importlib_metadata import entry_points + + +# AppConfig instances from custom Django apps +CUSTOM_DJANGO_APPS = [] + +logger = logging.getLogger(__name__) + + +def load(installed_apps, entry_point_group="airavata.djangoapp"): + for entry_point in entry_points(group=entry_point_group): + custom_app_class = entry_point.load() + custom_app_instance = custom_app_class( + entry_point.name, import_module(entry_point.module) + ) + CUSTOM_DJANGO_APPS.append(custom_app_instance) + # Create path to AppConfig class (otherwise the ready() method doesn't get + # called) + logger.info(f"adding dynamic Django app {entry_point.name}") + installed_apps.append("{}.{}".format(entry_point.module, entry_point.attr)) + + +def merge_setting_dict(default, custom_setting): + # FIXME: only handles dict settings, doesn't handle lists + if isinstance(custom_setting, dict): + for k in custom_setting.keys(): + if k not in default: + default[k] = custom_setting[k] + else: + raise Exception( + "Custom django app setting conflicts with " + "key {} in {}".format(k, default) + ) + + +def merge_settings(settings_module): + for custom_django_app in CUSTOM_DJANGO_APPS: + if hasattr(custom_django_app, "merge_settings"): + custom_django_app.merge_settings(settings_module) + elif hasattr(custom_django_app, "settings"): + # This approach is deprecated, use 'merge_settings' instead + # Merge settings from custom Django apps + # NOTE: only handles WEBPACK_LOADER additions + print( + f"{type(custom_django_app).__name__}.settings attr is deprecated, use merge_settings instead" + ) + s = custom_django_app.settings + merge_setting_dict( + getattr(settings_module, "WEBPACK_LOADER"), + getattr(s, "WEBPACK_LOADER", {}), + ) diff --git a/airavata-django-portal/django_airavata/dynamic_apps/context_processors.py b/airavata-django-portal/django_airavata/dynamic_apps/context_processors.py new file mode 100644 index 000000000..dcaa2361e --- /dev/null +++ b/airavata-django-portal/django_airavata/dynamic_apps/context_processors.py @@ -0,0 +1,130 @@ +import copy +from importlib import import_module +import logging +import re + +from django_airavata.dynamic_apps import CUSTOM_DJANGO_APPS + +logger = logging.getLogger(__name__) + + +def custom_app_registry(request): + """Put custom Django apps into the context.""" + custom_apps = CUSTOM_DJANGO_APPS.copy() + custom_apps = [ + _enhance_custom_app_config(app) + for app in custom_apps + if (getattr(app, "enabled", None) is None or app.enabled(request)) + ] + custom_apps.sort(key=lambda app: app.verbose_name.lower()) + current_custom_app = _get_current_app(request, custom_apps) + return { + "custom_apps": custom_apps, + "current_custom_app": current_custom_app, + "custom_app_nav": ( + _get_app_nav(request, current_custom_app) if current_custom_app else None + ), + } + + +def _enhance_custom_app_config(app): + """As necessary add default values for properties to custom AppConfigs.""" + app.url_app_name = _get_url_app_name(app) + app.url_home = _get_url_home(app) + app.fa_icon_class = _get_fa_icon_class(app) + app.app_description = _get_app_description(app) + return app + + +def _get_url_app_name(app_config): + """Return the urls namespace for the given AppConfig instance.""" + urls = _get_app_urls(app_config) + return getattr(urls, "app_name", None) + + +def _get_url_home(app_config): + """Get named URL of home page of app.""" + if hasattr(app_config, "url_home"): + return app_config.url_home + else: + return _get_default_url_home(app_config) + + +def _get_default_url_home(app_config): + """Return first url pattern as a default.""" + urls = _get_app_urls(app_config) + app_name = _get_url_app_name(app_config) + logger.warning( + "Custom Django app {} has no URL namespace " + "defined".format(app_config.label) + ) + first_named_url = None + for urlpattern in urls.urlpatterns: + if hasattr(urlpattern, "name"): + first_named_url = urlpattern.name + break + if not first_named_url: + raise Exception(f"{urls} has no named urls, can't figure out default home URL") + if app_name: + return app_name + ":" + first_named_url + else: + return first_named_url + + +def _get_fa_icon_class(app_config): + """Return Font Awesome icon class to use for app.""" + if hasattr(app_config, "fa_icon_class"): + return app_config.fa_icon_class + else: + return "fa-circle" + + +def _get_app_description(app_config): + """Return brief description of app.""" + return getattr(app_config, "app_description", None) + + +def _get_app_urls(app_config): + return import_module(".urls", app_config.name) + + +def _get_current_app(request, apps): + current_app = [ + app + for app in apps + if request.resolver_match + and app.url_app_name == request.resolver_match.app_name + ] + return current_app[0] if len(current_app) > 0 else None + + +def _get_app_nav(request, current_app): + if hasattr(current_app, "nav"): + # Copy and filter current_app's nav items + nav = [ + item + for item in copy.copy(current_app.nav) + if "enabled" not in item or item["enabled"](request) + ] + # convert "/djangoapp/path/in/app" to "path/in/app" + app_path = "/".join(request.path.split("/")[2:]) + for nav_item in nav: + if "active_prefixes" in nav_item: + if re.match("|".join(nav_item["active_prefixes"]), app_path): + nav_item["active"] = True + else: + nav_item["active"] = False + else: + # 'active_prefixes' is optional, and if not specified, assume + # current item is active + nav_item["active"] = True + else: + # Default to the home view in the app + nav = [ + { + "label": current_app.verbose_name, + "icon": "fa " + current_app.fa_icon_class, + "url": current_app.url_home, + } + ] + return nav diff --git a/airavata-django-portal/django_airavata/dynamic_apps/urls.py b/airavata-django-portal/django_airavata/dynamic_apps/urls.py new file mode 100644 index 000000000..915b1eb4c --- /dev/null +++ b/airavata-django-portal/django_airavata/dynamic_apps/urls.py @@ -0,0 +1,12 @@ +from django_airavata.dynamic_apps import CUSTOM_DJANGO_APPS +from django.conf.urls import include +from django.urls import path + +urlpatterns = [] +for custom_django_app in CUSTOM_DJANGO_APPS: + # Custom Django apps may define a url_prefix, otherwise label will be used + # as url prefix + url_prefix = getattr(custom_django_app, "url_prefix", custom_django_app.label) + urlpatterns.append( + path(f"{url_prefix}/", include(custom_django_app.name + ".urls")) + ) diff --git a/airavata-django-portal/django_airavata/middleware.py b/airavata-django-portal/django_airavata/middleware.py index 8c18c81d7..44cc46737 100644 --- a/airavata-django-portal/django_airavata/middleware.py +++ b/airavata-django-portal/django_airavata/middleware.py @@ -1,10 +1,9 @@ import logging -import thrift -import thrift.transport.TTransport +from django.conf import settings from django.shortcuts import render -from . import utils +from .utils import create_airavata_client logger = logging.getLogger(__name__) @@ -14,14 +13,21 @@ class AiravataClientMiddleware: self.get_response = get_response def __call__(self, request): - with utils.airavata_api_client_pool.connection() as airavata_client: - request.airavata_client = airavata_client + access_token = _get_access_token(request) + gateway_id = settings.GATEWAY_ID + request.airavata_client = create_airavata_client(access_token, gateway_id) + try: response = self.get_response(request) - + except Exception as e: + logger.exception("Error during request processing") + raise + finally: + request.airavata_client.close() return response def process_exception(self, request, exception): - if isinstance(exception, thrift.transport.TTransport.TTransportException): + # Handle connection errors to the Airavata API server + if isinstance(exception, ConnectionError): return render( request, 'django_airavata/error_page.html', @@ -29,27 +35,11 @@ class AiravataClientMiddleware: context={ 'title': 'Airavata is down', 'text': """The Airavata API server is not reachable. Please try again."""}) - else: - return None - - -def profile_service_client(get_response): - """Open and close Profile Service client for each request. + return None - Usage: - request.profile_service['group_manager'].getGroup( - request.authz_token, groupId) - """ - - def middleware(request): - request.profile_service = { - 'group_manager': utils.group_manager_client_pool, - 'iam_admin': utils.iamadmin_client_pool, - 'tenant_profile': utils.tenant_profile_client_pool, - 'user_profile': utils.user_profile_client_pool, - } - response = get_response(request) - - return response - return middleware +def _get_access_token(request): + """Extract access token from request auth or session.""" + if hasattr(request, 'auth') and request.auth is not None: + return request.auth + return request.session.get('ACCESS_TOKEN', '') diff --git a/airavata-django-portal/django_airavata/settings.py b/airavata-django-portal/django_airavata/settings.py index df495996a..68cc93e8a 100644 --- a/airavata-django-portal/django_airavata/settings.py +++ b/airavata-django-portal/django_airavata/settings.py @@ -13,7 +13,8 @@ https://docs.djangoproject.com/en/1.10/ref/settings/ import os import sys -from airavata_django_portal_commons import dynamic_apps +from django_airavata.dynamic_apps import load as _load_dynamic_apps +from django_airavata.dynamic_apps import merge_settings as _merge_dynamic_settings # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -77,8 +78,6 @@ INSTALLED_APPS = [ # django-webpack-loader 'webpack_loader', - # Airavata Django Portal SDK - 'airavata_django_portal_sdk', ] # List of app labels for Airavata apps that should be hidden from menus @@ -95,9 +94,7 @@ MIDDLEWARE = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django_airavata.apps.auth.middleware.authz_token_middleware', 'django_airavata.middleware.AiravataClientMiddleware', - 'django_airavata.middleware.profile_service_client', - # Needs to come after authz_token_middleware, airavata_client and - # profile_service_client + # Needs to come after authz_token_middleware and AiravataClientMiddleware 'django_airavata.apps.auth.middleware.gateway_groups_middleware', # Wagtail related middleware 'wagtail.contrib.redirects.middleware.RedirectMiddleware', @@ -118,7 +115,7 @@ TEMPLATES = [ 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', 'django_airavata.context_processors.airavata_app_registry', - 'airavata_django_portal_commons.dynamic_apps.context_processors.custom_app_registry', + 'django_airavata.dynamic_apps.context_processors.custom_app_registry', 'django_airavata.context_processors.get_notifications', 'django_airavata.context_processors.user_session_data', 'django_airavata.context_processors.google_analytics_tracking_id', @@ -277,10 +274,6 @@ AUTHENTICATION_OPTIONS = { # for the access token parameter (defaults to 'access_token'). ACCESS_TOKEN_REDIRECT_ALLOWED_URIS = [] -# Seconds each connection in the pool is able to stay alive. If open connection -# has lived longer than this period, it will be closed. -# (https://github.com/Thriftpy/thrift_connector) -THRIFT_CLIENT_POOL_KEEPALIVE = 5 # Webpack loader WEBPACK_LOADER = { @@ -636,8 +629,8 @@ except ImportError: # ... # ) # -dynamic_apps.load(INSTALLED_APPS, "airavata.djangoapp") +_load_dynamic_apps(INSTALLED_APPS, "airavata.djangoapp") # Merge WEBPACK_LOADER settings from custom Django apps settings_module = sys.modules[__name__] -dynamic_apps.merge_settings(settings_module) +_merge_dynamic_settings(settings_module) diff --git a/airavata-django-portal/django_airavata/urls.py b/airavata-django-portal/django_airavata/urls.py index 5c0dcf78d..552fc7342 100644 --- a/airavata-django-portal/django_airavata/urls.py +++ b/airavata-django-portal/django_airavata/urls.py @@ -32,8 +32,7 @@ urlpatterns = [ re_path(r'^api/', include('django_airavata.apps.api.urls')), re_path(r'^groups/', include('django_airavata.apps.groups.urls')), re_path(r'^dataparsers/', include('django_airavata.apps.dataparsers.urls')), - path('sdk/', include('airavata_django_portal_sdk.urls')), - re_path(r'^home$', views.home, name='home'), +re_path(r'^home$', views.home, name='home'), re_path(r'^cms/', include(wagtailadmin_urls)), re_path(r'^documents/', include(wagtaildocs_urls)), # For testing, developing error pages @@ -41,7 +40,7 @@ urlpatterns = [ re_path(r'^403/', views.error403), re_path(r'^404/', views.error404), re_path(r'^500/', views.error500), - path('', include('airavata_django_portal_commons.dynamic_apps.urls')), + path('', include('django_airavata.dynamic_apps.urls')), path('', include(wagtail_urls)), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/airavata-django-portal/django_airavata/utils.py b/airavata-django-portal/django_airavata/utils.py index a9576c739..8a23c2eb2 100644 --- a/airavata-django-portal/django_airavata/utils.py +++ b/airavata-django-portal/django_airavata/utils.py @@ -1,355 +1,18 @@ import logging -import ssl -import queue -from django.conf import settings -from contextlib import contextmanager -import thrift_connector.connection_pool as connection_pool -from airavata.api import Airavata -from airavata.service.profile.groupmanager.cpi import GroupManagerService -from airavata.service.profile.groupmanager.cpi.constants import ( - GROUP_MANAGER_CPI_NAME -) -from airavata.service.profile.iam.admin.services.cpi import IamAdminServices -from airavata.service.profile.iam.admin.services.cpi.constants import ( - IAM_ADMIN_SERVICES_CPI_NAME -) -from airavata.service.profile.tenant.cpi import TenantProfileService -from airavata.service.profile.tenant.cpi.constants import ( - TENANT_PROFILE_CPI_NAME -) -from airavata.service.profile.user.cpi import UserProfileService -from airavata.service.profile.user.cpi.constants import USER_PROFILE_CPI_NAME from django.conf import settings -from thrift.protocol import TBinaryProtocol -from thrift.protocol.TMultiplexedProtocol import TMultiplexedProtocol -from thrift.transport import TSocket, TSSLSocket, TTransport - -log = logging.getLogger(__name__) - - -class ThriftConnectionException(Exception): - pass - - -class ThriftClientException(Exception): - pass - - -def get_unsecure_transport(hostname, port): - # Create a socket to the Airavata Server - transport = TSocket.TSocket(hostname, port) - # Use Buffered Protocol to speedup over raw sockets - transport = TTransport.TBufferedTransport(transport) - return transport +from airavata_sdk import AiravataClient +log = logging.getLogger(__name__) -def get_secure_transport(hostname, port): - # Create a socket to the Airavata Server - transport = TSSLSocket.TSSLSocket( - hostname, - port, - cert_reqs=ssl.CERT_REQUIRED, - ca_certs=settings.CA_CERTS_PATH, +def create_airavata_client(access_token, gateway_id): + """Create an AiravataClient instance for the given auth token.""" + return AiravataClient( + host=settings.AIRAVATA_API_HOST, + port=settings.AIRAVATA_API_PORT, + token=access_token, + gateway_id=gateway_id, + secure=getattr(settings, 'AIRAVATA_API_SECURE', False), ) - return TTransport.TBufferedTransport(transport) - - -def get_transport(hostname, port, secure=True): - if secure: - transport = get_secure_transport(hostname, port) - else: - transport = get_unsecure_transport(hostname, port) - return transport - - -def create_airavata_client(transport): - - # Airavata currently uses Binary Protocol - protocol = TBinaryProtocol.TBinaryProtocol(transport) - - # Create a Airavata client to use the protocol encoder - client = Airavata.Client(protocol) - return client - - -def get_binary_protocol(transport): - return TBinaryProtocol.TBinaryProtocol(transport) - - -def create_group_manager_client(transport): - protocol = get_binary_protocol(transport) - multiplex_prot = TMultiplexedProtocol(protocol, GROUP_MANAGER_CPI_NAME) - return GroupManagerService.Client(multiplex_prot) - - -def create_iamadmin_client(transport): - protocol = get_binary_protocol(transport) - multiplex_prot = TMultiplexedProtocol(protocol, - IAM_ADMIN_SERVICES_CPI_NAME) - return IamAdminServices.Client(multiplex_prot) - - -def create_tenant_profile_client(transport): - protocol = get_binary_protocol(transport) - multiplex_prot = TMultiplexedProtocol(protocol, TENANT_PROFILE_CPI_NAME) - return TenantProfileService.Client(multiplex_prot) - - -def create_user_profile_client(transport): - protocol = get_binary_protocol(transport) - multiplex_prot = TMultiplexedProtocol(protocol, USER_PROFILE_CPI_NAME) - return UserProfileService.Client(multiplex_prot) - - -def get_airavata_client(): - """Get Airavata API client as context manager (use in `with statement`).""" - return get_thrift_client(settings.AIRAVATA_API_HOST, - settings.AIRAVATA_API_PORT, - settings.AIRAVATA_API_SECURE, - create_airavata_client) - - -def get_group_manager_client(): - """Group Manager client as context manager (use in `with statement`).""" - return get_thrift_client(settings.PROFILE_SERVICE_HOST, - settings.PROFILE_SERVICE_PORT, - settings.PROFILE_SERVICE_SECURE, - create_group_manager_client) - - -def get_iam_admin_client(): - """IAM Admin client as context manager (use in `with statement`).""" - return get_thrift_client(settings.PROFILE_SERVICE_HOST, - settings.PROFILE_SERVICE_PORT, - settings.PROFILE_SERVICE_SECURE, - create_iamadmin_client) - - -def get_tenant_profile_client(): - """Tenant Profile client as context manager (use in `with statement`).""" - return get_thrift_client(settings.PROFILE_SERVICE_HOST, - settings.PROFILE_SERVICE_PORT, - settings.PROFILE_SERVICE_SECURE, - create_tenant_profile_client) - - -def get_user_profile_client(): - """User Profile client as context manager (use in `with statement`).""" - return get_thrift_client(settings.PROFILE_SERVICE_HOST, - settings.PROFILE_SERVICE_PORT, - settings.PROFILE_SERVICE_SECURE, - create_user_profile_client) - - -@contextmanager -def get_thrift_client(host, port, is_secure, client_generator): - transport = get_transport(host, port, is_secure) - client = client_generator(transport) - - try: - transport.open() - log.debug("Thrift connection opened to {}:{}, " - "secure={}".format(host, port, is_secure)) - try: - yield client - except Exception as e: - log.exception("Thrift client error occurred") - raise ThriftClientException( - "Thrift client error occurred: " + str(e)) from e - finally: - if transport.isOpen(): - transport.close() - log.debug("Thrift connection closed to {}:{}, " - "secure={}".format(host, port, is_secure)) - except ThriftClientException as tce: - # Allow thrift client errors to bubble up - raise tce - except Exception as e: - msg = "Failed to open thrift connection to {}:{}, secure={}".format( - host, port, is_secure) - log.debug(msg) - raise ThriftConnectionException(msg) from e - - -@contextmanager -def simple_thrift_connection(pool): - """Context manager for borrowing a connection from the pool.""" - conn = pool.get_connection() - try: - yield conn['client'] - finally: - pool.return_connection(conn) - - -class SimpleThriftPool: - """ - A thread-safe Thrift connection pool that uses raw Thrift and the TBufferedTransport. - """ - - def __init__(self, service, host, port, size=5, secure=False, ca_certs=None): - self._service = service - self._host = host - self._port = port - self._size = size - self._secure = secure - self._ca_certs = ca_certs or settings.CA_CERTS_PATH - self._pool = queue.Queue(maxsize=size) - self._initialize_pool() - - def _initialize_pool(self): - for _ in range(self._size): - self._pool.put(self._create_connection()) - - def _create_connection(self): - if self._secure: - socket = TSSLSocket.TSSLSocket( - host=self._host, - port=self._port, - cert_reqs=ssl.CERT_REQUIRED, - ca_certs=self._ca_certs, - ) - else: - socket = TSocket.TSocket(host=self._host, port=self._port) - transport = TTransport.TBufferedTransport(socket) - protocol = TBinaryProtocol.TBinaryProtocol(transport) - client = self._service.Client(protocol) - transport.open() - return {'client': client, 'transport': transport} - - def get_connection(self): - conn = self._pool.get() - if not self._is_alive(conn): - log.debug("Stale connection detected, creating new one") - try: - conn['transport'].close() - except Exception: - pass - conn = self._create_connection() - return conn - - def _is_alive(self, conn): - try: - if not conn['transport'].isOpen(): - return False - conn['client'].getAPIVersion() - return True - except Exception: - return False - - def return_connection(self, conn): - try: - if conn['transport'].isOpen(): - conn['transport'].close() - except Exception: - pass - self._pool.put(self._create_connection()) - - def connection(self): - return simple_thrift_connection(self) - - -class CustomThriftClient(connection_pool.ThriftClient): - secure = False - validate = False - - @classmethod - def get_socket_factory(cls): - if not cls.secure: - return super().get_socket_factory() - else: - def factory(host, port): - return TSSLSocket.TSSLSocket( - host, - port, - cert_reqs=ssl.CERT_REQUIRED, - ca_certs=settings.CA_CERTS_PATH, - ) - - return factory - - def ping(self): - try: - self.client.getAPIVersion() - except Exception as e: - log.debug("getAPIVersion failed: {}".format(str(e))) - raise - - -class MultiplexThriftClientMixin: - service_name = None - - @classmethod - def get_protoco_factory(cls): - def factory(transport): - protocol = TBinaryProtocol.TBinaryProtocol(transport) - multiplex_prot = TMultiplexedProtocol(protocol, cls.service_name) - return multiplex_prot - return factory - - -class AiravataAPIThriftClient(CustomThriftClient): - secure = settings.AIRAVATA_API_SECURE - - -class GroupManagerServiceThriftClient(MultiplexThriftClientMixin, - CustomThriftClient): - service_name = GROUP_MANAGER_CPI_NAME - secure = settings.PROFILE_SERVICE_SECURE - - -class IAMAdminServiceThriftClient(MultiplexThriftClientMixin, - CustomThriftClient): - service_name = IAM_ADMIN_SERVICES_CPI_NAME - secure = settings.PROFILE_SERVICE_SECURE - - -class TenantProfileServiceThriftClient(MultiplexThriftClientMixin, - CustomThriftClient): - service_name = TENANT_PROFILE_CPI_NAME - secure = settings.PROFILE_SERVICE_SECURE - - -class UserProfileServiceThriftClient(MultiplexThriftClientMixin, - CustomThriftClient): - service_name = USER_PROFILE_CPI_NAME - secure = settings.PROFILE_SERVICE_SECURE - - -airavata_api_client_pool = SimpleThriftPool( - Airavata, - settings.AIRAVATA_API_HOST, - settings.AIRAVATA_API_PORT, - secure=settings.AIRAVATA_API_SECURE, - ca_certs=settings.CA_CERTS_PATH, -) -group_manager_client_pool = connection_pool.ClientPool( - GroupManagerService, - settings.PROFILE_SERVICE_HOST, - settings.PROFILE_SERVICE_PORT, - connection_class=GroupManagerServiceThriftClient, - keepalive=settings.THRIFT_CLIENT_POOL_KEEPALIVE -) -iamadmin_client_pool = connection_pool.ClientPool( - IamAdminServices, - settings.PROFILE_SERVICE_HOST, - settings.PROFILE_SERVICE_PORT, - connection_class=IAMAdminServiceThriftClient, - keepalive=settings.THRIFT_CLIENT_POOL_KEEPALIVE -) -tenant_profile_client_pool = connection_pool.ClientPool( - TenantProfileService, - settings.PROFILE_SERVICE_HOST, - settings.PROFILE_SERVICE_PORT, - connection_class=TenantProfileServiceThriftClient, - keepalive=settings.THRIFT_CLIENT_POOL_KEEPALIVE -) -user_profile_client_pool = connection_pool.ClientPool( - UserProfileService, - settings.PROFILE_SERVICE_HOST, - settings.PROFILE_SERVICE_PORT, - connection_class=UserProfileServiceThriftClient, - keepalive=settings.THRIFT_CLIENT_POOL_KEEPALIVE -) diff --git a/airavata-django-portal/requirements.txt b/airavata-django-portal/requirements.txt index 2ef07015d..c3c89eb95 100644 --- a/airavata-django-portal/requirements.txt +++ b/airavata-django-portal/requirements.txt @@ -2,8 +2,6 @@ Django==3.2.18 requests==2.25.1 requests-oauthlib==0.7.0 -thrift==0.22.0 -thrift_connector==0.24 djangorestframework==3.12.4 django-webpack-loader==0.6.0 logging-formatter-anticrlf==1.2 @@ -24,8 +22,6 @@ grpcio-tools==1.51.1 ; python_version >= "3.7" grpcio==1.48.2 ; python_version < "3.7" grpcio==1.53.2 ; python_version >= "3.7" -airavata-django-portal-sdk==1.8.4 -airavata-python-sdk==2.2.7 -airavata-django-portal-commons==1.0.0 +airavata-python-sdk==3.0.0 -e "."
