【CMDB 项目架构】基于DRF+Django+Mysql+单例模式
CMDB 项目概述
项目概述
- 背景
- 大目标:运维自动化
- 小目标:资产管理, 降本增效
- 系统开发出来:可以自动采集资产信息,并且提供给其他系统数据支持
- 运维岗位
- IDC
- 业务
- 桌面
- 监控
- 物理机和虚拟机
项目架构
- 资产采集脚本:远程连接服务器,并获取服务器的资产信息,将资产信息汇报给API
- 资产管理系统:API 负责将资产信息写入数据库,且变会做资产变更记录,为以后搭建自动化运维平台,为其他系统提供数据
- 资产管控平台,为用户提供数据展示和报表。
资产采集方式
- 基于SSH模块:使用paramiko模块采集数据。适用100台左右的服务器群
- 基于 agent模式(client) 采集:
- 适用 服务器比较多的时候,数量1000台
- 每一台Server,存在一个py资产采集脚本,开启定时任务,直接与CMDB系统API交互
- ansible 软件。py编写基于主从架构
- 底层基于:paramiko模块,主节点能主动连接从节点采集信息
- saltstack,puppet 工具 py编写
项目实现(资产采集,API)
资产采集流程
- 脚本,实现资产数据的采集
- api 提供数据上传接口
采集不同硬件设备的数据
-
约束。面向对象中,对于子类的约束。 NotImplementedError
-
反射。根据字符串找到对应的类或者方法(根据字符串导入相关的包)
-
开放封闭原则 。 设计模式: 反射+开放封闭原则=工厂模式
开放:配置文件开发 封闭:对源码的修改封闭 # 动态变更的需求,及时修改配置文件
-
插件模式。插件功能过可扩展
-
多线程/多协程 提高采集速度
小结
-
为什么开发CMDB?
- 公司要搭建自动化运维平台,CMDB的平台搭建是基石 - 提高资产记录的准确性。通过CMDB可以实现资产信息的自动采集和资产变更记录管理
-
依据公司服务器的数量决定。>=100
-
CMDB 如何实现?
- cmdb 包含三部分:资产采集的中控机,API,资产管控平台 - 资产采集部分开发方式:通过paramiko远程操作服务器采集数据,将数据通过API汇报给CMDB平台。采用工厂模式,开放封闭原则,参考django的中间件等。 - API:基于restful 和 drf组件 实现。:资产的入库,资产变更处理 - 资产管控平台:对资产数据的展示 和 资产数据报表的处理。
单例模式
-
在面向对象中,使用单例模式。对实例对象可复用性
-
确保一个类只有一个实例
-
减少内存开销
-
类似于维护一个
全局变量
的变量 -
如何实现单利模式
__new__
实现单利模式- python 文件导入 实现单利模式
- 多线程 对 单利对象 上锁
-
使用场景
-
python 文件导入,也可实现单利模式
# from 和 import 导入一次后。不再导入第二次 得到的logger 对象就是一个 单利对象,不会创建第二个
-
数据库连接池
-
频繁创建对象场景
-
线程池
-
多线程遇到 IO 阻塞时,会造成重新 实例化对象。 解决办法:对创建对象的函数
__new__
方法 上锁🔒# -*-coding:utf-8-*- # 多线程时,需要对对象创建函数。上锁 import threading import time class Singleton(object): instance = None lock = threading.Lock() def __init__(self, i): self.i = i def __new__(cls, *args, **kwargs): # 如果有实例对象,直接返回 if cls.instance: return cls.instance # 上锁。阻止IO阻塞,线程被挂起 with cls.lock: if not cls.instance: time.sleep(1) # 模拟线程阻塞 cls.instance = object.__new__(cls) return cls.instance def task(args): obj = Singleton(args) print(obj) for i in range(10): thread = threading.Thread(target=task, args=(i,)) thread.start() time.sleep(10) return_obj = Singleton(1) print("return_obj:", return_obj)
-
-
日志文件 对象
-
网站计数器
-
django 使用到单例模式
- django-admin的admin.site.register()。 将model注册到 Register对象的字典中
- 配置文件 settings。 将全局配置gloabl_setting 和 自定义配置导入 ,django web对象中
-
资产采集补充
采集命令
-
CPU
dmidecode -q -t 17 2>/dev/null
-
Memory
日志处理
-
可以采用单利模式实现 logger
-
import traceback 模块。反应程序的堆栈信息
import traceback def run(): try: int("asd") except Exception as e: print(traceback.format_exc())
支持 agent 模式 (简单工厂模式)
CMDB 资产采集后为什么不直接放到数据库中?
-
避免高频写入,维护数据库连接多
-
采集的数据需要清洗
-
解耦设计:采集数据和程序执行
-
为其他系统提供数据接口。
资产数据入库
表结构设计
- 主机表
- 硬盘表
- 内存表
- NIC 网卡表
- 主板表
- 部门表
- 用户表
- RBAC ...
编写 api 接口,实现数据入库
利用 工厂模式,对采集的数据分化处理。
# drf 项目结构
- AutoServer # 项目
- api # 对外接口app
- plugins # 插件模块
- __init__.py # 学习django的 settings配置,导入 资产数据解析工厂对象,且是一个单例对象。
- base_data_analysis.py # 资产数据拆解基类
- disk_data_analysis.py # 磁盘信息的拆解处理类
...
### 使用如下面代码:⬇️
__init__.py 实现代码
# -*-coding:utf-8-*-
import importlib
from AutoServer.settings import CMDB_PLUGIN_DICT
class ProcessSeverInfoFactory(object):
def __init__(self):
pass
@staticmethod
def process_server_info(asset_data, server_obj):
"""
# 处理中控机,采集的资产信息
:param asset_data: # 全部资产数据
:param server_obj: # 主机外键
:return:
"""
for key, path in CMDB_PLUGIN_DICT.items():
data = asset_data.get(key, {}) # 每一种解析类对应的采集数据
if not data: # 没有采集该种类的数据,跳过
continue
module_path, class_name = path.rsplit(".", maxsplit=1)
module = importlib.import_module(module_path)
cls = getattr(module, class_name)
print("正在处理:", cls.__name__)
cls_obj = cls()
cls_obj.process(data, server_obj)
psi_factory = ProcessSeverInfoFactory()
views.py 视图脚本
# 导入 psi_factory 资产数据处理工厂单例对象
import datetime
import json
from django.db.models import Q
from rest_framework.response import Response
from rest_framework.views import APIView
from api.models import ServerModel, CpuModel, BoardModel, NicModel, DiskModel, MemoryModel
from api.plugins import psi_factory
class ServerAPIView(APIView):
def post(self, request, *args, **kwargs):
"""
接收资产数据,并添加到数据库中
:param request:
:param args:
:param kwargs:
:return:
"""
request_data = request.data or {}
# 1. Server 主机查询
hostname = request_data.get("hostname", "")
server_obj = ServerModel.objects.filter(hostname=hostname).first()
if not server_obj:
return Response("主机不存在")
# 2. 利用工厂模式,将不同类型的采集信息。分成模块化处理
asset_data = request_data.get("info", "")
psi_factory.process_server_info(asset_data, server_obj)
# 3. 更新主机的采集时间
server_obj.last_date = datetime.datetime.today()
server_obj.save()
return Response("ip:{},资产采集完毕!".format(hostname))
利用 集合的方式对数据实现增删改
-
新增 : 新提交 - db已有的
-
删除 : db已有的 - 新提交的
-
修改 : db已有的 & 新提交的
# 磁盘为例: # -*-coding:utf-8-*- from api.models import DiskModel, ServerAssetChangeRecordModel from api.plugins.base_data_analysis import BaseDataAnalysis class DiskDataAnalysis(BaseDataAnalysis): def process(self, data, server_obj): """ ### 利用 集合的特性:交集(更新),差集(新增,删除)。并集 # 1. 数据新增 # 2. 数据更新 # 3. 数据删除 # 4. 数据变更记录 :param data: :param server_obj: :return: """ if not data['status']: return record_msg_list = [] disk_info = data.get("data", {}) disk_query_set = DiskModel.objects.filter(server=server_obj) db_disk_query_dict = {disk_obj.slot: disk_obj for disk_obj in disk_query_set} # db中的:{"槽位":obj...} # 集合 new_disk_slot_set = disk_info.keys() old_disk_slot_set = db_disk_query_dict.keys() # 1. 新增 add_set = new_disk_slot_set - old_disk_slot_set batch_add_lst = [] for slot_index, new_value in disk_info.items(): if slot_index in add_set: batch_add_lst.append(DiskModel(**new_value, server=server_obj)) if add_set: DiskModel.objects.bulk_create(batch_add_lst, batch_size=10) msg = "【新增硬盘】在 {} 槽位增加了硬盘".format(",".join(add_set)) record_msg_list.append(msg) # 2. 删除 delete_set = old_disk_slot_set - new_disk_slot_set if delete_set: DiskModel.objects.filter(slot__in=delete_set).delete() msg = "【删除硬盘】在 {} 槽位删除了硬盘".format(",".join(delete_set)) record_msg_list.append(msg) # 3. 更新 update_set = old_disk_slot_set & new_disk_slot_set for update_slot_index in update_set: update_msg_lst = [] new_disk_dict = disk_info[update_slot_index] old_disk_object = db_disk_query_dict[update_slot_index] for update_new_key, update_new_val in new_disk_dict.items(): # 根据字段获取数据表中起的verbose_name verbose_name = DiskModel._meta.get_field(key).verbose_name # 利用反射知识。修改ORM if update_new_val != getattr(old_disk_object, update_new_key): msg = "{}由{}变更为{}".format(verbose_name, getattr(old_disk_object, update_new_key), update_new_val) update_msg_lst.append(msg) setattr(old_disk_object, update_new_key, update_new_val) if update_msg_lst: msg = "【更新硬盘】槽位[{}] : {} ".format(update_slot_index, ",".join(update_msg_lst)) record_msg_list.append(msg) old_disk_object.save() # 4. 日志 if record_msg_list: ServerAssetChangeRecordModel.objects.create(server=server_obj, content="\n".join(record_msg_list))
资产变更记录数据表存储的 字段改为 db数据表中的verbose_name
# 根据字段获取数据表中起的verbose_name
verbose_name = Server._meta.get_field(key).verbose_name
使用django orm 的Q实现复杂的查询条件
import datetime
import json
from django.db.models import Q
from rest_framework.response import Response
from rest_framework.views import APIView
from api.models import ServerModel, CpuModel, BoardModel, NicModel, DiskModel, MemoryModel
from api.plugins import psi_factory
class ServerAPIView(APIView):
def get(self, request, *args, **kwargs):
"""
# 查询 执行资产数据变更的 IP列表
:param request:
:param args:
:param kwargs:
:return:
"""
today = datetime.datetime.today()
ip_lst = ServerModel.objects \
.filter(status="online") \
.filter(Q(last_date__isnull=True) | Q(last_date__lt=today)) \
.values("hostname")
print("今日未采集IP:", ip_lst)
ip_lst = ["xxxx", ]
return Response(ip_lst)
资产采集小结
中控机 汇报给 API 的资产。需要做变更记录的处理
- 由于资产采集时,利用`工厂模式`实现可扩展插件。在API端也使用相同模式对数据进行一一处理
- 在拆解资产信息时,利用交集的特性(交集和差集)。实现删除/更新/新增
- 实现更新操作时,利用ORM+面向对象中的反射。实现对新旧资产数据的比对和记录的修改
插件模式(使用import_module导入子模块)
# 目录结构
- plugins
- __init__.py # ProcessFactory 工厂对象
- base_data_analysis.py # 基类
- board_data_analysis.py # 具体实现自类
### __init__.py 实现
# -*-coding:utf-8-*-
import importlib
from AutoServer.settings import CMDB_PLUGIN_DICT
class ProcessSeverInfoFactory(object):
def __init__(self):
pass
@staticmethod
def process_server_info(asset_data, server_obj):
"""
# 处理中控机,采集的资产信息
:param asset_data: # 全部资产数据
:param server_obj: # 主机外键
:return:
"""
for asset_class, path in CMDB_PLUGIN_DICT.items():
data = asset_data.get(asset_class, {}) # 每一种解析类对应的采集数据
if not data: # 没有采集该种类的数据,跳过
continue
module_path, class_name = path.rsplit(".", maxsplit=1)
module = importlib.import_module(module_path)
cls = getattr(module, class_name)
print("#" * 40)
print("资产采集正在解析:", cls.__name__)
cls_obj = cls(asset_class=asset_class)
cls_obj.process(data, server_obj)
print("资产采集解析完毕:", cls.__name__)
print("#" * 40)
psi_factory = ProcessSeverInfoFactory()
简单后台
-
新增Server
- Server_add.html
- Server_add_Model_Form
-
查询 Server列表
# -*-coding:utf-8-*- from django.shortcuts import render from api.models import ServerModel def server_index(request, *args, **kwargs): print(request, args, kwargs) server_query = ServerModel.objects.all() return render(request, "web/server_index.html", {"server_lst": server_query})
-
图表 统计不同部门的使用资产数量。饼图为例,highChars
# -*-coding:utf-8-*- from django.db.models import Count from django.http import JsonResponse from django.shortcuts import render from api.models import ServerModel def server_depart_analysis(request, *args, **kwargs): result = ServerModel.objects.all().values("depart__title").annotate(ct=Count('depart__title')) analysis_data_lst = [] for each_item in result: analysis_data_lst.append({ "name": each_item.get("depart__title", "未知"), "y": each_item.get("ct"), "sliced": True, "selected": True }) return JsonResponse(analysis_data_lst, safe=False)
-
前端示例代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>123</title> <!-- 最新版本的 Bootstrap 核心 CSS 文件 --> <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous"> <!-- 可选的 Bootstrap 主题文件(一般不用引入) --> <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous"> </head> <body> <div class="panel panel-default"> <!-- Default panel contents --> <div class="panel-heading">主机列表</div> <div class="panel-body"> <p>主机部门使用图</p> <div id="container" style="min-width:400px;height:400px"></div> </div> <!-- Table --> <table class="table"> <thead> <td>ID</td> <td>主机名</td> <td>状态</td> <td>最后更新</td> <td>部门</td> </thead> <tbody> {% for server_item in server_lst %} <tr> <td>{{ server_item.id }}</td> <td>{{ server_item.hostname }}</td> <td>{{ server_item.get_status_display }}</td> <td>{{ server_item.last_date|date:"Y-m-d" }}</td> <td>{{ server_item.depart.title }}</td> </tr> {% endfor %} </tbody> </table> </div> </body> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <!-- 最新的 Bootstrap 核心 JavaScript 文件 --> <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script> <script src="https://code.hcharts.cn/highcharts/highcharts.js"></script> <script src="https://code.hcharts.cn/highcharts/modules/exporting.js"></script> <script src="https://code.hcharts.cn/plugins/zh_cn.js"></script> <script> function depart_server_analysis(title, data) { // Build the chart Highcharts.chart('container', { chart: { plotBackgroundColor: null, plotBorderWidth: null, plotShadow: false, type: 'pie' }, title: { text: title }, tooltip: { pointFormat: '{series.name}: <b>{point.percentage:.1f}%</b>' }, plotOptions: { pie: { allowPointSelect: true, cursor: 'pointer', dataLabels: { enabled: false }, showInLegend: true } }, series: [{ name: 'Brands', colorByPoint: true, data: data }] }); } $.ajax({ url: "http://127.0.0.1:8000/v1/server/server_depart_analysis/", method: "GET" }).then(res => { console.log(res) let title = '2024年各个部门使用Server份额' depart_server_analysis(title, res) }) </script> </html>
部署项目
部署 资产采集项目 AutoClient
# 配置
- 数据提交API接口地址修改为AutoSever发布的地址
- 配置 采集资产模式。采用 saltStack/SSH
- 采集资产任务。采用定时任务 crontab 系统定时任务,执行时间1点30采集
- 30 1 * * * python3 app.py
部署 后台资产管理系统 AutoServer
- mysql 5.6
- django3.x
- uwsgi
- nginx 部署
linux 远程执行命令方式
ssh
# 基于ssh公私密钥
# 1. 生成公私密钥
ssh-keygen -t rsa # 四个回车
# 2. ssh-copy-id -i 拷贝公钥到远程服务器
ssh-copy-id -i ~/.ssh/id_rsa.pub root@xxx.xxx.xxx.xxx # 自动创建authorized_keys到远程目录
# 3. 修改资产采集项目配置。 采用ssh方式执行 linux 资产采集命令
setting.py 中的 : MODE = "SSH"
# 4. python2/3使用 paramiko 创建ssh执行对象
import paramiko
private_key = paramiko.RSAKey.from_private_key_file('/home/auto/.ssh/id_rsa')
# 创建SSH对象
ssh = paramiko.SSHClient()
# 允许连接不在know_hosts文件中的主机
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 连接服务器
ssh.connect(hostname='xxx.xxx.xxx.xxx', port=22, username='root', key=private_key)
# 执行命令
stdin, stdout, stderr = ssh.exec_command('df')
# 获取命令结果
result = stdout.read()
# 关闭连接
ssh.close()
SaltStack
# 基于 saltstack 工具
# 1. master 端 安装 salt
1. 安装salt-master
yum install salt-master
2. 修改配置文件:/etc/salt/master
interface: 0.0.0.0 # 表示Master的IP
3. 启动
service salt-master start
# 2. 远程服务器 安装 salt-minion
1. 安装salt-minion
yum install salt-minion
2. 修改配置文件 /etc/salt/minion
master: 10.211.55.4 # master的地址
或
master:
- 10.211.55.4
- 10.211.55.5
random_master: True
id: c2.salt.com # 客户端在salt-master中显示的唯一ID
3. 启动
service salt-minion start
# 3. 授权。即:远程服务器被master授权
salt-key -L # 查看已授权和未授权的slave
salt-key -a salve_id # 接受指定id的salve
salt-key -r salve_id # 拒绝指定id的salve
salt-key -d salve_id # 删除指定id的salve
# 4. 修改资产采集项目配置。采用SALT模式进行资产采集
setting.py 中的 : MODE = "SALT"
# 5. python2 不支持python3 需要具体情况具体分析
linux :
指令: salt 'c2.salt.com' cmd.run 'ifconfig'
python2:
import salt.client
local = salt.client.LocalClient()
result = local.cmd('c2.salt.com', 'cmd.run', ['ifconfig'])