1.什么是cmdb
配置管理数据库 ,存储基础设备的各种信息配置等
CMDB可以存储并自动发现整个IT网络上的各种信息,比如一个IT网络上有多少台服务器、多少存储、设备的品牌、资产编号、维护人员、所属部门、服务器上运营什么操作系统、操作系统的版本、操作系统上有哪些应用、每个应用的版本等等,不仅如此,CMDB还有一个非常重要的功能——存储不同资源之间的依赖关系,如果网络上某个节点出现问题(比如某个服务器down了),通过CMDB,可以判断因此受到影响的业务
CMDB由三个部分实现 : api系统(django) + 资产采集系统 + 后台管理系统
2.知识点分解
1)django实现api ,python普通项目实现client ,完成二者间的通信
api使用Django提供的rest_framwork模块
APIView避免csrf对post的限制
request.data中存放了客户端post请求的数据
Response方法将数据进行json转换
client使用requests和json模块
requests.get对api发起get请求获取到的数据可以使用content拿出Byte类型
requests.post对api发起post提交数据必须提交json编码后的类型
###api ##路由 urlpatterns = [ url(r'^asset/', views.Asset.as_view()), ] ##视图函数 from django.shortcuts import render, HttpResponse from rest_framework.views import APIView from rest_framework.response import Response class Asset(APIView): def get(self, request): server_info = ['master1', 'master2'] return Response(server_info) def post(self, request): print(request.data) return HttpResponse('200ok')
###client端
import requests import json client_info = {'hostname': 'c1.com'} r1 = requests.get( url='http://127.0.0.1:8000/api/asset/' ) r1_data = json.loads(r1.content.decode('utf-8')) print(r1_data) r2 = requests.post( ###post提交数据一定要加请求头部标记json格式 url='http://127.0.0.1:8000/api/asset/', data=json.dumps(client_info).encode('utf-8'), headers={ 'content-type': 'application/json' } )
2)通过字符串加载文件中的类 ,实现开放封闭必备小点
我们如何在配置中定义使用哪个文件中的类呢 ,使用字典 ,以点分割 目录.文件.类名
如 : agent模式对应了一个字符串它对应了 /src/engine/agent.py文件中的AgentHandler类
###settings.py
ENGINE = 'agent' ENGINE_DICT = { 'agent': 'src.engine.agent.AgentHandler', 'ssh': 'src.engine.ssh.SSHHandler', }
####设计一个函数完成从文件中获取类
import importlib
def import_string(class_string):
"""
根据配置中字符串获取文件中的类
:param 'src.engine.agent.AgentHandler',
module, engine = src.engine.agent 和 AgentHandler
importlib.import_module()就是import src.engine.agent
engine_class = 反射获取到AgentHandler类
"""
module, engine = class_string.rsplit('.', maxsplit=1)
module_file = importlib.import_module(module)
engine_class = getattr(module_file, engine)
return engine_class
3.CMDB资产采集系统简单实现要点
采集模式engine
agent模式 ,每台主机安装client ,执行命令将采集的数据上报api
中控机模式(ssh ansible) ,使用一台机器远程所有的主机 ,执行命令完成资产采集 ,再将数据上报api
调用issa层的api接口直接获取基础设备信息
程序设计思想
开放封闭原则 ,代码封闭 ,配置开放(定义当前的使用的engine与plugin),支持扩展
程序设计关注点
资产采集
engine采集模式可扩展 (抽象接口)
plugin命令插件可扩展 (抽象接口)
唯一标识
错误处理
日志
4.CMDB资产采集系统简单实现代码
1)API端后续完善 ,先使用标题2-1的api
2)新建项目 ,完善常规目录
3)程序入口/bin/client
执行run()
import os import sys sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from src.script import run if __name__ == '__main__': run()
4)资产采集上报入口/src/script
定义run()
根据配置settings实例化engine的对象 ,并执行对象的handler方法
from conf import settings from lib.import_class import import_string #标题2-2的根据配置字符串取类 def run(): """资产采集入口""" engine_path = settings.ENGINE_DICT.get(settings.ENGINE) engine_class = import_string(engine_path) obj = engine_class() obj.handler()
5)采集模式engine实现handler()方法
定义基类约束BaseHandler() 要求每个engine必须有handler()完成采集与上报, cmd()完成调用命令窗口
定义基类简化ssh这一类的插件SSHandSaltHandler(BaseHandler) ,这一类的handler()方法都是从api获取主机列表 ,远程采集设备信息 ,再提交api ,所以会出现同时对很多的主机进行连接 ,这里使用线程池
import requests from concurrent.futures import ThreadPoolExecutor from ..plugins import get_server_info import json class BaseHandler(): """ 定义engine的基类 ,每个engine都要有这两个方法 """ def handler(self): raise NotImplementedError('handler() must be Implemented') def cmd(self, command, hostname=None): raise NotImplementedError('cmd() must be Implemented') class SShandSaltHandler(BaseHandler): """ 简化ssh这一类engine的代码 """ def handler(self): # 1.获取主机列表 r1 = requests.get( url='http://127.0.0.1:8000/api/asset/', ) host_list = r1.json() # 2.创建线程池 pool = ThreadPoolExecutor(20) # 3.提交任务给线程池 for hostname in host_list: pool.submit(self.task, hostname) def task(self, hostname): """线程池任务""" info = get_server_info(self, hostname) r1 = requests.post( url='http://127.0.0.1:8000/api/asset/', data=json.dumps(info).encode('gbk'), headers={ 'content-type': 'application/json' } )
engine--agent模式实现handler()
cmd()方法使用subprocess模块完成本地命令调用
from src.engine.base import BaseHandler from ..plugins import get_server_info import requests import json class AgentHandler(BaseHandler): """定义cmd窗口 ,操控资产采集 + 上报""" def cmd(self, command, hostname=None): import subprocess return subprocess.getoutput(command) def handler(self): info = get_server_info(self) r1 = requests.post( url='http://127.0.0.1:8000/api/asset/', data=json.dumps(info).encode('gbk'), headers={ 'content-type': 'application/json' } )
engine--ssh模式实现handler()
cmd()方法使用paramiko模块完成远程命令调用 (秘钥需要配置在settings中)
from src.engine.base import SShandSaltHandler from conf import settings class SSHandler(SShandSaltHandler): """仅定义cmd即可 ,采集与上报在父类中""" def cmd(self, command, hostname=None): import paramiko private_key = paramiko.RSAKey.from_private_key_file(settings.SSH_PRIVATE_KEY) ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect(hostname=hostname, port=settings.SSH_PORT, username=settings.SSH_USER, pkey=private_key) stdin, stdout, stderr = ssh.exec_command(command) result = stdout.read() ssh.close() return result
6)handler方法中的数据采集使用了get_server_info()方法
script.py与__init__有异曲同工之处 ! 依据配置应用不同插件
from conf import settings from lib.import_class import import_string def get_server_info(handler, hostname=None): """采集信息入口""" info = {} for name, path in settings.PLUGINS_DICT.items(): """ {'disk':'src.plugins.disk.Disk'} """ plugin_class = import_string(path) obj = plugin_class() info = obj.process(handler, hostname) return info
7)命令插件plugins实现process()方法
定义基类约束BasePlugin
增加debug属性判断是否为调试模式 ,增加项目根路径
每个命令插件都要分为window与linux的具体系统判断执行什么命令
from conf import settings class BasePlugin: def __init__(self): self.debug = settings.DEBUG self.base_dir = settings.BASE_DIR def get_os(self, handler, hostname=None): ## 调试 # os = handler.cmd('命令',hostname) return 'linux' def win(self, handler, hostname=None): raise NotImplementedError('win() must be Implemented') def linux(self, handler, hostname=None): raise NotImplementedError('linux() must be Implemented') def process(self, handler, hostname=None): os = self.get_os(handler, hostname) if os == 'win': ret = self.win(handler, hostname) else: ret = self.linux(handler, hostname) return ret
查看硬盘的命令插件
其中process--->调用win或者linux--->执行handler对象的cmd()方法 (这个对象最开始就被当做参数传过来了)
其中还会有debug判断 ,如果是调试就直接从文件获取数据了
其中parse方法是将采集的数据格式化 ,需要根据api所需要的格式进行格式化
from .base import BasePlugin import os import re class Disk(BasePlugin): def win(self, handler, hostname=None): ret = handler.cmd('dir', hostname)[:60] return 'Disk' def linux(self, handler, hostname=None): if self.debug: with open(os.path.join(self.base_dir, 'files', 'disk.out')) as f: ret = f.read() else: ret = handler.cmd('ifconfig', hostname)[:60] return self.parse(ret) def parse(self, content): """ 解析shell命令返回结果 :param content: shell 命令结果 :return:解析后的结果 """ response = {} result = [] for row_line in content.split("\n\n\n\n"): result.append(row_line) for item in result: temp_dict = {} for row in item.split('\n'): if not row.strip(): continue if len(row.split(':')) != 2: continue key, value = row.split(':') name = self.mega_patter_match(key) if name: if key == 'Raw Size': raw_size = re.search('(\d+\.\d+)', value.strip()) if raw_size: temp_dict[name] = raw_size.group() else: raw_size = '0' else: temp_dict[name] = value.strip() if temp_dict: response[temp_dict['slot']] = temp_dict return response @staticmethod def mega_patter_match(needle): grep_pattern = {'Slot': 'slot', 'Raw Size': 'capacity', 'Inquiry': 'model', 'PD Type': 'pd_type'} for key, value in grep_pattern.items(): if needle.startswith(key): return value return False
查看内存的命令插件
from .base import BasePlugin import os from lib import convert class Memory(BasePlugin): def win(self, handler, hostname=None): """ windowns下执行命令 :param handler: :param hostname: :return: """ ret = handler.cmd('dir', hostname)[:60] return 'Disk' def linux(self, handler, hostname=None): if self.debug: with open(os.path.join(self.base_dir, 'files', 'memory.out')) as f: ret = f.read() else: ret = handler.cmd('lsblk', hostname)[:60] return ret def parse(self, content): """ 解析shell命令返回结果 :param content: shell 命令结果 :return:解析后的结果 """ ram_dict = {} key_map = { 'Size': 'capacity', 'Locator': 'slot', 'Type': 'model', 'Speed': 'speed', 'Manufacturer': 'manufacturer', 'Serial Number': 'sn', } devices = content.split('Memory Device') for item in devices: item = item.strip() if not item: continue if item.startswith('#'): continue segment = {} lines = item.split('\n\t') for line in lines: if len(line.split(':')) > 1: key, value = line.split(':') else: key = line.split(':')[0] value = "" if key in key_map: if key == 'Size': segment[key_map['Size']] = convert.convert_mb_to_gb(value, 0) else: segment[key_map[key.strip()]] = value.strip() ram_dict[segment['slot']] = segment return ram_dict
5.简单一些想法完成开放封闭
首先属于同类功能 ,这类的功能是需要不停增加的 ,或者说可以选用的都通过配置实现 ,使用抽象类约束
配置 :指定操作者 ,操作者使用的工具
生成操作者(script)
handler操作者: 可以操作任意工具
生成操作者使用的工具(__init__)
plugin工具: 网卡查看工具 硬盘查看工具查看