【CMDB 專案架構】基於DRF+Django+Mysql+單例模式

染指未来發表於2024-04-17

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'])
  

相關文章