1 CMDB概念
CMDB : Configuraion Management Database配置管理数据库,CMDB存储与管理企业IT架构中设备的各种配置信息,它与所有服务支持和服务交付流程都紧密相连,支持这些流程的运转、发挥配置信息的价值,同时依赖于相关流程保证数据的准确性。
在实际项目中,CMDB常常被认为是构建其他ITIL流程的基础而有限考虑,ITIL项目的成败与是否成功建立CMDB有非常大的关系。70%~80%的IT相关问题与环境的变更有着更直接的关系。实施变更的难点和重点并不是工具,而是流程。即通过一个自动化的,可重复的流程管理变更,使得当变更发生的时候,有一个标准化的流程去执行,能够预测到这个变更对整个系统管理产生的影响,并对这些影响进行评估和控制。而变更管理流程自动化的实现关键就是CMDB。
CMDB工具中至少包括这几种关键的功能:整合、调和、同步、映射和可视化。
整合:是指能够充分利用来自其他数据源的信息,对CMDB中包含的记录源属性进行存取,将多个数据源合并至一个视图中,生成连同来自CMDB和其他数据源信息在内的报告。
调和:调和能力是指通过对来字每个数据源的匹配字段进行对比,保证CMDB中的记录在多个数据源中没有重复现象,维持CMDB中每个配置项目数据源的完整性;自动调整流程使得初始实施、数据库管理员的手动运作和现场维护支持工作将至最低。
同步:是指确保CMDB中的信息能够反映联合数据源的更新情况,在联合数据源更新频率的基础上确定CMDB更新日程,按照经过批准的变更来更新CMDB,找出未被批准的变更。
应用映射与可视化:说明应用间的关系并反应应用和其他组件之间的依存关系,了解变更造成的影响并帮助诊断问题。
CMDB是运维自动化项目,它可以减少人工干预,降低人员成本。
功能:自动装机、实时监控、自动化部署软件,建立在他们的基础上是资产信息变更记录(资产管控自动进行汇报)
2 资产采集
2.1 资产采集分析
CMDB资产管理项目的代码可以分为:资产采集部分,api部分,后台管理部分。
资产采集部分:
--- 采集资产:在各个服务器(在CMDB中是客户端)执行采集数据信息命令,使用正则或字符串匹配方式获取想要数据。
如果此服务器是中控机,需要有执行命令的各个主机名,和执行的命令。
如果服务器为agent,则只需要执行命令。
--- 兼容性(各种资产采集的架构:agent,SSH或者salt都可以兼容)
--- 汇报数据
需要复习知识点:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 1 高内聚,低耦合 2 2 反射: getattr(obj,'xxx') 3 3 导入模块: 4 import re 5 import importlib 6 importlib.import_module('re') 7 django中是如何导入模块的: 8 m = importlib.import_module('django.middleware.clickjacking') 9 cls = getattr(m,'XFrameOptionsMiddleware') 10 cls() 11 4 面向对象: 12 (1) 13 class Foo: 14 def __init__(self,xx): 15 pass 16 @classmethod 17 def instance(cls): 18 return cls() 19 def process(self): 20 pass 21 if hasattr(Foo,'instance'): 22 obj=Foo.instance() 23 else: 24 obj=Foo() 25 obj.process() 26 (2) 27 class A: 28 def f1(self): 29 self.f2() 30 def f2(self): 31 self('A.f2') 32 class B: 33 def f2(self): 34 print('B.f2') 35 obj = B() 36 obj.f1()
2.2 资产采集的四种方案
2.2.1 Agent方式
使用场景:主机数量多
使用架构: 数据库------api程序----------各个主机
使用:
agent自动采集是在各个主机上运行程序,采集数据。需要使用subprocess和requests模块
1 v1= subprocess.getoutput('ifconfig') 2 #并发送 3 url="http://127.0.0.1:8000/asset.html" 4 response = request.post(url,data={'k1':value1,'k2':value2}) 5 print(response.text)
当agent采集数据发送到api程序时,api主要的任务是:1 url写路由 2 接收自动采集的数据的格式 3 返回值
2.2.2 SSH方式
使用场景:主机数量少,ssh连接即使慢也没事
使用架构:数据库 ----api程序----中控机----各个主机
中控机需要远程获取各个主机的信息:python中ssh登录各个主机,执行命令,得到数据
第三方的批量软件: fabric ansible 也是使用ssh原理
需要使用Paramiko模块:
1 import paramiko 2 # 创建SSH对象 3 ssh = paramiko.SSHClient() 4 # 允许连接不在know_hosts文件中的主机 5 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 6 # 连接服务器 7 ssh.connect(hostname='192.168.11.98',port=22,username='wupeiqi',password='123') 8 #执行命令 9 stdin,stdout,stderr=ssh.exec_command('ls') 10 #获取命令结果 11 result= stdout.read() 12 #关闭连接 13 ssh.close()
2.2.3 saltstack方式
saltstack: master主服务器 salve客户端
数据库 ----api程序----saltstack服务的master端----各个slave主机
v = subprocess.getoutput(salt "*" cmd.run "ls")
根据正则表达式提交到数据库
saltstack应用
1 安装和配置
1 """ salt服务器安装和配置 2 1. 安装salt-master 3 yum install salt-master 4 2. 修改配置文件:/etc/salt/master 5 interface: 0.0.0.0 # 表示Master的IP 6 3. 启动 7 service salt-master start 8 9 """
1 """ 2 salt客户端安装和配置 3 1. 安装salt-minion 4 yum install salt-minion 5 6 2. 修改配置文件 /etc/salt/minion 7 master: 10.211.55.4 # master的地址 8 或 9 master: 10 - 10.211.55.4 11 - 10.211.55.5 12 random_master: True 13 14 id: c2.salt.com # 客户端在salt-master中显示的唯一ID 15 3. 启动 16 service salt-minion start 17 """
2 授权
1 """ 2 salt-key -L # 查看已授权和未授权的slave 3 salt-key -a salve_id # 接受指定id的salve 4 salt-key -r salve_id # 拒绝指定id的salve 5 salt-key -d salve_id # 删除指定id的salve 6 """
3 执行命令
在master服务器上对salve进行远程操作
(1)使用salt自己命令
1 salt 'c2.salt.com' cmd.run 'ifconfig'
(2)使用salt.client模块
1 import salt.client 2 local = salt.client.LocalClient() 3 result = local.cmd('c2.salt.com', 'cmd.run', ['ifconfig'])
2.2.4 puppet方式
使用场景:公司现在在使用puppet
使用架构:数据库 ----api程序----puppet服务的master端----各个slave主机
2.3 创建资产采集代码的目录
bin # 可执行文件 config # 配置文件 -settings.py lib # 公共的模块 - conf -config.py - global_setting.py src # 业务代码目录 # log # 日志 一般不放这里,写在系统的某个目录
2.4 资产采集代码中的配置文件
有两个配置文件:用户自定义配置文件 , 默认配置文件。 配置文件中的变量名一般都是大写。
使用一个配置文件将两个配置文件结合起来
此例django的默认配置文件: from django.conf import global_settings
此例django中from django.conf import settings 会把默认配置和自定义配置合起来,使用settings.使用所有的配置
lib/conf/config.py 配置文件代码:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 import os 2 import importlib 3 from . import global_settings 4 class Settings(object): 5 def __init__(self): 6 # ######## 找到默认配置 ######## 先找默认,后找自定义配置 7 for name in dir(global_settings): 8 if name.isupper(): 9 value = getattr(global_settings,name) 10 setattr(self,name,value) 11 12 # ######## 找到自定义配置 ######## 13 # os.environ是全局环境变量,是字典类型 14 # 根据字符串导入模块 15 settings_module = os.environ.get('USER_SETTINGS') 16 if not settings_module: #如果没有自定义配置文件,直接使用默认文件 17 return 18 m = importlib.import_module(settings_module) 19 for name in dir(m): # dir() 可以得到此变量的所有属性 20 if name.isupper(): # 配置文件中的变量名一般大写 21 value = getattr(m,name) # 拿到自定义配置 22 setattr(self,name,value) # 将自定义配置的键和值设置在当前配置文件中 23 settings = Settings()
使用
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 import os 2 os.environ['USER_SETTINGS'] = "config.settings" #os.environ 系统环境变量 ,只在当前运行程序生效 3 import sys # 将代码模块路径放到sys中 4 BASEDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 sys.path.append(BASEDIR) 6 from lib.conf.config import settings #只需要导入settings,默认和自定义配置文件都会导入 7 print(settings.USER) 8 print(settings.EMAIL)
一般软件的配置文件都是这样做,有两个配置文件,没有自定义就是用默认配置文件。
2.5 可插拨式资产采集插件
公司采集资产有差别,因此插件最好是可插拨式
在src目录下创建plugins文件夹下basic,board,cpu,disk,menery,nic的py文件,默认采集basic,board,cpu,disk,menery,nic信息
2.5.1 定义插件类
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
# src/plugins/nic.py中的Nic类 class Nic(object): def process(self): return 'Nic' # src/plugins/board.py中的Board类 class Board(object): def process(self): return 'Board' #.....
2.5.2 插件类写到配置文件中
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
#condif/settings.py自定义配置文件写入 PLUGINS_DICT = { 'basic': "src.plugins.basic.Basic", 'board': "src.plugins.board.Board", 'cpu': "src.plugins.cpu.Cpu", 'disk': "src.plugins.disk.Disk", 'memory': "src.plugins.memory.Memory", 'nic': "src.plugins.nic.Nic", }
2.5.3 可插拨式采集资产插件的使用方法
当导入src/plugins里面模块时时,首先会执行src/plugins/__init__.py,src/plugins/__init__.py导入配置文件,获取插件的目录和类名,并导入。根据类名,执行其方法采集资产。
不同架构执行命令: 例如salt和agent方式不同,在代码上涉及判断
(1)-- 插件上使用继承解决
继承的类里面command函数需要判断配置文件中的采集数据方式,根据不同的方式执行命令
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
# src/plugins/base.py 在command方法中写判断采集数据类型 class BasePlugin(object): def command(self,cmd): if "salt": pass elif "SSH": pass # .... # src/plugins/cpu.py 各个插件继承BasePlugin类,使用其command方法 from .base import BasePlugin class Cpu(BasePlugin): def process(self): self.command('xxx') return "123321123"
(2)-- 插件上多传一个命令参数
在src/plugins/__init__.py中定义command函数,并将command函数在执行插件时作为参数传过去
插拔式插件需要有command函数的形参,不用继承,还可以使用__init__.py的所有属性
command函数需要判断配置文件中的采集数据方式,根据不同的方式执行命令
2.5.4 插件部分代码运行逻辑
请求会先执行src/plugins/__init__.py中所有代码
首先PluginManager的__init__函数,在配置文件中注册的所有插件拿出,
PluginManager的exec_plugin函数执行各个插件下的process方法,参数为command函数,接收各个process函数返回值,并返回最终值
各个插件的process方法会拿各个插件自己的命令执行command函数,接收command函数的返回值,并返回
command根据采集数据的模式不同执行命令,并返回值
发给api的信息,最好的类型时字典:
{
cpu: 'xxx',
disk:'...'
}
2.5.5 资产查询插件中的钩子
如果你想在构造方法(就是__init__)创建的时候想让插件模块自定义一些操作,可以使用@classmethod和initial函数来做。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
# src/plugins/__init__.py中PluginManager类的exec_plugin函数 def exec_plugins(self): # 获取所有的插件,并执行插件并返回值 response = {} for k,v in self.plugin_dict.items(): # 'basic': "src.plugins.basic.Basic" # result= "根据v获取类,并执行其方法采集资产" module_path,class_name=v.rsplit('.',1) m = importlib.import_module(module_path) cls = getattr(m,class_name) # 找到类 if hasattr(cls,'initial'): obj = cls.initial() else: obj = cls() result = obj.process(self.command) response[k]= result return response
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
# src/plugins/cpu.py中Cpu类的initial函数 class Cpu(object): def __init__(self): pass @classmethod def initial(cls): return cls() def process(self,command_func): return 'Cpu'
2.5.6 插件对获取的数据进行处理
src/plugins/cpu.py下的类Cpu中parse方法
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
def parse(self, content): """ 解析shell命令返回结果 :param content: shell 命令结果 :return:解析后的结果 """ response = {'cpu_count': 0, 'cpu_physical_count': 0, 'cpu_model': ''} cpu_physical_set = set() content = content.strip() for item in content.split('\n\n'): for row_line in item.split('\n'): key, value = row_line.split(':') key = key.strip() if key == 'processor': response['cpu_count'] += 1 elif key == 'physical id': cpu_physical_set.add(value) elif key == 'model name': if not response['cpu_model']: response['cpu_model'] = value response['cpu_physical_count'] = len(cpu_physical_set) return response
2.5.7 插件获取到错误堆栈信息时的处理
trackback模块
traceback.format_exc() 错误信息堆栈
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
def exec_plugin(self): """ 获取所有的插件,并执行获取插件返回值 :return: """ response = {} for k,v in self.plugin_dict.items(): # 'basic': "src.plugins.basic.Basic", ret = {'status':True,'data':None} try: module_path, class_name = v.rsplit('.', 1) m = importlib.import_module(module_path) cls = getattr(m,class_name) if hasattr(cls,'initial'): obj = cls.initial() else: obj = cls() result = obj.process(self.command,self.debug) # result = "根据v获取类,并执行其方法采集资产" ret['data'] = result except Exception as e: ret['status'] = False ret['data'] = "[%s][%s] 采集数据出现错误 : %s" %(self.hostname if self.hostname else "AGENT",k,traceback.format_exc()) # traceback.format_exc() 错误信息堆栈 response[k] = ret return response ''' { 'disk':{'status':True,'data':'xxx'}, 'nic':{'status':False,'data':'xxxxxx'} } '''
2.5.8 向api发送数据
支持3种模式:
agent直接发送就行
中控机向后台发送,ssh或者salt应该先从后台获取未采集数据的主机列表,再循环主机列表从而获取各个客户端的主机信息,最后将信息数据返回给后台服务器。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
#向api发送数据 和采集资产整合起来 import requests from lib.conf.config import settings from src.plugins import PluginManager import json class Base(object): def post_asse(self,server_info): requests.post(settings.API,json=server_info) # server_info 是字典里面有字典,不能直接传过去,需要json转化 # body:json.dumps(server_info) # headers = {'content-type':'application/json'} # json.loads(request.body) class Agent(Base): def execute(self): server_info = PluginManager().exec_plugin() self.post_asse(server_info) class SSHSALT(Base): def get_host(self): # 获取未采集的主机列表 response=requests.get(settings.API) result = json.loads(requests.text) # “{status:'True',data:['c1.com','c2.com']}” if result['status']: return return result['data'] def execute(self): host_list = self.get_host() for host in host_list: server_info = PluginManager(host).exec_plugins() self.post_asse(server_info)
插件只是用来采集资产,并且自己判断是哪种模式
client.py 是组织插件的功能和拿到数据发送给api的业务
问题:
发送到api时还可以加密
还有并发的问题,使用线程池
2.5.9 唯一标识
创造唯一标识时,之前必须要做标准化 。 比如认为主板SN 物理机的话可以作为唯一标识,虚拟机的话就不是唯一标识了。
标准化:主机名不重复;对于agent方式,主机名是唯一标识,可以将主机名写在客户端服务器的某个文件中,采集数据时可以进行主机名比较,如果不一致就拿文件中的主机名。
标准化:
- 主机名不重复
- 流程:
- 资产录入,机房,机柜,机柜位置
- 装机时,需要将服务信息录入CMDB,c1.com (手动录入或者cobber装机软件录入)
- 资产采集:c1.com
标准化创造唯一标识步骤:
(1)装系统,初始化软件(CMDB),运行CMDB:
- 通过命令获取主机名
- 写入本地指定文件
(2) 将资产信息发送到API
(3) 获取资产信息:
- 本地文件主机名 != 命令获取的主机名(按照文件中的主机名)
- 本地文件主机名 == 命令获取的主机名
最终流程:
标准化:主机名不重复;流程标准化(装机同时,主机名在cmdb中设置)
服务器资产采集(Agent):
a. 第一次:文件不存在,或内容为空;
采集资产:
- 主机名写入文件
- 发送API
b. 第N次:采集资产,主机名:文件中获取
agent方式唯一标识代码:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
class Agent(Base): def execute(self): server_info = PluginManager().exec_plugin() hostname = server_info['basic']['data']['hostname'] certname = open(settings.CERT_PATH,'r',encoding='utf-8').read() if not certname.strip(): with open(settings.CERT_PATH,'w',encoding='utf-8') as f: f.write(hostname) else: server_info['basic']['data']['hostname'] = certname self.post_asse(server_info)
SSH或Salt方式:中控机从后台获取未采集主机名列表:【c1.com 】,再直接去找客户端,本身拿到的主机名就是唯一标识
2.5.10 基于线程池实现并发采集资产
Ssh和salt的采集资产方式才可以有线程池。 提高并发:线程、进程
Python2:
线程池:无
进程池:有
Python3:
线程池:有
进程池:有
线程池简单例子:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
import time from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor def task(i): time.sleep(1) print(i) p = ThreadPoolExecutor(10) for row in range(100): p.submit(task,row)
进程池简单例子:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
import time from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor def task(i): time.sleep(1) print(i) p = ProcessPoolExecutor(10) for row in range(100): p.submit(task,row)
线程池在CMDB中采集数据时的应用:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
# src/client.py class SSHSALT(Base): def get_host(self): # 获取未采集的主机列表 response=requests.get(settings.API) result = json.loads(requests.text) # “{status:'True',data:['c1.com','c2.com']}” if result['status']: return return result['data'] def run(self,host): server_info = PluginManager(host).exec_plugins() self.post_asse(server_info) def execute(self): host_list = self.get_host() from concurrent.futures import ThreadPoolExecutor pool = ThreadPoolExecutor(10) for host in host_list: pool.submit(self.run,host)
2.5.11 代码
略