diff --git a/wavefront/server/apps/call_processing/call_processing/cache/cache_manager.py b/wavefront/server/apps/call_processing/call_processing/cache/cache_manager.py index f30d3b89..f442d78c 100644 --- a/wavefront/server/apps/call_processing/call_processing/cache/cache_manager.py +++ b/wavefront/server/apps/call_processing/call_processing/cache/cache_manager.py @@ -3,19 +3,45 @@ import time from typing import Any, Dict, Optional, Union +from azure.core.exceptions import ClientAuthenticationError +from azure.identity import DefaultAzureCredential from call_processing.log.logger import logger +from redis import Connection from redis import ConnectionError from redis import ConnectionPool from redis import Redis from redis import RedisError +from redis import SSLConnection from redis import TimeoutError +from redis.credentials import CredentialProvider from tenacity import retry from tenacity import retry_if_exception_type from tenacity import stop_after_attempt from tenacity import wait_exponential +class AzureManagedRedisProvider(CredentialProvider): + """ + Adapter to bridge Azure Identity with Redis CredentialProvider. + Azure Managed Redis requires 'default' as the username and the + Entra ID access token as the password. + """ + + def __init__(self): + self.credential = DefaultAzureCredential() + self.scope = 'https://redis.azure.com/.default' + self.username = os.getenv('REDIS_USERNAME', 'default') + + def get_credentials(self): + try: + token = self.credential.get_token(self.scope) + return (self.username, token.token) + except ClientAuthenticationError as e: + logger.error(f'Azure authentication failed: {e}') + raise + + class CacheManager: def __init__( self, @@ -51,19 +77,41 @@ def _create_connection_pool( pool_size: int, ) -> ConnectionPool: try: - return ConnectionPool( - host=str(os.getenv('REDIS_HOST', 'localhost')), - port=int(os.getenv('REDIS_PORT', 6379)), - db=int(os.getenv('REDIS_DB', 0)), - max_connections=pool_size, - socket_timeout=socket_timeout, - socket_keepalive=socket_keepalive, - socket_connect_timeout=connection_timeout, - retry_on_timeout=True, - health_check_interval=30, - encoding='utf-8', - decode_responses=True, - ) + host = os.getenv('REDIS_HOST', 'localhost') + port = int(os.getenv('REDIS_PORT', 6379)) + protocol = os.getenv('REDIS_PROTOCOL', 'redis') + password = os.getenv('REDIS_PASSWORD') + cloud_provider = os.getenv('CLOUD_PROVIDER', '').lower() + + connection_class = Connection + if protocol == 'rediss' or port == 10000: + logger.info(f'Using SSLConnection for Redis (Port: {port})') + connection_class = SSLConnection + + pool_kwargs = { + 'connection_class': connection_class, + 'host': host, + 'port': port, + 'db': int(os.getenv('REDIS_DB', 0)), + 'max_connections': pool_size, + 'socket_timeout': socket_timeout, + 'socket_keepalive': socket_keepalive, + 'socket_connect_timeout': connection_timeout, + 'retry_on_timeout': True, + 'health_check_interval': 30, + 'encoding': 'utf-8', + 'decode_responses': True, + } + + if cloud_provider == 'azure' and not password: + logger.info( + 'Configuring Azure Entra ID (Workload Identity) authentication' + ) + pool_kwargs['credential_provider'] = AzureManagedRedisProvider() + elif password: + pool_kwargs['password'] = password + + return ConnectionPool(**pool_kwargs) except Exception as e: logger.error(f'Failed to create connection pool: {e}s') raise diff --git a/wavefront/server/apps/call_processing/pyproject.toml b/wavefront/server/apps/call_processing/pyproject.toml index 8e7f4644..a32d1c80 100644 --- a/wavefront/server/apps/call_processing/pyproject.toml +++ b/wavefront/server/apps/call_processing/pyproject.toml @@ -19,6 +19,7 @@ dependencies = [ "httpx>=0.27.0", # Redis and caching "redis>=5.0.0", + "azure-identity>=1.17.0,<2.0.0", "tenacity>=8.0.0", # Pipecat and voice processing "pipecat-ai[websocket,cartesia,google,silero,deepgram,groq,runner,azure,local-smart-turn-v3,sarvam,tracing]==0.0.103", diff --git a/wavefront/server/modules/db_repo_module/db_repo_module/cache/cache_manager.py b/wavefront/server/modules/db_repo_module/db_repo_module/cache/cache_manager.py index 14501e24..18eaa80c 100644 --- a/wavefront/server/modules/db_repo_module/db_repo_module/cache/cache_manager.py +++ b/wavefront/server/modules/db_repo_module/db_repo_module/cache/cache_manager.py @@ -2,19 +2,45 @@ import time from typing import Any, List, Optional, Union +from azure.core.exceptions import ClientAuthenticationError +from azure.identity import DefaultAzureCredential from common_module.common_cache import CommonCache from common_module.log.logger import logger +from redis import Connection from redis import ConnectionError from redis import ConnectionPool from redis import Redis from redis import RedisError +from redis import SSLConnection from redis import TimeoutError +from redis.credentials import CredentialProvider from tenacity import retry from tenacity import retry_if_exception_type from tenacity import stop_after_attempt from tenacity import wait_exponential +class AzureManagedRedisProvider(CredentialProvider): + """ + Adapter to bridge Azure Identity with Redis CredentialProvider. + Azure Managed Redis requires 'default' as the username and the + Entra ID access token as the password. + """ + + def __init__(self): + self.credential = DefaultAzureCredential() + self.scope = 'https://redis.azure.com/.default' + self.username = os.getenv('REDIS_USERNAME', 'default') + + def get_credentials(self): + try: + token = self.credential.get_token(self.scope) + return (self.username, token.token) + except ClientAuthenticationError as e: + logger.error(f'Azure authentication failed: {e}') + raise + + class CacheManager(CommonCache): def __init__( self, @@ -58,19 +84,41 @@ def _create_connection_pool( pool_size: int, ) -> ConnectionPool: try: - return ConnectionPool( - host=str(os.getenv('REDIS_HOST', 'localhost')), - port=int(os.getenv('REDIS_PORT', 6379)), - db=int(os.getenv('REDIS_DB', 0)), - max_connections=pool_size, - socket_timeout=socket_timeout, - socket_keepalive=socket_keepalive, - socket_connect_timeout=connection_timeout, - retry_on_timeout=True, - health_check_interval=30, - encoding='utf-8', - decode_responses=True, - ) + host = os.getenv('REDIS_HOST', 'localhost') + port = int(os.getenv('REDIS_PORT', 6379)) + protocol = os.getenv('REDIS_PROTOCOL', 'redis') + password = os.getenv('REDIS_PASSWORD') + cloud_provider = os.getenv('CLOUD_PROVIDER', '').lower() + + connection_class = Connection + if protocol == 'rediss' or port == 10000: + logger.info(f'Using SSLConnection for Redis (Port: {port})') + connection_class = SSLConnection + + pool_kwargs = { + 'connection_class': connection_class, + 'host': host, + 'port': port, + 'db': int(os.getenv('REDIS_DB', 0)), + 'max_connections': pool_size, + 'socket_timeout': socket_timeout, + 'socket_keepalive': socket_keepalive, + 'socket_connect_timeout': connection_timeout, + 'retry_on_timeout': True, + 'health_check_interval': 30, + 'encoding': 'utf-8', + 'decode_responses': True, + } + + if cloud_provider == 'azure' and not password: + logger.info( + 'Configuring Azure Entra ID (Workload Identity) authentication' + ) + pool_kwargs['credential_provider'] = AzureManagedRedisProvider() + elif password: + pool_kwargs['password'] = password + + return ConnectionPool(**pool_kwargs) except Exception as e: logger.error(f'Failed to create connection pool: {e}s') raise diff --git a/wavefront/server/modules/db_repo_module/pyproject.toml b/wavefront/server/modules/db_repo_module/pyproject.toml index a1bdcda4..cd083132 100644 --- a/wavefront/server/modules/db_repo_module/pyproject.toml +++ b/wavefront/server/modules/db_repo_module/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "sqlalchemy>=2.0.36,<3.0.0", "alembic>=1.14.1,<2.0.0", "redis>=5.2.1,<6.0.0", + "azure-identity>=1.17.0,<2.0.0", "pgvector>=0.4.1", "tenacity>=8.1.0,<9.0.0", "psycopg[binary,pool]>=3.2.3,<4.0.0", diff --git a/wavefront/server/uv.lock b/wavefront/server/uv.lock index 50740354..cdec14d9 100644 --- a/wavefront/server/uv.lock +++ b/wavefront/server/uv.lock @@ -666,6 +666,7 @@ name = "call-processing" version = "0.1.0" source = { editable = "apps/call_processing" } dependencies = [ + { name = "azure-identity" }, { name = "dependency-injector" }, { name = "fastapi" }, { name = "httpx" }, @@ -683,6 +684,7 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "azure-identity", specifier = ">=1.17.0,<2.0.0" }, { name = "dependency-injector", specifier = ">=4.46.0,<5.0.0" }, { name = "fastapi", specifier = ">=0.115.2,<1.0.0" }, { name = "httpx", specifier = ">=0.27.0" }, @@ -1042,6 +1044,7 @@ version = "0.1.0" source = { editable = "modules/db_repo_module" } dependencies = [ { name = "alembic" }, + { name = "azure-identity" }, { name = "common-module" }, { name = "dependency-injector" }, { name = "pgvector" }, @@ -1054,6 +1057,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "alembic", specifier = ">=1.14.1,<2.0.0" }, + { name = "azure-identity", specifier = ">=1.17.0,<2.0.0" }, { name = "common-module", editable = "modules/common_module" }, { name = "dependency-injector", specifier = ">=4.42.0,<5.0.0" }, { name = "pgvector", specifier = ">=0.4.1" },