# !/usr/bin/env python
# -*- coding: utf8 -*-
from nacos import NacosException, NacosClient
from nacos.client import process_common_config_params
from nacos.params import *
from nacos.files import *
from nacos.commons import *
from urllib.error import HTTPError
from http import HTTPStatus
from pyfsops import success, EXCEPTIONS
import requests, json, random, logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class NacosApi(NacosClient):
def __init__(self, server_addresses, username='nacos', password='nacos', namespace='public'):
super(NacosClient, self).__init__()
NacosClient.__init__(self, server_addresses, namespace=namespace, username=username,
password=password)
self.server_addresses = server_addresses
def add_naming_instance(self, service_name, ip, port, cluster_name=None, weight=1.0, metadata=None,
enable=True, healthy=True, ephemeral=True, group_name='DEFAULT_GROUP'):
'''注册实例'''
params = {
"ip": ip,
"port": port,
"serviceName": service_name,
"weight": weight,
"enable": enable,
"healthy": healthy,
"clusterName": cluster_name,
"ephemeral": ephemeral,
"groupName": group_name,
"username": self.username,
"password": self.password,
}
self._build_metadata(metadata, params)
if self.namespace:
params["namespaceId"] = self.namespace
try:
resp = self._do_sync_req("/nacos/v2/ns/instance", None, None,
params, self.default_timeout, "POST", "naming")
c = resp.read()
return c.decode()
except HTTPError as e:
if e.code == HTTPStatus.FORBIDDEN:
raise NacosException("Insufficient privilege.")
else:
raise NacosException("Request Error, code is %s" % e.code)
except Exception as e:
# self.logger.exception("[add-naming-instance] exception %s occur" % str(e))
raise Exception("[add-naming-instance] exception %s occur" % str(e))
def query_nacos_instance(self, serviceName, clusterName='DEFAULT', groupName='DEFAULT_GROUP'):
try:
rq = self.list_naming_instance(service_name=serviceName, clusters=clusterName, group_name=groupName,
healthy_only=True)
ret = success.copy()
ret['data'] = rq['hosts']
except (Exception, ConnectionError, NacosException) as e:
ret = EXCEPTIONS.code['error'].copy()
ret['message'] = '查询 NACOS 服务 {0} 失败,原因:{1}'.format(serviceName, e)
return ret
def get_accessToken(self):
try:
nacos_address = random.choice(self.server_addresses.split(','))
login_url = nacos_address + '/nacos/v1/auth/login'
login_params = {'username': self.username, 'password': self.password}
rq = requests.post(login_url, data=login_params)
rs = rq.json()
return rs
except HTTPError as e:
if e.code == HTTPStatus.FORBIDDEN:
raise NacosException("Insufficient privilege.")
else:
raise NacosException("Request Error, code is %s" % e.code)
except (Exception, ConnectionError) as e:
raise NacosException("[get-accessToken] exception %s occur" % str(e))
def list_naming_service_v2(self, clusters=None, namespace_id=None, group_name=None, pageNo=None, pageSize=None,
selector=None):
"""
查询符合条件的服务列表
:param clusters: 集群名称 字符串,多个集群用逗号分隔
:param namespace_id: 命名空间ID
:param group_name: 分组名
:param pageNo: 当前页
:param pageSize: 页条目数
"""
params = {
"selector": {
"type": "none", # 返回当前Selector类型
"contextType": "NONE" # 返回当前Selector所需的资源类型
}
}
if pageNo and pageSize:
params.update({"pageNo": pageNo, "pageSize": pageSize})
if clusters is not None:
params["clusters"] = clusters
if selector is not None:
# 查询服务列表的时候也有个名为selector的参数,作用是可以根据服务以及实例中的元数据参数metadata进行服务过滤。
# 这个表达式为:{“type”:“label”,“expression”:“INSTANCE.metadata.xxx = ‘xxx’”} 或
# {“type”:“label”,“expression”:“SERVICE.metadata.xxx = ‘xxx’”}。
params["selector"] = selector
namespace_id = namespace_id or self.namespace
if namespace_id:
params["namespaceId"] = namespace_id
group_name = group_name or 'DEFAULT_GROUP'
if group_name:
params['groupName'] = group_name
try:
resp = self._do_sync_req("/nacos/v2/ns/service/list", None, params, None, self.default_timeout, "GET")
c = resp.read()
return json.loads(c.decode("UTF-8"))
except HTTPError as e:
if e.code == HTTPStatus.FORBIDDEN:
raise NacosException("Insufficient privilege.")
else:
raise NacosException("Request Error, code is %s" % e.code)
except Exception as e:
raise NacosException("[list-naming-service] exception %s occur" % str(e))
def get_naming_service(self, service_name, namespace_id=None, group_name=None):
params = {
"serviceName": service_name
}
params["namespaceId"] = namespace_id or self.namespace
params['groupName'] = group_name or 'DEFAULT_GROUP'
try:
resp = self._do_sync_req("/nacos/v2/ns/service", None, params, None, self.default_timeout, "GET")
c = resp.read()
return json.loads(c.decode("UTF-8"))
except HTTPError as e:
if e.code == HTTPStatus.FORBIDDEN:
raise NacosException("Insufficient privilege.")
elif e.code == 400:
# 搜不到 code=400
c = e.read()
return json.loads(c.decode("UTF-8"))
else:
raise NacosException("Request Error, code is %s" % e.code)
except Exception as e:
raise NacosException("[get-naming-service] exception %s occur" % str(e))
def list_naming_configs_v2(self, namespace_id=None):
"""
获取指定命名空间下的配置信息列表
:param namespace_id: 命名空间ID
"""
params = {}
namespace_id = namespace_id or self.namespace
if namespace_id:
params["namespaceId"] = namespace_id
try:
resp = self._do_sync_req("/nacos/v2/cs/history/configs", None, params, None, self.default_timeout, "GET")
c = resp.read()
return json.loads(c.decode("UTF-8"))
except HTTPError as e:
if e.code == HTTPStatus.FORBIDDEN:
raise NacosException("Insufficient privilege.")
else:
raise NacosException("Request Error, code is %s" % e.code)
except Exception as e:
raise NacosException("[list-naming-configs] exception %s occur" % str(e))
def list_namespaces_v2(self):
"""
查询当前所有的命名空间
"""
try:
resp = self._do_sync_req("/nacos/v2/console/namespace/list", None, None, None, self.default_timeout, "GET")
c = resp.read()
return json.loads(c.decode("UTF-8"))
except HTTPError as e:
if e.code == HTTPStatus.FORBIDDEN:
raise NacosException("Insufficient privilege.")
else:
raise NacosException("Request Error, code is %s" % e.code)
except Exception as e:
raise NacosException("[list-namespaces] exception %s occur" % str(e))
def get_config_v2(self, data_id, group, namespace_id=None, timeout=None, no_snapshot=None):
no_snapshot = self.no_snapshot if no_snapshot is None else no_snapshot
data_id, group = process_common_config_params(data_id, group)
params = {
"dataId": data_id,
"group": group,
}
namespace_id = namespace_id or self.namespace
if namespace_id:
params["namespaceId"] = namespace_id
cache_key = group_key(data_id, group, self.namespace)
# get from failover
content = read_file_str(self.failover_base, cache_key)
if content is None:
logger.debug("[get-config] failover config is not exist for %s, try to get from server" % cache_key)
else:
logger.debug(
"[get-config] get %s from failover directory, content is %s" % (cache_key, truncate(content)))
return content
# get from server
try:
resp = self._do_sync_req("/nacos/v2/cs/config", None, params, None, timeout or self.default_timeout)
content = resp.read().decode("UTF-8")
except HTTPError as e:
if e.code == HTTPStatus.NOT_FOUND:
logger.warning(
"[get-config] config not found for data_id:%s, group:%s, namespace:%s, try to delete snapshot" % (
data_id, group, self.namespace))
self.delete_file(self.snapshot_base, cache_key)
return None
elif e.code == HTTPStatus.CONFLICT:
logger.error(
"[get-config] config being modified concurrently for data_id:%s, group:%s, namespace:%s" % (
data_id, group, self.namespace))
elif e.code == HTTPStatus.FORBIDDEN:
logger.error("[get-config] no right for data_id:%s, group:%s, namespace:%s" % (
data_id, group, self.namespace))
raise NacosException("Insufficient privilege.")
else:
logger.error("[get-config] error code [:%s] for data_id:%s, group:%s, namespace:%s" % (
e.code, data_id, group, self.namespace))
if no_snapshot:
raise
except Exception as e:
logger.exception("[get-config] exception %s occur" % str(e))
if no_snapshot:
raise
if no_snapshot:
return content
if content is not None:
logger.info(
"[get-config] content from server:%s, data_id:%s, group:%s, namespace:%s, try to save snapshot" % (
truncate(content), data_id, group, self.namespace))
try:
self.save_file(self.snapshot_base, cache_key, content)
except Exception as e:
logger.exception("[get-config] save snapshot failed for %s, data_id:%s, group:%s, namespace:%s" % (
data_id, group, self.namespace, str(e)))
return content
logger.error("[get-config] get config from server failed, try snapshot, data_id:%s, group:%s, namespace:%s" % (
data_id, group, self.namespace))
content = read_file_str(self.snapshot_base, cache_key)
if content is None:
logger.warning("[get-config] snapshot is not exist for %s." % cache_key)
else:
logger.debug(
"[get-config] get %s from snapshot directory, content is %s" % (cache_key, truncate(content)))
return content
def list_naming_instance_v2(self, service_name, clusters=None, namespace_id=None, group_name=None,
healthy_only=False):
"""
:param service_name: 服务名
:param clusters: 集群名称 字符串,多个集群用逗号分隔
:param namespace_id: 命名空间ID
:param group_name: 分组名
:param healthy_only: 是否只返回健康实例 否,默认为false
"""
params = {
"serviceName": service_name,
"healthyOnly": healthy_only
}
if clusters is not None:
params["clusters"] = clusters
namespace_id = namespace_id or self.namespace
if namespace_id:
params["namespaceId"] = namespace_id
group_name = group_name or 'DEFAULT_GROUP'
if group_name:
params['groupName'] = group_name
try:
resp = self._do_sync_req("/nacos/v2/ns/instance/list", None, params, None, self.default_timeout, "GET")
c = resp.read()
logger.info("[list-naming-instance] service_name:%s, namespace:%s, server response:%s" %
(service_name, self.namespace, c))
return json.loads(c.decode("UTF-8"))
except HTTPError as e:
if e.code == HTTPStatus.FORBIDDEN:
raise NacosException("Insufficient privilege.")
else:
raise NacosException("Request Error, code is %s" % e.code)
except Exception as e:
logger.exception("[list-naming-instance] exception %s occur" % str(e))
raise
def publish_config_v2(self, data_id, group, content, namespace_id=None, tag=None, configTags=None, app_name=None,
config_type=None, timeout=None, desc=None):
'''
:param namespaceId: 命名空间,默认为public与 ''相同
:param data_id: 配置名
:param group: 配置组名
:param tag: 标签
:param configTags: 配置标签列表,可多个,逗号分隔
:param content:
:param app_name: 应用名
:param config_type: 配置类型
:param desc : 配置描述
:param timeout:
:return:
'''
if content is None:
raise NacosException("Can not publish none content, use remove instead.")
data_id, group = process_common_config_params(data_id, group)
if type(content) == bytes:
content = content.decode("UTF-8")
logger.info("[publish] data_id:%s, group:%s, namespace:%s, content:%s, timeout:%s" % (
data_id, group, self.namespace, truncate(content), timeout))
params = {
"dataId": data_id,
"group": group,
"content": content.encode("UTF-8"),
"username": self.username,
"password": self.password
}
namespace_id = namespace_id or self.namespace
if namespace_id:
params["namespaceId"] = namespace_id
if app_name: params["appName"] = app_name
if desc: params["desc"] = desc
if tag: params["tag"] = tag
if configTags: params["configTags"] = configTags
if config_type: params["type"] = config_type
headers = {"Content-Type": "application/x-www-form-urlencoded"}
try:
resp = self._do_sync_req("/nacos/v2/cs/config", headers, None, params,
timeout or self.default_timeout, "POST")
c = resp.read()
logger.info("[publish] publish content, group:%s, data_id:%s, server response:%s" % (
group, data_id, c))
return c.decode()
except HTTPError as e:
if e.code == HTTPStatus.FORBIDDEN:
raise NacosException("Insufficient privilege.")
else:
raise NacosException("Request Error, code is %s" % e.code)
except Exception as e:
logger.exception("[publish] exception %s occur" % str(e))
raise
def modify_naming_instance_v2(self, service_name, ip, port, cluster_name=None, weight=None, metadata=None,
enable=None, ephemeral=True, group_name='DEFAULT_GROUP'):
logger.info("[modify-naming-instance] ip:%s, port:%s, service_name:%s, namespace:%s" % (
ip, port, service_name, self.namespace))
params = {
"ip": ip,
"port": port,
"serviceName": service_name,
"ephemeral": ephemeral,
"groupName": group_name
}
if cluster_name is not None:
params["clusterName"] = cluster_name
if enable is not None:
params["enable"] = enable
if weight is not None:
params["weight"] = weight
self._build_metadata(metadata, params)
if self.namespace:
params["namespaceId"] = self.namespace
try:
resp = self._do_sync_req("/nacos/v1/ns/instance", None, None, params, self.default_timeout, "PUT", "naming")
c = resp.read()
logger.info("[modify-naming-instance] ip:%s, port:%s, service_name:%s, namespace:%s, server response:%s" % (
ip, port, service_name, self.namespace, c))
return c.decode()
except HTTPError as e:
if e.code == HTTPStatus.FORBIDDEN:
raise NacosException("Insufficient privilege.")
else:
raise NacosException("Request Error, code is %s" % e.code)
except Exception as e:
logger.exception("[modify-naming-instance] exception %s occur" % str(e))
raise
使用示例
from pyfsops.nacos_api import NacosApi
server_addresses = ','.join(config.NACOS_INFO['address'])
client = NacosApi(server_addresses=server_addresses,
namespace=config.NACOS_INFO['namespace'],
username=config.NACOS_INFO['username'],
password=config.NACOS_INFO['password'])
instance_ip = tools.get_localhost_ip()
def register_nacos():
rq = client.add_naming_instance(service_name="xxxxxxxxxxx", ip=instance_ip, port=18080,
cluster_name="DEFAULT")
rs = json.loads(rq)
if rs['code'] != 0:
logger.error(rs)
# 注册nacos
register_nacos()
def nacos_healthy():
'''发送心跳'''
rq = client.send_heartbeat(service_name="xxxxxxxxxxx", ip=instance_ip, port=18080,
cluster_name="DEFAULT")
# logger.info(rq)
if not rq['lightBeatEnabled']:
logger.error(rq)
在manage.py里可以添加计划任务维持心跳
f = open("scheduler.lock", "wb")
try:
# 为了保证任务只执行一次 需要添加文件锁
fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
scheduler = APScheduler()
scheduler.init_app(app)
scheduler.add_job(func=nacos_healthy, trigger='interval', seconds=15, id="xxxxxxxxxxx", timezone='Asia/Shanghai')
scheduler.start()
except Exception as e:
print(e)