【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'])
  
posted @ 2024-04-17 00:23  染指未来  阅读(94)  评论(0编辑  收藏  举报