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