事件推送网关:让cmdb告别“花瓶”

前言

众所周知cmdb在运维过程当中的重要性,可是咱们不但愿它是一个“花瓶”,所以《运维思索:cmdb打通zabbix、jumpserver探索》成了咱们当前面临的一个课题。而咱们能够借助cmdb的事件推送来解决此问题,所以引入了事件推送网关。python

事件推送网关是蓝鲸cmdb事件推送的一个目标系统,当cmdb中配置信息发生变化时,会实时通知到事件推送网关,由网关统一关联到各运维子系统,如jumpserver、zabbix等,实现配置信息的一致性同步,为上层应用作好数据支撑。nginx

各系统的一致性同步的前提是全部的配置信息来源为cmdb,这就要求资产配置必须经过cmdb配置,由事件推送统一同步,而不是在各系统手动配置(仅指资产分组等基础配置),不然最终将打破一致性同步,此时cmdb将走上“花瓶”之路。redis

解决方案

在正式使用事件推送网关前,咱们须要了解cmdb各类事件推送发出的HTTP请求区别,以便咱们的网关作出相应的操做。django

1.开发框架

事件推送网关是经过python3.9+django3.2开发,用于cmdb事件推送进行回调。json

# 1.python环境
conda create -n gateway python=3.9
source activate gateway
pip install django redis 

# 2.建立项目
django-admin startproject gateway
cd gateway
python manage.py startapp gw_cmdb

# 3.settings配置
vim gateway/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # 添加app
    'gw_cmdb'
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    # 关闭csrf验证
    #'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

# 4.日志配置(默认在项目目录gateway下logs目录下)
cur_path = os.path.dirname(os.path.realpath(__file__))  # log_path是存放日志的路径
log_path = os.path.join(os.path.dirname(cur_path), 'logs')
if not os.path.exists(log_path): os.mkdir(log_path)  # 若是不存在这个logs文件夹,就自动建立一个 
LOGGING = {
    'version': 1,
    'disable_existing_loggers': True,
    'formatters': {
        # 日志格式
        'standard': {
            'format': '[%(asctime)s] [%(filename)s:%(lineno)d] [%(module)s:%(funcName)s] '
                      '[%(levelname)s]- %(message)s'},
        'simple': {  # 简单格式
            'format': '%(levelname)s %(message)s'
        },
    },
    # 过滤
    'filters': {
    },
    # 定义具体处理日志的方式
    'handlers': {
        # 默认记录全部日志
        'default': {
            'level': 'INFO',
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': os.path.join(log_path, 'all-{}.log'.format(time.strftime('%Y-%m-%d'))),
            'maxBytes': 1024 * 1024 * 5,  # 文件大小
            'backupCount': 5,  # 备份数
            'formatter': 'standard',  # 输出格式
            'encoding': 'utf-8',  # 设置默认编码,不然打印出来汉字乱码
        },
        # 输出错误日志
        'error': {
            'level': 'ERROR',
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': os.path.join(log_path, 'error-{}.log'.format(time.strftime('%Y-%m-%d'))),
            'maxBytes': 1024 * 1024 * 5,  # 文件大小
            'backupCount': 5,  # 备份数
            'formatter': 'standard',  # 输出格式
            'encoding': 'utf-8',  # 设置默认编码
        },
        # 控制台输出
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'formatter': 'standard'
        },
        # 输出info日志
        'info': {
            'level': 'INFO',
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': os.path.join(log_path, 'info-{}.log'.format(time.strftime('%Y-%m-%d'))),
            'maxBytes': 1024 * 1024 * 5,
            'backupCount': 5,
            'formatter': 'standard',
            'encoding': 'utf-8',  # 设置默认编码
        },
    },
    # 配置用哪几种 handlers 来处理日志
    'loggers': {
        # 类型 为 django 处理全部类型的日志, 默认调用
        'django': {
            'handlers': ['default', 'console'],
            'level': 'INFO',
            'propagate': False
        },
        # log 调用时须要看成参数传入
        'log': {
            'handlers': ['error', 'info', 'console', 'default'],
            'level': 'INFO',
            'propagate': True
        },
    }
}

复制代码

2.目录结构

D:\work\blueking>tree /f gateway
└─gateway
    │  manage.py
    │
    ├─gateway
    │  │  asgi.py
    │  │  settings.py
    │  │  urls.py
    │  │  wsgi.py
    │  │  __init__.py
    │
    └─gw_cmdb
        │  admin.py
        │  apps.py
        │  models.py
        │  urls.py
        │  views.py
        │  __init__.py
        │
        ├─common 
        │    cmdb.py
        │
        ├─jumpserver(暂未开放)
        ├─zabbix(暂未开放)
复制代码

其中:vim

  • gateway 为 项目目录;api

  • gw_cmdb 为app目录;markdown

  • gw_cmdb/views.py 为统一接收cmdb事件推送的http请求,并关联其余子系统的模块;session

  • gw_cmdb/common/cmdb.py 为解析views.py接收 http请求类型的模块目录,返回指定数据格式的参数;app

  • gw_cmdb/jumpserver 为网关实现jumpserver相关操做的模块总目录;

  • gw_cmdb/zabbix 为网关实现zabbix相关操做的模块总目录;

3.部署

# 1.项目路由
vim gateway/urls.py
from django.contrib import admin
from django.urls import path
from django.conf.urls import include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('cmdb/',include('gw_cmdb.urls'))
]

# 2.app路由
from django.urls import path
from . import views

# 统一接收cmdb事件推送的http请求;
urlpatterns = [
    path(r'',views.cmdb_request),
]

# 3.cmdb_request
# 接收cmdb事件推送网关的http请求
vim gw_cmdb/view.py
from django.shortcuts import render
from django.http import HttpRequest,HttpResponse
from .common.cmdb import cmdb
from .zabbix.main import zabbix_main
import json
import logging

logger = logging.getLogger('log')

# Create your views here
def cmdb_request(request):
    if request.method == 'POST':
        data = json.loads(request.body)
        logger.info('cmdb发送消息:{}'.format(data))
        ## 获取指定数据格式的参数
        res=cmdb(data)
        ##是否须要联动zabbix及jumpserver
        if res['result'] == 1:
            return HttpResponse("ok")
        else:
            logger.info(res['data'])
            #zabbix
            return HttpResponse("ok")
            #jumpserver
            return HttpResponse("ok")
    else:
        logger.info('本接口只支持POST模式')
        return HttpResponse("本接口只支持POST模式")

        
# 4.启动
python manage.py runserver 0.0.0.0:8000
复制代码

4.事件推送解析

事件推送网关启动后,咱们在蓝鲸cmdb上的操做触发后,cmdb将回调事件推送网关,网关将打印接收的请求:

(1)将主机(10.164.193.138)从“空闲机”转移至“回收计划(集群)”-“未分类(模块)”

查看日志gateway/logs/info-2021-05-21.log

8.png

由图可知,cmdb事件推送作了如下操做:

  • delete动做:将主机从“空闲机” 删除;

  • create动做:在“回收计划”集群中建立主机;

  • update动做:在集群中关联相关模块“未分类”,重复2次;

此时,这4个请求request_id相同,并且update动做重复了2次。

(2)将主机(10.164.193.138)追加新模块“nginx(模块)”

9.png

由图可知,cmdb事件推送作了如下操做:

  • delete动做:将主机从“未分类(模块)” 删除;

  • create动做:在“未分类(模块)”集群中建立主机;

  • create动做:在“nginx(模块)”集群中建立主机;

  • update动做:在集群中关联相关模块“未分类”、“nginx”,重复3次;

此时,这6个请求request_id相同,并且update动做重复了2次。

(3)将主机(10.164.193.138)只删除模块“nginx(模块)”

10.png

由图可知,cmdb事件推送作了如下操做:

  • delete动做:将主机从“未分类(模块)” 删除;

  • delete动做:将主机从“nginx(模块)” 删除;

  • create动做:在“未分类(模块)”集群中建立主机;

  • update动做:在集群中关联相关模块“未分类”,重复3次;

此时,这6个请求request_id相同,并且update动做重复了3次。

(4)将主机(10.164.193.138)转移至“空闲机”

11.png

由图可知,cmdb事件推送作了如下操做:

  • delete动做:将主机从“未分类(模块)” 删除;

  • create动做:在“空闲机(既是集群又是模块)”集群中建立主机;

  • update动做:在集群中关联相关模块“未分类”,重复2次;

此时,这6个请求request_id相同,并且update动做重复了2次。

综合以上4种状况,咱们能够得出如下结论:

  1. 每次配置变动的流程为:删除(delete)--建立(create)--更新(update);

  2. update的次数=delete次数+create次数;

  3. 咱们最终关心的结果是update,而不是delete和create,但因为update重复,所以咱们须要对update过滤并去重,可借助redis+request_id(后期须要再增长ip信息)做为惟一key去重;

基于update动做的结果,咱们能够过滤出真正须要的参数。

参数解析

从日志打印看,update的信息有点冗余,此时就须要对结果进行解析,获取指定的数据格式。

vim gw_cmdb/common/cmdb.py
import redis
import json
import hashlib

r = redis.StrictRedis(host='127.0.0.1',port=6379,db=1)

def cmdb(data):
    ##定义数据格式
    datajson={'key':'','data':{'ip':'','group':[]}}
    ##获取本次cmdb动做id 
    ##判断是为cmdb变更操做
    if data['action'] == 'update':
        for i in data['data']:
            datajson['data']['ip'] = i['cur_data']['bk_host_innerip']
            grouplist = i['cur_data']['associations']
            for j in grouplist:
                groupname = grouplist[j]['bk_set_name']+"_"+grouplist[j]['bk_biz_name']+"_"+grouplist[j]['bk_module_name']
                datajson['data']['group'].append(groupname)
            datajson['key']= hashlib.md5((data['request_id']+ i['cur_data']['bk_host_innerip']).encode('utf-8')).hexdigest()
        rkey = r.hget('cmdb',datajson['key'])
        if rkey is None:
            r.hset('cmdb',datajson['key'],json.dumps(datajson['data']))
            result = {
                'result': 0,
                'data': datajson
            }
        else:
            result = {
                'result': 1,
                'data': datajson
            }
    else:
        result = {
                'result': 1,
                'data': datajson
            }
        return result
    return result
复制代码

(1)将主机(10.164.193.138)从“空闲机”转移至“回收计划(集群)”-“未分类(模块)”

12.png

排除delete、create、update 的动做请求后,就能够获得咱们定义的数据格式结果:

{'key': '3005575b6d1b681c236896a8d35d199e', 'data': {'ip': '10.164.193.138', 'group': ['回收计划_回收计划_未分类']}}
复制代码

(2)将主机(10.164.193.138)追加新模块“nginx(模块)”

13.png

排除delete、create、update 的动做请求后,就能够获得咱们定义的数据格式结果:

{'key': '3005575b6d1b681c236896a8d35d199e', 'data': {'ip': '10.164.193.138', 'group': ['回收计划_回收计划_未分类', '回收计划_回收计划_nginx']}}
复制代码

(3)将主机(10.164.193.138)转移至“空闲机”

14.png

排除delete、create、update 的动做请求后,就能够获得咱们定义的数据格式结果:

{'key': '3005575b6d1b681c236896a8d35d199e', 'data': {'ip': '10.164.193.138', 'group': ['空闲机池_回收计划_空闲机']}}
复制代码

最后将根据得到指定数据格式的参数,传递给各子系统api用于主机分组配置等基础信息一致性同步,这样就保证了cmdb的统一数据源特性。

守护进程

为保证咱们的网关可以自启动,咱们使用supervisor进行守护,配置以下:

# 1.安装
yum install supervisor
#开机启动
systemctl enable supervisord
#查看是否开机自启动
systemctl is-enabled supervisord

# 2.配置文件
vim /etc/supervisord.d/gateway.ini
[program:gateway]
;cmdb事件推送wa男公关
;启动用户
user=root
;程序启动命令
command=/usr/local/miniconda/envs/gateway/bin/python manage.py runserver 0.0.0.0:8000
;程序启动目录
directory=/app/python/gateway
;在supervisord启动时自启动
autostart=true
;程序异常退出后自动重启,可选值:[unexpected,true,false],默认为unexpected
autorestart=true
;启动10秒后没有异常退出,就表示进程正常启动了
startsecs=10
;启动失败自动重试次数
startretries=3

# 3.启动
supervisorctl update
supervisorctl status gateway
supervisorctl start gateway
复制代码

总结

事件推送网关想法始于不断的重复维护cmdb,每次都耗费了大量的精力,投入产出比彻底不匹配!虽然就cmdb来讲,并非非用不可,而是考虑到这是一种行业标准,是运维路上的基石,咱们想走更远而不是知足于现状。

试想一下,维护一套数据源,节省n套关联系统的基础信息维护的时间,这难道不香吗?

相关文章
相关标签/搜索