├── test ├── __init__.py ├── client_v2_test.py └── config_client_v2_test.py ├── v2 ├── __init__.py └── nacos │ ├── common │ ├── __init__.py │ ├── preserved_metadata_key.py │ ├── nacos_exception.py │ ├── payload_registry.py │ ├── auth.py │ ├── client_config_builder.py │ ├── constants.py │ └── client_config.py │ ├── config │ ├── __init__.py │ ├── cache │ │ ├── __init__.py │ │ ├── config_info_cache.py │ │ └── config_subscribe_manager.py │ ├── model │ │ ├── __init__.py │ │ ├── config_response.py │ │ ├── config_param.py │ │ ├── config_request.py │ │ └── config.py │ ├── util │ │ ├── __init__.py │ │ └── config_client_util.py │ ├── encryption │ │ ├── __init__.py │ │ ├── plugin │ │ │ ├── __init__.py │ │ │ ├── encryption_plugin.py │ │ │ ├── kms_aes_128_encrytion_plugin.py │ │ │ ├── kms_aes_256_encrytion_plugin.py │ │ │ ├── kms_base_encryption_plugin.py │ │ │ └── kms_encrytion_plugin.py │ │ ├── kms_client.py │ │ └── kms_handler.py │ ├── filter │ │ ├── __init__.py │ │ ├── config_filter.py │ │ └── config_encryption_filter.py │ ├── remote │ │ ├── __init__.py │ │ ├── config_grpc_connection_event_listener.py │ │ └── config_change_notify_request_handler.py │ └── nacos_config_service.py │ ├── naming │ ├── __init__.py │ ├── cache │ │ ├── __init__.py │ │ ├── subscribe_callback_wrapper.py │ │ ├── subscribe_manager.py │ │ ├── service_info_updater.py │ │ └── service_info_cache.py │ ├── event │ │ ├── __init__.py │ │ └── instance_change_notifier.py │ ├── model │ │ ├── __init__.py │ │ ├── naming_response.py │ │ ├── naming_param.py │ │ ├── naming_request.py │ │ ├── instance.py │ │ └── service.py │ ├── redo │ │ ├── __init__.py │ │ ├── naming_redo_data.py │ │ └── naming_grpc_redo_service.py │ ├── util │ │ ├── __init__.py │ │ ├── naming_client_util.py │ │ └── naming_remote_constants.py │ └── remote │ │ ├── __init__.py │ │ └── naming_push_request_handler.py │ ├── proto │ ├── __init__.py │ └── nacos_grpc_service.proto │ ├── redo │ ├── __init__.py │ ├── async_rlock.py │ ├── redo_data.py │ └── abstract_redo_service.py │ ├── utils │ ├── __init__.py │ ├── md5_util.py │ ├── content_util.py │ ├── hmac_util.py │ ├── net_util.py │ ├── encode_util.py │ ├── common_util.py │ ├── aes_util.py │ └── file_util.py │ ├── transport │ ├── __init__.py │ ├── grpcauto │ │ ├── __init__.py │ │ ├── nacos_grpc_service_pb2.py │ │ └── nacos_grpc_service_pb2_grpc.py │ ├── model │ │ ├── __init__.py │ │ ├── server_info.py │ │ ├── internal_response.py │ │ ├── rpc_response.py │ │ ├── internal_request.py │ │ └── rpc_request.py │ ├── connection_event_listener.py │ ├── server_request_handler.py │ ├── connection.py │ ├── grpc_connection.py │ ├── auth_client.py │ ├── http_agent.py │ ├── grpc_util.py │ ├── rpc_client_factory.py │ ├── nacos_server_connector.py │ └── grpc_client.py │ ├── __init__.py │ └── nacos_client.py ├── nacos ├── exception.py ├── __init__.py ├── params.py ├── commons.py ├── auth.py ├── files.py ├── task.py ├── listener.py └── timer.py ├── requirements.txt ├── .gitignore └── setup.py /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/naming/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/proto/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/redo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/config/cache/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/config/model/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/config/util/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/naming/cache/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/naming/event/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/naming/model/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/naming/redo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/naming/util/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/transport/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/config/encryption/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/config/filter/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/config/remote/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/naming/remote/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/transport/grpcauto/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/config/encryption/plugin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/nacos/transport/model/__init__.py: -------------------------------------------------------------------------------- 1 | from v2.nacos.transport.model.internal_response import ServerCheckResponse -------------------------------------------------------------------------------- /nacos/exception.py: -------------------------------------------------------------------------------- 1 | class NacosException(Exception): 2 | pass 3 | 4 | 5 | class NacosRequestException(NacosException): 6 | pass 7 | -------------------------------------------------------------------------------- /v2/nacos/naming/event/instance_change_notifier.py: -------------------------------------------------------------------------------- 1 | from threading import RLock 2 | 3 | 4 | class InstancesChangeNotifier: 5 | pass 6 | -------------------------------------------------------------------------------- /nacos/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import NacosClient, NacosException, DEFAULTS, DEFAULT_GROUP_NAME 2 | __version__ = client.VERSION 3 | 4 | __all__ = ["NacosClient", "NacosException", "DEFAULTS", DEFAULT_GROUP_NAME] 5 | -------------------------------------------------------------------------------- /v2/nacos/utils/md5_util.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | 4 | def md5(content: str): 5 | if content: 6 | md = hashlib.md5() 7 | md.update(content.encode('utf-8')) 8 | return md.hexdigest() 9 | return "" 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles>=24.1.0 2 | aiohttp>=3.10.11 3 | alibabacloud_kms20160120>=2.2.3 4 | alibabacloud_tea_openapi>=0.3.12 5 | grpcio>=1.66.1 6 | protobuf>=3.20.3 7 | psutil>=5.9.5 8 | pycryptodome>=3.19.1 9 | pydantic>=2.10.4 10 | -------------------------------------------------------------------------------- /v2/nacos/config/util/config_client_util.py: -------------------------------------------------------------------------------- 1 | from v2.nacos.common.constants import Constants 2 | 3 | 4 | def get_config_cache_key(data_id: str, group: str, tenant: str): 5 | return f"{data_id}{Constants.CONFIG_INFO_SPLITER}{group}{Constants.CONFIG_INFO_SPLITER}{tenant}" 6 | -------------------------------------------------------------------------------- /v2/nacos/utils/content_util.py: -------------------------------------------------------------------------------- 1 | SHOW_CONTENT_SIZE = 100 2 | 3 | 4 | def truncate_content(content: str): 5 | if content == "": 6 | return "" 7 | if len(content) <= SHOW_CONTENT_SIZE: 8 | return content 9 | return content[:SHOW_CONTENT_SIZE] + "..." 10 | -------------------------------------------------------------------------------- /v2/nacos/transport/connection_event_listener.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class ConnectionEventListener(ABC): 5 | 6 | @abstractmethod 7 | async def on_connected(self) -> None: 8 | pass 9 | 10 | @abstractmethod 11 | async def on_disconnect(self) -> None: 12 | pass 13 | -------------------------------------------------------------------------------- /v2/nacos/naming/redo/naming_redo_data.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from v2.nacos.redo.redo_data import RedoData 4 | 5 | 6 | class NamingRedoData(RedoData): 7 | 8 | def __init__(self, data: Any, service_name: str, group_name: str) -> None: 9 | super().__init__(data) 10 | self.service_name = service_name 11 | self.group_name = group_name 12 | -------------------------------------------------------------------------------- /v2/nacos/common/preserved_metadata_key.py: -------------------------------------------------------------------------------- 1 | class PreservedMetadataKeys: 2 | REGISTER_SOURCE = "preserved.register.source" 3 | 4 | HEART_BEAT_TIMEOUT = "preserved.heart.beat.timeout" 5 | 6 | IP_DELETE_TIMEOUT = "preserved.ip.delete.timeout" 7 | 8 | HEART_BEAT_INTERVAL = "preserved.heart.beat.interval" 9 | 10 | INSTANCE_ID_GENERATOR = "preserved.instance.id.generator" 11 | -------------------------------------------------------------------------------- /v2/nacos/utils/hmac_util.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hashlib 3 | import hmac 4 | 5 | 6 | def sign_with_hmac_sha1_encrypt(encrypt_text: str, encrypt_key: str): 7 | if not encrypt_key: 8 | encrypt_key = "" 9 | key = encrypt_key.encode() 10 | mac = hmac.new(key, digestmod=hashlib.sha1) 11 | mac.update(encrypt_text.encode()) 12 | 13 | return base64.b64encode(mac.digest()).decode() 14 | -------------------------------------------------------------------------------- /v2/nacos/naming/util/naming_client_util.py: -------------------------------------------------------------------------------- 1 | from v2.nacos.common.constants import Constants 2 | 3 | 4 | def get_group_name(service_name, group_name): 5 | return f"{group_name}{Constants.SERVICE_INFO_SPLITER}{service_name}" 6 | 7 | 8 | def get_service_cache_key(service_name, clusters): 9 | if not clusters: 10 | return service_name 11 | return f"{service_name}{Constants.SERVICE_INFO_SPLITER}{clusters}" 12 | -------------------------------------------------------------------------------- /v2/nacos/naming/util/naming_remote_constants.py: -------------------------------------------------------------------------------- 1 | class NamingRemoteConstants: 2 | REGISTER_INSTANCE = "registerInstance" 3 | 4 | BATCH_REGISTER_INSTANCE = "batchRegisterInstance" 5 | 6 | DE_REGISTER_INSTANCE = "deregisterInstance" 7 | 8 | QUERY_SERVICE = "queryService" 9 | 10 | SUBSCRIBE_SERVICE = "subscribeService" 11 | 12 | NOTIFY_SUBSCRIBER = "notifySubscriber" 13 | 14 | LIST_SERVICE = "listService" 15 | 16 | FORWARD_INSTANCE = "forwardInstance" 17 | 18 | FORWARD_HEART_BEAT = "forwardHeartBeat" 19 | -------------------------------------------------------------------------------- /nacos/params.py: -------------------------------------------------------------------------------- 1 | VALID_CHAR = set(['_', '-', '.', ':']) 2 | PARAM_KEYS = ["data_id", "group"] 3 | DEFAULT_GROUP_NAME = "DEFAULT_GROUP" 4 | 5 | 6 | def is_valid(param): 7 | if not param: 8 | return False 9 | for i in param: 10 | if i.isalpha() or i.isdigit() or i in VALID_CHAR: 11 | continue 12 | return False 13 | return True 14 | 15 | 16 | def check_params(params): 17 | for p in PARAM_KEYS: 18 | if p in params and not is_valid(params[p]): 19 | return False 20 | return True 21 | 22 | 23 | def group_key(data_id, group, namespace): 24 | return "+".join([data_id, group, namespace]) 25 | 26 | 27 | def parse_key(key): 28 | sp = key.split("+") 29 | return sp[0], sp[1], sp[2] 30 | -------------------------------------------------------------------------------- /v2/nacos/utils/net_util.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from functools import lru_cache 3 | 4 | import psutil 5 | 6 | from v2.nacos.common.nacos_exception import NacosException, INVALID_INTERFACE_ERROR 7 | 8 | 9 | class NetUtils: 10 | @staticmethod 11 | @lru_cache(maxsize=1) 12 | def get_local_ip(): 13 | try: 14 | for interface, addrs in psutil.net_if_addrs().items(): 15 | for addr in addrs: 16 | if addr.family == socket.AF_INET and not addr.address.startswith("127."): 17 | return addr.address 18 | raise NacosException(INVALID_INTERFACE_ERROR, "no valid non-loopback IPv4 interface found") 19 | except socket.gaierror as err: 20 | raise NacosException(INVALID_INTERFACE_ERROR, f"failed to query local IP address, error: {str(err)}") 21 | -------------------------------------------------------------------------------- /v2/nacos/transport/model/server_info.py: -------------------------------------------------------------------------------- 1 | from v2.nacos.common.constants import Constants 2 | 3 | 4 | class ServerInfo: 5 | def __init__(self, server_ip: str, server_port: int): 6 | self.server_ip = server_ip 7 | self.server_port = server_port 8 | 9 | def get_address(self): 10 | return self.server_ip + Constants.COLON + str(self.server_port) 11 | 12 | def get_server_ip(self): 13 | return self.server_ip 14 | 15 | def set_server_ip(self, server_ip): 16 | self.server_ip = server_ip 17 | 18 | def get_server_port(self): 19 | return self.server_port 20 | 21 | def set_server_port(self, server_port): 22 | self.server_port = server_port 23 | 24 | def __str__(self): 25 | return "{serverIp='" + str(self.server_ip) + "', server main port=" + str(self.server_port) + "}" 26 | -------------------------------------------------------------------------------- /v2/nacos/common/nacos_exception.py: -------------------------------------------------------------------------------- 1 | CLIENT_INVALID_PARAM = -400 2 | 3 | CLIENT_DISCONNECT = -401 4 | 5 | CLIENT_OVER_THRESHOLD = -503 6 | 7 | INVALID_PARAM = 400 8 | 9 | NO_RIGHT = 403 10 | 11 | NOT_FOUND = 404 12 | 13 | CONFLICT = 409 14 | 15 | SERVER_ERROR = 500 16 | 17 | BAD_GATEWAY = 502 18 | 19 | OVER_THRESHOLD = 503 20 | 21 | INVALID_SERVER_STATUS = 300 22 | 23 | UN_REGISTER = 301 24 | 25 | NO_HANDLER = 302 26 | 27 | INVALID_INTERFACE_ERROR = -403 28 | 29 | RESOURCE_NOT_FOUND = -404 30 | 31 | HTTP_CLIENT_ERROR_CODE = -500 32 | 33 | 34 | class NacosException(Exception): 35 | """Custom exception class with an error code attribute.""" 36 | 37 | def __init__(self, error_code, message="An error occurred"): 38 | self.error_code = error_code 39 | self.message = message 40 | super().__init__(f'Error [{error_code}]: {message}') 41 | -------------------------------------------------------------------------------- /v2/nacos/config/encryption/plugin/encryption_plugin.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from v2.nacos.config.model.config_param import HandlerParam 4 | 5 | 6 | class EncryptionPlugin(ABC): 7 | 8 | @abstractmethod 9 | def encrypt(self, handler_param: HandlerParam) -> HandlerParam: 10 | pass 11 | 12 | @abstractmethod 13 | def decrypt(self, handler_param: HandlerParam) -> HandlerParam: 14 | pass 15 | 16 | @abstractmethod 17 | def generate_secret_key(self, handler_param: HandlerParam) -> HandlerParam: 18 | pass 19 | 20 | @abstractmethod 21 | def algorithm_name(self): 22 | pass 23 | 24 | @abstractmethod 25 | def encrypt_secret_key(self, handler_param: HandlerParam) -> str: 26 | pass 27 | 28 | @abstractmethod 29 | def decrypt_secret_key(self, handler_param: HandlerParam) -> str: 30 | pass 31 | -------------------------------------------------------------------------------- /v2/nacos/common/payload_registry.py: -------------------------------------------------------------------------------- 1 | class PayloadRegistry: 2 | _REGISTRY_REQUEST = {} 3 | 4 | @classmethod 5 | def init(cls, payloads): 6 | cls.payloads = payloads 7 | cls.scan() 8 | 9 | @classmethod 10 | def scan(cls): 11 | for payload_class in cls.payloads: 12 | cls.register(payload_class.__name__, payload_class) 13 | 14 | @classmethod 15 | def register(cls, type_name, clazz): 16 | if isinstance(clazz, type) and any("Abstract" in b.__name__ for b in clazz.__bases__): 17 | return 18 | if type_name in cls._REGISTRY_REQUEST: 19 | raise RuntimeError(f"Fail to register, type:{type_name}, clazz:{clazz.__name__}") 20 | cls._REGISTRY_REQUEST[type_name] = clazz 21 | 22 | @classmethod 23 | def get_class_by_type(cls, type_name): 24 | return cls._REGISTRY_REQUEST.get(type_name) 25 | -------------------------------------------------------------------------------- /nacos/commons.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def synchronized_with_attr(attr_name): 5 | def decorator(func): 6 | def synced_func(*args, **kws): 7 | self = args[0] 8 | lock = getattr(self, attr_name) 9 | with lock: 10 | return func(*args, **kws) 11 | 12 | return synced_func 13 | 14 | return decorator 15 | 16 | 17 | def truncate(ori_str, length=100): 18 | if not ori_str: 19 | return "" 20 | return ori_str[:length] + "..." if len(ori_str) > length else ori_str 21 | 22 | 23 | def python_version_bellow(version): 24 | if not version: 25 | return False 26 | 27 | sp = [int(s) for s in version.split(".")] 28 | for i in range(len(sp) if len(sp) <= 3 else 3): 29 | if sp[i] == sys.version_info[i]: 30 | continue 31 | 32 | return sys.version_info[i] < sp[i] 33 | 34 | return False -------------------------------------------------------------------------------- /v2/nacos/utils/encode_util.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | 4 | def str_to_bytes(text: str, encoding: str = 'utf-8') -> bytes: 5 | """ 6 | 将字符串转换为字节。 7 | 8 | :param text: 要转换的字符串 9 | :param encoding: 字符串的编码方式,默认为 'utf-8' 10 | :return: 转换后的字节 11 | """ 12 | return text.encode(encoding) 13 | 14 | 15 | def bytes_to_str(bytes_, encoding: str = 'utf-8'): 16 | if not bytes_: 17 | return "" 18 | # Directly decode the UTF-8 bytes back to a string 19 | return bytes_.decode(encoding) 20 | 21 | 22 | def decode_base64(bytes_: bytes): 23 | return base64.b64decode(bytes_) 24 | 25 | 26 | def encode_base64(bytes_): 27 | # Simply encode the input bytes to Base64 28 | return base64.b64encode(bytes_).decode('utf-8') # Decoding to string for consistency with Go's behavior 29 | 30 | 31 | def urlsafe_b64encode(bytes_): 32 | return base64.urlsafe_b64encode(bytes_).decode('utf-8') 33 | -------------------------------------------------------------------------------- /nacos/auth.py: -------------------------------------------------------------------------------- 1 | class Credentials(object): 2 | def __init__(self, access_key_id, access_key_secret, security_token=None): 3 | self.access_key_id = access_key_id 4 | self.access_key_secret = access_key_secret 5 | self.security_token = security_token 6 | 7 | def get_access_key_id(self): 8 | return self.access_key_id 9 | 10 | def get_access_key_secret(self): 11 | return self.access_key_secret 12 | 13 | def get_security_token(self): 14 | return self.security_token 15 | 16 | class CredentialsProvider(object): 17 | def get_credentials(self): 18 | return 19 | 20 | 21 | class StaticCredentialsProvider(CredentialsProvider): 22 | def __init__(self, access_key_id="", access_key_secret="", security_token=""): 23 | self.credentials = Credentials(access_key_id, access_key_secret, security_token) 24 | 25 | def get_credentials(self): 26 | return self.credentials -------------------------------------------------------------------------------- /v2/nacos/utils/common_util.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | 4 | from pydantic import BaseModel 5 | 6 | from v2.nacos.common.constants import Constants 7 | 8 | 9 | def get_current_time_millis(): 10 | t = time.time() 11 | return int(round(t * 1000)) 12 | 13 | 14 | def to_json_string(obj: BaseModel): 15 | try: 16 | return obj.model_dump_json() 17 | except (TypeError, ValueError) as e: 18 | print(f"Error serializing object to JSON: {e}") 19 | return None 20 | 21 | 22 | def to_json_obj(body): 23 | try: 24 | return json.loads(body) 25 | except (TypeError, ValueError) as e: 26 | print(f"Error serializing object to OBJ: {e}") 27 | return None 28 | 29 | 30 | def to_json(obj): 31 | d = {} 32 | d.update(obj.__dict__) 33 | return d 34 | 35 | 36 | def vars_obj(obj): 37 | try: 38 | return vars(obj) 39 | except (TypeError, ValueError) as e: 40 | print(f"Error serializing obj to dict: {e}") 41 | return None 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /v2/nacos/transport/server_request_handler.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Optional 3 | 4 | from v2.nacos.transport.model.internal_request import ClientDetectionRequest 5 | from v2.nacos.transport.model.internal_response import ClientDetectionResponse 6 | from v2.nacos.transport.model.rpc_request import Request 7 | from v2.nacos.transport.model.rpc_response import Response 8 | 9 | 10 | class IServerRequestHandler(ABC): 11 | 12 | @abstractmethod 13 | def name(self) -> str: 14 | pass 15 | 16 | @abstractmethod 17 | async def request_reply(self, request: Request) -> Optional[Response]: 18 | pass 19 | 20 | 21 | class ClientDetectionRequestHandler(IServerRequestHandler): 22 | def name(self) -> str: 23 | return "ClientDetectionRequestHandler" 24 | 25 | async def request_reply(self, request: Request) -> Optional[Response]: 26 | if not isinstance(request, ClientDetectionRequest): 27 | return None 28 | 29 | return ClientDetectionResponse() 30 | 31 | -------------------------------------------------------------------------------- /v2/nacos/transport/connection.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from v2.nacos.transport.model.rpc_request import Request 4 | from v2.nacos.transport.model.rpc_response import Response 5 | from v2.nacos.transport.model.server_info import ServerInfo 6 | 7 | 8 | class IConnection(ABC): 9 | @abstractmethod 10 | def request(self, request: Request, timeout_mills: int) -> Response: 11 | pass 12 | 13 | @abstractmethod 14 | def close(self): 15 | pass 16 | 17 | 18 | class Connection(IConnection, ABC): 19 | def __init__(self, connection_id, server_info: ServerInfo): 20 | self.connection_id = connection_id 21 | self.abandon = False 22 | self.server_info = server_info 23 | 24 | def get_connection_id(self) -> str: 25 | return self.connection_id 26 | 27 | def get_server_info(self) -> ServerInfo: 28 | return self.server_info 29 | 30 | def set_abandon(self, flag: bool): 31 | self.abandon = flag 32 | 33 | def is_abandon(self): 34 | return self.abandon 35 | -------------------------------------------------------------------------------- /v2/nacos/config/encryption/plugin/kms_aes_128_encrytion_plugin.py: -------------------------------------------------------------------------------- 1 | from v2.nacos.common.constants import Constants 2 | from v2.nacos.config.encryption.plugin.kms_encrytion_plugin import KmsEncryptionPlugin 3 | from v2.nacos.config.encryption.kms_client import KmsClient 4 | from v2.nacos.config.model.config_param import HandlerParam 5 | 6 | 7 | class KmsAes128EncryptionPlugin(KmsEncryptionPlugin): 8 | def __init__(self, kms_client: KmsClient): 9 | super().__init__(kms_client) 10 | self.ALGORITHM = 'cipher-kms-aes-128' 11 | 12 | def generate_secret_key(self, handler_param: HandlerParam) -> HandlerParam: 13 | key_id = handler_param.key_id if handler_param.key_id.strip() else Constants.MSE_KMS_V1_DEFAULT_KEY_ID 14 | 15 | plain_secret_key, encryted_secret_key = self.kms_client.generate_secret_key(key_id, 'AES_128') 16 | handler_param.plain_data_key = plain_secret_key 17 | handler_param.encrypted_data_key = encryted_secret_key 18 | return handler_param 19 | 20 | def algorithm_name(self): 21 | return self.ALGORITHM 22 | -------------------------------------------------------------------------------- /v2/nacos/config/encryption/plugin/kms_aes_256_encrytion_plugin.py: -------------------------------------------------------------------------------- 1 | from v2.nacos.common.constants import Constants 2 | from v2.nacos.config.encryption.plugin.kms_encrytion_plugin import KmsEncryptionPlugin 3 | from v2.nacos.config.encryption.kms_client import KmsClient 4 | from v2.nacos.config.model.config_param import HandlerParam 5 | 6 | 7 | class KmsAes256EncryptionPlugin(KmsEncryptionPlugin): 8 | def __init__(self, kms_client: KmsClient): 9 | super().__init__(kms_client) 10 | self.ALGORITHM = 'cipher-kms-aes-256' 11 | 12 | def generate_secret_key(self, handler_param: HandlerParam) -> HandlerParam: 13 | key_id = handler_param.key_id if handler_param.key_id.strip() else Constants.MSE_KMS_V1_DEFAULT_KEY_ID 14 | 15 | plain_secret_key, encryted_secret_key = self.kms_client.generate_secret_key(key_id, 'AES_256') 16 | handler_param.plain_data_key = plain_secret_key 17 | handler_param.encrypted_data_key = encryted_secret_key 18 | return handler_param 19 | 20 | def algorithm_name(self): 21 | return self.ALGORITHM 22 | -------------------------------------------------------------------------------- /v2/nacos/config/remote/config_grpc_connection_event_listener.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from v2.nacos.config.cache.config_subscribe_manager import ConfigSubscribeManager 4 | from v2.nacos.transport.connection_event_listener import ConnectionEventListener 5 | from v2.nacos.transport.rpc_client import RpcClient 6 | 7 | 8 | class ConfigGrpcConnectionEventListener(ConnectionEventListener): 9 | 10 | def __init__(self, logger, config_subscribe_manager: ConfigSubscribeManager, 11 | execute_config_listen_channel: asyncio.Queue, rpc_client: RpcClient): 12 | self.logger = logger 13 | self.config_subscribe_manager = config_subscribe_manager 14 | self.execute_config_listen_channel = execute_config_listen_channel 15 | self.rpc_client = rpc_client 16 | 17 | async def on_connected(self) -> None: 18 | self.logger.info(f"{self.rpc_client.name} rpc client connected,notify listen config") 19 | await self.execute_config_listen_channel.put(None) 20 | 21 | async def on_disconnect(self) -> None: 22 | task_id = self.rpc_client.labels["taskId"] 23 | await self.config_subscribe_manager.batch_set_config_changed(int(task_id)) 24 | -------------------------------------------------------------------------------- /v2/nacos/utils/aes_util.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | from Crypto.Cipher import AES 4 | 5 | from v2.nacos.utils.encode_util import str_to_bytes, bytes_to_str, decode_base64 6 | 7 | 8 | def pad(byte_array: bytes) -> bytes: 9 | """ 10 | pkcs5 padding 11 | """ 12 | block_size = AES.block_size 13 | pad_len = block_size - len(byte_array) % block_size 14 | return byte_array + (bytes([pad_len]) * pad_len) 15 | 16 | 17 | # pkcs5 - unpadding 18 | def unpad(byte_array: bytes) -> bytes: 19 | return byte_array[:-ord(byte_array[-1:])] 20 | 21 | 22 | def encrypt(message: str, key: str) -> str: 23 | byte_array = str_to_bytes(message) 24 | key_bytes = decode_base64(str_to_bytes(key)) 25 | aes = AES.new(key_bytes, AES.MODE_ECB) 26 | padded = pad(byte_array) 27 | encrypted = aes.encrypt(padded) 28 | return base64.b64encode(encrypted).decode('utf-8') 29 | 30 | 31 | def decrypt(encr_data: str, key: str) -> str: 32 | byte_array = decode_base64(str_to_bytes(encr_data)) 33 | key_bytes = decode_base64(str_to_bytes(key)) 34 | aes = AES.new(key_bytes, AES.MODE_ECB) 35 | decrypted = aes.decrypt(byte_array) 36 | return bytes_to_str(unpad(decrypted)) 37 | -------------------------------------------------------------------------------- /v2/nacos/common/auth.py: -------------------------------------------------------------------------------- 1 | class Credentials(object): 2 | def __init__(self, access_key_id, access_key_secret, security_token=None): 3 | self.access_key_id = access_key_id 4 | self.access_key_secret = access_key_secret 5 | self.security_token = security_token 6 | 7 | def get_access_key_id(self): 8 | return self.access_key_id 9 | 10 | def get_access_key_secret(self): 11 | return self.access_key_secret 12 | 13 | def get_security_token(self): 14 | return self.security_token 15 | 16 | class CredentialsProvider(object): 17 | def get_credentials(self): 18 | return 19 | 20 | 21 | class StaticCredentialsProvider(CredentialsProvider): 22 | def __init__(self, access_key_id="", access_key_secret="", security_token=""): 23 | self.credentials = Credentials(access_key_id, access_key_secret, security_token) 24 | 25 | def get_credentials(self): 26 | return self.credentials 27 | 28 | def set_access_key_id(self, access_key_id): 29 | self.credentials.access_key_id = access_key_id 30 | 31 | def set_access_key_secret(self, access_key_secret): 32 | self.credentials.access_key_secret = access_key_secret -------------------------------------------------------------------------------- /v2/nacos/config/model/config_response.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | 3 | from pydantic import BaseModel 4 | 5 | from v2.nacos.transport.model.rpc_response import Response 6 | 7 | 8 | class ConfigContext(BaseModel): 9 | group: str = '' 10 | dataId: str = '' 11 | tenant: str = '' 12 | 13 | 14 | class ConfigChangeBatchListenResponse(Response): 15 | changedConfigs: List[ConfigContext] = [] 16 | 17 | def get_response_type(self) -> str: 18 | return "ConfigChangeBatchListenResponse" 19 | 20 | 21 | class ConfigQueryResponse(Response): 22 | content: Optional[str] = '' 23 | encryptedDataKey: Optional[str] = '' 24 | contentType: Optional[str] = '' 25 | md5: Optional[str] = '' 26 | lastModified: Optional[int] = '' 27 | isBeta: bool = False 28 | tag: bool = False 29 | 30 | def get_response_type(self) -> str: 31 | return "ConfigQueryResponse" 32 | 33 | 34 | class ConfigPublishResponse(Response): 35 | 36 | def get_response_type(self) -> str: 37 | return "ConfigPublishResponse" 38 | 39 | 40 | class ConfigRemoveResponse(Response): 41 | 42 | def get_response_type(self) -> str: 43 | return "ConfigRemoveResponse" 44 | -------------------------------------------------------------------------------- /v2/nacos/transport/model/internal_response.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from v2.nacos.transport.model.rpc_response import Response 4 | 5 | 6 | class NotifySubscriberResponse(Response): 7 | def get_response_type(self) -> str: 8 | return "NotifySubscriberResponse" 9 | 10 | 11 | class ConnectResetResponse(Response): 12 | def get_response_type(self) -> str: 13 | return "ConnectResetResponse" 14 | 15 | 16 | class ClientDetectionResponse(Response): 17 | 18 | def get_response_type(self) -> str: 19 | return "ClientDetectionResponse" 20 | 21 | 22 | class ServerCheckResponse(Response): 23 | connectionId: Optional[str] = '' 24 | 25 | def get_response_type(self) -> str: 26 | return "ServerCheckResponse" 27 | 28 | def set_connection_id(self, connection_id: str) -> None: 29 | self.connectionId = connection_id 30 | 31 | def get_connection_id(self) -> str: 32 | return self.connectionId 33 | 34 | 35 | class HealthCheckResponse(Response): 36 | def get_response_type(self): 37 | return "HealthCheckResponse" 38 | 39 | 40 | class ErrorResponse(Response): 41 | def get_response_type(self): 42 | return "ErrorResponse" 43 | -------------------------------------------------------------------------------- /v2/nacos/transport/model/rpc_response.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class Response(BaseModel, ABC): 7 | resultCode: int = 200 8 | errorCode: int = 0 9 | message: str = '' 10 | requestId: str = '' 11 | 12 | @classmethod 13 | def convert(cls, obj: object): 14 | new_obj = cls() 15 | for key, value in obj.__dict__.items(): 16 | new_obj.__dict__[key] = value 17 | return new_obj 18 | 19 | def set_request_id(self, request_id: str): 20 | self.requestId = request_id 21 | 22 | def is_success(self) -> bool: 23 | return self.errorCode == 0 24 | 25 | def get_error_code(self) -> int: 26 | return self.errorCode 27 | 28 | def get_result_code(self) -> int: 29 | return self.resultCode 30 | 31 | def get_message(self) -> str: 32 | return self.message 33 | 34 | def __str__(self): 35 | return "Response{resultCode=" + str(self.resultCode) + ", errorCode=" + str(self.errorCode) + ", message='" \ 36 | + self.message + "'" + ", requestId='" + self.requestId + "'}" 37 | 38 | @abstractmethod 39 | def get_response_type(self) -> str: 40 | pass 41 | -------------------------------------------------------------------------------- /v2/nacos/config/model/config_param.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from enum import Enum 3 | 4 | from pydantic import BaseModel 5 | 6 | 7 | class Listener(ABC): 8 | 9 | @abstractmethod 10 | def listen(self, namespace: str, group: str, data_id: str, content: str): 11 | raise NotImplementedError("Subclasses should implement this method.") 12 | 13 | 14 | class UsageType(Enum): 15 | request_type = "RequestType" 16 | response_type = "ResponseType" 17 | 18 | 19 | class SearchConfigParam(BaseModel): 20 | search: str = '' 21 | dataId: str = '' 22 | group: str = '' 23 | tag: str = '' 24 | appName: str = '' 25 | pageNo: int = 0 26 | pageSize: int = 0 27 | 28 | 29 | class ConfigParam(BaseModel): 30 | data_id: str = '' 31 | group: str = '' 32 | content: str = '' 33 | tag: str = '' 34 | app_name: str = '' 35 | beta_ips: str = '' 36 | cas_md5: str = '' 37 | type: str = '' 38 | src_user: str = '' 39 | encrypted_data_key: str = '' 40 | kms_key_id: str = '' 41 | usage_type: str = '' 42 | 43 | 44 | class HandlerParam(BaseModel): 45 | data_id: str = '' 46 | content: str = '' 47 | encrypted_data_key: str = '' 48 | plain_data_key: str = '' 49 | key_id: str = '' 50 | -------------------------------------------------------------------------------- /v2/nacos/naming/remote/naming_push_request_handler.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from v2.nacos.naming.cache.service_info_cache import ServiceInfoCache 4 | from v2.nacos.naming.model.naming_request import NotifySubscriberRequest 5 | from v2.nacos.naming.model.naming_response import NotifySubscriberResponse 6 | from v2.nacos.transport.model.rpc_request import Request 7 | from v2.nacos.transport.model.rpc_response import Response 8 | from v2.nacos.transport.server_request_handler import IServerRequestHandler 9 | 10 | 11 | class NamingPushRequestHandler(IServerRequestHandler): 12 | 13 | def name(self) -> str: 14 | return "NamingPushRequestHandler" 15 | 16 | def __init__(self, logger, service_info_cache: ServiceInfoCache): 17 | self.logger = logger 18 | self.service_info_cache = service_info_cache 19 | 20 | async def request_reply(self, request: Request) -> Optional[Response]: 21 | if not isinstance(request, NotifySubscriberRequest): 22 | return None 23 | 24 | self.logger.info("received naming push service info: %s,ackId:%s", str(request.serviceInfo), 25 | request.requestId) 26 | await self.service_info_cache.process_service(request.serviceInfo) 27 | return NotifySubscriberResponse() 28 | -------------------------------------------------------------------------------- /v2/nacos/transport/model/internal_request.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from typing import Optional 3 | 4 | from v2.nacos.transport.model.rpc_request import Request 5 | 6 | CONNECTION_RESET_REQUEST_TYPE = "ConnectResetRequest" 7 | CLIENT_DETECTION_REQUEST_TYPE = "ClientDetectionRequest" 8 | 9 | 10 | class InternalRequest(Request, ABC): 11 | 12 | def get_module(self) -> str: 13 | return 'internal' 14 | 15 | 16 | class HealthCheckRequest(InternalRequest): 17 | 18 | def get_request_type(self): 19 | return "HealthCheckRequest" 20 | 21 | 22 | class ConnectResetRequest(InternalRequest): 23 | serverIp: Optional[str] 24 | serverPort: Optional[str] 25 | 26 | def get_request_type(self) -> str: 27 | return CONNECTION_RESET_REQUEST_TYPE 28 | 29 | 30 | class ClientDetectionRequest(InternalRequest): 31 | def get_request_type(self) -> str: 32 | return CLIENT_DETECTION_REQUEST_TYPE 33 | 34 | 35 | class ServerCheckRequest(InternalRequest): 36 | 37 | def get_request_type(self): 38 | return "ServerCheckRequest" 39 | 40 | 41 | class ConnectionSetupRequest(InternalRequest): 42 | clientVersion: Optional[str] = '' 43 | tenant: Optional[str] = '' 44 | labels: dict = {} 45 | 46 | def get_request_type(self): 47 | return "ConnectionSetupRequest" 48 | -------------------------------------------------------------------------------- /v2/nacos/transport/model/rpc_request.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class Request(BaseModel, ABC): 7 | headers: dict = {} 8 | requestId: str = '' 9 | module: str = '' 10 | 11 | def put_all_headers(self, headers: dict): 12 | if not headers: 13 | return 14 | self.headers.update(headers) 15 | 16 | def put_header(self, key: str, value: str) -> None: 17 | self.headers[key] = value 18 | 19 | def clear_headers(self): 20 | self.headers.clear() 21 | 22 | def get_header(self, key: str, default_value=None) -> str: 23 | return self.headers[key] if self.headers[key] else default_value 24 | 25 | def get_headers(self) -> dict: 26 | return self.headers 27 | 28 | def get_request_id(self) -> str: 29 | return self.requestId 30 | 31 | @abstractmethod 32 | def get_module(self) -> str: 33 | pass 34 | 35 | @abstractmethod 36 | def get_request_type(self) -> str: 37 | pass 38 | 39 | def __str__(self): 40 | return self.__class__.__name__ + "{headers" + str(self.headers) if self.headers else "None" + ", requestId='" + \ 41 | self.requestId + "'}" 42 | -------------------------------------------------------------------------------- /v2/nacos/naming/model/naming_response.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Any, List 2 | 3 | from v2.nacos.naming.model.service import Service 4 | from v2.nacos.transport.model.rpc_response import Response 5 | 6 | 7 | class NotifySubscriberResponse(Response): 8 | def get_response_type(self) -> str: 9 | return "NotifySubscriberResponse" 10 | 11 | class QueryServiceResponse(Response): 12 | serviceInfo: Optional[Service] = None 13 | 14 | def get_service_info(self) -> Service: 15 | return self.serviceInfo 16 | 17 | def get_response_type(self) -> str: 18 | return "QueryServiceResponse" 19 | 20 | class SubscribeServiceResponse(Response): 21 | serviceInfo: Optional[Service] = None 22 | 23 | def get_response_type(self) -> str: 24 | return "SubscribeServiceResponse" 25 | 26 | def get_service_info(self) -> Service: 27 | return self.serviceInfo 28 | 29 | 30 | class InstanceResponse(Response): 31 | def get_response_type(self) -> str: 32 | return "InstanceResponse" 33 | 34 | 35 | class BatchInstanceResponse(Response): 36 | def get_response_type(self) -> str: 37 | return "BatchInstanceResponse" 38 | 39 | 40 | class ServiceListResponse(Response): 41 | count: int 42 | serviceNames: List[str] 43 | 44 | def get_response_type(self) -> str: 45 | return "ServiceListResponse" 46 | -------------------------------------------------------------------------------- /v2/nacos/config/remote/config_change_notify_request_handler.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from v2.nacos.config.cache.config_subscribe_manager import ConfigSubscribeManager 4 | from v2.nacos.config.model.config_request import ConfigChangeNotifyRequest 5 | from v2.nacos.transport.model.internal_response import NotifySubscriberResponse 6 | from v2.nacos.transport.model.rpc_request import Request 7 | from v2.nacos.transport.model.rpc_response import Response 8 | from v2.nacos.transport.server_request_handler import IServerRequestHandler 9 | 10 | 11 | class ConfigChangeNotifyRequestHandler(IServerRequestHandler): 12 | 13 | def name(self): 14 | return "ConfigChangeNotifyRequestHandler" 15 | 16 | def __init__(self, logger, config_subscribe_manager: ConfigSubscribeManager, client_name: str): 17 | self.logger = logger 18 | self.config_subscribe_manager = config_subscribe_manager 19 | self.client_name = client_name 20 | 21 | async def request_reply(self, request: Request) -> Optional[Response]: 22 | if not isinstance(request, ConfigChangeNotifyRequest): 23 | return None 24 | 25 | self.logger.info( 26 | f"received config change push,clientName:{self.client_name},dataId:{request.dataId},group:{request.group},tenant:{request.tenant}") 27 | await self.config_subscribe_manager.notify_config_changed(request.dataId, request.group, 28 | self.config_subscribe_manager.namespace_id) 29 | return NotifySubscriberResponse() 30 | -------------------------------------------------------------------------------- /v2/nacos/proto/nacos_grpc_service.proto: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright 1999-2020 Alibaba Group Holding Ltd. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | syntax = "proto3"; 19 | 20 | import "google/protobuf/any.proto"; 21 | import "google/protobuf/timestamp.proto"; 22 | 23 | option java_multiple_files = true; 24 | option java_package = "com.alibaba.nacos.api.grpc.auto"; 25 | 26 | message Metadata { 27 | string type = 3; 28 | string clientIp = 8; 29 | map headers = 7; 30 | } 31 | 32 | 33 | message Payload { 34 | Metadata metadata = 2; 35 | google.protobuf.Any body = 3; 36 | } 37 | 38 | service RequestStream { 39 | // build a streamRequest 40 | rpc requestStream (Payload) returns (stream Payload) { 41 | } 42 | } 43 | 44 | service Request { 45 | // Sends a commonRequest 46 | rpc request (Payload) returns (Payload) { 47 | } 48 | } 49 | 50 | service BiRequestStream { 51 | // Sends a commonRequest 52 | rpc requestBiStream (stream Payload) returns (stream Payload) { 53 | } 54 | } -------------------------------------------------------------------------------- /v2/nacos/__init__.py: -------------------------------------------------------------------------------- 1 | from .common.client_config import (KMSConfig, 2 | GRPCConfig, 3 | TLSConfig, 4 | ClientConfig) 5 | from .common.client_config_builder import ClientConfigBuilder 6 | from .common.nacos_exception import NacosException 7 | from .config.model.config_param import ConfigParam 8 | from .config.nacos_config_service import NacosConfigService 9 | from .naming.model.instance import Instance 10 | from .naming.model.naming_param import (RegisterInstanceParam, 11 | BatchRegisterInstanceParam, 12 | DeregisterInstanceParam, 13 | ListInstanceParam, 14 | SubscribeServiceParam, 15 | GetServiceParam, 16 | ListServiceParam) 17 | from .naming.model.service import (Service, 18 | ServiceList) 19 | from .naming.nacos_naming_service import NacosNamingService 20 | 21 | __all__ = [ 22 | "KMSConfig", 23 | "GRPCConfig", 24 | "TLSConfig", 25 | "ClientConfig", 26 | "ClientConfigBuilder", 27 | "NacosException", 28 | "ConfigParam", 29 | "NacosConfigService", 30 | "Instance", 31 | "Service", 32 | "ServiceList", 33 | "RegisterInstanceParam", 34 | "BatchRegisterInstanceParam", 35 | "DeregisterInstanceParam", 36 | "ListInstanceParam", 37 | "SubscribeServiceParam", 38 | "GetServiceParam", 39 | "ListServiceParam", 40 | "NacosNamingService" 41 | ] 42 | -------------------------------------------------------------------------------- /v2/nacos/redo/async_rlock.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Optional 3 | 4 | 5 | class AsyncRLock: 6 | """ 7 | 异步可重入锁,支持超时和更完整的功能 8 | """ 9 | 10 | def __init__(self): 11 | self._lock = asyncio.Lock() 12 | self._owner: Optional[asyncio.Task] = None 13 | self._count = 0 14 | # 用于调试和监控 15 | self._acquire_stack = [] 16 | 17 | async def acquire(self, timeout: Optional[float] = None) -> bool: 18 | """ 19 | 获取锁 20 | 21 | Args: 22 | timeout: 超时时间,None表示无限等待 23 | 24 | Returns: 25 | bool: 是否成功获取锁 26 | """ 27 | current_task = asyncio.current_task() 28 | 29 | # 如果当前任务已经持有锁 30 | if self._owner == current_task: 31 | self._count += 1 32 | return True 33 | 34 | # 尝试获取底层锁 35 | try: 36 | if timeout is not None: 37 | await asyncio.wait_for(self._lock.acquire(), timeout=timeout) 38 | else: 39 | await self._lock.acquire() 40 | 41 | self._owner = current_task 42 | self._count = 1 43 | return True 44 | except asyncio.TimeoutError: 45 | return False 46 | 47 | def release(self): 48 | """释放锁""" 49 | current_task = asyncio.current_task() 50 | 51 | if self._owner != current_task: 52 | raise RuntimeError("Cannot release un-acquired lock") 53 | 54 | self._count -= 1 55 | if self._count == 0: 56 | self._owner = None 57 | self._lock.release() 58 | 59 | def locked(self) -> bool: 60 | """检查锁是否被持有""" 61 | return self._lock.locked() 62 | 63 | def owned(self) -> bool: 64 | """检查当前任务是否持有锁""" 65 | return self._owner == asyncio.current_task() 66 | 67 | async def __aenter__(self): 68 | await self.acquire() 69 | return self 70 | 71 | async def __aexit__(self, exc_type, exc_val, exc_tb): 72 | self.release() -------------------------------------------------------------------------------- /v2/nacos/config/filter/config_filter.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import List 3 | 4 | from v2.nacos.config.model.config_param import ConfigParam 5 | 6 | 7 | class IConfigFilter(ABC): 8 | @abstractmethod 9 | def do_filter(self, config_param): 10 | pass 11 | 12 | @abstractmethod 13 | def get_order(self): 14 | pass 15 | 16 | @abstractmethod 17 | def get_filter_name(self): 18 | pass 19 | 20 | 21 | class ConfigFilterChainManager: 22 | def __init__(self): 23 | self.config_filters = [] 24 | 25 | def add_filter(self, conf_filter: IConfigFilter) -> None: 26 | for existing_filter in self.config_filters: 27 | if conf_filter.get_filter_name() == existing_filter.get_filter_name(): 28 | return 29 | for i, existing_filter in enumerate(self.config_filters): 30 | if conf_filter.get_order() < existing_filter.get_order(): 31 | self.config_filters.insert(i, conf_filter) 32 | return 33 | self.config_filters.append(conf_filter) 34 | 35 | def get_filters(self) -> List[IConfigFilter]: 36 | return self.config_filters 37 | 38 | def do_filters(self, param: ConfigParam) -> None: 39 | for config_filter in self.config_filters: 40 | config_filter.do_filter(param) 41 | 42 | def do_filter_by_name(self, param: ConfigParam, name: str) -> None: 43 | for config_filter in self.config_filters: 44 | if config_filter.get_filter_name() == name: 45 | config_filter.do_filter(param) 46 | return 47 | raise ValueError(f"Cannot find the filter with name {name}") -------------------------------------------------------------------------------- /v2/nacos/config/model/config_request.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Optional, List, Dict 3 | 4 | from v2.nacos.config.model.config import ConfigListenContext 5 | from v2.nacos.transport.model.rpc_request import Request 6 | 7 | CONFIG_CHANGE_NOTIFY_REQUEST_TYPE = "ConfigChangeNotifyRequest" 8 | 9 | 10 | class AbstractConfigRequest(Request, ABC): 11 | group: Optional[str] 12 | dataId: Optional[str] 13 | tenant: Optional[str] = '' 14 | 15 | def get_module(self): 16 | return "config" 17 | 18 | def get_request_type(self) -> str: 19 | """ 20 | 提供一个默认实现或抛出NotImplementedError,明确指示子类需要覆盖此方法。 21 | """ 22 | raise NotImplementedError("Subclasses should implement this method.") 23 | 24 | 25 | class ConfigBatchListenRequest(AbstractConfigRequest): 26 | listen: bool = True 27 | configListenContexts: List[ConfigListenContext] = [] 28 | 29 | def get_request_type(self): 30 | return "ConfigBatchListenRequest" 31 | 32 | 33 | class ConfigChangeNotifyRequest(AbstractConfigRequest): 34 | 35 | def get_request_type(self): 36 | return "ConfigChangeNotifyRequest" 37 | 38 | 39 | class ConfigQueryRequest(AbstractConfigRequest): 40 | tag: Optional[str] = '' 41 | 42 | def get_request_type(self): 43 | return "ConfigQueryRequest" 44 | 45 | 46 | class ConfigPublishRequest(AbstractConfigRequest): 47 | content: Optional[str] 48 | casMd5: Optional[str] 49 | additionMap: Dict[str, str] = {} 50 | 51 | def get_request_type(self): 52 | return "ConfigPublishRequest" 53 | 54 | 55 | class ConfigRemoveRequest(AbstractConfigRequest): 56 | 57 | def get_request_type(self): 58 | return "ConfigRemoveRequest" 59 | -------------------------------------------------------------------------------- /v2/nacos/config/filter/config_encryption_filter.py: -------------------------------------------------------------------------------- 1 | from v2.nacos.common.client_config import ClientConfig 2 | from v2.nacos.common.constants import Constants 3 | 4 | from v2.nacos.config.encryption.kms_handler import KMSHandler 5 | from v2.nacos.config.filter.config_filter import IConfigFilter 6 | from v2.nacos.config.model.config_param import ConfigParam, HandlerParam, UsageType 7 | 8 | 9 | def _param_check(param: ConfigParam): 10 | if param.data_id.startswith(Constants.CIPHER_PRE_FIX) and len(param.content.strip()) != 0: 11 | return False 12 | return True 13 | 14 | 15 | class ConfigEncryptionFilter(IConfigFilter): 16 | 17 | def __init__(self, client_config: ClientConfig): 18 | self.kms_handler = KMSHandler(client_config.kms_config) 19 | 20 | def do_filter(self, param: ConfigParam) -> None: 21 | if param.usage_type == UsageType.request_type.value: 22 | encryption_param = HandlerParam(data_id=param.data_id, content=param.content, key_id=param.kms_key_id) 23 | self.kms_handler.encrypt_handler(encryption_param) 24 | param.content = encryption_param.content 25 | param.encrypted_data_key = encryption_param.encrypted_data_key 26 | 27 | elif param.usage_type == UsageType.response_type.value: 28 | decryption_param = HandlerParam(data_id=param.data_id, content=param.content, 29 | encrypted_data_key=param.encrypted_data_key) 30 | self.kms_handler.decrypt_handler(decryption_param) 31 | param.content = decryption_param.content 32 | 33 | def get_order(self) -> int: 34 | return 0 35 | 36 | def get_filter_name(self) -> str: 37 | return "defaultConfigEncryptionFilter" 38 | -------------------------------------------------------------------------------- /v2/nacos/naming/model/naming_param.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Callable, List, Dict 2 | from pydantic import BaseModel 3 | 4 | from v2.nacos.common.constants import Constants 5 | 6 | 7 | class RegisterInstanceParam(BaseModel): 8 | ip: str 9 | port: int 10 | weight: float = 1.0 11 | enabled: bool = True 12 | healthy: bool = True 13 | metadata: Dict[str, str] = {} 14 | cluster_name: str = '' 15 | service_name: str 16 | group_name: str = Constants.DEFAULT_GROUP 17 | ephemeral: bool = True 18 | 19 | 20 | class BatchRegisterInstanceParam(BaseModel): 21 | service_name: str 22 | group_name: str = Constants.DEFAULT_GROUP 23 | instances: List[RegisterInstanceParam] = [] 24 | 25 | 26 | class DeregisterInstanceParam(BaseModel): 27 | ip: str 28 | port: int 29 | cluster_name: str = '' 30 | service_name: str 31 | group_name: str = Constants.DEFAULT_GROUP 32 | ephemeral: bool = True 33 | 34 | 35 | class ListInstanceParam(BaseModel): 36 | service_name: str 37 | group_name: str = Constants.DEFAULT_GROUP 38 | clusters: List[str] = [] 39 | subscribe: bool = True 40 | healthy_only: Optional[bool] 41 | 42 | 43 | class SubscribeServiceParam(BaseModel): 44 | service_name: str 45 | group_name: str = Constants.DEFAULT_GROUP 46 | clusters: List[str] = [] 47 | subscribe_callback: Optional[Callable] = None 48 | 49 | 50 | class GetServiceParam(BaseModel): 51 | service_name: str 52 | group_name: str = Constants.DEFAULT_GROUP 53 | clusters: List[str] = [] 54 | 55 | 56 | class ListServiceParam(BaseModel): 57 | namespace_id: str = Constants.DEFAULT_NAMESPACE_ID 58 | group_name: str = Constants.DEFAULT_GROUP 59 | page_no: int = 1 60 | page_size: int = 10 61 | -------------------------------------------------------------------------------- /v2/nacos/naming/cache/subscribe_callback_wrapper.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod, ABC 2 | from collections.abc import Callable 3 | from typing import Optional, List 4 | 5 | from v2.nacos import Instance, Service 6 | 7 | 8 | class Selector(ABC): 9 | 10 | @abstractmethod 11 | def select_instance(self, service:Service) -> Optional[List[Instance]]: 12 | pass 13 | 14 | 15 | class ClusterSelector(Selector): 16 | 17 | def select_instance(self, service: Service) -> Optional[List[Instance]]: 18 | if self.cluster_names == "": 19 | return service.hosts 20 | return [host for host in service.hosts if host.clusterName in self.clusters] 21 | 22 | def __init__(self,clusters:Optional[List[str]]): 23 | if not clusters: 24 | self.clusters = [] 25 | self.cluster_names = "" 26 | return 27 | 28 | self.clusters: List[str] = clusters 29 | self.cluster_names: str = join_cluster(clusters) 30 | 31 | def __eq__(self, other): 32 | if not isinstance(other, ClusterSelector): 33 | return False 34 | return self.cluster_names == other.cluster_names 35 | 36 | 37 | def join_cluster(clusters:List[str]): 38 | if not clusters: 39 | return "" 40 | # 使用set实现去重和过滤空字符串 41 | unique_set = {item.strip() for item in clusters if item and item.strip()} 42 | 43 | # 转换为列表并排序 44 | unique_list = sorted(list(unique_set)) 45 | 46 | # 使用逗号连接 47 | return ",".join(unique_list) 48 | 49 | class SubscribeCallbackFuncWrapper: 50 | 51 | def __init__(self, selector:Selector, callback_func:Callable): 52 | self.selector = selector 53 | self.callback_func = callback_func 54 | 55 | async def notify_listener(self, service: Service): 56 | instances = self.selector.select_instance(service) 57 | if self.callback_func is None: 58 | return 59 | return await self.callback_func(instances) 60 | -------------------------------------------------------------------------------- /v2/nacos/config/encryption/kms_client.py: -------------------------------------------------------------------------------- 1 | from alibabacloud_kms20160120 import models as kms_20160120_models 2 | from alibabacloud_kms20160120.client import Client 3 | from alibabacloud_tea_openapi import models as open_api_models 4 | 5 | from v2.nacos.common.client_config import KMSConfig 6 | from v2.nacos.utils.encode_util import bytes_to_str 7 | 8 | 9 | class KmsClient: 10 | def __init__(self, client: Client): 11 | self.client = client 12 | 13 | @staticmethod 14 | def create_kms_client(kms_config: KMSConfig): 15 | config = open_api_models.Config( 16 | access_key_id=kms_config.access_key, 17 | access_key_secret=kms_config.secret_key, 18 | endpoint=kms_config.endpoint) 19 | config.protocol = "https" 20 | client = Client(config) 21 | kms_client = KmsClient(client) 22 | return kms_client 23 | 24 | def encrypt(self, content: str, key_id: str): 25 | encrypt_request = kms_20160120_models.EncryptRequest() 26 | encrypt_request.plaintext = content.encode("utf-8") 27 | encrypt_request.key_id = key_id 28 | encrypt_response = self.client.encrypt(encrypt_request) 29 | return encrypt_response.body.ciphertext_blob 30 | 31 | def decrypt(self, content: str): 32 | decrypt_request = kms_20160120_models.DecryptRequest(ciphertext_blob=content) 33 | decrypt_response = self.client.decrypt(decrypt_request) 34 | return decrypt_response.body.plaintext 35 | 36 | def generate_secret_key(self, key_id: str, key_spec: str): 37 | request = kms_20160120_models.GenerateDataKeyRequest() 38 | request.key_id = key_id 39 | request.key_spec = key_spec 40 | resp = self.client.generate_data_key(request) 41 | return resp.body.plaintext, resp.body.ciphertext_blob 42 | -------------------------------------------------------------------------------- /v2/nacos/config/encryption/plugin/kms_base_encryption_plugin.py: -------------------------------------------------------------------------------- 1 | from v2.nacos.common.constants import Constants 2 | from v2.nacos.common.nacos_exception import NacosException, INVALID_PARAM 3 | from v2.nacos.config.encryption.kms_client import KmsClient 4 | from v2.nacos.config.encryption.plugin.kms_encrytion_plugin import KmsEncryptionPlugin 5 | from v2.nacos.config.model.config_param import HandlerParam 6 | 7 | 8 | class KmsBaseEncryptionPlugin(KmsEncryptionPlugin): 9 | def __init__(self, kms_client: KmsClient): 10 | super().__init__(kms_client) 11 | self.ALGORITHM = 'cipher' 12 | 13 | def encrypt(self, handler_param: HandlerParam) -> HandlerParam: 14 | key_id = handler_param.key_id if handler_param.key_id.strip() else Constants.MSE_KMS_V1_DEFAULT_KEY_ID 15 | 16 | if len(handler_param.content) == 0: 17 | raise NacosException(INVALID_PARAM, "encrypt empty content error") 18 | encrypted_content = self.kms_client.encrypt(handler_param.content, key_id) 19 | handler_param.content = encrypted_content 20 | return handler_param 21 | 22 | def decrypt(self, handler_param: HandlerParam) -> HandlerParam: 23 | if len(handler_param.content) == 0: 24 | raise NacosException(INVALID_PARAM, "decrypt empty content error") 25 | plain_content = self.kms_client.decrypt(handler_param.content) 26 | handler_param.content = plain_content 27 | return handler_param 28 | 29 | def generate_secret_key(self, handler_param: HandlerParam) -> HandlerParam: 30 | return handler_param 31 | 32 | def algorithm_name(self): 33 | return self.ALGORITHM 34 | 35 | def encrypt_secret_key(self, handler_param: HandlerParam) -> str: 36 | return "" 37 | 38 | def decrypt_secret_key(self, handler_param: HandlerParam) -> str: 39 | return "" 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # nacos data 107 | test/nacos-data 108 | 109 | # idea and pycharm settings 110 | .idea/ -------------------------------------------------------------------------------- /nacos/files.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import logging 3 | import sys 4 | 5 | try: 6 | import fcntl 7 | 8 | use_fcntl = True 9 | except: 10 | use_fcntl = False 11 | 12 | logger = logging.getLogger("nacos") 13 | 14 | 15 | def read_file_str(base, key): 16 | content = read_file(base, key) 17 | return content.decode("UTF-8") if type(content) == bytes else content 18 | 19 | 20 | def read_file(base, key): 21 | file_path = os.path.join(base, key) 22 | if not os.path.exists(file_path): 23 | return None 24 | 25 | try: 26 | if sys.version_info[0] == 3: 27 | with open(file_path, "r+", encoding="UTF-8", newline="") as f: 28 | lock_file(f) 29 | return f.read() 30 | else: 31 | with open(file_path, "r+") as f: 32 | lock_file(f) 33 | return f.read() 34 | except OSError: 35 | logger.exception("[read-file] read file failed, file path:%s" % file_path) 36 | return None 37 | 38 | 39 | def save_file(base, key, content): 40 | file_path = os.path.join(base, key) 41 | if not os.path.isdir(base): 42 | try: 43 | os.makedirs(base) 44 | except OSError: 45 | logger.warning("[save-file] dir %s is already exist" % base) 46 | 47 | try: 48 | with open(file_path, "wb") as f: 49 | lock_file(f) 50 | f.write(content if type(content) == bytes else content.encode("UTF-8")) 51 | 52 | except OSError: 53 | logger.exception("[save-file] save file failed, file path:%s" % file_path) 54 | 55 | 56 | def delete_file(base, key): 57 | file_path = os.path.join(base, key) 58 | try: 59 | os.remove(file_path) 60 | except OSError: 61 | logger.warning("[delete-file] file not exists, file path:%s" % file_path) 62 | 63 | 64 | def lock_file(f): 65 | if use_fcntl: 66 | fcntl.flock(f, fcntl.LOCK_EX) 67 | -------------------------------------------------------------------------------- /v2/nacos/transport/grpc_connection.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import grpc 4 | 5 | from v2.nacos.common.nacos_exception import NacosException 6 | from v2.nacos.transport.connection import Connection 7 | from v2.nacos.transport.grpc_util import GrpcUtils 8 | from v2.nacos.transport.grpcauto.nacos_grpc_service_pb2 import Payload 9 | from v2.nacos.transport.grpcauto.nacos_grpc_service_pb2_grpc import RequestStub, BiRequestStreamStub 10 | from v2.nacos.transport.model.rpc_request import Request 11 | from v2.nacos.transport.model.rpc_response import Response 12 | 13 | 14 | class GrpcConnection(Connection): 15 | def __init__(self, server_info, connection_id, channel, client: RequestStub, bi_stream_client: BiRequestStreamStub): 16 | super().__init__(connection_id=connection_id, server_info=server_info) 17 | self.channel = channel 18 | self.client = client 19 | self.bi_stream_client = bi_stream_client 20 | self.queue = asyncio.Queue() 21 | 22 | async def request(self, request: Request, timeout_millis) -> Response: 23 | payload = GrpcUtils.convert_request_to_payload(request) 24 | response_payload = await self.client.request(payload, timeout=timeout_millis / 1000.0) 25 | return GrpcUtils.parse(response_payload) 26 | 27 | def set_channel(self, channel: grpc.Channel) -> None: 28 | self.channel = channel 29 | 30 | async def close(self) -> None: 31 | if self.channel: 32 | await self.channel.close() 33 | 34 | async def send_bi_request(self, payload: Payload) -> None: 35 | await self.queue.put(payload) 36 | 37 | async def request_payloads(self): 38 | while True: 39 | try: 40 | payload = await self.queue.get() 41 | yield payload 42 | except NacosException: 43 | pass 44 | 45 | def bi_stream_send(self): 46 | return self.bi_stream_client.requestBiStream(self.request_payloads()) 47 | -------------------------------------------------------------------------------- /v2/nacos/naming/cache/subscribe_manager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Dict, List, Callable 3 | 4 | from v2.nacos.naming.cache.subscribe_callback_wrapper import \ 5 | SubscribeCallbackFuncWrapper 6 | from v2.nacos.naming.model.service import Service 7 | from v2.nacos.naming.util.naming_client_util import get_service_cache_key 8 | 9 | 10 | class SubscribeManager: 11 | def __init__(self): 12 | self.callback_func_wrapper_map: Dict[str, List[SubscribeCallbackFuncWrapper]] = {} 13 | self.mux = asyncio.Lock() 14 | 15 | async def is_subscribed(self, service_name: str, clusters: str) -> bool: 16 | key = get_service_cache_key(service_name, clusters) 17 | return key in self.callback_func_wrapper_map 18 | 19 | async def add_callback_func(self, service_name: str, clusters: str, callback_func_wrapper: SubscribeCallbackFuncWrapper): 20 | key = get_service_cache_key(service_name, clusters) 21 | async with self.mux: 22 | if key not in self.callback_func_wrapper_map: 23 | self.callback_func_wrapper_map[key] = [] 24 | self.callback_func_wrapper_map[key].append(callback_func_wrapper) 25 | 26 | async def remove_callback_func(self, service_name: str, clusters: str, callback_func_wrapper: SubscribeCallbackFuncWrapper): 27 | key = get_service_cache_key(service_name, clusters) 28 | async with self.mux: 29 | if key in self.callback_func_wrapper_map: 30 | self.callback_func_wrapper_map[key] = [func for func in self.callback_func_wrapper_map[key] 31 | if func.callback_func != callback_func_wrapper.callback_func 32 | or func.selector != callback_func_wrapper.selector] 33 | if not self.callback_func_wrapper_map[key]: 34 | del self.callback_func_wrapper_map[key] 35 | 36 | async def service_changed(self, cache_key: str, service: Service): 37 | if cache_key in self.callback_func_wrapper_map: 38 | for callback_func in self.callback_func_wrapper_map[cache_key]: 39 | await callback_func.notify_listener(service) 40 | -------------------------------------------------------------------------------- /v2/nacos/transport/auth_client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | 4 | from v2.nacos.common.client_config import ClientConfig 5 | from v2.nacos.common.nacos_exception import NacosException, SERVER_ERROR 6 | from v2.nacos.transport.http_agent import HttpAgent 7 | 8 | 9 | class AuthClient: 10 | def __init__(self, logger, client_config: ClientConfig, get_server_list_func, http_agent: HttpAgent): 11 | self.logger = logger 12 | self.username = client_config.username 13 | self.password = client_config.password 14 | self.client_config = client_config 15 | self.get_server_list = get_server_list_func 16 | self.http_agent = http_agent 17 | self.access_token = None 18 | self.token_ttl = 0 19 | self.last_refresh_time = 0 20 | self.token_expired_time = None 21 | 22 | async def get_access_token(self, force_refresh=False): 23 | current_time = time.time() 24 | if self.access_token and not force_refresh and self.token_expired_time > current_time: 25 | return self.access_token 26 | 27 | params = { 28 | "username": self.username, 29 | "password": self.password 30 | } 31 | 32 | server_list = self.get_server_list() 33 | for server_address in server_list: 34 | url = server_address + "/nacos/v1/auth/users/login" 35 | resp, error = await self.http_agent.request(url, "POST", None, params, None) 36 | if not resp or error: 37 | self.logger.warning(f"[get-access-token] request {url} failed, error: {error}") 38 | continue 39 | 40 | response_data = json.loads(resp.decode("UTF-8")) 41 | self.access_token = response_data.get('accessToken') 42 | self.token_ttl = response_data.get('tokenTtl', 18000) # 默认使用返回值,无返回则使用18000秒 43 | self.token_expired_time = current_time + self.token_ttl - 10 # 更新 Token 的过期时间 44 | self.logger.info( 45 | f"[get_access_token] AccessToken: {self.access_token}, TTL: {self.token_ttl}, force_refresh: {force_refresh}") 46 | return self.access_token 47 | raise NacosException(SERVER_ERROR, "get access token failed") 48 | -------------------------------------------------------------------------------- /v2/nacos/naming/model/naming_request.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from typing import Optional, Any, List 3 | 4 | from v2.nacos.naming.model.instance import Instance 5 | from v2.nacos.naming.model.service import Service 6 | from v2.nacos.transport.model.rpc_request import Request 7 | 8 | 9 | class AbstractNamingRequest(Request, ABC): 10 | namespace: Optional[str] = '' 11 | serviceName: Optional[str] = '' 12 | groupName: Optional[str] = '' 13 | 14 | def get_module(self): 15 | return "naming" 16 | 17 | def get_request_type(self) -> str: 18 | """ 19 | 提供一个默认实现或抛出NotImplementedError,明确指示子类需要覆盖此方法。 20 | """ 21 | raise NotImplementedError("Subclasses should implement this method.") 22 | 23 | 24 | NOTIFY_SUBSCRIBER_REQUEST_TYPE = "NotifySubscriberRequest" 25 | 26 | class ServiceQueryRequest(AbstractNamingRequest): 27 | cluster: Optional[str] 28 | healthOnly: Optional[bool] 29 | udpPort: Optional[int] = None 30 | 31 | def get_request_type(self) -> str: 32 | return 'ServiceQueryRequest' 33 | 34 | 35 | class InstanceRequest(AbstractNamingRequest): 36 | type: Optional[str] 37 | instance: Optional[Instance] 38 | 39 | def get_request_type(self) -> str: 40 | return 'InstanceRequest' 41 | 42 | class PersistentInstanceRequest(AbstractNamingRequest): 43 | type: Optional[str] 44 | instance: Optional[Instance] 45 | def get_request_type(self) -> str: 46 | return 'PersistentInstanceRequest' 47 | 48 | 49 | class BatchInstanceRequest(AbstractNamingRequest): 50 | type: Optional[str] 51 | instances: Optional[List[Instance]] 52 | 53 | def get_request_type(self) -> str: 54 | return 'BatchInstanceRequest' 55 | 56 | 57 | class NotifySubscriberRequest(AbstractNamingRequest): 58 | serviceInfo: Optional[Service] 59 | 60 | def get_request_type(self) -> str: 61 | return 'NotifySubscriberRequest' 62 | 63 | 64 | class ServiceListRequest(AbstractNamingRequest): 65 | pageNo: Optional[int] 66 | pageSize: Optional[int] 67 | 68 | def get_request_type(self) -> str: 69 | return 'ServiceListRequest' 70 | 71 | 72 | class SubscribeServiceRequest(AbstractNamingRequest): 73 | subscribe: Optional[bool] 74 | clusters: Optional[str] 75 | 76 | def get_request_type(self) -> str: 77 | return 'SubscribeServiceRequest' 78 | -------------------------------------------------------------------------------- /v2/nacos/config/encryption/plugin/kms_encrytion_plugin.py: -------------------------------------------------------------------------------- 1 | from v2.nacos.common.constants import Constants 2 | from v2.nacos.common.nacos_exception import NacosException, INVALID_PARAM 3 | from v2.nacos.config.encryption.plugin.encryption_plugin import EncryptionPlugin 4 | from v2.nacos.config.encryption.kms_client import KmsClient 5 | from v2.nacos.config.model.config_param import HandlerParam 6 | from v2.nacos.utils import aes_util 7 | from v2.nacos.utils.encode_util import decode_base64, str_to_bytes 8 | 9 | 10 | class KmsEncryptionPlugin(EncryptionPlugin): 11 | def __init__(self, kms_client: KmsClient): 12 | self.ALGORITHM = 'cipher-kms' 13 | self.kms_client = kms_client 14 | 15 | @staticmethod 16 | def param_check(handler_param: HandlerParam): 17 | if not handler_param.plain_data_key.strip(): 18 | raise NacosException(INVALID_PARAM, "empty plain_data_key error") 19 | if not handler_param.content.strip(): 20 | raise NacosException(INVALID_PARAM, "encrypt empty content error") 21 | 22 | def encrypt(self, handler_param: HandlerParam) -> HandlerParam: 23 | self.param_check(handler_param) 24 | handler_param.content = aes_util.encrypt( 25 | key=handler_param.plain_data_key, 26 | message=handler_param.content) 27 | return handler_param 28 | 29 | def decrypt(self, handler_param: HandlerParam) -> HandlerParam: 30 | self.param_check(handler_param) 31 | handler_param.content = aes_util.decrypt( 32 | key=handler_param.plain_data_key, 33 | encr_data=handler_param.content) 34 | return handler_param 35 | 36 | def generate_secret_key(self, handler_param: HandlerParam) -> HandlerParam: 37 | pass 38 | 39 | def algorithm_name(self): 40 | pass 41 | 42 | def encrypt_secret_key(self, handler_param: HandlerParam) -> str: 43 | key_id = handler_param.key_id if handler_param.key_id.strip() else Constants.MSE_KMS_V1_DEFAULT_KEY_ID 44 | if len(handler_param.plain_data_key) == 0: 45 | raise NacosException(INVALID_PARAM, "empty plain_data_key error") 46 | return self.kms_client.encrypt(handler_param.plain_data_key, key_id) 47 | 48 | def decrypt_secret_key(self, handler_param: HandlerParam) -> str: 49 | if len(handler_param.encrypted_data_key) == 0: 50 | raise NacosException(INVALID_PARAM, "empty encrypted data key error") 51 | return self.kms_client.decrypt(handler_param.encrypted_data_key) 52 | -------------------------------------------------------------------------------- /nacos/task.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import logging 3 | import time 4 | 5 | 6 | class HeartbeatInfo: 7 | def __init__(self, 8 | service_name, 9 | ip, 10 | port, 11 | cluster_name, 12 | group_name, 13 | weight, 14 | heartbeat_interval, 15 | metadata, 16 | ): 17 | self.service_name = service_name 18 | self.ip = ip 19 | self.port = port 20 | self.cluster_name = cluster_name 21 | self.group_name = group_name 22 | self.weight = weight 23 | self.metadata = metadata 24 | self.heartbeat_interval = heartbeat_interval 25 | 26 | 27 | class HeartbeatTask(threading.Thread): 28 | def __init__(self, 29 | beat_info: HeartbeatInfo, 30 | client, 31 | ): 32 | self.logger = logging.getLogger('nacos.client') 33 | super().__init__() 34 | self.beat_info = beat_info 35 | self.client = client 36 | self.daemon = True 37 | self.stopped = False 38 | 39 | def run(self): 40 | self.logger.info("[auto-beat-task] beat task start, ip:%s, port:%s, service_name:%s, group_name:%s" % ( 41 | self.beat_info.ip, self.beat_info.port, self.beat_info.service_name, self.beat_info.group_name)) 42 | while not self.stopped: 43 | try: 44 | self.client.send_heartbeat(self.beat_info.service_name, 45 | self.beat_info.ip, 46 | self.beat_info.port, 47 | self.beat_info.cluster_name, 48 | self.beat_info.weight, 49 | self.beat_info.metadata, 50 | True, 51 | self.beat_info.group_name) 52 | time.sleep(self.beat_info.heartbeat_interval) 53 | except Exception as e: 54 | self.logger.error("[auto-beat-task] beat task error: %s" % e) 55 | time.sleep(self.beat_info.heartbeat_interval) 56 | self.logger.info("[auto-beat-task] beat task stopped, ip:%s, port:%s, service_name:%s, group_name:%s" % ( 57 | self.beat_info.ip, self.beat_info.port, self.beat_info.service_name, self.beat_info.group_name)) 58 | 59 | def stop(self): 60 | self.stopped = True 61 | -------------------------------------------------------------------------------- /v2/nacos/nacos_client.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from logging.handlers import TimedRotatingFileHandler 4 | 5 | from v2.nacos.common.client_config import ClientConfig 6 | from v2.nacos.common.nacos_exception import NacosException, INVALID_PARAM 7 | from v2.nacos.transport.http_agent import HttpAgent 8 | 9 | 10 | class NacosClient: 11 | def __init__(self, client_config: ClientConfig, log_file: str): 12 | if not client_config: 13 | raise NacosException(INVALID_PARAM, "client config is required") 14 | 15 | self.logger = None 16 | self.init_log(client_config, log_file) 17 | 18 | if client_config.timeout_ms <= 0: 19 | client_config.timeout_ms = 10 * 1000 20 | 21 | if client_config.heart_beat_interval <= 0: 22 | client_config.heart_beat_interval = 5 * 1000 23 | 24 | self.client_config = client_config 25 | self.http_agent = HttpAgent(self.logger, client_config.tls_config, client_config.timeout_ms) 26 | 27 | def init_log(self, client_config: ClientConfig, module): 28 | log_level = client_config.log_level or logging.INFO 29 | 30 | if client_config.cache_dir == '': 31 | client_config.cache_dir = os.path.join(os.path.expanduser("~"), "nacos", "cache") 32 | 33 | if not client_config.cache_dir.endswith(os.path.sep): 34 | client_config.cache_dir += os.path.sep 35 | 36 | if client_config.log_dir is None or client_config.log_dir.strip() == '': 37 | client_config.log_dir = os.path.join(os.path.expanduser("~"), "logs", "nacos") 38 | 39 | if not client_config.log_dir.endswith(os.path.sep): 40 | client_config.log_dir += os.path.sep 41 | 42 | os.makedirs(client_config.log_dir, exist_ok=True) 43 | os.makedirs(client_config.cache_dir, exist_ok=True) 44 | 45 | log_path = client_config.log_dir + module + ".log" 46 | self.logger = logging.getLogger(module) 47 | 48 | file_handler = TimedRotatingFileHandler(log_path, when="midnight", interval=1, 49 | backupCount=client_config.log_rotation_backup_count, 50 | encoding='utf-8') 51 | self.logger.setLevel(log_level) 52 | 53 | formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') 54 | file_handler.setFormatter(formatter) 55 | self.logger.addHandler(file_handler) 56 | self.logger.propagate = False 57 | self.logger.info(f"log directory: {client_config.log_dir}.") 58 | -------------------------------------------------------------------------------- /v2/nacos/transport/http_agent.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | from http import HTTPStatus 3 | from urllib.parse import urlencode 4 | import aiohttp 5 | 6 | from v2.nacos.common.client_config import TLSConfig 7 | 8 | HTTP_STATUS_SUCCESS = 200 9 | 10 | 11 | class HttpAgent: 12 | def __init__(self, logger, tls_config: TLSConfig, default_timeout): 13 | self.logger = logger 14 | self.tls_config = tls_config 15 | self.default_timeout = default_timeout 16 | 17 | self.ssl_context = None 18 | if tls_config and tls_config.enabled: 19 | ctx = ssl.create_default_context( 20 | cafile=tls_config.ca_file) if tls_config.ca_file else ssl.create_default_context() 21 | 22 | if self.tls_config.cert_file and self.tls_config.key_file: 23 | ctx.load_cert_chain(certfile=self.tls_config.cert_file, keyfile=self.tls_config.key_file) 24 | self.ssl_context = ctx 25 | 26 | async def request(self, url: str, method: str, headers: dict = None, params: dict = None, data: dict = None): 27 | if not headers: 28 | headers = {} 29 | 30 | if params: 31 | url += '?' + urlencode(params) 32 | 33 | data = urlencode(data).encode() if data else None 34 | 35 | self.logger.debug( 36 | f"[http-request] url: {url}, headers: {headers}, params: {params}, data: {data}, timeout: {self.default_timeout}") 37 | 38 | try: 39 | if not url.startswith("http"): 40 | url = f"http://{url}" 41 | 42 | connector = aiohttp.TCPConnector(ssl=self.ssl_context) if self.ssl_context else aiohttp.TCPConnector() 43 | async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=self.default_timeout), 44 | connector=connector) as session: 45 | async with session.request(method, url, headers=headers, data=data) as response: 46 | if response.status == HTTPStatus.OK: 47 | return await response.read(), None 48 | else: 49 | error_msg = f"HTTP error: {response.status} - {response.reason}" 50 | self.logger.debug(f"[http-request] {error_msg}") 51 | return None, error_msg 52 | 53 | except aiohttp.ClientError as e: 54 | self.logger.warning(f"[http-request] client error: {e}") 55 | return None, e 56 | except Exception as e: 57 | self.logger.warning(f"[http-request] unexpected error: {e}") 58 | return None, e 59 | -------------------------------------------------------------------------------- /v2/nacos/redo/redo_data.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any 3 | 4 | 5 | class RedoType(Enum): 6 | """Redo type enumeration.""" 7 | REGISTER = "REGISTER" 8 | UNREGISTER = "UNREGISTER" 9 | NONE = "NONE" 10 | REMOVE = "REMOVE" 11 | 12 | class RedoData: 13 | def __init__(self, data: Any) -> None: 14 | """Initialize RedoData with default values.""" 15 | self._expected_registered: bool = True 16 | self._registered: bool = False 17 | self._unregistering: bool = False 18 | self.data: Any = data 19 | 20 | def set_expected_registered(self, registered: bool) -> None: 21 | """Set the expected registration status.""" 22 | self._expected_registered = registered 23 | 24 | def is_expected_registered(self) -> bool: 25 | """Check if the data is expected to be registered.""" 26 | return self._expected_registered 27 | 28 | def is_registered(self) -> bool: 29 | """Check if the data is currently registered.""" 30 | return self._registered 31 | 32 | def is_unregistering(self) -> bool: 33 | """Check if the data is currently unregistering.""" 34 | return self._unregistering 35 | 36 | def set_registered(self, registered: bool) -> None: 37 | """Set the registration status.""" 38 | self._registered = registered 39 | 40 | def set_unregistering(self, unregistering: bool) -> None: 41 | """Set the unregistering status.""" 42 | self._unregistering = unregistering 43 | 44 | def get(self) -> Any: 45 | """Get the data.""" 46 | return self.data 47 | 48 | def set(self, data: Any) -> None: 49 | """Set the data.""" 50 | self.data = data 51 | 52 | def registered(self) -> None: 53 | """Mark the data as registered.""" 54 | self._registered = True 55 | self._unregistering = False 56 | 57 | def unregistered(self) -> None: 58 | """Mark the data as unregistered.""" 59 | self._registered = False 60 | self._unregistering = True 61 | 62 | def is_need_redo(self) -> bool: 63 | """Check if redo operation is needed.""" 64 | return not RedoType.NONE == self.get_redo_type() 65 | 66 | def get_redo_type(self) -> RedoType: 67 | """ 68 | Get redo type for current redo data without expected state. 69 | 70 | Returns: 71 | RedoType: The type of redo operation needed. 72 | """ 73 | if self.is_registered() and not self.is_unregistering(): 74 | return RedoType.NONE if self._expected_registered else RedoType.UNREGISTER 75 | elif self.is_registered() and self.is_unregistering(): 76 | return RedoType.UNREGISTER 77 | elif not self.is_registered() and not self.is_unregistering(): 78 | return RedoType.REGISTER 79 | else: 80 | return RedoType.REGISTER if self._expected_registered else RedoType.REMOVE -------------------------------------------------------------------------------- /nacos/listener.py: -------------------------------------------------------------------------------- 1 | # -*- coding=utf-8 -*- 2 | from abc import abstractmethod 3 | 4 | 5 | class Event(object): 6 | ADDED = "ADDED" 7 | MODIFIED = "MODIFIED" 8 | DELETED = "DELETED" 9 | pass 10 | 11 | 12 | class AbstractListener(object): 13 | def __init__(self, listener_name): 14 | self._listener_name = listener_name 15 | 16 | @property 17 | def listener_name(self): 18 | return self._listener_name 19 | 20 | @abstractmethod 21 | def launch(self, *args, **kwargs): 22 | pass 23 | 24 | 25 | class AbstractListenerManager(object): 26 | @abstractmethod 27 | def manager_context(self): 28 | pass 29 | 30 | @abstractmethod 31 | def add_listener(self, listener): 32 | pass 33 | 34 | @abstractmethod 35 | def remove_listener(self, listener_name): 36 | pass 37 | 38 | @abstractmethod 39 | def empty_listeners(self): 40 | pass 41 | 42 | @abstractmethod 43 | def do_launch(self): 44 | pass 45 | 46 | 47 | class SubscribeListener(AbstractListener): 48 | def __init__(self, fn, listener_name): 49 | """ 50 | 51 | :rtype: object 52 | """ 53 | super(SubscribeListener, self).__init__(listener_name) 54 | self._fn = fn 55 | 56 | def launch(self, event, *args, **kwargs): 57 | self._fn(event, *args, **kwargs) 58 | 59 | 60 | class SimpleListenerManager(AbstractListenerManager): 61 | 62 | def __init__(self): 63 | # listener_name --> listener 64 | self._listener_container = dict() 65 | 66 | @property 67 | def manager_context(self): 68 | return self._listener_container 69 | 70 | def merge_listeners(self, other_manager): 71 | if not other_manager or not isinstance(other_manager, AbstractListenerManager): 72 | return 73 | for listener_name, listener in other_manager.manager_context(): 74 | self._listener_container[listener_name] = listener 75 | 76 | def all_listeners(self): 77 | return self._listener_container 78 | 79 | def add_listener(self, listener): 80 | self._listener_container[listener.listener_name] = listener 81 | return self 82 | 83 | def add_listeners(self, *listeners): 84 | [self.add_listener(listener) for listener in listeners] 85 | return self 86 | 87 | def remove_listener(self, listener_name): 88 | if listener_name not in self._listener_container.keys(): 89 | return self 90 | self._listener_container.pop(listener_name) 91 | return self 92 | 93 | def empty_listeners(self): 94 | self._listener_container.clear() 95 | 96 | def do_launch(self, *args, **kwargs): 97 | for _, listener in self._listener_container.items(): 98 | listener.launch(*args, **kwargs) 99 | -------------------------------------------------------------------------------- /v2/nacos/config/cache/config_info_cache.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from v2.nacos.common.client_config import ClientConfig 5 | from v2.nacos.common.constants import Constants 6 | from v2.nacos.config.util.config_client_util import get_config_cache_key 7 | from v2.nacos.utils.file_util import read_file, write_to_file 8 | 9 | FAILOVER_FILE_SUFFIX = "_failover" 10 | ENCRYPTED_DATA_KEY_FILE_NAME = "encrypted-data-key" 11 | 12 | 13 | class ConfigInfoCache: 14 | def __init__(self, client_config: ClientConfig): 15 | self.logger = logging.getLogger(Constants.CONFIG_MODULE) 16 | self.config_cache_dir = os.path.join(client_config.cache_dir, Constants.CONFIG_MODULE) 17 | self.namespace_id = client_config.namespace_id 18 | 19 | async def write_config_to_cache(self, cache_key: str, content: str, encrypted_data_key: str): 20 | file_path = os.path.join(self.config_cache_dir, cache_key) 21 | encrypted_data_key_file_path = os.path.join(self.config_cache_dir, ENCRYPTED_DATA_KEY_FILE_NAME, 22 | cache_key) 23 | await write_to_file(self.logger, file_path, content) 24 | await write_to_file(self.logger, encrypted_data_key_file_path, encrypted_data_key) 25 | 26 | async def get_config_cache(self, data_id: str, group: str): 27 | cache_key = get_config_cache_key(data_id, group, self.namespace_id) 28 | file_path = os.path.join(self.config_cache_dir, cache_key) 29 | config_content = await read_file(self.logger, file_path) 30 | if not data_id.startswith(Constants.CIPHER_PRE_FIX): 31 | return config_content, "" 32 | else: 33 | encrypted_data_key_file_path = os.path.join(self.config_cache_dir, ENCRYPTED_DATA_KEY_FILE_NAME, 34 | cache_key) 35 | config_encrypted_data_key = await read_file(self.logger, encrypted_data_key_file_path) 36 | return config_content, config_encrypted_data_key 37 | 38 | async def get_fail_over_config_cache(self, data_id: str, group: str): 39 | cache_key = get_config_cache_key(data_id, group, self.namespace_id) + FAILOVER_FILE_SUFFIX 40 | file_path = os.path.join(self.config_cache_dir, cache_key) 41 | config_content = await read_file(self.logger, file_path) 42 | if not config_content: 43 | return "", "" 44 | self.logger.info(f"get fail over content, namespace:{self.namespace_id}, group:{group}, dataId:{data_id}") 45 | encrypted_data_key_path = os.path.join(self.config_cache_dir, ENCRYPTED_DATA_KEY_FILE_NAME, 46 | cache_key) 47 | config_encrypted_data_key = await read_file(self.logger, encrypted_data_key_path) 48 | return config_content, config_encrypted_data_key 49 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import sys 4 | from shutil import rmtree 5 | 6 | from setuptools import find_packages, setup, Command 7 | 8 | # just run `python setup.py upload` 9 | here = os.path.abspath(os.path.dirname(__file__)) 10 | 11 | with io.open(os.path.join(here, 'README.md'), encoding='UTF-8') as f: 12 | long_description = '\n' + f.read() 13 | 14 | 15 | def read_requirements(): 16 | with open(os.path.join(here, 'requirements.txt'), encoding='utf-8') as f: 17 | return f.read().splitlines() 18 | 19 | 20 | class UploadCommand(Command): 21 | """Support setup.py upload.""" 22 | 23 | description = 'Build and publish the package.' 24 | user_options = [] 25 | 26 | @staticmethod 27 | def status(s): 28 | """Prints things in bold.""" 29 | print('\033[1m{0}\033[0m'.format(s)) 30 | 31 | def initialize_options(self): 32 | pass 33 | 34 | def finalize_options(self): 35 | pass 36 | 37 | def run(self): 38 | try: 39 | self.status('Removing previous builds...') 40 | rmtree(os.path.join(here, 'dist')) 41 | except OSError: 42 | pass 43 | 44 | self.status('Building Source and Wheel (universal) distribution...') 45 | os.system( 46 | '{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) 47 | 48 | self.status('Uploading the package to PyPi via Twine...') 49 | os.system('twine upload dist/*') 50 | 51 | sys.exit() 52 | 53 | 54 | setup( 55 | name="nacos-sdk-python", 56 | version="2.0.5", 57 | packages=find_packages( 58 | exclude=["test", "*.tests", "*.tests.*", "tests.*", "tests"]), 59 | url="https://github.com/nacos-group/nacos-sdk-python", 60 | license="Apache License 2.0", 61 | classifiers=[ 62 | "Topic :: Software Development :: Libraries :: Python Modules", 63 | "Operating System :: OS Independent", 64 | "Programming Language :: Python", 65 | "Programming Language :: Python :: 2.7", 66 | "Programming Language :: Python :: 3.6", 67 | "Programming Language :: Python :: 3.7" 68 | ], 69 | keywords=['nacos', 'nacos-sdk-python'], 70 | author="nacos", 71 | author_email="755063194@qq.com", 72 | description="Python client for Nacos.", 73 | long_description=long_description, 74 | long_description_content_type="text/markdown", 75 | install_requires=read_requirements(), 76 | # $ setup.py publish support. 77 | cmdclass={ 78 | 'upload': UploadCommand, 79 | }, 80 | # python_requires='>=3.6', 81 | project_urls={ 82 | 'Documentation': 'https://github.com/nacos-group/nacos-sdk-python', 83 | 'Source': 'https://github.com/nacos-group/nacos-sdk-python', 84 | 'Nacos Open API Guide': 'https://nacos.io/en-us/docs/open-api.html' 85 | }, 86 | ) 87 | -------------------------------------------------------------------------------- /v2/nacos/config/encryption/kms_handler.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from v2.nacos.common.client_config import KMSConfig 4 | from v2.nacos.common.constants import Constants 5 | from v2.nacos.common.nacos_exception import NacosException, INVALID_PARAM 6 | from v2.nacos.config.encryption.kms_client import KmsClient 7 | from v2.nacos.config.encryption.plugin.encryption_plugin import EncryptionPlugin 8 | from v2.nacos.config.encryption.plugin.kms_aes_128_encrytion_plugin import KmsAes128EncryptionPlugin 9 | from v2.nacos.config.encryption.plugin.kms_aes_256_encrytion_plugin import KmsAes256EncryptionPlugin 10 | from v2.nacos.config.encryption.plugin.kms_base_encryption_plugin import KmsBaseEncryptionPlugin 11 | from v2.nacos.config.model.config_param import HandlerParam 12 | 13 | 14 | class KMSHandler: 15 | def __init__(self, kms_config: KMSConfig): 16 | self.kms_plugins: Dict[str, EncryptionPlugin] = {} 17 | self.kms_client = KmsClient.create_kms_client(kms_config) 18 | kms_aes_128_encryption_plugin = KmsAes128EncryptionPlugin(self.kms_client) 19 | self.kms_plugins[kms_aes_128_encryption_plugin.algorithm_name()] = kms_aes_128_encryption_plugin 20 | kms_aes_256_encryption_plugin = KmsAes256EncryptionPlugin(self.kms_client) 21 | self.kms_plugins[kms_aes_256_encryption_plugin.algorithm_name()] = kms_aes_256_encryption_plugin 22 | kms_base_encryption_plugin = KmsBaseEncryptionPlugin(self.kms_client) 23 | self.kms_plugins[kms_base_encryption_plugin.algorithm_name()] = kms_base_encryption_plugin 24 | 25 | def find_encryption_service(self, data_id: str): 26 | for algorithm_name in self.kms_plugins: 27 | if data_id.startswith(algorithm_name): 28 | return self.kms_plugins[algorithm_name] 29 | raise NacosException(INVALID_PARAM, f"encryption plugin service not found, data_id:{data_id}") 30 | 31 | @staticmethod 32 | def check_param(handler_param: HandlerParam): 33 | if not handler_param.data_id.startswith(Constants.CIPHER_PRE_FIX): 34 | raise NacosException(INVALID_PARAM, "dataId prefix should start with 'cipher-'") 35 | if len(handler_param.content) == 0: 36 | raise NacosException(INVALID_PARAM, "encrypt empty content error") 37 | 38 | def encrypt_handler(self, handler_param: HandlerParam): 39 | self.check_param(handler_param) 40 | plugin = self.find_encryption_service(handler_param.data_id) 41 | handler_param = plugin.generate_secret_key(handler_param) 42 | return plugin.encrypt(handler_param) 43 | 44 | def decrypt_handler(self, handler_param: HandlerParam): 45 | self.check_param(handler_param) 46 | plugin = self.find_encryption_service(handler_param.data_id) 47 | handler_param.plain_data_key = plugin.decrypt_secret_key(handler_param) 48 | return plugin.decrypt(handler_param) 49 | -------------------------------------------------------------------------------- /v2/nacos/utils/file_util.py: -------------------------------------------------------------------------------- 1 | import os 2 | from logging import Logger 3 | from typing import Optional 4 | 5 | import aiofiles 6 | 7 | os_type = os.name 8 | 9 | 10 | def mkdir_if_necessary(create_dir: str): 11 | if os_type == 'nt' and os.path.isabs(create_dir): 12 | if len(create_dir) < 2 or create_dir[1] != ':': 13 | raise ValueError("Invalid absolute path for Windows") 14 | os.makedirs(create_dir, exist_ok=True) 15 | 16 | 17 | def is_file_exist(file_path: str): 18 | if not file_path: 19 | return False 20 | return os.path.exists(file_path) 21 | 22 | 23 | async def read_file(logger: Logger, file_path: str) -> str: 24 | """ 25 | 读取指定文件的内容。 26 | 27 | :param logger: logger 28 | :param file_path: 文件路径 29 | :return: 文件内容(字符串) 30 | """ 31 | try: 32 | async with aiofiles.open(file_path, 'r', encoding='utf-8') as file: 33 | file_content = await file.read() 34 | return file_content 35 | except FileNotFoundError: 36 | logger.warning(f"File not found: {file_path}") 37 | return "" 38 | except PermissionError: 39 | logger.error(f"Permission denied to read file: {file_path}") 40 | return "" 41 | except Exception as e: 42 | logger.error(f"Error reading file: {file_path}, error: {e}") 43 | return "" 44 | 45 | 46 | async def read_all_files_in_dir(logger: Logger, dir_path: str) -> Optional[dict]: 47 | """ 48 | 读取指定文件夹下所有文件的内容。 49 | 50 | :param logger: logger 51 | :param dir_path: 文件夹路径 52 | :return: 包含文件名和内容的字典 53 | """ 54 | if not is_file_exist(dir_path): 55 | logger.error(f"directory not found: {dir_path}") 56 | return None 57 | 58 | if not os.path.isdir(dir_path): 59 | logger.error(f"path is not a directory: {dir_path}") 60 | return None 61 | 62 | try: 63 | file_contents = {} 64 | for file_name in os.listdir(dir_path): 65 | file_path = os.path.join(dir_path, file_name) 66 | if os.path.isfile(file_path): 67 | content = await read_file(logger, file_path) 68 | file_contents[file_name] = content 69 | continue 70 | return file_contents 71 | except Exception as e: 72 | logger.error(f"Error reading directory: {dir_path}, error: {e}") 73 | return None 74 | 75 | 76 | async def write_to_file(logger: Logger, file_path: str, content: str) -> None: 77 | """ 78 | 将内容写入指定文件。 79 | 80 | :param logger: logger 81 | :param file_path: 文件路径 82 | :param content: 要写入的内容 83 | """ 84 | mkdir_if_necessary(os.path.dirname(file_path)) 85 | 86 | try: 87 | async with aiofiles.open(file_path, 'w', encoding='utf-8') as file: 88 | await file.write(content) 89 | except PermissionError: 90 | logger.error(f"Permission denied to write file: {file_path},content: {content}") 91 | raise PermissionError 92 | except Exception as e: 93 | logger.error(f"Error writing to file: {file_path}, content: {content}, error: {e}") 94 | raise e 95 | -------------------------------------------------------------------------------- /v2/nacos/transport/grpcauto/nacos_grpc_service_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # NO CHECKED-IN PROTOBUF GENCODE 4 | # source: nacos_grpc_service.proto 5 | # Protobuf Python Version: 5.27.2 6 | """Generated protocol buffer code.""" 7 | from google.protobuf import descriptor as _descriptor 8 | from google.protobuf import descriptor_pool as _descriptor_pool 9 | from google.protobuf import runtime_version as _runtime_version 10 | from google.protobuf import symbol_database as _symbol_database 11 | from google.protobuf.internal import builder as _builder 12 | _runtime_version.ValidateProtobufRuntimeVersion( 13 | _runtime_version.Domain.PUBLIC, 14 | 5, 15 | 27, 16 | 2, 17 | '', 18 | 'nacos_grpc_service.proto' 19 | ) 20 | # @@protoc_insertion_point(imports) 21 | 22 | _sym_db = _symbol_database.Default() 23 | 24 | 25 | from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 26 | from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 27 | 28 | 29 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18nacos_grpc_service.proto\x1a\x19google/protobuf/any.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x83\x01\n\x08Metadata\x12\x0c\n\x04type\x18\x03 \x01(\t\x12\x10\n\x08\x63lientIp\x18\x08 \x01(\t\x12\'\n\x07headers\x18\x07 \x03(\x0b\x32\x16.Metadata.HeadersEntry\x1a.\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"J\n\x07Payload\x12\x1b\n\x08metadata\x18\x02 \x01(\x0b\x32\t.Metadata\x12\"\n\x04\x62ody\x18\x03 \x01(\x0b\x32\x14.google.protobuf.Any28\n\rRequestStream\x12\'\n\rrequestStream\x12\x08.Payload\x1a\x08.Payload\"\x00\x30\x01\x32*\n\x07Request\x12\x1f\n\x07request\x12\x08.Payload\x1a\x08.Payload\"\x00\x32>\n\x0f\x42iRequestStream\x12+\n\x0frequestBiStream\x12\x08.Payload\x1a\x08.Payload\"\x00(\x01\x30\x01\x42#\n\x1f\x63om.alibaba.nacos.api.grpc.autoP\x01\x62\x06proto3') 30 | 31 | _globals = globals() 32 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 33 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'nacos_grpc_service_pb2', _globals) 34 | if not _descriptor._USE_C_DESCRIPTORS: 35 | _globals['DESCRIPTOR']._loaded_options = None 36 | _globals['DESCRIPTOR']._serialized_options = b'\n\037com.alibaba.nacos.api.grpc.autoP\001' 37 | _globals['_METADATA_HEADERSENTRY']._loaded_options = None 38 | _globals['_METADATA_HEADERSENTRY']._serialized_options = b'8\001' 39 | _globals['_METADATA']._serialized_start=89 40 | _globals['_METADATA']._serialized_end=220 41 | _globals['_METADATA_HEADERSENTRY']._serialized_start=174 42 | _globals['_METADATA_HEADERSENTRY']._serialized_end=220 43 | _globals['_PAYLOAD']._serialized_start=222 44 | _globals['_PAYLOAD']._serialized_end=296 45 | _globals['_REQUESTSTREAM']._serialized_start=298 46 | _globals['_REQUESTSTREAM']._serialized_end=354 47 | _globals['_REQUEST']._serialized_start=356 48 | _globals['_REQUEST']._serialized_end=398 49 | _globals['_BIREQUESTSTREAM']._serialized_start=400 50 | _globals['_BIREQUESTSTREAM']._serialized_end=462 51 | # @@protoc_insertion_point(module_scope) 52 | -------------------------------------------------------------------------------- /v2/nacos/naming/cache/service_info_updater.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from v2.nacos.common.constants import Constants 5 | from v2.nacos.naming.cache.service_info_cache import ServiceInfoCache 6 | from v2.nacos.naming.util.naming_client_util import get_group_name 7 | from v2.nacos.utils.common_util import get_current_time_millis 8 | 9 | 10 | class ServiceInfoUpdater: 11 | def __init__(self, service_info_holder: ServiceInfoCache, 12 | update_thread_num: int, naming_proxy): 13 | self.logger = logging.getLogger(Constants.NAMING_MODULE) 14 | self.service_info_holder = service_info_holder 15 | self.update_thread_num = update_thread_num 16 | 17 | from v2.nacos.naming.remote.naming_grpc_client_proxy import \ 18 | NamingGRPCClientProxy 19 | if not isinstance(naming_proxy, NamingGRPCClientProxy): 20 | raise TypeError( 21 | "client_proxy must be NamingGRPCClientProxy instance") 22 | self.naming_proxy = naming_proxy 23 | 24 | self.stop_event = asyncio.Event() 25 | self.semaphore = asyncio.Semaphore(update_thread_num) 26 | 27 | async def async_update_service(self): 28 | while not self.stop_event.is_set(): 29 | try: 30 | # 创建任务列表 31 | tasks = [] 32 | 33 | # 遍历所有服务 34 | for key, service in self.service_info_holder.service_info_map.items(): 35 | # 获取上次更新时间 36 | if not await self.service_info_holder.is_subscribed( 37 | get_group_name(service.name,service.groupName),service.clusters): 38 | continue 39 | 40 | last_ref_time = self.service_info_holder.update_time_map.get( 41 | key, 0) 42 | # 检查是否需要更新 43 | if get_current_time_millis() - last_ref_time > service.cacheMillis: 44 | # 创建受限的任务(使用信号量控制并发) 45 | task = self._create_limited_task( 46 | self.update_service_now, 47 | service.name, service.groupName, 48 | service.clusters 49 | ) 50 | tasks.append(task) 51 | 52 | # 并发执行所有需要更新的任务 53 | if tasks: 54 | await asyncio.gather(*tasks, return_exceptions=True) 55 | 56 | # 等待1秒或直到被取消 57 | try: 58 | await asyncio.wait_for(self.stop_event.wait(), timeout=1.0) 59 | break # 如果事件被设置则退出循环 60 | except asyncio.TimeoutError: 61 | # 正常超时,继续下一轮循环 62 | continue 63 | 64 | except asyncio.CancelledError: 65 | self.logger.info("Service update task was cancelled") 66 | break 67 | except Exception as e: 68 | self.logger.error(f"Error in service update loop: {e}") 69 | await asyncio.sleep(1) # 出错时也等待避免忙循环 70 | 71 | async def _create_limited_task(self, func, *args): 72 | """创建受信号量限制的任务""" 73 | async with self.semaphore: 74 | return await func(*args) 75 | 76 | async def update_service_now(self, service_name: str, group_name: str, 77 | clusters: str): 78 | try: 79 | service = await self.naming_proxy.query_instance_of_service( 80 | service_name, group_name, clusters, False) 81 | # 处理服务信息 82 | await self.service_info_holder.process_service(service) 83 | 84 | except Exception as e: 85 | logging.error( 86 | f"QueryInstances error, serviceName:{service_name}, cluster:{clusters}, err:{e}") 87 | 88 | def stop(self): 89 | """停止服务更新""" 90 | self.stop_event.set() -------------------------------------------------------------------------------- /v2/nacos/naming/model/instance.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from pydantic import BaseModel 4 | 5 | from v2.nacos.common.constants import Constants 6 | from v2.nacos.common.nacos_exception import NacosException, INVALID_PARAM 7 | from v2.nacos.common.preserved_metadata_key import PreservedMetadataKeys 8 | 9 | 10 | class Instance(BaseModel): 11 | instanceId: str = '' 12 | ip: str 13 | port: int 14 | weight: float = 1.0 15 | healthy: bool = True 16 | enabled: bool = True 17 | ephemeral: bool = True 18 | clusterName: str = '' 19 | serviceName: str = '' 20 | metadata: dict = {} 21 | 22 | def __str__(self): 23 | return f"Instance({', '.join(f'{key}={value!r}' for key, value in self.__dict__.items())})" 24 | 25 | def to_inet_addr(self): 26 | return self.ip + ":" + str(self.port) 27 | 28 | def is_ephemeral(self) -> bool: 29 | return self.ephemeral 30 | 31 | def get_weight(self): 32 | return self.weight 33 | 34 | def add_metadata(self, key: str, value: str) -> None: 35 | if self.metadata is None: 36 | self.metadata = {} 37 | self.metadata[key] = value 38 | 39 | def get_instance_heart_beat_interval(self): 40 | return self.__get_metadata_by_key_with_int_default(PreservedMetadataKeys.HEART_BEAT_INTERVAL, 41 | Constants.DEFAULT_HEART_BEAT_INTERVAL) 42 | 43 | def get_instance_heart_beat_timeout(self): 44 | return self.__get_metadata_by_key_with_int_default(PreservedMetadataKeys.HEART_BEAT_INTERVAL, 45 | Constants.DEFAULT_HEART_BEAT_TIMEOUT) 46 | 47 | def get_ip_delete_timeout(self): 48 | return self.__get_metadata_by_key_with_int_default(PreservedMetadataKeys.IP_DELETE_TIMEOUT, 49 | Constants.DEFAULT_IP_DELETE_TIMEOUT) 50 | 51 | def get_instance_id_generator(self): 52 | return self.__get_metadata_by_key_with_str_default(PreservedMetadataKeys.INSTANCE_ID_GENERATOR, 53 | Constants.DEFAULT_INSTANCE_ID_GENERATOR) 54 | 55 | def check_instance_is_legal(self): 56 | if self.get_instance_heart_beat_timeout() < self.get_instance_heart_beat_interval() or \ 57 | self.get_ip_delete_timeout() < self.get_instance_heart_beat_interval(): 58 | raise NacosException( 59 | INVALID_PARAM, 60 | "Instance 'heart beat interval' must less than 'heart beat timeout' and 'ip delete timeout'." 61 | ) 62 | 63 | def contains_metadata(self, key: str) -> bool: 64 | if not self.metadata: 65 | return False 66 | return key in self.metadata.keys() 67 | 68 | def __get_metadata_by_key_with_int_default(self, key: str, default_value: int) -> int: 69 | if not self.metadata or key not in self.metadata: 70 | return default_value 71 | value = self.metadata[key] 72 | 73 | pattern = re.compile(Constants.NUMBER_PATTERN) 74 | 75 | if value.strip() and re.match(pattern, value): 76 | return int(value) 77 | 78 | return default_value 79 | 80 | def __get_metadata_by_key_with_str_default(self, key: str, default_value: str) -> str: 81 | if not self.metadata: 82 | return default_value 83 | return self.metadata[key] 84 | -------------------------------------------------------------------------------- /v2/nacos/naming/model/service.py: -------------------------------------------------------------------------------- 1 | import time 2 | import urllib.parse 3 | from typing import Optional, List 4 | 5 | from pydantic import BaseModel 6 | 7 | from v2.nacos.common.constants import Constants 8 | from v2.nacos.common.nacos_exception import NacosException 9 | from v2.nacos.naming.model.instance import Instance 10 | 11 | EMPTY = "" 12 | 13 | ALL_IPS = "000--00-ALL_IPS--00--000" 14 | 15 | SPLITER = "@@" 16 | 17 | DEFAULT_CHARSET = "UTF-8" 18 | 19 | 20 | class Service(BaseModel): 21 | name: str 22 | groupName: str 23 | clusters: Optional[str] = '' 24 | cacheMillis: int = 1000 25 | hosts: List[Instance] = [] 26 | lastRefTime: int = 0 27 | checksum: str = "" 28 | allIps: bool = False 29 | reachProtectionThreshold: bool = False 30 | jsonFromServer: str = "" 31 | 32 | def init_from_key(self, key=None): 33 | if key: 34 | max_index = 2 35 | cluster_index = 2 36 | service_name_index = 1 37 | group_index = 0 38 | keys = key.split(Constants.SERVICE_INFO_SPLITER) 39 | if len(keys) >= max_index + 1: 40 | self.groupName = keys[group_index] 41 | self.name = keys[service_name_index] 42 | self.clusters = keys[cluster_index] 43 | elif len(keys) == max_index: 44 | self.groupName = keys[group_index] 45 | self.name = keys[service_name_index] 46 | else: 47 | raise NacosException("Can't parse out 'group_name', but it must not None!") 48 | 49 | def get_ip_count(self): 50 | return len(self.hosts) 51 | 52 | def is_expired(self): 53 | return int(round(time.time() * 1000)) - self.lastRefTime > self.cacheMillis 54 | 55 | def add_host(self, host): 56 | self.hosts.append(host) 57 | 58 | def add_all_hosts(self, hosts): 59 | self.hosts.extend(hosts) 60 | 61 | def is_valid(self): 62 | return self.hosts != [] 63 | 64 | def validate(self): 65 | if self.allIps: 66 | return True 67 | 68 | if not self.hosts: 69 | return False 70 | 71 | valid_hosts = [] 72 | for host in self.hosts: 73 | if not host.is_healthy(): 74 | continue 75 | 76 | for i in range(host.get_weight()): 77 | valid_hosts.append(i) 78 | 79 | return len(valid_hosts) > 0 80 | 81 | def get_key_default(self): 82 | service_name = self.get_grouped_service_name() 83 | return self.get_key(service_name, self.clusters) 84 | 85 | def get_key_encoded(self): 86 | service_name = self.get_grouped_service_name().encode("utf-8") 87 | service_name = urllib.parse.quote(service_name) 88 | return self.get_key(service_name, self.clusters) 89 | 90 | def get_grouped_service_name(self): 91 | service_name = self.name 92 | if self.groupName and Constants.SERVICE_INFO_SPLITER not in service_name: 93 | service_name = self.groupName + Constants.SERVICE_INFO_SPLITER + service_name 94 | return service_name 95 | 96 | @staticmethod 97 | def from_key(key: str): 98 | info = key.split(Constants.SERVICE_INFO_SPLITER) 99 | if len(info) > 2: 100 | service = Service(name=info[1], groupName=info[0], clusters=info[2]) 101 | else: 102 | service = Service(name=info[1], groupName=info[0], clusters="") 103 | return service 104 | 105 | def get_hosts_str(self): 106 | hosts_str = "" 107 | for host in self.hosts: 108 | hosts_str += host.json() + ";" 109 | return hosts_str 110 | 111 | class Config: 112 | arbitrary_types_allowed = True 113 | 114 | 115 | class ServiceList(BaseModel): 116 | count: int 117 | services: List[str] 118 | -------------------------------------------------------------------------------- /v2/nacos/transport/grpc_util.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from google.protobuf.any_pb2 import Any 4 | 5 | from v2.nacos.common.nacos_exception import NacosException, SERVER_ERROR 6 | from v2.nacos.config.model.config_request import ConfigChangeNotifyRequest 7 | from v2.nacos.config.model.config_response import ConfigPublishResponse, ConfigQueryResponse, \ 8 | ConfigChangeBatchListenResponse, ConfigRemoveResponse 9 | from v2.nacos.naming.model.naming_request import NotifySubscriberRequest 10 | from v2.nacos.naming.model.naming_response import InstanceResponse, \ 11 | SubscribeServiceResponse, BatchInstanceResponse, \ 12 | ServiceListResponse, QueryServiceResponse 13 | from v2.nacos.transport.grpcauto.nacos_grpc_service_pb2 import Payload, Metadata 14 | from v2.nacos.transport.model import ServerCheckResponse 15 | from v2.nacos.transport.model.internal_request import ClientDetectionRequest 16 | from v2.nacos.transport.model.internal_response import ErrorResponse, HealthCheckResponse 17 | from v2.nacos.transport.model.rpc_request import Request 18 | from v2.nacos.transport.model.rpc_response import Response 19 | from v2.nacos.utils.net_util import NetUtils 20 | 21 | 22 | class GrpcUtils: 23 | SERVICE_INFO_KEY = "serviceInfo" 24 | 25 | remote_type = { 26 | "QueryServiceResponse": QueryServiceResponse, 27 | "ServerCheckResponse": ServerCheckResponse, 28 | "NotifySubscriberRequest": NotifySubscriberRequest, 29 | "ErrorResponse": ErrorResponse, 30 | "InstanceResponse": InstanceResponse, 31 | "ServiceListResponse": ServiceListResponse, 32 | "BatchInstanceResponse": BatchInstanceResponse, 33 | "ClientDetectionRequest": ClientDetectionRequest, 34 | "HealthCheckResponse": HealthCheckResponse, 35 | "SubscribeServiceResponse": SubscribeServiceResponse, 36 | "ConfigPublishResponse": ConfigPublishResponse, 37 | "ConfigQueryResponse": ConfigQueryResponse, 38 | "ConfigChangeNotifyRequest": ConfigChangeNotifyRequest, 39 | "ConfigChangeBatchListenResponse": ConfigChangeBatchListenResponse, 40 | "ConfigRemoveResponse": ConfigRemoveResponse 41 | } 42 | 43 | @staticmethod 44 | def convert_request_to_payload(request: Request): 45 | payload_metadata = Metadata(type=request.get_request_type(), clientIp=NetUtils.get_local_ip(), 46 | headers=request.get_headers()) 47 | 48 | payload_body_bytes = json.dumps(request, default=GrpcUtils.to_json).encode('utf-8') 49 | payload_body = Any(value=payload_body_bytes) 50 | payload = Payload(metadata=payload_metadata, body=payload_body) 51 | return payload 52 | 53 | @staticmethod 54 | def convert_response_to_payload(response: Response): 55 | metadata = Metadata(type=response.get_response_type(), clientIp=NetUtils.get_local_ip()) 56 | 57 | payload_body_bytes = json.dumps(response, default=GrpcUtils.to_json).encode('utf-8') 58 | payload_body = Any(value=payload_body_bytes) 59 | payload = Payload(metadata=metadata, body=payload_body) 60 | return payload 61 | 62 | @staticmethod 63 | def parse(payload: Payload): 64 | metadata_type = payload.metadata.type 65 | if metadata_type and metadata_type in GrpcUtils.remote_type.keys(): 66 | json_dict = json.loads(payload.body.value.decode('utf-8')) 67 | response_class = GrpcUtils.remote_type[metadata_type] 68 | obj = response_class.model_validate(json_dict) 69 | 70 | if isinstance(obj, Request): 71 | obj.put_all_headers(payload.metadata.headers) 72 | return obj 73 | else: 74 | raise NacosException(SERVER_ERROR, "unknown payload type:" + payload.metadata.type) 75 | 76 | @staticmethod 77 | def to_json(obj): 78 | d = {} 79 | d.update(obj.__dict__) 80 | return d 81 | 82 | 83 | def parse_payload_to_response(payload): 84 | body = payload.body 85 | response = Response(**body) 86 | return response 87 | -------------------------------------------------------------------------------- /v2/nacos/config/model/config.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Optional, Callable, List 3 | 4 | from pydantic import BaseModel 5 | 6 | from v2.nacos.common.nacos_exception import NacosException, INVALID_PARAM 7 | from v2.nacos.config.filter.config_filter import ConfigFilterChainManager 8 | from v2.nacos.config.model.config_param import ConfigParam, UsageType 9 | 10 | 11 | class ConfigItem(BaseModel): 12 | id: str = '' 13 | dataId: str = '' 14 | group: str = '' 15 | content: str = '' 16 | md5: Optional[str] = '' 17 | tenant: str = '' 18 | appname: str = '' 19 | 20 | 21 | class ConfigPage(BaseModel): 22 | totalCount: int = 0 23 | pageNumber: int = 0 24 | pagesAvailable: int = 0 25 | pageItems: List[ConfigItem] = [] 26 | 27 | 28 | class ConfigListenContext(BaseModel): 29 | group: str = '' 30 | md5: str = '' 31 | dataId: str = '' 32 | tenant: str = '' 33 | 34 | 35 | class ConfigContext(BaseModel): 36 | group: str = '' 37 | dataId: str = '' 38 | tenant: str = '' 39 | 40 | 41 | class SubscribeCacheData: 42 | def __init__(self, data_id: str, group: str, tenant: str, content: str, md5: str, 43 | encrypted_data_key: str, 44 | chain_manager: ConfigFilterChainManager, content_type: str = '', 45 | is_sync_with_server: bool = False): 46 | self.data_id = data_id 47 | self.group = group 48 | self.tenant = tenant 49 | self.content = content 50 | self.content_type = content_type 51 | self.md5 = md5 52 | self.cache_data_listeners: List[CacheDataListenerWrap] = [] 53 | self.encrypted_data_key = encrypted_data_key 54 | self.task_id = 0 55 | self.config_chain_manager = chain_manager 56 | self.is_sync_with_server = is_sync_with_server 57 | self.lock = asyncio.Lock() 58 | 59 | async def add_listener(self, listener: Optional[Callable]): 60 | if listener is None: 61 | raise NacosException(INVALID_PARAM, "cache data listener is None") 62 | async with self.lock: 63 | if any(CacheDataListenerWrap(listener, self.md5) == existing_listener for existing_listener in 64 | self.cache_data_listeners): 65 | return 66 | 67 | self.cache_data_listeners.append(CacheDataListenerWrap(listener, self.md5)) 68 | 69 | async def remove_listener(self, listener: Optional[Callable]): 70 | if listener is None: 71 | return 72 | async with self.lock: 73 | self.cache_data_listeners = [existing_listener for existing_listener in self.cache_data_listeners 74 | if existing_listener.listener != listener] 75 | 76 | async def execute_listener(self): 77 | async with self.lock: 78 | for listener_wrap in self.cache_data_listeners: 79 | if listener_wrap.last_md5 != self.md5: 80 | listener_wrap.last_md5 = self.md5 81 | param = ConfigParam(data_id=self.data_id, 82 | group=self.group, 83 | content=self.content, 84 | encrypted_data_key=self.encrypted_data_key, 85 | usage_type=UsageType.response_type.value 86 | ) 87 | self.config_chain_manager.do_filters(param) 88 | decrypted_content = param.content 89 | await listener_wrap.listener(self.tenant, self.group, self.data_id, decrypted_content) 90 | 91 | 92 | class CacheDataListenerWrap: 93 | def __init__(self, listener: Callable, last_md5): 94 | self.listener = listener 95 | self.last_md5 = last_md5 96 | 97 | def __eq__(self, other): 98 | if not isinstance(other, CacheDataListenerWrap): 99 | return False 100 | return self.listener == other.listener and self.last_md5 == other.last_md5 101 | 102 | def __hash__(self): 103 | return hash((self.listener, self.last_md5)) 104 | -------------------------------------------------------------------------------- /v2/nacos/transport/rpc_client_factory.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | from typing import Dict 4 | 5 | from v2.nacos.common.client_config import ClientConfig 6 | from v2.nacos.common.nacos_exception import NacosException, CLIENT_INVALID_PARAM, INVALID_PARAM 7 | from v2.nacos.transport.grpc_client import GrpcClient 8 | from v2.nacos.transport.nacos_server_connector import NacosServerConnector 9 | from v2.nacos.transport.rpc_client import RpcClient, ConnectionType 10 | 11 | 12 | class RpcClientFactory: 13 | 14 | def __init__(self, logger): 15 | self.client_map = {} 16 | self.logger = logger 17 | self.lock = asyncio.Lock() 18 | 19 | def get_all_client_entries(self) -> Dict[str, RpcClient]: 20 | return self.client_map 21 | 22 | def get_client(self, client_name: str) -> RpcClient: 23 | return self.client_map[client_name] 24 | 25 | async def create_client(self, client_name: str, connection_type: ConnectionType, labels: Dict[str, str], 26 | client_config: ClientConfig, nacos_server: NacosServerConnector) -> RpcClient: 27 | async with self.lock: 28 | client = None 29 | if client_name not in self.client_map.keys(): 30 | self.logger.info("create new rpc client: " + client_name) 31 | if connection_type == ConnectionType.GRPC: 32 | client = GrpcClient(self.logger, client_name, client_config, nacos_server) 33 | 34 | if not client: 35 | raise NacosException(CLIENT_INVALID_PARAM, "unsupported connection type: " + str(connection_type)) 36 | 37 | self.logger.info(f"init app conn labels from client config,{client_config.app_conn_labels}") 38 | app_conn_labels_env = get_app_labels_from_env() 39 | self.logger.info(f"init app conn labels from env,{app_conn_labels_env}") 40 | app_conn_labels = merge_app_labels(client_config.app_conn_labels, app_conn_labels_env) 41 | self.logger.info("final app conn labels: " + str(app_conn_labels)) 42 | app_conn_labels = add_prefix_for_each_key(app_conn_labels, "app_") 43 | if len(app_conn_labels) > 0: 44 | client.put_all_labels(app_conn_labels) 45 | 46 | client.put_all_labels(labels) 47 | self.client_map[client_name] = client 48 | return client 49 | return self.client_map[client_name] 50 | 51 | async def shutdown_all_clients(self): 52 | for client in self.client_map.values(): 53 | await client.shutdown() 54 | 55 | 56 | def get_app_labels_from_env() -> dict: 57 | config_map = {} 58 | # nacos_config_gray_label 59 | gray_label = os.getenv("nacos_config_gray_label") 60 | if gray_label: 61 | config_map["nacos_config_gray_label"] = gray_label 62 | 63 | # nacos_app_conn_labels 64 | conn_labels = os.getenv("nacos_app_conn_labels") 65 | if conn_labels: 66 | labels_map = parse_labels(conn_labels) 67 | config_map.update(labels_map) 68 | 69 | return config_map 70 | 71 | 72 | def parse_labels(raw_labels: str) -> dict: 73 | if not raw_labels.strip(): 74 | return {} 75 | 76 | result_map = {} 77 | labels = raw_labels.split(",") 78 | for label in labels: 79 | if label.strip(): 80 | kv = label.split("=") 81 | if len(kv) == 2: 82 | key = kv[0].strip() 83 | value = kv[1].strip() 84 | result_map[key] = value 85 | else: 86 | raise NacosException(INVALID_PARAM, f"unknown label format: {label}") 87 | return result_map 88 | 89 | 90 | def merge_app_labels(app_labels_appointed: dict, app_labels_env: dict) -> dict: 91 | preferred = os.getenv("nacos_app_conn_labels_preferred", "").lower() 92 | prefer_first = preferred != "env" 93 | return merge_maps(app_labels_appointed, app_labels_env, prefer_first) 94 | 95 | 96 | def merge_maps(map1: dict, map2: dict, prefer_first: bool) -> dict: 97 | result = {} # Start with map1 98 | if map1: 99 | result.update(map1) 100 | 101 | for k, v in map2.items(): 102 | if not (prefer_first and k in result): 103 | result[k] = v 104 | 105 | return result 106 | 107 | 108 | def add_prefix_for_each_key(m: dict, prefix: str) -> dict: 109 | if not m: 110 | return m 111 | 112 | new_map = {} 113 | for k, v in m.items(): 114 | if k.strip(): 115 | new_key = prefix + k 116 | new_map[new_key] = v 117 | return new_map 118 | -------------------------------------------------------------------------------- /v2/nacos/common/client_config_builder.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | from v2.nacos.common.auth import CredentialsProvider, StaticCredentialsProvider 4 | from v2.nacos.common.client_config import ClientConfig, GRPCConfig 5 | from v2.nacos.common.client_config import KMSConfig 6 | from v2.nacos.common.client_config import TLSConfig 7 | from v2.nacos.common.constants import Constants 8 | 9 | 10 | class ClientConfigBuilder: 11 | def __init__(self): 12 | self._config = ClientConfig() 13 | 14 | def server_address(self, server_address: str) -> "ClientConfigBuilder": 15 | if server_address is not None and server_address.strip() != "": 16 | for server_address in server_address.strip().split(','): 17 | self._config.server_list.append(server_address.strip()) 18 | return self 19 | 20 | def endpoint(self, endpoint) -> "ClientConfigBuilder": 21 | self._config.endpoint = endpoint 22 | return self 23 | 24 | def namespace_id(self, namespace_id: str) -> "ClientConfigBuilder": 25 | if namespace_id is None: 26 | namespace_id = Constants.DEFAULT_NAMESPACE_ID 27 | self._config.namespace_id = namespace_id 28 | return self 29 | 30 | def timeout_ms(self, timeout_ms) -> "ClientConfigBuilder": 31 | self._config.timeout_ms = timeout_ms 32 | return self 33 | 34 | def heart_beat_interval(self, heart_beat_interval) -> "ClientConfigBuilder": 35 | self._config.heart_beat_interval = heart_beat_interval 36 | return self 37 | 38 | def log_level(self, log_level) -> "ClientConfigBuilder": 39 | self._config.log_level = log_level 40 | return self 41 | 42 | def log_dir(self, log_dir: str) -> "ClientConfigBuilder": 43 | self._config.log_dir = log_dir 44 | return self 45 | 46 | def access_key(self, access_key: str) -> "ClientConfigBuilder": 47 | if not self._config.credentials_provider: 48 | self._config.credentials_provider = StaticCredentialsProvider(access_key_id=access_key) 49 | else: 50 | self._config.credentials_provider.set_access_key_id(access_key) 51 | return self 52 | 53 | def secret_key(self, secret_key: str) -> "ClientConfigBuilder": 54 | if not self._config.credentials_provider: 55 | self._config.credentials_provider = StaticCredentialsProvider(access_key_secret=secret_key) 56 | else: 57 | self._config.credentials_provider.set_access_key_secret(secret_key) 58 | return self 59 | 60 | def credentials_provider(self, credentials_provider: CredentialsProvider) -> "ClientConfigBuilder": 61 | self._config.credentials_provider = credentials_provider 62 | return self 63 | 64 | def username(self, username: str) -> "ClientConfigBuilder": 65 | self._config.username = username 66 | return self 67 | 68 | def password(self, password: str) -> "ClientConfigBuilder": 69 | self._config.password = password 70 | return self 71 | 72 | def cache_dir(self, cache_dir: str) -> "ClientConfigBuilder": 73 | self._config.cache_dir = cache_dir 74 | return self 75 | 76 | def tls_config(self, tls_config: TLSConfig) -> "ClientConfigBuilder": 77 | self._config.tls_config = tls_config 78 | return self 79 | 80 | def kms_config(self, kms_config: KMSConfig) -> "ClientConfigBuilder": 81 | self._config.kms_config = kms_config 82 | return self 83 | 84 | def grpc_config(self, grpc_config: GRPCConfig) -> "ClientConfigBuilder": 85 | self._config.grpc_config = grpc_config 86 | return self 87 | 88 | def load_cache_at_start(self, load_cache_at_start: bool) -> "ClientConfigBuilder": 89 | self._config.load_cache_at_start = load_cache_at_start 90 | return self 91 | 92 | def app_conn_labels(self, app_conn_labels: dict) -> "ClientConfigBuilder": 93 | if self._config.app_conn_labels is None: 94 | self._config.app_conn_labels = {} 95 | self._config.app_conn_labels.update(app_conn_labels) 96 | return self 97 | 98 | def endpoint_query_header(self, endpoint_query_header: Dict[str, str]) -> "ClientConfigBuilder": 99 | if self._config.endpoint_query_header is None: 100 | self._config.endpoint_query_header = {} 101 | self._config.endpoint_query_header.update(endpoint_query_header) 102 | return self 103 | 104 | def async_update_service(self, async_update_service: bool) -> "ClientConfigBuilder": 105 | self._config.set_async_update_service(async_update_service) 106 | return self 107 | 108 | def update_thread_num(self, update_thread_num: int) -> "ClientConfigBuilder": 109 | self._config.update_thread_num = update_thread_num 110 | return self 111 | 112 | def build(self): 113 | return self._config 114 | -------------------------------------------------------------------------------- /v2/nacos/transport/nacos_server_connector.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from random import randrange 3 | from typing import List, Optional 4 | 5 | from v2.nacos.common.client_config import ClientConfig 6 | from v2.nacos.common.constants import Constants 7 | from v2.nacos.common.nacos_exception import NacosException, INVALID_PARAM, INVALID_SERVER_STATUS 8 | from v2.nacos.transport.auth_client import AuthClient 9 | from v2.nacos.transport.http_agent import HttpAgent 10 | 11 | 12 | class NacosServerConnector: 13 | def __init__(self, logger, client_config: ClientConfig, http_agent: HttpAgent): 14 | self.logger = logger 15 | 16 | if len(client_config.server_list) == 0 and not client_config.endpoint: 17 | raise NacosException(INVALID_PARAM, "both server list and endpoint are empty") 18 | 19 | self.client_config = client_config 20 | self.server_list = client_config.server_list 21 | self.current_index = 0 22 | self.http_agent = http_agent 23 | self.endpoint = client_config.endpoint 24 | self.server_list_lock = asyncio.Lock() 25 | 26 | self.refresh_server_list_internal = 30 # second 27 | if len(self.server_list) != 0: 28 | self.current_index = randrange(0, len(self.server_list)) 29 | 30 | if client_config.username and client_config.password: 31 | self.auth_client = AuthClient(self.logger, client_config, self.get_server_list, http_agent) 32 | asyncio.create_task(self.auth_client.get_access_token(True)) 33 | 34 | async def init(self): 35 | if len(self.server_list) != 0: 36 | return 37 | 38 | await self._get_server_list_from_endpoint() 39 | if len(self.server_list) == 0: 40 | raise NacosException(INVALID_SERVER_STATUS, "server list is empty") 41 | asyncio.create_task(self._refresh_server_srv_if_need()) 42 | 43 | async def _get_server_list_from_endpoint(self) -> Optional[List[str]]: 44 | if not self.endpoint or self.endpoint.strip() == "": 45 | return None 46 | 47 | url = self.endpoint.strip() + self.client_config.endpoint_context_path + "/serverlist" 48 | server_list = [] 49 | try: 50 | response, err = await self.http_agent.request(url, "GET", self.client_config.endpoint_query_header, None, 51 | None) 52 | if err: 53 | self.logger.error("[get-server-list] get server list from endpoint failed,url:%s, err:%s", url, err) 54 | return None 55 | else: 56 | self.logger.debug("[get-server-list] content from endpoint,url:%s,response:%s", url, response) 57 | if response: 58 | for server_info in response.decode('utf-8').strip().split("\n"): 59 | sp = server_info.strip().split(":") 60 | if len(sp) == 1: 61 | server_list.append((sp[0] + ":" + str(Constants.DEFAULT_PORT))) 62 | else: 63 | server_list.append(server_info) 64 | 65 | if len(server_list) != 0 and set(server_list) != set(self.server_list): 66 | async with self.server_list_lock: 67 | old_server_list = self.server_list 68 | self.server_list = server_list 69 | self.current_index = randrange(0, len(self.server_list)) 70 | self.logger.info("nacos server list is updated from %s to %s", 71 | str(old_server_list), str(server_list)) 72 | except Exception as e: 73 | self.logger.error("[get-server-list] get server list from endpoint failed,url:%s, err:%s", url, e) 74 | return server_list 75 | 76 | async def _refresh_server_srv_if_need(self): 77 | while True: 78 | await asyncio.sleep(self.refresh_server_list_internal) 79 | 80 | server_list = await self._get_server_list_from_endpoint() 81 | 82 | if not server_list or len(server_list) == 0: 83 | self.logger.warning("failed to get server list from endpoint, endpoint: " + self.endpoint) 84 | 85 | def get_server_list(self): 86 | return self.server_list 87 | 88 | def get_next_server(self): 89 | if not self.server_list: 90 | raise NacosException(INVALID_SERVER_STATUS, 'server list is empty') 91 | self.current_index = (self.current_index + 1) % len(self.server_list) 92 | return self.server_list[self.current_index] 93 | 94 | async def inject_security_info(self, headers): 95 | if self.client_config.username and self.client_config.password: 96 | access_token = await self.auth_client.get_access_token(False) 97 | if access_token is not None and access_token != "": 98 | headers[Constants.ACCESS_TOKEN] = access_token 99 | return 100 | -------------------------------------------------------------------------------- /v2/nacos/config/cache/config_subscribe_manager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from logging import Logger 3 | from typing import Optional, Callable, List, Dict 4 | 5 | from v2.nacos.common.constants import Constants 6 | from v2.nacos.config.cache.config_info_cache import ConfigInfoCache 7 | from v2.nacos.config.filter.config_filter import ConfigFilterChainManager 8 | from v2.nacos.config.model.config import SubscribeCacheData 9 | from v2.nacos.config.util.config_client_util import get_config_cache_key 10 | from v2.nacos.utils import md5_util 11 | from v2.nacos.utils.md5_util import md5 12 | 13 | 14 | class ConfigSubscribeManager: 15 | def __init__(self, logger: Logger, config_info_cache: ConfigInfoCache, namespace_id: str, 16 | config_filter_chain_manager: ConfigFilterChainManager, 17 | execute_config_listen_channel: asyncio.Queue): 18 | self.subscribe_cache_map: Dict[str, SubscribeCacheData] = {} 19 | self.logger = logger 20 | self.lock = asyncio.Lock() 21 | self.namespace_id = namespace_id 22 | self.config_filter_chain_manager = config_filter_chain_manager 23 | self.config_info_cache = config_info_cache 24 | self.execute_config_listen_channel = execute_config_listen_channel 25 | 26 | async def add_listener(self, data_id: str, group_name: str, tenant: str, 27 | listener: Optional[Callable]): 28 | cache_key = get_config_cache_key(data_id, group_name, tenant) 29 | async with self.lock: 30 | if cache_key in self.subscribe_cache_map: 31 | subscribe_cache = self.subscribe_cache_map[cache_key] 32 | else: 33 | content, encrypted_data_key = await self.config_info_cache.get_config_cache(data_id, group_name) 34 | md5_str = md5_util.md5(content) 35 | subscribe_cache = SubscribeCacheData( 36 | data_id=data_id, 37 | group=group_name, 38 | tenant=self.namespace_id, 39 | content=content, 40 | md5=md5_str, 41 | chain_manager=self.config_filter_chain_manager, 42 | encrypted_data_key=encrypted_data_key) 43 | subscribe_cache.task_id = len(self.subscribe_cache_map) // Constants.PER_TASK_CONFIG_SIZE 44 | self.subscribe_cache_map[cache_key] = subscribe_cache 45 | await subscribe_cache.add_listener(listener) 46 | 47 | async def remove_listener(self, data_id: str, group_name: str, tenant: str, listener: Optional[Callable]): 48 | if listener is None: 49 | return 50 | cache_key = get_config_cache_key(data_id, group_name, tenant) 51 | async with self.lock: 52 | subscribe_cache = self.subscribe_cache_map.get(cache_key) 53 | if not subscribe_cache: 54 | return 55 | 56 | await subscribe_cache.remove_listener(listener) 57 | 58 | async def notify_config_changed(self, data_id: str, group_name: str, tenant: str): 59 | cache_key = get_config_cache_key(data_id, group_name, tenant) 60 | async with self.lock: 61 | subscribe_cache = self.subscribe_cache_map.get(cache_key) 62 | if not subscribe_cache: 63 | return 64 | async with subscribe_cache.lock: 65 | subscribe_cache.is_sync_with_server = False 66 | self.subscribe_cache_map[cache_key] = subscribe_cache 67 | 68 | await self.execute_config_listen_channel.put(None) 69 | 70 | async def batch_set_config_changed(self, task_id: int): 71 | for cache_data in self.subscribe_cache_map.values(): 72 | if cache_data.task_id == task_id: 73 | async with cache_data.lock: 74 | cache_data.is_sync_with_server = False 75 | 76 | async def update_subscribe_cache(self, data_id: str, group_name: str, tenant: str, content: str, 77 | encrypted_data_key: str): 78 | cache_key = get_config_cache_key(data_id, group_name, tenant) 79 | async with self.lock: 80 | subscribe_cache = self.subscribe_cache_map.get(cache_key) 81 | if not subscribe_cache: 82 | return 83 | subscribe_cache.content = content 84 | subscribe_cache.encrypted_data_key = encrypted_data_key 85 | subscribe_cache.md5 = md5(content) 86 | subscribe_cache.is_sync_with_server = True 87 | 88 | await subscribe_cache.execute_listener() 89 | 90 | async def execute_listener_and_build_tasks(self, is_sync_all: bool): 91 | listen_fetch_task_map: Dict[int, List[SubscribeCacheData]] = {} 92 | for cache_data in self.subscribe_cache_map.values(): 93 | if cache_data.is_sync_with_server: 94 | await cache_data.execute_listener() 95 | if not is_sync_all: 96 | continue 97 | if cache_data.task_id not in listen_fetch_task_map: 98 | listen_fetch_task_map[cache_data.task_id] = [] 99 | listen_fetch_task_map[cache_data.task_id].append(cache_data) 100 | return listen_fetch_task_map 101 | -------------------------------------------------------------------------------- /nacos/timer.py: -------------------------------------------------------------------------------- 1 | # -*- coding=utf-8 -*- 2 | 3 | """ 4 | Verion: 1.0 5 | Since : 2.7.18 6 | Author: zhangjian 7 | Site: https://github.com/xarrow/ 8 | File: timer.py 9 | Time: 2020/8/30 10 | 11 | Add New Functional nacos-sdk-python 12 | """ 13 | 14 | import threading 15 | 16 | 17 | class NacosTimer(object): 18 | __slots__ = ['_name', '_timer', '_fn', '_interval', '_ignore_ex', '_on_result', '_on_exception', 19 | '_args', '_kwargs'] 20 | 21 | def __init__(self, 22 | name, 23 | fn, 24 | interval=7, 25 | *args, 26 | **kwargs): 27 | """ 28 | NacosTimer 29 | :param name: timer name 30 | :param fn: function which scheduler 31 | :param interval: scheduler interval, default 7s 32 | :param args: args in function 33 | :param kwargs: kwargs in function 34 | """ 35 | # 36 | self._name = name 37 | # Thread.Timer 38 | self._timer = None 39 | # function which callable 40 | self._fn = fn 41 | # timer interval default 7s 42 | self._interval = interval 43 | # whether ignore invoke exception 44 | self._ignore_ex = False 45 | self._on_result = None 46 | self._on_exception = None 47 | # function args 48 | self._args = args 49 | # function kwargs 50 | self._kwargs = kwargs 51 | 52 | @property 53 | def name(self): 54 | return self._name 55 | 56 | def set_name(self, name): 57 | self._name = name 58 | return self 59 | 60 | @property 61 | def fn(self): 62 | return self._fn 63 | 64 | def set_fn(self, fn): 65 | self._fn = fn 66 | return self 67 | 68 | @property 69 | def interval(self, ): 70 | return self._interval 71 | 72 | def set_interval(self, interval): 73 | self._interval = interval 74 | return self 75 | 76 | @property 77 | def ignore_ex(self): 78 | return self._ignore_ex 79 | 80 | def set_ignore_ex(self, ignore_ex): 81 | self._ignore_ex = ignore_ex 82 | return self 83 | 84 | @property 85 | def on_result(self): 86 | return self._on_result 87 | 88 | def set_on_result(self, fn): 89 | self._on_result = fn 90 | return self 91 | 92 | @property 93 | def on_exception(self): 94 | return self._on_exception 95 | 96 | def set_on_exception(self, fn): 97 | self._on_exception = fn 98 | return self 99 | 100 | def alive(self): 101 | if self._timer is None: 102 | return False 103 | return self._timer.is_alive() 104 | 105 | def scheduler(self): 106 | try: 107 | res = self._fn(*self._args, **self._kwargs) 108 | if self._on_result: 109 | self._on_result(res) 110 | except Exception as ex: 111 | if self._on_exception: 112 | self._on_exception(ex) 113 | if not self._ignore_ex: 114 | # stop timer 115 | raise ex 116 | self._timer = threading.Timer(self._interval, self.scheduler, ) 117 | self._timer.start() 118 | 119 | def cancel(self): 120 | if self._timer: 121 | self._timer.cancel() 122 | 123 | 124 | class NacosTimerManager(object): 125 | def __init__(self, ): 126 | self._timers_container = {} 127 | self._executed = False 128 | 129 | def all_timers(self): 130 | return self._timers_container 131 | 132 | def add_timer(self, timer): 133 | self._timers_container[timer.name] = timer 134 | return self 135 | 136 | def execute(self): 137 | """ 138 | scheduler all timer in manager 139 | :return: None 140 | """ 141 | if self._executed: 142 | return 143 | for name, timer in self._timers_container.items(): 144 | if timer.alive(): 145 | continue 146 | timer.scheduler() 147 | self._executed = True 148 | 149 | def cancel_timer(self, timer_name=None, ): 150 | """ 151 | cancel timer , and nacos timer still in container 152 | it can execute again. 153 | :param timer_name: 154 | :return: None 155 | """ 156 | timer = self._timers_container.get(timer_name) 157 | if timer: 158 | timer.cancel() 159 | 160 | def cancel(self): 161 | """ 162 | cancel all timer in container 163 | :return: None 164 | """ 165 | for _, timer in self._timers_container.items(): 166 | timer.cancel() 167 | 168 | def stop_timer(self, timer_name): 169 | """ 170 | cancel nacos timer and remove it from timer container 171 | :param timer_name: 172 | :return: None 173 | """ 174 | self.cancel_timer(timer_name) 175 | self._timers_container.pop(timer_name) 176 | 177 | def stop(self): 178 | """ 179 | remove all timer, and it can not execute again 180 | """ 181 | self.cancel() 182 | self._timers_container.clear() 183 | -------------------------------------------------------------------------------- /v2/nacos/common/constants.py: -------------------------------------------------------------------------------- 1 | class Constants: 2 | NAMING_MODULE = "naming" 3 | 4 | CONFIG_MODULE = "config" 5 | 6 | LABEL_SOURCE = "source" 7 | 8 | LABEL_SOURCE_SDK = "sdk" 9 | 10 | LABEL_SOURCE_CLUSTER = "cluster" 11 | 12 | LABEL_MODULE = "module" 13 | 14 | CLIENT_VERSION = "Nacos-Python-Client:v2.0.0" 15 | 16 | DATA_IN_BODY_VERSION = 204 17 | 18 | DEFAULT_GROUP = "DEFAULT_GROUP" 19 | 20 | WEB_CONTEXT = "/nacos" 21 | 22 | APPNAME = "AppName" 23 | 24 | UNKNOWN_APP = "UnknownApp" 25 | 26 | DEFAULT_DOMAINNAME = "commonconfig.config-host.taobao.com" 27 | 28 | DAILY_DOMAINNAME = "commonconfig.taobao.net" 29 | 30 | NULL = "" 31 | 32 | DATAID = "dataId" 33 | 34 | GROUP = "group" 35 | 36 | DEFAULT_HEARTBEAT_INTERVAL = 5 37 | 38 | LAST_MODIFIED = "Last-Modified" 39 | 40 | ACCEPT_ENCODING = "Accept-Encoding" 41 | 42 | CONTENT_ENCODING = "Content-Encoding" 43 | 44 | PROBE_MODIFY_REQUEST = "Listening-Configs" 45 | 46 | PROBE_MODIFY_RESPONSE = "Probe-Modify-Response" 47 | 48 | PROBE_MODIFY_RESPONSE_NEW = "Probe-Modify-Response-New" 49 | 50 | USE_ZIP = "true" 51 | 52 | CONTENT_MD5 = "Content-MD5" 53 | 54 | CONFIG_VERSION = "Config-Version" 55 | 56 | CONFIG_TYPE = "Config-Type" 57 | 58 | ENCRYPTED_DATA_KEY = "Encrypted-Data-Key" 59 | 60 | IF_MODIFIED_SINCE = "If-Modified-Since" 61 | 62 | SPACING_INTERVAL = "client-spacing-interval" 63 | 64 | BASE_PATH = "/v1/cs" 65 | 66 | SERVICE_BASE_PATH = "/v1/ns" 67 | 68 | CONFIG_CONTROLLER_PATH = BASE_PATH + "/configs" 69 | 70 | TOKEN = "token" 71 | 72 | ACCESS_TOKEN = "accessToken" 73 | 74 | TOKEN_TTL = "tokenTtl" 75 | 76 | GLOBAL_ADMIN = "globalAdmin" 77 | 78 | USERNAME = "username" 79 | 80 | TOKEN_REFRESH_WINDOW = "tokenRefreshWindow" 81 | 82 | # second. 83 | ASYNC_UPDATE_ADDRESS_INTERVAL = 300 84 | 85 | # second. 86 | POLLING_INTERVAL_TIME = 15 87 | 88 | # millisecond. 89 | ONCE_TIMEOUT = 2000 90 | 91 | # millisecond. 92 | SO_TIMEOUT = 60000 93 | 94 | # millisecond. 95 | CONFIG_LONG_POLL_TIMEOUT = 30000 96 | 97 | # millisecond. 98 | MIN_CONFIG_LONG_POLL_TIMEOUT = 10000 99 | 100 | # millisecond. 101 | CONFIG_RETRY_TIME = 2000 102 | 103 | # Maximum number of retries. 104 | MAX_RETRY = 3 105 | 106 | # millisecond. 107 | RECV_WAIT_TIMEOUT = ONCE_TIMEOUT * 5 108 | 109 | ENCODE = "UTF-8" 110 | 111 | MAP_FILE = "map-file.js" 112 | 113 | FLOW_CONTROL_THRESHOLD = 20 114 | 115 | FLOW_CONTROL_SLOT = 10 116 | 117 | FLOW_CONTROL_INTERVAL = 1000 118 | 119 | DEFAULT_PROTECT_THRESHOLD = 0.0 120 | 121 | LINE_SEPARATOR = chr(1) 122 | 123 | WORD_SEPARATOR = chr(2) 124 | 125 | LONGPOLLING_LINE_SEPARATOR = "\r\n" 126 | 127 | CLIENT_APPNAME_HEADER = "Client-AppName" 128 | 129 | APP_NAME_HEADER = "AppName" 130 | 131 | CLIENT_REQUEST_TS_HEADER = "Client-RequestTS" 132 | 133 | CLIENT_REQUEST_TOKEN_HEADER = "Client-RequestToken" 134 | 135 | ATOMIC_MAX_SIZE = 1000 136 | 137 | NAMING_INSTANCE_ID_SPLITTER = "#" 138 | 139 | NAMING_INSTANCE_ID_SEG_COUNT = 4 140 | 141 | NAMING_HTTP_HEADER_SPLITTER = "\\|" 142 | 143 | DEFAULT_CLUSTER_NAME = "DEFAULT" 144 | 145 | DEFAULT_HEART_BEAT_TIMEOUT = 15000 146 | 147 | DEFAULT_IP_DELETE_TIMEOUT = 30000 148 | 149 | DEFAULT_HEART_BEAT_INTERVAL = 5000 150 | 151 | DEFAULT_NAMESPACE_ID = "" 152 | 153 | DEFAULT_USE_CLOUD_NAMESPACE_PARSING = True 154 | 155 | WRITE_REDIRECT_CODE = 307 156 | 157 | RESPONSE_SUCCESS_CODE = 200 158 | 159 | REQUEST_DOMAIN_RETRY_TIME = 3 160 | 161 | SERVICE_INFO_SPLITER = "@@" 162 | 163 | CONFIG_INFO_SPLITER = "@@" 164 | 165 | SERVICE_INFO_SPLIT_COUNT = 2 166 | 167 | NULL_STRING = "null" 168 | 169 | NUMBER_PATTERN = "^\\d+$" 170 | 171 | ANY_PATTERN = ".*" 172 | 173 | DEFAULT_INSTANCE_ID_GENERATOR = "simple" 174 | 175 | SNOWFLAKE_INSTANCE_ID_GENERATOR = "snowflake" 176 | 177 | HTTP_PREFIX = "http" 178 | 179 | ALL_PATTERN = "*" 180 | 181 | COLON = ":" 182 | 183 | LINE_BREAK = "\n" 184 | 185 | POUND = "#" 186 | 187 | VIPSERVER_TAG = "Vipserver-Tag" 188 | 189 | AMORY_TAG = "Amory-Tag" 190 | 191 | LOCATION_TAG = "Location-Tag" 192 | 193 | CHARSET_KEY = "charset" 194 | 195 | EX_CONFIG_INFO = "exConfigInfo" 196 | 197 | GROUP_NAME_KEY = "groupName" 198 | 199 | SERVICE_NAME_KEY = "serviceName" 200 | 201 | CIPHER_PRE_FIX = "cipher-" 202 | 203 | DEFAULT_PORT = 8848 204 | 205 | GRPC_MAX_RECEIVE_MESSAGE_LENGTH = 100 * 1024 * 1024 206 | 207 | GRPC_KEEPALIVE_TIME_MILLS = 60 * 1000 208 | 209 | GRPC_INITIAL_WINDOW_SIZE = 10 * 1024 * 1024 210 | 211 | GRPC_INITIAL_CONN_WINDOW_SIZE = 10 * 1024 * 1024 212 | 213 | KEEP_ALIVE_TIME_MILLS = 5000 214 | 215 | INTERNAL_TIME_MILLS = 3000 216 | 217 | DEFAULT_GRPC_TIMEOUT_MILLS = 3000 218 | 219 | DEFAULT_TIMEOUT_MILLS = 10000 220 | 221 | PER_TASK_CONFIG_SIZE = 3000 222 | 223 | MSE_KMS_V1_DEFAULT_KEY_ID = "alias/acs/mse" 224 | 225 | KMS_AES_128_ALGORITHM_NAME = "AES_128" 226 | 227 | KMS_AES_256_ALGORITHM_NAME = "AES_256" 228 | -------------------------------------------------------------------------------- /v2/nacos/config/nacos_config_service.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import copy 3 | import time 4 | from typing import Callable 5 | 6 | from v2.nacos.common.client_config import ClientConfig 7 | from v2.nacos.common.constants import Constants 8 | from v2.nacos.common.nacos_exception import NacosException, INVALID_PARAM 9 | from v2.nacos.config.cache.config_info_cache import ConfigInfoCache 10 | from v2.nacos.config.filter.config_encryption_filter import ConfigEncryptionFilter 11 | from v2.nacos.config.filter.config_filter import ConfigFilterChainManager 12 | from v2.nacos.config.model.config_param import UsageType, ConfigParam 13 | from v2.nacos.config.remote.config_grpc_client_proxy import ConfigGRPCClientProxy 14 | from v2.nacos.nacos_client import NacosClient 15 | 16 | 17 | class NacosConfigService(NacosClient): 18 | def __init__(self, client_config: ClientConfig): 19 | super().__init__(client_config, Constants.CONFIG_MODULE) 20 | self.lock = asyncio.Lock() 21 | self.config_filter_chain_manager = ConfigFilterChainManager() 22 | self.namespace_id = client_config.namespace_id 23 | self.config_info_cache = ConfigInfoCache(client_config) 24 | self.last_all_sync_time = time.time() 25 | self.grpc_client_proxy = ConfigGRPCClientProxy(client_config, self.http_agent, self.config_info_cache, 26 | self.config_filter_chain_manager) 27 | 28 | if client_config.kms_config and client_config.kms_config.enabled: 29 | config_encryption_filter = ConfigEncryptionFilter(client_config) 30 | self.config_filter_chain_manager.add_filter(config_encryption_filter) 31 | self.logger.info("config encryption filter initialized") 32 | 33 | @staticmethod 34 | async def create_config_service(client_config: ClientConfig): 35 | config_service = NacosConfigService(client_config) 36 | await config_service.grpc_client_proxy.start() 37 | return config_service 38 | 39 | async def get_config(self, param: ConfigParam) -> str: 40 | if not param.data_id or not param.data_id.strip(): 41 | raise NacosException(INVALID_PARAM, "data_id can not be empty") 42 | 43 | if not param.group: 44 | param.group = Constants.DEFAULT_GROUP 45 | 46 | content, encrypted_data_key = await self.config_info_cache.get_fail_over_config_cache(param.data_id, 47 | param.group) 48 | if not content: 49 | try: 50 | content, encrypted_data_key = await self.grpc_client_proxy.query_config(param.data_id, param.group) 51 | except NacosException as e: 52 | if e.error_code == 400: 53 | if self.client_config.disable_use_config_cache: 54 | self.logger.warning( 55 | "failed to get config from server,and is not allowed to read local cache,error:%s", 56 | str(e)) 57 | raise e 58 | return await self.config_info_cache.get_config_cache(param.data_id, param.group) 59 | raise e 60 | 61 | deep_copy_param = copy.deepcopy(param) 62 | deep_copy_param.encrypted_data_key = encrypted_data_key 63 | deep_copy_param.content = content 64 | deep_copy_param.usage_type = UsageType.response_type.value 65 | self.config_filter_chain_manager.do_filters(deep_copy_param) 66 | return deep_copy_param.content 67 | 68 | async def publish_config(self, param: ConfigParam) -> bool: 69 | if not param.data_id or not param.data_id.strip(): 70 | raise NacosException(INVALID_PARAM, "data_id can not be empty") 71 | 72 | if not param.content or not param.content.strip(): 73 | raise NacosException(INVALID_PARAM, "config content can not be empty") 74 | 75 | if not param.group: 76 | param.group = Constants.DEFAULT_GROUP 77 | 78 | param.usage_type = UsageType.request_type.value 79 | 80 | self.config_filter_chain_manager.do_filters(param) 81 | 82 | return await self.grpc_client_proxy.publish_config(param) 83 | 84 | async def remove_config(self, param: ConfigParam): 85 | if not param.data_id or not param.data_id.strip(): 86 | raise NacosException(INVALID_PARAM, "data_id can not be empty") 87 | 88 | if not param.group: 89 | param.group = Constants.DEFAULT_GROUP 90 | 91 | return await self.grpc_client_proxy.remove_config(param.group, param.data_id) 92 | 93 | async def add_listener(self, data_id: str, group: str, listener: Callable) -> None: 94 | if not data_id or not data_id.strip(): 95 | raise NacosException(INVALID_PARAM, "data_id can not be empty") 96 | 97 | if not group: 98 | group = Constants.DEFAULT_GROUP 99 | 100 | if listener is None: 101 | raise NacosException(INVALID_PARAM, "config listener can not be null") 102 | 103 | return await self.grpc_client_proxy.add_listener(data_id, group, listener) 104 | 105 | async def remove_listener(self, data_id: str, group: str, listener: Callable): 106 | if not data_id or not data_id.strip(): 107 | raise NacosException(INVALID_PARAM, "data_id can not be empty") 108 | 109 | if not group: 110 | group = Constants.DEFAULT_GROUP 111 | 112 | if listener is None: 113 | raise NacosException(INVALID_PARAM, "config listener can not be null") 114 | 115 | return await self.grpc_client_proxy.remove_listener(data_id, group, listener) 116 | 117 | async def server_health(self) -> bool: 118 | return await self.grpc_client_proxy.server_health() 119 | 120 | async def shutdown(self): 121 | """关闭资源服务""" 122 | await self.grpc_client_proxy.close_client() 123 | -------------------------------------------------------------------------------- /v2/nacos/common/client_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from v2.nacos.common.constants import Constants 4 | from v2.nacos.common.nacos_exception import NacosException, INVALID_PARAM 5 | from v2.nacos.common.auth import StaticCredentialsProvider 6 | 7 | 8 | class KMSConfig: 9 | def __init__(self, enabled=False, endpoint='', access_key='', secret_key='', client_key_content='', password=''): 10 | self.enabled = enabled # 是否启用kms 11 | self.endpoint = endpoint # kms服务的地址 12 | self.client_key_content = client_key_content 13 | self.password = password 14 | self.access_key = access_key 15 | self.secret_key = secret_key 16 | 17 | 18 | class TLSConfig: 19 | def __init__(self, enabled=False, appointed=False, ca_file='', cert_file='', 20 | key_file='', server_name_override=''): 21 | self.enabled = enabled # 是否启用tls 22 | self.appointed = appointed # 指明是否使用预设的配置 23 | self.ca_file = ca_file # CA证书文件的路径 24 | self.cert_file = cert_file # 客户端证书文件的路径 25 | self.key_file = key_file # 私钥文件的路径 26 | self.server_name_override = server_name_override # 服务器名称覆盖(用于测试) 27 | 28 | def __str__(self): 29 | return str(self.__dict__) 30 | 31 | 32 | class GRPCConfig: 33 | def __init__(self, max_receive_message_length=Constants.GRPC_MAX_RECEIVE_MESSAGE_LENGTH, 34 | max_keep_alive_ms=Constants.GRPC_KEEPALIVE_TIME_MILLS, 35 | initial_window_size=Constants.GRPC_INITIAL_WINDOW_SIZE, 36 | initial_conn_window_size=Constants.GRPC_INITIAL_CONN_WINDOW_SIZE, 37 | grpc_timeout=Constants.DEFAULT_GRPC_TIMEOUT_MILLS): 38 | self.max_receive_message_length = max_receive_message_length 39 | self.max_keep_alive_ms = max_keep_alive_ms 40 | self.initial_window_size = initial_window_size 41 | self.initial_conn_window_size = initial_conn_window_size 42 | self.grpc_timeout = grpc_timeout 43 | 44 | 45 | class ClientConfig: 46 | def __init__(self, server_addresses=None, endpoint=None, namespace_id='', context_path='', access_key=None, 47 | secret_key=None, username=None, password=None, app_name='', app_key='', log_dir='', log_level=None, 48 | log_rotation_backup_count=None, app_conn_labels=None, credentials_provider=None): 49 | self.server_list = [] 50 | try: 51 | if server_addresses is not None and server_addresses.strip() != "": 52 | for server_address in server_addresses.strip().split(','): 53 | self.server_list.append(server_address.strip()) 54 | except Exception: 55 | raise NacosException(INVALID_PARAM, "server_addresses is invalid") 56 | 57 | self.endpoint = endpoint 58 | self.endpoint_context_path = Constants.WEB_CONTEXT 59 | self.endpoint_query_header = None 60 | self.namespace_id = namespace_id 61 | self.credentials_provider = credentials_provider if credentials_provider else StaticCredentialsProvider(access_key, secret_key) 62 | self.context_path = context_path 63 | self.username = username # the username for nacos auth 64 | self.password = password # the password for nacos auth 65 | self.app_name = app_name 66 | self.app_key = app_key 67 | self.cache_dir = '' 68 | self.disable_use_config_cache = False 69 | self.log_dir = log_dir 70 | self.log_level = logging.INFO if log_level is None else log_level # the log level for nacos client, default value is logging.INFO: log_level 71 | self.log_rotation_backup_count = 7 if log_rotation_backup_count is None else log_rotation_backup_count 72 | self.timeout_ms = 10 * 1000 # timeout for requesting Nacos server, default value is 10000ms 73 | self.heart_beat_interval = 5 * 1000 # the time interval for sending beat to server,default value is 5000ms 74 | self.kms_config = KMSConfig(enabled=False) 75 | self.tls_config = TLSConfig(enabled=False) 76 | self.grpc_config = GRPCConfig() 77 | self.load_cache_at_start = True 78 | self.update_cache_when_empty = False 79 | self.app_conn_labels = app_conn_labels 80 | self.async_update_service = False 81 | self.update_thread_num = 5 82 | 83 | def set_log_level(self, log_level): 84 | self.log_level = log_level 85 | return self 86 | 87 | def set_cache_dir(self, cache_dir): 88 | self.cache_dir = cache_dir 89 | return self 90 | 91 | def set_log_dir(self, log_dir): 92 | self.log_dir = log_dir 93 | return self 94 | 95 | def set_timeout_ms(self, timeout_ms): 96 | self.timeout_ms = timeout_ms 97 | return self 98 | 99 | def set_heart_beat_interval(self, heart_beat_interval): 100 | self.heart_beat_interval = heart_beat_interval 101 | return self 102 | 103 | def set_tls_config(self, tls_config: TLSConfig): 104 | self.tls_config = tls_config 105 | return self 106 | 107 | def set_kms_config(self, kms_config: KMSConfig): 108 | self.kms_config = kms_config 109 | return self 110 | 111 | def set_grpc_config(self, grpc_config: GRPCConfig): 112 | self.grpc_config = grpc_config 113 | return self 114 | 115 | def set_load_cache_at_start(self, load_cache_at_start): 116 | self.load_cache_at_start = load_cache_at_start 117 | return self 118 | 119 | def set_update_cache_when_empty(self, update_cache_when_empty: bool): 120 | self.update_cache_when_empty = update_cache_when_empty 121 | return self 122 | 123 | def set_endpoint_context_path(self, endpoint_context_path): 124 | self.endpoint_context_path = endpoint_context_path 125 | return self 126 | 127 | def set_app_conn_labels(self, app_conn_labels: dict): 128 | self.app_conn_labels = app_conn_labels 129 | return self 130 | 131 | def set_async_update_service(self, async_update_service: bool): 132 | self.async_update_service = async_update_service 133 | return self 134 | 135 | def set_update_thread_num(self, update_thread_num: int): 136 | self.update_thread_num = update_thread_num 137 | return self 138 | -------------------------------------------------------------------------------- /v2/nacos/redo/abstract_redo_service.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from abc import abstractmethod, ABC 4 | from typing import Dict, Set, Optional 5 | 6 | from v2.nacos.redo.async_rlock import AsyncRLock 7 | from v2.nacos.redo.redo_data import RedoData 8 | from v2.nacos.transport.connection_event_listener import ConnectionEventListener 9 | 10 | 11 | class AbstractRedoService(ConnectionEventListener,ABC): 12 | 13 | def __init__(self, module:str): 14 | self._module = module 15 | self._logger = logging.getLogger(module) 16 | self._connected : bool = False 17 | self._stop_event = asyncio.Event() 18 | self._execute_redo_channel = asyncio.Queue() 19 | self._listen_task = asyncio.create_task(self._execute_redo_task()) 20 | self._redo_data_map : Dict[str, Dict[str, RedoData]] = {} 21 | self._locks: Dict[str, AsyncRLock] = {} 22 | 23 | async def on_connected(self) -> None: 24 | self._connected = True 25 | 26 | async def on_disconnect(self) -> None: 27 | self._connected = False 28 | for key, redo_data_dict in self._redo_data_map.items(): 29 | async with self.get_lock_for_class(key): 30 | for redo_data in redo_data_dict.values(): 31 | redo_data.set_registered(False) 32 | 33 | async def _execute_redo_task(self): 34 | while not self._stop_event.is_set(): 35 | try: 36 | await asyncio.wait_for(self._execute_redo_channel.get(), 37 | timeout=30) 38 | except asyncio.TimeoutError: 39 | self._logger.debug(" Execute redo task timeout occurred") 40 | except asyncio.CancelledError: 41 | return 42 | 43 | if not self._connected: 44 | self._logger.warning("Grpc Connection is disconnect, skip current redo task") 45 | return 46 | 47 | try: 48 | await self.redo_task() 49 | except Exception as e: 50 | self._logger.error("Execute redo task error, error message: %s", e) 51 | 52 | @abstractmethod 53 | async def redo_task(self): 54 | pass 55 | 56 | def get_lock_for_class(self, data_type: str) -> AsyncRLock: 57 | """为特定类类型获取锁,如果不存在则创建""" 58 | if data_type not in self._locks: 59 | self._locks[data_type] = AsyncRLock() 60 | return self._locks[data_type] 61 | 62 | async def cached_redo_data(self, key: str, redo_data: RedoData, 63 | data_type: str) -> None: 64 | 65 | if data_type not in self._redo_data_map: 66 | self._redo_data_map[data_type] = {} 67 | 68 | actual_redo_data = self._redo_data_map[data_type] 69 | lock = self.get_lock_for_class(data_type) 70 | async with lock: 71 | actual_redo_data[key] = redo_data 72 | 73 | async def remove_redo_data(self, key: str, data_type: str) -> None: 74 | """ 75 | 删除redo数据 76 | 77 | Args: 78 | key: redo数据的键 79 | data_type: 存储在RedoData中的数据类型 80 | """ 81 | if data_type not in self._redo_data_map: 82 | self._redo_data_map[data_type] = {} 83 | 84 | actual_redo_data = self._redo_data_map[data_type] 85 | lock = self.get_lock_for_class(data_type) 86 | async with lock: 87 | redo_data = actual_redo_data.get(key) 88 | if redo_data is not None and not redo_data.is_expected_registered(): 89 | actual_redo_data.pop(key, None) 90 | 91 | async def data_registered(self, key: str, data_type: str) -> None: 92 | """ 93 | 数据注册成功,标记注册状态为True 94 | 95 | Args: 96 | key: redo数据的键 97 | data_type: 存储在RedoData中的类类型 98 | """ 99 | if data_type not in self._redo_data_map: 100 | self._redo_data_map[data_type] = {} 101 | 102 | actual_redo_data = self._redo_data_map[data_type] 103 | lock = self.get_lock_for_class(data_type) 104 | async with lock: 105 | redo_data = actual_redo_data.get(key) 106 | if redo_data is not None: 107 | redo_data.registered() 108 | 109 | async def data_deregister(self, key: str, data_type:str) -> None: 110 | """ 111 | 数据注销,标记注销状态为True 112 | 113 | Args: 114 | key: redo数据的键 115 | data_type: 存储在RedoData中的类类型 116 | """ 117 | if data_type not in self._redo_data_map: 118 | self._redo_data_map[data_type] = {} 119 | 120 | actual_redo_data = self._redo_data_map[data_type] 121 | lock = self.get_lock_for_class(data_type) 122 | async with lock: 123 | redo_data = actual_redo_data.get(key) 124 | if redo_data is not None: 125 | redo_data.set_unregistering(True) 126 | redo_data.set_expected_registered(False) 127 | 128 | async def data_deregistered(self, key: str, data_type: str) -> None: 129 | """ 130 | 数据注销完成,标记注销状态 131 | 132 | Args: 133 | key: redo数据的键 134 | data_type: 存储在RedoData中的类类型 135 | """ 136 | if data_type not in self._redo_data_map: 137 | self._redo_data_map[data_type] = {} 138 | 139 | actual_redo_data = self._redo_data_map[data_type] 140 | lock = self.get_lock_for_class(data_type) 141 | async with lock: 142 | redo_data = actual_redo_data.get(key) 143 | if redo_data is not None: 144 | redo_data.unregistered() 145 | 146 | async def is_data_registered(self, key: str, data_type: str) -> bool: 147 | """ 148 | 判断数据是否已注册到服务器 149 | 150 | Args: 151 | key: redo数据的键 152 | data_type: 存储在RedoData中的类类型 153 | 154 | Returns: 155 | bool: 如果已注册返回True,否则返回False 156 | """ 157 | if data_type not in self._redo_data_map: 158 | self._redo_data_map[data_type] = {} 159 | 160 | actual_redo_data = self._redo_data_map[data_type] 161 | lock = self.get_lock_for_class(data_type) 162 | async with lock: 163 | redo_data = actual_redo_data.get(key) 164 | return redo_data is not None and redo_data.is_registered() 165 | 166 | async def find_redo_data(self, data_type: str) -> Set[RedoData]: 167 | """ 168 | 查找所有需要redo的数据 169 | 170 | Args: 171 | data_type: 存储在RedoData中的类类型 172 | 173 | Returns: 174 | Set[RedoData]: 需要redo的数据集合 175 | """ 176 | result: Set[RedoData] = set() 177 | if data_type not in self._redo_data_map: 178 | self._redo_data_map[data_type] = {} 179 | 180 | actual_redo_data = self._redo_data_map[data_type] 181 | lock = self.get_lock_for_class(data_type) 182 | async with lock: 183 | for redo_data in actual_redo_data.values(): 184 | if redo_data.is_need_redo(): 185 | result.add(redo_data) 186 | 187 | return result 188 | 189 | async def get_redo_data(self, key: str, data_type: str) -> Optional[RedoData]: 190 | """ 191 | 获取缓存的redo数据 192 | 193 | Args: 194 | key: redo数据的键 195 | data_type: 存储在RedoData中的类类型 196 | 197 | Returns: 198 | RedoData: 缓存的redo数据 199 | """ 200 | if data_type not in self._redo_data_map: 201 | self._redo_data_map[data_type] = {} 202 | 203 | actual_redo_data = self._redo_data_map[data_type] 204 | lock = self.get_lock_for_class(data_type) 205 | async with lock: 206 | return actual_redo_data.get(key) 207 | 208 | 209 | async def start_redo_task(self): 210 | await self._execute_redo_channel.put(None) 211 | 212 | async def close_client(self): 213 | self._logger.info(f"{self._module} close redo service ...") 214 | self._stop_event.set() 215 | await self._listen_task 216 | self._redo_data_map.clear() 217 | self._locks.clear() 218 | 219 | 220 | 221 | -------------------------------------------------------------------------------- /v2/nacos/naming/cache/service_info_cache.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import logging 4 | import os 5 | from typing import Callable, Optional, List, Dict 6 | 7 | from v2.nacos.common.client_config import ClientConfig 8 | from v2.nacos.common.constants import Constants 9 | from v2.nacos.naming.cache.subscribe_callback_wrapper import \ 10 | SubscribeCallbackFuncWrapper 11 | from v2.nacos.naming.cache.subscribe_manager import SubscribeManager 12 | from v2.nacos.naming.model.instance import Instance 13 | from v2.nacos.naming.model.service import Service 14 | from v2.nacos.naming.util.naming_client_util import get_service_cache_key, get_group_name 15 | from v2.nacos.utils.common_util import get_current_time_millis, to_json_string 16 | from v2.nacos.utils.file_util import read_all_files_in_dir, write_to_file 17 | 18 | 19 | class ServiceInfoCache: 20 | def __init__(self, client_config: ClientConfig): 21 | self.logger = logging.getLogger(Constants.NAMING_MODULE) 22 | self.cache_dir = os.path.join(client_config.cache_dir, Constants.NAMING_MODULE, client_config.namespace_id) 23 | self.service_info_map: Dict[str, Service] = {} 24 | self.update_time_map = {} 25 | self.lock = asyncio.Lock() 26 | self.sub_callback_manager = SubscribeManager() 27 | self.update_cache_when_empty = client_config.update_cache_when_empty 28 | if client_config.load_cache_at_start: 29 | asyncio.create_task(self.load_cache_from_disk()) 30 | 31 | async def load_cache_from_disk(self): 32 | cache_file_content_dict = await read_all_files_in_dir(self.logger, self.cache_dir) 33 | if cache_file_content_dict is None: 34 | return 35 | 36 | service_map = {} 37 | for file_name, cache_content in cache_file_content_dict.items(): 38 | try: 39 | service_data = json.loads(cache_content) 40 | service = Service(**service_data) 41 | 42 | if len(service.hosts) == 0: 43 | self.logger.warning( 44 | f"instance cache list for service:{service.name} is empty, json string:{cache_content}") 45 | 46 | if service is None: 47 | continue 48 | cache_key = get_service_cache_key(get_group_name(service.name, service.groupName), service.clusters) 49 | service_map[cache_key] = service 50 | except json.JSONDecodeError as e: 51 | self.logger.error(f"failed to parse json:{cache_content}, err:{e}") 52 | continue 53 | self.logger.info(f"finish loading name cache, total file size: {len(cache_file_content_dict)}") 54 | 55 | if service_map is None or len(service_map) == 0: 56 | self.logger.info("[load_cache_from_disk] no cache file found, skip loading cache from disk.") 57 | return 58 | 59 | self.service_info_map = service_map 60 | self.logger.info("[load_cache_from_disk] loaded {%s} entries cache from disk.", len(service_map)) 61 | 62 | async def process_service_json(self, data: str): 63 | try: 64 | service_data = json.loads(data) 65 | service = Service(**service_data) 66 | 67 | if service is None: 68 | return 69 | except json.JSONDecodeError as e: 70 | self.logger.error(f"failed to parse json:{data}, err:{e}") 71 | return 72 | await self.process_service(service) 73 | 74 | async def process_service(self, service: Service): 75 | if service is None: 76 | return 77 | 78 | if not self.update_cache_when_empty and len(service.hosts) == 0: 79 | # 如果服务实例列表是空的且update_cache_when_empty为假,则跳过更新缓存 80 | self.logger.warning( 81 | f"instance list is empty, skipping update as update_cache_when_empty is set to False. service name: {service.name}") 82 | return 83 | 84 | cache_key = get_service_cache_key(get_group_name(service.name, service.groupName), service.clusters) 85 | 86 | async with self.lock: 87 | old_service = self.service_info_map.get(cache_key, None) 88 | if old_service is not None and old_service.lastRefTime >= service.lastRefTime: 89 | self.logger.warning( 90 | f"out of date data received, old-t: {old_service.lastRefTime}, new-t: {service.lastRefTime}") 91 | return 92 | 93 | # 更新时间和服务信息 94 | self.update_time_map[cache_key] = get_current_time_millis() 95 | self.service_info_map[cache_key] = service 96 | 97 | if not old_service or self.check_instance_changed(old_service, service): 98 | self.logger.info(f"service key: {cache_key} was updated to: {str(service)}") 99 | await write_to_file(self.logger, os.path.join(self.cache_dir, cache_key), to_json_string(service)) 100 | await self.sub_callback_manager.service_changed(cache_key, service) 101 | self.logger.info(f"current service map size: {len(self.service_info_map)}") 102 | 103 | async def get_service_info(self, service_name, group_name, clusters) -> Service: 104 | cache_key = get_service_cache_key(get_group_name(service_name, group_name), clusters) 105 | async with self.lock: 106 | service = self.service_info_map.get(cache_key) 107 | self.logger.info( 108 | f"get service info from cache, key: {cache_key},instances:{service.hosts if service is not None else 'None'}") 109 | return service 110 | 111 | def check_instance_changed(self, old_service: Optional[Service], new_service: Service): 112 | if old_service is None: 113 | return True 114 | if len(old_service.hosts) != len(new_service.hosts): 115 | return True 116 | 117 | old_ref_time = old_service.lastRefTime 118 | new_ref_time = new_service.lastRefTime 119 | if old_ref_time > new_ref_time: 120 | self.logger.warning(f"out of date data received, old-t: {old_ref_time}, new-t: {new_ref_time}") 121 | return False 122 | 123 | # 排序实例列表并比较,函数需要你实现 124 | old_instance = self.sort_instances(old_service.hosts) 125 | new_instance = self.sort_instances(new_service.hosts) 126 | return old_instance != new_instance 127 | 128 | @staticmethod 129 | def sort_instances(instances: List[Instance]) -> List[Instance]: 130 | def instance_key(instance: Instance) -> (int, int): 131 | ip_num = int(''.join(instance.ip.split('.'))) 132 | return ip_num, instance.port 133 | 134 | return sorted(instances, key=instance_key) 135 | 136 | async def register_callback(self, service_name: str, clusters: str, callback_func_wrapper: SubscribeCallbackFuncWrapper): 137 | await self.sub_callback_manager.add_callback_func(service_name, clusters, callback_func_wrapper) 138 | 139 | async def deregister_callback(self, service_name: str, clusters: str, callback_func_wrapper: SubscribeCallbackFuncWrapper): 140 | await self.sub_callback_manager.remove_callback_func(service_name, clusters, callback_func_wrapper) 141 | 142 | async def is_subscribed(self, service_name: str, clusters: str) -> bool: 143 | return await self.sub_callback_manager.is_subscribed(service_name, clusters) 144 | -------------------------------------------------------------------------------- /v2/nacos/naming/redo/naming_grpc_redo_service.py: -------------------------------------------------------------------------------- 1 | from typing import List, Set, cast 2 | 3 | from v2.nacos import Instance 4 | from v2.nacos.common.constants import Constants 5 | from v2.nacos.naming.redo.naming_redo_data import NamingRedoData 6 | from v2.nacos.naming.util.naming_client_util import get_group_name, \ 7 | get_service_cache_key 8 | from v2.nacos.redo.abstract_redo_service import AbstractRedoService 9 | from v2.nacos.redo.redo_data import RedoData, RedoType 10 | 11 | INSTANCE_REDO_DATA_TYPE = "InstanceRedoData" 12 | SUBSCRIBE_REDO_DATA_TYPE = "SubscribeRedoData" 13 | 14 | 15 | class NamingGrpcRedoService(AbstractRedoService): 16 | 17 | def __init__(self, client_proxy): 18 | super().__init__(Constants.NAMING_MODULE) 19 | from v2.nacos.naming.remote.naming_grpc_client_proxy import \ 20 | NamingGRPCClientProxy 21 | if not isinstance(client_proxy, NamingGRPCClientProxy): 22 | raise TypeError( 23 | "client_proxy must be NamingGRPCClientProxy instance") 24 | self.proxy = client_proxy 25 | 26 | async def redo_task(self): 27 | await self.redo_for_instances() 28 | await self.redo_for_subscribes() 29 | 30 | async def redo_for_instances(self): 31 | for redo_data in await self.find_instance_redo_data(): 32 | try: 33 | await self.redo_for_instance(redo_data) 34 | except Exception as e: 35 | self._logger.error( 36 | f"Redo instance operation {redo_data.get_redo_type()} for service:{redo_data.service_name} group:{redo_data.group_name} failed, error:{e}") 37 | 38 | async def redo_for_instance(self, instance_redo_data: NamingRedoData): 39 | redo_type = instance_redo_data.get_redo_type() 40 | self._logger.info( 41 | f"Redo instance operation {redo_type} for service:{instance_redo_data.service_name} group:{instance_redo_data.group_name}") 42 | if redo_type is RedoType.REGISTER: 43 | if not self.proxy.server_health(): 44 | return 45 | await self.process_instance_register_redo_type(instance_redo_data) 46 | elif redo_type is RedoType.UNREGISTER: 47 | if not self.proxy.server_health(): 48 | return 49 | await self.proxy.deregister_instance( 50 | instance_redo_data.service_name, 51 | instance_redo_data.group_name, 52 | instance_redo_data.get() 53 | ) 54 | elif redo_type is RedoType.REMOVE: 55 | await self.remove_instance_for_redo(instance_redo_data.service_name, 56 | instance_redo_data.group_name) 57 | 58 | async def process_instance_register_redo_type(self, 59 | instance_redo_data: NamingRedoData): 60 | data = instance_redo_data.get() 61 | if isinstance(data, List): 62 | await self.proxy.batch_register_instance( 63 | instance_redo_data.service_name, 64 | instance_redo_data.group_name, 65 | data) 66 | return 67 | await self.proxy.register_instance( 68 | instance_redo_data.service_name, 69 | instance_redo_data.group_name, 70 | data 71 | ) 72 | 73 | async def redo_for_subscribes(self): 74 | for redo_data in await self.find_subscribe_redo_data(): 75 | try: 76 | await self.redo_for_subscribe(redo_data) 77 | except Exception as e: 78 | self._logger.error( 79 | f"Redo subscribe operation {redo_data.get_redo_type()} for service:{redo_data.service_name} group:{redo_data.group_name} failed, error:{e}") 80 | 81 | async def redo_for_subscribe(self, subscribe_redo_data: NamingRedoData): 82 | redo_type = subscribe_redo_data.get_redo_type() 83 | self._logger.info( 84 | f"Redo subscribe operation {redo_type} for service:{subscribe_redo_data.service_name} group:{subscribe_redo_data.group_name}") 85 | if redo_type is RedoType.REGISTER: 86 | if not self.proxy.server_health(): 87 | return 88 | await self.proxy.subscribe(subscribe_redo_data.service_name, 89 | subscribe_redo_data.group_name, 90 | subscribe_redo_data.get()) 91 | elif redo_type is RedoType.UNREGISTER: 92 | if not self.proxy.server_health(): 93 | return 94 | await self.proxy.unsubscribe( 95 | subscribe_redo_data.service_name, 96 | subscribe_redo_data.group_name, 97 | subscribe_redo_data.get() 98 | ) 99 | elif redo_type is RedoType.REMOVE: 100 | await self.remove_subscribe_for_redo( 101 | subscribe_redo_data.service_name, 102 | subscribe_redo_data.group_name, subscribe_redo_data.get()) 103 | 104 | async def cache_instance_for_redo(self, service_name: str, group_name: str, 105 | instance: Instance) -> None: 106 | key = get_group_name(service_name, group_name) 107 | redo_data = NamingRedoData( 108 | data=instance, 109 | service_name=service_name, 110 | group_name=group_name, 111 | ) 112 | await super().cached_redo_data(key, redo_data, INSTANCE_REDO_DATA_TYPE) 113 | 114 | async def cache_instances_for_redo(self, service_name: str, group_name: str, 115 | instances: List[Instance]) -> None: 116 | key = get_group_name(service_name, group_name) 117 | redo_data = NamingRedoData( 118 | data=instances, 119 | service_name=service_name, 120 | group_name=group_name, 121 | ) 122 | await super().cached_redo_data(key, redo_data, INSTANCE_REDO_DATA_TYPE) 123 | 124 | async def instance_registered(self, service_name: str, 125 | group_name: str) -> None: 126 | key = get_group_name(service_name, group_name) 127 | await super().data_registered(key, INSTANCE_REDO_DATA_TYPE) 128 | 129 | async def instance_deregister(self, service_name: str, 130 | group_name: str) -> None: 131 | key = get_group_name(service_name, group_name) 132 | await super().data_deregister(key, INSTANCE_REDO_DATA_TYPE) 133 | 134 | async def instance_deregistered(self, service_name: str, 135 | group_name: str) -> None: 136 | key = get_group_name(service_name, group_name) 137 | await super().data_deregistered(key, INSTANCE_REDO_DATA_TYPE) 138 | 139 | async def remove_instance_for_redo(self, service_name: str, 140 | group_name: str) -> None: 141 | key = get_group_name(service_name, group_name) 142 | await super().remove_redo_data(key, INSTANCE_REDO_DATA_TYPE) 143 | 144 | async def find_instance_redo_data(self) -> Set[NamingRedoData]: 145 | redo_data_set = await super().find_redo_data(INSTANCE_REDO_DATA_TYPE) 146 | return cast(Set[NamingRedoData], redo_data_set) 147 | 148 | async def find_instance_redo_data_by_service_key(self, 149 | service_name: str, group_name: str) -> NamingRedoData | None: 150 | service_key = get_group_name(service_name, group_name) 151 | redo_data = await super().get_redo_data(service_key, 152 | INSTANCE_REDO_DATA_TYPE) 153 | if isinstance(redo_data, NamingRedoData): 154 | return redo_data 155 | 156 | async def cache_subscribe_for_redo(self, service_name: str, group_name: str, 157 | cluster: str): 158 | service_key = get_group_name(service_name, group_name) 159 | key = get_service_cache_key(service_key, cluster) 160 | redo_data = NamingRedoData( 161 | data=cluster, 162 | service_name=service_name, 163 | group_name=group_name, 164 | ) 165 | await super().cached_redo_data(key, redo_data, SUBSCRIBE_REDO_DATA_TYPE) 166 | 167 | async def is_subscribe_registered(self, service_name: str, group_name: str, 168 | cluster: str): 169 | service_key = get_group_name(service_name, group_name) 170 | key = get_service_cache_key(service_key, cluster) 171 | return await super().is_data_registered(key, SUBSCRIBE_REDO_DATA_TYPE) 172 | 173 | async def subscribe_registered(self, service_name: str, group_name: str, 174 | cluster: str) -> None: 175 | service_key = get_group_name(service_name, group_name) 176 | key = get_service_cache_key(service_key, cluster) 177 | await super().data_registered(key, SUBSCRIBE_REDO_DATA_TYPE) 178 | 179 | async def subscribe_deregister(self, service_name: str, group_name: str, 180 | cluster: str) -> None: 181 | service_key = get_group_name(service_name, group_name) 182 | key = get_service_cache_key(service_key, cluster) 183 | await super().data_deregister(key, SUBSCRIBE_REDO_DATA_TYPE) 184 | 185 | async def subscribe_deregistered(self, service_name: str, group_name: str, 186 | cluster: str) -> None: 187 | service_key = get_group_name(service_name, group_name) 188 | key = get_service_cache_key(service_key, cluster) 189 | await super().data_deregistered(key, SUBSCRIBE_REDO_DATA_TYPE) 190 | 191 | async def remove_subscribe_for_redo(self, service_name: str, 192 | group_name: str, cluster: str) -> None: 193 | service_key = get_group_name(service_name, group_name) 194 | key = get_service_cache_key(service_key, cluster) 195 | await super().remove_redo_data(key, SUBSCRIBE_REDO_DATA_TYPE) 196 | 197 | async def find_subscribe_redo_data(self) -> Set[NamingRedoData]: 198 | redo_data_set = await super().find_redo_data(SUBSCRIBE_REDO_DATA_TYPE) 199 | return cast(Set[NamingRedoData], redo_data_set) 200 | -------------------------------------------------------------------------------- /v2/nacos/transport/grpc_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Optional 3 | 4 | import grpc 5 | import pydantic 6 | 7 | from v2.nacos.common.client_config import ClientConfig 8 | from v2.nacos.common.constants import Constants 9 | from v2.nacos.common.nacos_exception import NacosException, CLIENT_DISCONNECT 10 | from v2.nacos.transport.connection import Connection 11 | from v2.nacos.transport.grpc_connection import GrpcConnection 12 | from v2.nacos.transport.grpc_util import GrpcUtils 13 | from v2.nacos.transport.grpcauto.nacos_grpc_service_pb2_grpc import BiRequestStreamStub, RequestStub 14 | from v2.nacos.transport.model.internal_request import ConnectionSetupRequest, ServerCheckRequest 15 | from v2.nacos.transport.model.internal_response import ServerCheckResponse 16 | from v2.nacos.transport.model.rpc_request import Request 17 | from v2.nacos.transport.model.server_info import ServerInfo 18 | from v2.nacos.transport.nacos_server_connector import NacosServerConnector 19 | from v2.nacos.transport.rpc_client import RpcClient, ConnectionType 20 | 21 | 22 | class GrpcClient(RpcClient): 23 | 24 | def __init__(self, logger, name: str, client_config: ClientConfig, nacos_server: NacosServerConnector): 25 | super().__init__(logger=logger, name=name, nacos_server=nacos_server) 26 | self.logger = logger 27 | self.tls_config = client_config.tls_config 28 | self.grpc_config = client_config.grpc_config 29 | self.tenant = client_config.namespace_id 30 | 31 | async def _create_new_managed_channel(self, server_ip, grpc_port): 32 | options = [ 33 | ('grpc.max_call_recv_msg_size', self.grpc_config.max_receive_message_length), 34 | ('grpc.keepalive_time_ms', self.grpc_config.max_keep_alive_ms), 35 | ('grpc.use_local_subchannel_pool', 1), # 禁用全局连接池 36 | ('grpc.so_reuseport', 0) # 禁止端口复用 37 | ] 38 | 39 | if self.tls_config and self.tls_config.enabled: 40 | with open(self.tls_config.ca_file, 'rb') as f: 41 | root_certificates = f.read() 42 | 43 | with open(self.tls_config.cert_file, 'rb') as f: 44 | cert_chain = f.read() 45 | 46 | with open(self.tls_config.key_file, 'rb') as f: 47 | private_key = f.read() 48 | 49 | credentials = grpc.ssl_channel_credentials(root_certificates=root_certificates, 50 | private_key=private_key, 51 | certificate_chain=cert_chain) 52 | 53 | channel = grpc.aio.secure_channel(f'{server_ip}:{grpc_port}', credentials=credentials, 54 | options=options) 55 | else: 56 | channel = grpc.aio.insecure_channel(f'{server_ip}:{grpc_port}', 57 | options=options) 58 | try: 59 | await asyncio.wait_for(channel.channel_ready(), self.grpc_config.grpc_timeout / 1000) 60 | except asyncio.TimeoutError as e: 61 | await channel.close() 62 | raise NacosException(CLIENT_DISCONNECT, 'failed to connect nacos server') from e 63 | else: 64 | return channel 65 | 66 | async def _server_check(self, server_ip, server_port, channel_stub: RequestStub): 67 | for i in range(self.RETRY_TIMES): 68 | try: 69 | server_check_request = ServerCheckRequest() 70 | response_payload = await channel_stub.request( 71 | GrpcUtils.convert_request_to_payload(server_check_request), 72 | timeout=self.grpc_config.grpc_timeout / 1000.0) 73 | server_check_response = GrpcUtils.parse(response_payload) 74 | if not server_check_response or not isinstance(server_check_response, ServerCheckResponse): 75 | return None 76 | 77 | if 300 <= server_check_response.get_error_code() < 400: 78 | self.logger.error( 79 | f"server check fail for {server_ip}:{server_port}, error code = {server_check_response.get_error_code()}") 80 | await asyncio.sleep(1) 81 | continue 82 | 83 | return server_check_response 84 | except grpc.FutureTimeoutError: 85 | self.logger.error(f"server check timed out for {server_ip}:{server_port}") 86 | continue 87 | except grpc.aio.AioRpcError as e: 88 | raise NacosException(error_code=e.code(), message=e.details()) 89 | except Exception as e: 90 | self.logger.error(f"server check fail for {server_ip}:{server_port}, error = {e}") 91 | if self.tls_config and self.tls_config.enabled: 92 | self.logger.error("current client requires tls encrypted, server must support tls, please check.") 93 | return None 94 | 95 | async def connect_to_server(self, server_info: ServerInfo) -> Optional[Connection]: 96 | managed_channel = await self._create_new_managed_channel(server_info.server_ip, server_info.server_port) 97 | # Create a stub 98 | channel_stub = RequestStub(managed_channel) 99 | server_check_response = await self._server_check(server_info.server_ip, server_info.server_port, 100 | channel_stub) 101 | if not server_check_response: 102 | await self._shunt_down_channel(managed_channel) 103 | return None 104 | 105 | connection_id = server_check_response.get_connection_id() 106 | self.logger.info( 107 | f"connect to server success,labels:{self.labels},tenant:{self.tenant},connection_id:{connection_id}") 108 | bi_request_stream_stub = BiRequestStreamStub(managed_channel) 109 | grpc_conn = GrpcConnection(server_info, connection_id, managed_channel, 110 | channel_stub, bi_request_stream_stub) 111 | 112 | connection_setup_request = ConnectionSetupRequest(clientVersion=Constants.CLIENT_VERSION, tenant=self.tenant, 113 | labels=self.labels) 114 | await grpc_conn.send_bi_request(GrpcUtils.convert_request_to_payload(connection_setup_request)) 115 | asyncio.create_task(self._server_request_watcher(grpc_conn)) 116 | await asyncio.sleep(0.1) 117 | return grpc_conn 118 | 119 | async def _handle_server_request(self, request: Request, grpc_connection: GrpcConnection): 120 | request_type = request.get_request_type() 121 | server_request_handler_instance = self.server_request_handler_mapping.get(request_type) 122 | if not server_request_handler_instance: 123 | self.logger.error("unsupported payload type:%s, grpc connection id:%s", request_type, 124 | grpc_connection.get_connection_id()) 125 | return 126 | response = await server_request_handler_instance.request_reply(request) 127 | if not response: 128 | self.logger.warning("failed to process server request,connection_id:%s,ackID:%s", 129 | grpc_connection.get_connection_id(), request.get_request_id()) 130 | return 131 | 132 | try: 133 | response.set_request_id(request.requestId) 134 | 135 | await grpc_connection.send_bi_request(GrpcUtils.convert_response_to_payload(response)) 136 | 137 | except Exception as e: 138 | if isinstance(e, EOFError): 139 | self.logger.error( 140 | f"{grpc_connection.get_connection_id()} connection closed before response could be sent, ackId->{request.requestId}") 141 | else: 142 | self.logger.error( 143 | f"{grpc_connection.get_connection_id()} failed to send response:{response.get_response_type()}, ackId:{request.requestId},error:{str(e)}") 144 | 145 | async def _server_request_watcher(self, grpc_conn: GrpcConnection): 146 | async for payload in grpc_conn.bi_stream_send(): 147 | try: 148 | self.logger.info("receive stream server request, connection_id:%s, original info: %s" 149 | % (grpc_conn.get_connection_id(), str(payload))) 150 | request = GrpcUtils.parse(payload) 151 | if request: 152 | await self._handle_server_request(request, grpc_conn) 153 | 154 | except Exception as e: 155 | self.logger.error(f"[{grpc_conn.connection_id}] handle server request occur exception: {e}") 156 | 157 | @staticmethod 158 | async def _shunt_down_channel(channel): 159 | if channel: 160 | await channel.close() 161 | 162 | def get_connection_type(self): 163 | return ConnectionType.GRPC 164 | 165 | def get_rpc_port_offset(self) -> int: 166 | return 1000 167 | -------------------------------------------------------------------------------- /test/client_v2_test.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import unittest 4 | from typing import List 5 | 6 | from v2.nacos import ConfigParam 7 | from v2.nacos.common.client_config import GRPCConfig 8 | from v2.nacos.common.client_config_builder import ClientConfigBuilder 9 | from v2.nacos.naming.model.instance import Instance 10 | from v2.nacos.naming.model.naming_param import RegisterInstanceParam, DeregisterInstanceParam, \ 11 | BatchRegisterInstanceParam, GetServiceParam, ListServiceParam, SubscribeServiceParam, ListInstanceParam 12 | from v2.nacos.naming.nacos_naming_service import NacosNamingService 13 | from v2.nacos.config.nacos_config_service import NacosConfigService 14 | from v2.nacos.common.auth import CredentialsProvider, Credentials 15 | 16 | client_config = (ClientConfigBuilder() 17 | .access_key(os.getenv('NACOS_ACCESS_KEY')) 18 | .secret_key(os.getenv('NACOS_SECRET_KEY')) 19 | .server_address(os.getenv('NACOS_SERVER_ADDR', 'localhost:8848')) 20 | .log_level('INFO') 21 | .grpc_config(GRPCConfig(grpc_timeout=5000)) 22 | .build()) 23 | 24 | class CustomCredentialsProvider(CredentialsProvider): 25 | def __init__(self, ak="", sk="", token=""): 26 | self.credential = Credentials(ak, sk, token) 27 | 28 | def get_credentials(self): 29 | return self.credential 30 | 31 | class TestClientV2(unittest.IsolatedAsyncioTestCase): 32 | 33 | async def test_init_naming_and_config_service(self): 34 | config_client = await NacosConfigService.create_config_service(client_config) 35 | assert await config_client.server_health() 36 | naming_client = await NacosNamingService.create_naming_service(client_config) 37 | assert await naming_client.server_health() 38 | response = await naming_client.register_instance( 39 | request=RegisterInstanceParam(service_name='nacos.test.1', group_name='DEFAULT_GROUP', ip='1.1.1.1', 40 | port=7001, weight=1.0, cluster_name='c1', metadata={'a': 'b'}, 41 | enabled=True, 42 | healthy=True, ephemeral=True)) 43 | self.assertEqual(response, True) 44 | print('register instance') 45 | 46 | data_id = "com.alibaba.nacos.test.config" 47 | group = "DEFAULT_GROUP" 48 | 49 | content = await config_client.get_config(ConfigParam( 50 | data_id=data_id, 51 | group=group, 52 | )) 53 | 54 | assert content == "" 55 | await asyncio.sleep(10000) 56 | 57 | 58 | 59 | async def test_register_with_endpoint_and_fixed_ak(self): 60 | config = (ClientConfigBuilder() 61 | .access_key(os.getenv('NACOS_ACCESS_KEY')) 62 | .secret_key(os.getenv('NACOS_SECRET_KEY')) 63 | .endpoint(os.getenv('NACOS_SERVER_ENDPOINT', 'localhost:8848')) 64 | .log_level('INFO') 65 | .grpc_config(GRPCConfig(grpc_timeout=5000)) 66 | .build()) 67 | 68 | client = await NacosNamingService.create_naming_service(config) 69 | assert await client.server_health() 70 | 71 | async def test_register_with_endpoint_and_provider(self): 72 | config = (ClientConfigBuilder() 73 | .credentials_provider(CustomCredentialsProvider(os.getenv('NACOS_ACCESS_KEY'), os.getenv('NACOS_SECRET_KEY'))) 74 | .endpoint(os.getenv('NACOS_SERVER_ENDPOINT', 'localhost:8848')) 75 | .log_level('INFO') 76 | .grpc_config(GRPCConfig(grpc_timeout=5000)) 77 | .build()) 78 | 79 | client = await NacosNamingService.create_naming_service(config) 80 | assert await client.server_health() 81 | 82 | async def test_register(self): 83 | client = await NacosNamingService.create_naming_service(client_config) 84 | assert await client.server_health() 85 | 86 | async def cb(instance_list: List[Instance]): 87 | print('received subscribe callback', str(instance_list)) 88 | 89 | await client.subscribe( 90 | SubscribeServiceParam(service_name='nacos.test.1', group_name='DEFAULT_GROUP', subscribe_callback=cb)) 91 | 92 | print('subscribe service') 93 | 94 | response = await client.register_instance( 95 | request=RegisterInstanceParam(service_name='nacos.test.1', group_name='DEFAULT_GROUP', ip='1.1.1.1', 96 | port=7001, weight=1.0, cluster_name='c1', metadata={'a': 'b'}, 97 | enabled=True, 98 | healthy=True, ephemeral=True)) 99 | self.assertEqual(response, True) 100 | 101 | print('register instance') 102 | 103 | await asyncio.sleep(1) 104 | 105 | response = await client.update_instance( 106 | request=RegisterInstanceParam(service_name='nacos.test.1', group_name='DEFAULT_GROUP', ip='1.1.1.1', 107 | port=7001, weight=2.0, cluster_name='c1', metadata={'a': 'b'}, 108 | enabled=True, 109 | healthy=True, ephemeral=True)) 110 | self.assertEqual(response, True) 111 | 112 | print('update instance') 113 | 114 | await asyncio.sleep(1) 115 | 116 | response = await client.deregister_instance( 117 | request=DeregisterInstanceParam(service_name='nacos.test.1', group_name='DEFAULT_GROUP', ip='1.1.1.1', 118 | port=7001, cluster_name='c1', ephemeral=True) 119 | ) 120 | self.assertEqual(response, True) 121 | 122 | print('deregister instance') 123 | 124 | await asyncio.sleep(1) 125 | 126 | param1 = RegisterInstanceParam(service_name='nacos.test.1', 127 | group_name='DEFAULT_GROUP', 128 | ip='1.1.1.1', 129 | port=7001, 130 | weight=1.0, 131 | cluster_name='c1', 132 | metadata={'a': 'b'}, 133 | enabled=True, 134 | healthy=True, 135 | ephemeral=True 136 | ) 137 | param2 = RegisterInstanceParam(service_name='nacos.test.1', 138 | group_name='DEFAULT_GROUP', 139 | ip='1.1.1.1', 140 | port=7002, 141 | weight=1.0, 142 | cluster_name='c1', 143 | metadata={'a': 'b'}, 144 | enabled=True, 145 | healthy=True, 146 | ephemeral=True 147 | ) 148 | param3 = RegisterInstanceParam(service_name='nacos.test.1', 149 | group_name='DEFAULT_GROUP', 150 | ip='1.1.1.1', 151 | port=7003, 152 | weight=1.0, 153 | cluster_name='c1', 154 | metadata={'a': 'b'}, 155 | enabled=True, 156 | healthy=False, 157 | ephemeral=True 158 | ) 159 | response = await client.batch_register_instances( 160 | request=BatchRegisterInstanceParam(service_name='nacos.test.1', group_name='DEFAULT_GROUP', 161 | instances=[param1, param2, param3])) 162 | self.assertEqual(response, True) 163 | 164 | print('batch register instance') 165 | await asyncio.sleep(1) 166 | 167 | service = await client.get_service( 168 | GetServiceParam(service_name='nacos.test.1', group_name='DEFAULT_GROUP', cluster_name='c1')) 169 | print('get service', str(service)) 170 | assert service.name == 'nacos.test.1' 171 | 172 | service_list = await client.list_services(ListServiceParam()) 173 | assert service_list.count == 1 174 | 175 | instance_list = await client.list_instances(ListInstanceParam(service_name='nacos.test.1', healthy_only=True)) 176 | assert len(instance_list) == 2 177 | 178 | instance_list = await client.list_instances(ListInstanceParam(service_name='nacos.test.1', healthy_only=False)) 179 | assert len(instance_list) == 1 180 | 181 | instance_list = await client.list_instances(ListInstanceParam(service_name='nacos.test.1', healthy_only=None)) 182 | assert len(instance_list) == 3 183 | 184 | await client.unsubscribe( 185 | SubscribeServiceParam(service_name='nacos.test.1', group_name='DEFAULT_GROUP', subscribe_callback=cb)) 186 | 187 | await client.shutdown() 188 | 189 | 190 | if __name__ == '__main__': 191 | unittest.main() 192 | -------------------------------------------------------------------------------- /v2/nacos/transport/grpcauto/nacos_grpc_service_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | """Client and server classes corresponding to protobuf-defined services.""" 3 | import grpc 4 | 5 | import v2.nacos.transport.grpcauto.nacos_grpc_service_pb2 as nacos__grpc__service__pb2 6 | 7 | GRPC_GENERATED_VERSION = '1.66.1' 8 | GRPC_VERSION = grpc.__version__ 9 | _version_not_supported = False 10 | 11 | try: 12 | from grpc._utilities import first_version_is_lower 13 | 14 | _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) 15 | except ImportError: 16 | _version_not_supported = True 17 | 18 | if _version_not_supported: 19 | raise RuntimeError( 20 | f'The grpc package installed is at version {GRPC_VERSION},' 21 | + f' but the generated code in nacos_grpc_service_pb2_grpc.py depends on' 22 | + f' grpcio>={GRPC_GENERATED_VERSION}.' 23 | + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' 24 | + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' 25 | ) 26 | 27 | 28 | class RequestStreamStub(object): 29 | """Missing associated documentation comment in .proto file.""" 30 | 31 | def __init__(self, channel): 32 | """Constructor. 33 | 34 | Args: 35 | channel: A grpc.Channel. 36 | """ 37 | self.requestStream = channel.unary_stream( 38 | '/RequestStream/requestStream', 39 | request_serializer=nacos__grpc__service__pb2.Payload.SerializeToString, 40 | response_deserializer=nacos__grpc__service__pb2.Payload.FromString, 41 | _registered_method=True) 42 | 43 | 44 | class RequestStreamServicer(object): 45 | """Missing associated documentation comment in .proto file.""" 46 | 47 | def requestStream(self, request, context): 48 | """build a streamRequest 49 | """ 50 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 51 | context.set_details('Method not implemented!') 52 | raise NotImplementedError('Method not implemented!') 53 | 54 | 55 | def add_RequestStreamServicer_to_server(servicer, server): 56 | rpc_method_handlers = { 57 | 'requestStream': grpc.unary_stream_rpc_method_handler( 58 | servicer.requestStream, 59 | request_deserializer=nacos__grpc__service__pb2.Payload.FromString, 60 | response_serializer=nacos__grpc__service__pb2.Payload.SerializeToString, 61 | ), 62 | } 63 | generic_handler = grpc.method_handlers_generic_handler( 64 | 'RequestStream', rpc_method_handlers) 65 | server.add_generic_rpc_handlers((generic_handler,)) 66 | server.add_registered_method_handlers('RequestStream', rpc_method_handlers) 67 | 68 | 69 | # This class is part of an EXPERIMENTAL API. 70 | class RequestStream(object): 71 | """Missing associated documentation comment in .proto file.""" 72 | 73 | @staticmethod 74 | def requestStream(request, 75 | target, 76 | options=(), 77 | channel_credentials=None, 78 | call_credentials=None, 79 | insecure=False, 80 | compression=None, 81 | wait_for_ready=None, 82 | timeout=None, 83 | metadata=None): 84 | return grpc.experimental.unary_stream( 85 | request, 86 | target, 87 | '/RequestStream/requestStream', 88 | nacos__grpc__service__pb2.Payload.SerializeToString, 89 | nacos__grpc__service__pb2.Payload.FromString, 90 | options, 91 | channel_credentials, 92 | insecure, 93 | call_credentials, 94 | compression, 95 | wait_for_ready, 96 | timeout, 97 | metadata, 98 | _registered_method=True) 99 | 100 | 101 | class RequestStub(object): 102 | """Missing associated documentation comment in .proto file.""" 103 | 104 | def __init__(self, channel): 105 | """Constructor. 106 | 107 | Args: 108 | channel: A grpc.Channel. 109 | """ 110 | self.request = channel.unary_unary( 111 | '/Request/request', 112 | request_serializer=nacos__grpc__service__pb2.Payload.SerializeToString, 113 | response_deserializer=nacos__grpc__service__pb2.Payload.FromString, 114 | _registered_method=True) 115 | 116 | 117 | class RequestServicer(object): 118 | """Missing associated documentation comment in .proto file.""" 119 | 120 | def request(self, request, context): 121 | """Sends a commonRequest 122 | """ 123 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 124 | context.set_details('Method not implemented!') 125 | raise NotImplementedError('Method not implemented!') 126 | 127 | 128 | def add_RequestServicer_to_server(servicer, server): 129 | rpc_method_handlers = { 130 | 'request': grpc.unary_unary_rpc_method_handler( 131 | servicer.request, 132 | request_deserializer=nacos__grpc__service__pb2.Payload.FromString, 133 | response_serializer=nacos__grpc__service__pb2.Payload.SerializeToString, 134 | ), 135 | } 136 | generic_handler = grpc.method_handlers_generic_handler( 137 | 'Request', rpc_method_handlers) 138 | server.add_generic_rpc_handlers((generic_handler,)) 139 | server.add_registered_method_handlers('Request', rpc_method_handlers) 140 | 141 | 142 | # This class is part of an EXPERIMENTAL API. 143 | class Request(object): 144 | """Missing associated documentation comment in .proto file.""" 145 | 146 | @staticmethod 147 | def request(request, 148 | target, 149 | options=(), 150 | channel_credentials=None, 151 | call_credentials=None, 152 | insecure=False, 153 | compression=None, 154 | wait_for_ready=None, 155 | timeout=None, 156 | metadata=None): 157 | return grpc.experimental.unary_unary( 158 | request, 159 | target, 160 | '/Request/request', 161 | nacos__grpc__service__pb2.Payload.SerializeToString, 162 | nacos__grpc__service__pb2.Payload.FromString, 163 | options, 164 | channel_credentials, 165 | insecure, 166 | call_credentials, 167 | compression, 168 | wait_for_ready, 169 | timeout, 170 | metadata, 171 | _registered_method=True) 172 | 173 | 174 | class BiRequestStreamStub(object): 175 | """Missing associated documentation comment in .proto file.""" 176 | 177 | def __init__(self, channel): 178 | """Constructor. 179 | 180 | Args: 181 | channel: A grpc.Channel. 182 | """ 183 | self.requestBiStream = channel.stream_stream( 184 | '/BiRequestStream/requestBiStream', 185 | request_serializer=nacos__grpc__service__pb2.Payload.SerializeToString, 186 | response_deserializer=nacos__grpc__service__pb2.Payload.FromString, 187 | _registered_method=True) 188 | 189 | 190 | class BiRequestStreamServicer(object): 191 | """Missing associated documentation comment in .proto file.""" 192 | 193 | def requestBiStream(self, request_iterator, context): 194 | """Sends a commonRequest 195 | """ 196 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 197 | context.set_details('Method not implemented!') 198 | raise NotImplementedError('Method not implemented!') 199 | 200 | 201 | def add_BiRequestStreamServicer_to_server(servicer, server): 202 | rpc_method_handlers = { 203 | 'requestBiStream': grpc.stream_stream_rpc_method_handler( 204 | servicer.requestBiStream, 205 | request_deserializer=nacos__grpc__service__pb2.Payload.FromString, 206 | response_serializer=nacos__grpc__service__pb2.Payload.SerializeToString, 207 | ), 208 | } 209 | generic_handler = grpc.method_handlers_generic_handler( 210 | 'BiRequestStream', rpc_method_handlers) 211 | server.add_generic_rpc_handlers((generic_handler,)) 212 | server.add_registered_method_handlers('BiRequestStream', rpc_method_handlers) 213 | 214 | 215 | # This class is part of an EXPERIMENTAL API. 216 | class BiRequestStream(object): 217 | """Missing associated documentation comment in .proto file.""" 218 | 219 | @staticmethod 220 | def requestBiStream(request_iterator, 221 | target, 222 | options=(), 223 | channel_credentials=None, 224 | call_credentials=None, 225 | insecure=False, 226 | compression=None, 227 | wait_for_ready=None, 228 | timeout=None, 229 | metadata=None): 230 | return grpc.experimental.stream_stream( 231 | request_iterator, 232 | target, 233 | '/BiRequestStream/requestBiStream', 234 | nacos__grpc__service__pb2.Payload.SerializeToString, 235 | nacos__grpc__service__pb2.Payload.FromString, 236 | options, 237 | channel_credentials, 238 | insecure, 239 | call_credentials, 240 | compression, 241 | wait_for_ready, 242 | timeout, 243 | metadata, 244 | _registered_method=True) 245 | -------------------------------------------------------------------------------- /test/config_client_v2_test.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import unittest 4 | 5 | from v2.nacos.common.client_config import GRPCConfig, KMSConfig 6 | from v2.nacos.common.client_config_builder import ClientConfigBuilder 7 | from v2.nacos.config.model.config_param import ConfigParam 8 | from v2.nacos.config.nacos_config_service import NacosConfigService 9 | from v2.nacos.common.auth import CredentialsProvider, Credentials 10 | 11 | client_config = (ClientConfigBuilder() 12 | # .access_key(os.getenv('NACOS_ACCESS_KEY')) 13 | # .secret_key(os.getenv('NACOS_SECRET_KEY')) 14 | .username(os.getenv('NACOS_USERNAME')) 15 | .password(os.getenv('NACOS_PASSWORD')) 16 | .server_address(os.getenv('NACOS_SERVER_ADDR', 'localhost:8848')) 17 | .log_level('INFO') 18 | .grpc_config(GRPCConfig(grpc_timeout=5000)) 19 | .build()) 20 | 21 | 22 | class CustomCredentialsProvider(CredentialsProvider): 23 | def __init__(self, ak="", sk="", token=""): 24 | self.credential = Credentials(ak, sk, token) 25 | 26 | def get_credentials(self): 27 | return self.credential 28 | 29 | 30 | class TestClientV2(unittest.IsolatedAsyncioTestCase): 31 | async def test_publish_config(self): 32 | client = await NacosConfigService.create_config_service(client_config) 33 | assert await client.server_health() 34 | 35 | data_id = "com.alibaba.nacos.test.config" 36 | group = "DEFAULT_GROUP" 37 | 38 | content = await client.get_config(ConfigParam( 39 | data_id=data_id, 40 | group=group, 41 | )) 42 | 43 | assert content == "" 44 | 45 | res = await client.publish_config(ConfigParam( 46 | data_id=data_id, 47 | group=group, 48 | content="Hello world") 49 | ) 50 | assert res 51 | print("success to publish") 52 | 53 | await asyncio.sleep(0.2) 54 | content = await client.get_config(ConfigParam( 55 | data_id=data_id, 56 | group=group, 57 | )) 58 | 59 | assert content == "Hello world" 60 | print("success to get config") 61 | 62 | res = await client.remove_config(ConfigParam( 63 | data_id=data_id, 64 | group=group 65 | )) 66 | 67 | assert res 68 | print("success to remove") 69 | 70 | await asyncio.sleep(0.1) 71 | content = await client.get_config(ConfigParam( 72 | data_id=data_id, 73 | group=group, 74 | )) 75 | 76 | assert content == "" 77 | 78 | await client.shutdown() 79 | 80 | async def test_config_listener(self): 81 | client = await NacosConfigService.create_config_service(client_config) 82 | assert await client.server_health() 83 | 84 | dataID = "com.alibaba.nacos.test.config" 85 | groupName = "DEFAULT_GROUP" 86 | 87 | async def config_listener1(tenant, data_id, group, content): 88 | print("listen1, tenant:{} data_id:{} group:{} content:{}".format(tenant, data_id, group, content)) 89 | 90 | async def config_listener2(tenant, data_id, group, content): 91 | print("listen2, tenant:{} data_id:{} group:{} content:{}".format(tenant, data_id, group, content)) 92 | 93 | await client.add_listener(dataID, groupName, config_listener1) 94 | await client.add_listener(dataID, groupName, config_listener2) 95 | 96 | await asyncio.sleep(3) 97 | 98 | res = await client.publish_config(ConfigParam( 99 | data_id=dataID, 100 | group=groupName, 101 | content="Hello world") 102 | ) 103 | assert res 104 | print("success to publish") 105 | 106 | await asyncio.sleep(3) 107 | 108 | res = await client.publish_config(ConfigParam( 109 | data_id=dataID, 110 | group=groupName, 111 | content="Hello world2") 112 | ) 113 | assert res 114 | 115 | await asyncio.sleep(3) 116 | 117 | await client.remove_listener(dataID, groupName, config_listener1) 118 | 119 | await asyncio.sleep(3) 120 | 121 | res = await client.publish_config(ConfigParam( 122 | data_id=dataID, 123 | group=groupName, 124 | content="Hello world3") 125 | ) 126 | assert res 127 | 128 | await asyncio.sleep(3) 129 | 130 | res = await client.remove_config(ConfigParam( 131 | data_id=dataID, 132 | group=groupName 133 | )) 134 | 135 | assert res 136 | print("success to remove") 137 | await asyncio.sleep(3) 138 | await client.shutdown() 139 | 140 | async def test_cipher_config(self): 141 | kms_config = KMSConfig( 142 | enabled=True, 143 | endpoint=os.getenv('KMS_ENDPOINT'), 144 | access_key=os.getenv('NACOS_ACCESS_KEY'), 145 | secret_key=os.getenv('NACOS_SECRET_KEY'), 146 | ) 147 | 148 | client_config.set_kms_config(kms_config) 149 | 150 | client = await NacosConfigService.create_config_service(client_config) 151 | 152 | dataID = "cipher-kms-aes-128-crypt" 153 | groupName = "DEFAULT_GROUP" 154 | 155 | res = await client.publish_config( 156 | param=ConfigParam( 157 | data_id=dataID, 158 | group=groupName, 159 | content="加密内容-128", 160 | kms_key_id=os.getenv("KMS_KEY_ID"))) 161 | 162 | assert res 163 | print("success to publish") 164 | await asyncio.sleep(0.1) 165 | 166 | content = await client.get_config(ConfigParam( 167 | data_id=dataID, 168 | group=groupName, 169 | kms_key_id=os.getenv("KMS_KEY_ID") 170 | )) 171 | print("success to get config:" + content) 172 | assert content == '加密内容-128' 173 | 174 | dataID = "cipher-kms-aes-256-crypt" 175 | groupName = "DEFAULT_GROUP" 176 | 177 | res = await client.publish_config( 178 | param=ConfigParam( 179 | data_id=dataID, 180 | group=groupName, 181 | content="加密内容-256", 182 | kms_key_id=os.getenv("KMS_KEY_ID"))) 183 | 184 | assert res 185 | print("success to publish") 186 | await asyncio.sleep(0.1) 187 | 188 | content = await client.get_config(ConfigParam( 189 | data_id=dataID, 190 | group=groupName, 191 | kms_key_id=os.getenv("KMS_KEY_ID") 192 | )) 193 | print("success to get config:" + content) 194 | assert content == '加密内容-256' 195 | await asyncio.sleep(5) 196 | await client.shutdown() 197 | 198 | async def test_cipher_config_listener(self): 199 | kms_config = KMSConfig( 200 | enabled=True, 201 | endpoint=os.getenv("KMS_ENDPOINT"), 202 | access_key=os.getenv('NACOS_ACCESS_KEY'), 203 | secret_key=os.getenv('NACOS_SECRET_KEY'), 204 | ) 205 | client_cfg = (ClientConfigBuilder() 206 | .access_key(os.getenv('NACOS_ACCESS_KEY')) 207 | .secret_key(os.getenv('NACOS_SECRET_KEY')) 208 | .server_address(os.getenv('NACOS_SERVER_ADDR', 'localhost:8848')) 209 | .log_level('INFO') 210 | .kms_config(kms_config) 211 | .grpc_config(GRPCConfig(grpc_timeout=5000)) 212 | .build()) 213 | 214 | client = await NacosConfigService.create_config_service(client_cfg) 215 | 216 | dataID = "cipher-kms-aes-128-crypt" 217 | groupName = "DEFAULT_GROUP" 218 | 219 | async def config_listener(tenant, data_id, group, content): 220 | print("listen1, tenant:{} data_id:{} group:{} content:{}".format(tenant, data_id, group, content)) 221 | 222 | await client.add_listener(dataID, groupName, config_listener) 223 | 224 | await asyncio.sleep(3) 225 | res = await client.publish_config( 226 | param=ConfigParam( 227 | data_id=dataID, 228 | group=groupName, 229 | content="加密内容-1", 230 | kms_key_id=os.getenv("KMS_KEY_ID"))) 231 | 232 | assert res 233 | print("success to publish") 234 | 235 | await asyncio.sleep(3) 236 | 237 | res = await client.publish_config( 238 | param=ConfigParam( 239 | data_id=dataID, 240 | group=groupName, 241 | content="加密内容-2", 242 | kms_key_id=os.getenv("KMS_KEY_ID"))) 243 | assert res 244 | 245 | await asyncio.sleep(3) 246 | 247 | await client.shutdown() 248 | 249 | async def _gray_config(self, client_cfg): 250 | client = await NacosConfigService.create_config_service(client_cfg) 251 | 252 | dataID = "com.alibaba.nacos.test.config.gray" 253 | groupName = "DEFAULT_GROUP" 254 | 255 | async def config_listener(tenant, data_id, group, content): 256 | print("listen1, tenant:{} data_id:{} group:{} content:{}".format(tenant, data_id, group, content)) 257 | 258 | await client.add_listener(dataID, groupName, config_listener) 259 | 260 | await asyncio.sleep(1000) 261 | 262 | async def test_gray_config_with_fixed_ak(self): 263 | client_cfg = (ClientConfigBuilder() 264 | .access_key(os.getenv('NACOS_ACCESS_KEY')) 265 | .secret_key(os.getenv('NACOS_SECRET_KEY')) 266 | .server_address(os.getenv('NACOS_SERVER_ADDR', 'localhost:8848')) 267 | .log_level('INFO') 268 | .app_conn_labels({"k1": "v1", "k2": "v2", "nacos_config_gray_label": "gray"}) 269 | .grpc_config(GRPCConfig(grpc_timeout=5000)) 270 | .build()) 271 | await self._gray_config(client_cfg) 272 | 273 | async def test_gray_config_with_provider(self): 274 | client_cfg = (ClientConfigBuilder() 275 | .credentials_provider( 276 | CustomCredentialsProvider(os.getenv('NACOS_ACCESS_KEY'), os.getenv('NACOS_SECRET_KEY'))) 277 | .server_address(os.getenv('NACOS_SERVER_ADDR', 'localhost:8848')) 278 | .log_level('INFO') 279 | .app_conn_labels({"k1": "v1", "k2": "v2", "nacos_config_gray_label": "gray"}) 280 | .grpc_config(GRPCConfig(grpc_timeout=5000)) 281 | .build()) 282 | await self._gray_config(client_cfg) 283 | --------------------------------------------------------------------------------