Title

CMDB信息采集设计(基础未完成版)

一采集客户端目录结构设计(参考ATM)

-bin :可执行文件 ( start.py/run.py)

-conf:配置文件目录 config.py

-lib :第三方文件目录

-src/core: 核心的源代码文件目录

-db:省略,可以用数据库代替了

-log:省略,记录文件日志(注意:日志并不是存放在这,而是通过logging模块存放在/var/logs/中)

-test:测试文件目录

 

重点:反射,魔法方法很重要,django的请求流程

二.CMDB的高级配置文件的设置

目录设置

 

1.如何获取配置信息

lib/config/conf.py


# (3)导入自定义配置
from conf import config
# (4)导入全局配置
from . import global_settings

# (5)写__init__方法
class mySettings():
   # (6)集成用户自定义的配置和全局配置在这里吗
   def __init__(self):
       # (7)获取全局配置信息,顺序是先全局后自定义,这样自定义的属性可以覆盖掉全局的属性
       for k in dir(global_settings):
           if k.isupper():
               # 通过getattr获取到用户信息
               v = getattr(global_settings, k)
               # 通过setattr设置当前类的属性
               setattr(self, k, v)
       #(8)获取用户自定义信息
       for k in dir(config):
           if k.isupper():
               # 通过getattr获取到用户信息
               v = getattr(config,k)
               # 通过setattr设置当前类的属性
               setattr(self, k, v)


settings = mySettings()

bin/start.py

from lib.config.conf import settings

# 此时当我们执行settings就会实例化lib下面的config中conf里面的mySettings类,实例化即执行__init__方法,这样就能通过settings获取
# 自定义或者全局的配置属性,因此第一个重点就在写__init__方法
if __name__ == '__main__':
   print(settings.EMAL_PORT)

2.如何实现两套架构方案并可以切换采集方法

2.1:如何实现控制采集与否

  • 第一种方法

bin/start.py

from lib.config.conf import settings

# 此时当我们执行settings就会实例化lib下面的config中conf里面的mySettings类,实例化即执行__init__方法,这样就能通过settings获取
# 自定义或者全局的配置属性,因此第一个重点就在写__init__方法
if __name__ == '__main__':
   # print(settings.EMAL_PORT)\
   # 判断如果自定义配置中的MODE为agent则走第一套方案
   if settings.MODE == 'agent':
       import subprocess

       res = subprocess.getoutput('hostname')  # 否则走第二套
   else:
       import paramiko

       # 创建ssh对象
       ssh = paramiko.SSHClient()
       # 允许链接不在know_hosts文件中的主机
       ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
       # 链接服务器(虚拟机设置的地址)
       ssh.connect(hostname='10.0.0.200', port=22, username='root', password='123456')
       # 执行命令
       stdin, stdout, stderr = ssh.exec_command('hostname')
       # 获取命令结果
       result = stdout.red()
       ssh.close()

如果上述代码这样写的话,会带来如下问题:

  • 耦合度太高

  • 结构混乱,导致查找问题的时候不方便

  • 业务逻辑代码不能写在启动文件中

问:如何解决上述存在的问题? 答:将每一个功能都封装成一个文件,比如说采集磁盘的信息,可以搞一个disk.py文件,这个文件中所有的代码都是要和采集磁盘相关的,不能有其他 的相关代码。以此类推,采集CPU的信息,也要搞一个cpu.py文件. 这种思想就是高内聚低耦合思想(高内聚:即每一个代码文件都是有关与同一个内容的,不要有其他相关或无关的代码;低耦合:即代码拆分成多个文件)

  • 第二种方法

 

 

重点伪代码:

bin/start.py

from src.plugins.basic import Basic
from src.plugins.cpu import Cpu
from src.plugins.memory import Memory
from src.plugins.disk import Disk
# 此时当我们执行settings就会实例化lib下面的config中conf里面的mySettings类,实例化即执行__init__方法,这样就能通过settings获取
# 自定义或者全局的配置属性,因此第一个重点就在写__init__方法
if __name__ == '__main__':
src/plugins/basic
# 9.服务器基础信息,包括服务器的主机名,操作系统等
class Basic:
   def __init__(self):
       pass
   
# 自定义一个方法执行linux命名
   def process(self):
       return 'basic...'
   
   
src/plugins/cpu
# 10.采集cpu信息的文件
class Cpu:
   def __init__(self):
       pass

   def process(self):
       return 'cpu...'
   

   
src/plugins/disk
# 11.采集磁盘信息的文件
class Disk:
   def __init__(self):
       pass
   # 自定义一个方法执行linux命名
   def process(self):
       return 'disk...'
   
src/plugins/memory
   
# 12.采集内存所有的信息
class Memory:
   def __init__(self):
       pass

   def process(self):
       return 'memory...'

但是上述第二种方法并不完美,结构冗余繁杂,

解决的方案是:将这些采集的插件写到配置文件中统一管理,参考django的中间件, 注释中间件就不会起作用,做成可插拔式的采集

 

第三种方法:

重点伪代码:

conf/config.py

在自定义配置文件中将采集插件做成类似于中间件的格式

# 模仿django的中间件
PLUGIN_DICT = {
   'basc':'src.plugins.basic.Basic',
   'cpu':'src.plugins.cpu.Cpu',
   'disk':'src.plugins.disk.Disk',
   'memory':'src.plugins.memory.Memory',
}

在核心文件夹的插件文件夹下的init文件中设置管理插件的类

src/plugins/_ _init__.py

from lib.config.conf import settings
# importlib下面有一个import_module方法能够导入字符串路径
import importlib


# 13 设置管理插件的类
class PluginsManager:
   def __init__(self):
       self.plugins_dict = settings.PLUGIN_DICT

   # 管理配置文件插件,采集数据,会从conf/config.py这个配置文件里面读取中间件配置,
   # 循环导入模块进而获取中间件中的插件方法,process来获取数据信息
   def execute(self):
       # 循环读取配置
       response = {}
       for k, v in self.plugins_dict.items():
           '''
          k: basic
          v: src.plugins.basic.Basic
          '''
           # 循环导入配置,获取路径和方法名
           '''
          modul_path:src.plugins.basic
          class_name:Basic
          '''
           modul_path, class_name = v.rsplit('.', 1)
           # 此时modul_path是字符串,不是真的路径,无法直接导入
           # importlib下面有一个import_module方法能够导入字符串路径
           m = importlib.import_module(modul_path)
           # print(m)
           # print(type(m))
           '''
          打印结果
          <module 'src.plugins.basic' from 'G:\\sendcmdb\\src\\plugins\\basic.py'>
          <class 'module'>
          <module 'src.plugins.cpu' from 'G:\\sendcmdb\\src\\plugins\\cpu.py'>
          <class 'module'>
          <module 'src.plugins.disk' from 'G:\\sendcmdb\\src\\plugins\\disk.py'>
          <class 'module'>
          <module 'src.plugins.memory' from 'G:\\sendcmdb\\src\\plugins\\memory.py'>
          <class 'module'>
          '''
           # 反射获取类
           cls = getattr(m, class_name)
           # print(cls)
           '''
          打印结果:
          <class 'src.plugins.basic.Basic'>
          <class 'src.plugins.cpu.Cpu'>
          <class 'src.plugins.disk.Disk'>
          <class 'src.plugins.memory.Memory'>
          '''
           # 实例化类中,执行process方法(这就是鸭子方法,为什么都要设置相同的process方法)
           ret = cls().process()
           # print(ret)
           '''
          # 打印结果:
          basic...
          cpu...
          disk...
          memory...
          '''
           # 需要对ret做一下处理,改为字典格式,新建一个response空字典在上面
           response[k] = ret
       return response

start.py

# 因为是写在init里面,可以直接导入过来
from src.plugins import PluginsManager
if __name__ == '__main__':
# 第三种方法
   res = PluginsManager().execute()
   print(res)
   '''
  打印结果:
  {'basc': 'basic...', 'cpu': 'cpu...', 'disk': 'disk...', 'memory': 'memory...'}
  '''
 

这样只需要通过修改配置文件中的PLUGIN_DICT就可以控制信息的采集,叫做可插拔式采集

2.2.如何采集信息

伪代码:采用if判断采集信息用那种方法

src/plugins/basic

# 9.服务器基础信息,包括服务器的主机名,操作系统等
from lib.config.conf import settings
class Basic:
   def __init__(self):
       pass

   def process(self):
       # 判断如果MODE为agent则走第一套方案
       if settings.MODE == 'agent':
           import subprocess

           res = subprocess.getoutput('hostname')  # 否则走第二套
       else:
           import paramiko

           # 创建ssh对象
           ssh = paramiko.SSHClient()
           # 允许链接不在know_hosts文件中的主机
           ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
           # 链接服务器(虚拟机设置的地址)
           ssh.connect(hostname='10.0.0.200', port=22, username='root', password='123456')
           # 执行命令
           stdin, stdout, stderr = ssh.exec_command('hostname')
           # 获取命令结果
           result = stdout.red()
           ssh.close()
       return 'basic...'

缺点:插件代码,判断方案冗余,

解决方案: 1.继承 每一个子类都要继承父类的方法,通过传参的方式执行不一样的命令 2.将函数名当成一个参数传给另一个函数,从而执行此函数

from lib.config.conf import settings
# importlib下面有一个import_module方法能够导入字符串路径
import importlib


# 13 设置管理插件的类
class PluginsManager:
   def __init__(self):
       self.plugins_dict = settings.PLUGIN_DICT
       # 设置debug属性
       self.debug = settings.DEBUG

   # 管理配置文件插件,采集数据,会从conf/config.py这个配置文件里面读取中间件配置,
   # 循环导入模块进而获取中间件中的插件方法,process来获取数据信息
   def execute(self):
       # 循环读取配置
       response = {}
       for k, v in self.plugins_dict.items():
           '''
          k: basic
          v: src.plugins.basic.Basic
          '''
           # 循环导入配置,获取路径和方法名
           '''
          modul_path:src.plugins.basic
          class_name:Basic
          '''
           modul_path, class_name = v.rsplit('.', 1)
           # 此时modul_path是字符串,不是真的路径,无法直接导入
           # importlib下面有一个import_module方法能够导入字符串路径
           m = importlib.import_module(modul_path)
           # print(m)
           # print(type(m))
           '''
          打印结果
          <module 'src.plugins.basic' from 'G:\\sendcmdb\\src\\plugins\\basic.py'>
          <class 'module'>
          <module 'src.plugins.cpu' from 'G:\\sendcmdb\\src\\plugins\\cpu.py'>
          <class 'module'>
          <module 'src.plugins.disk' from 'G:\\sendcmdb\\src\\plugins\\disk.py'>
          <class 'module'>
          <module 'src.plugins.memory' from 'G:\\sendcmdb\\src\\plugins\\memory.py'>
          <class 'module'>
          '''
           # 反射获取类
           cls = getattr(m, class_name)
           # print(cls)
           '''
          打印结果:
          <class 'src.plugins.basic.Basic'>
          <class 'src.plugins.cpu.Cpu'>
          <class 'src.plugins.disk.Disk'>
          <class 'src.plugins.memory.Memory'>
          '''
           # 实例化类中,执行process方法(这就是鸭子方法,为什么都要设置相同的process方法)
           # 将command_func函数名传过来,即在process函数中执行command_func函数
           ret = cls().process(self.command_func,self.debug)
           # print(ret)
           '''
          # 打印结果:
          basic...
          cpu...
          disk...
          memory...
          '''
           # 需要对ret做一下处理,改为字典格式,新建一个response空字典在上面
           response[k] = ret
       return response

   # 定义一个函数,接收需要查询的命令,以及判断那种采集方式
   def command_func(self,cmd):
       # 判断如果MODE为agent则走第一套方案
       if settings.MODE == 'agent':
           import subprocess

           res = subprocess.getoutput(cmd)  # 否则走第二套
           return res
       else:
           import paramiko

           # 创建ssh对象
           ssh = paramiko.SSHClient()
           # 允许链接不在know_hosts文件中的主机
           ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
           # 链接服务器(虚拟机设置的地址)
           ssh.connect(hostname='10.0.0.200', port=22, username='root', password='123456')
           # 执行命令
           stdin, stdout, stderr = ssh.exec_command(cmd)
           # 获取命令结果
           result = stdout.red()
           ssh.close()
           return result

 

# 11.采集磁盘信息的文件
class Disk:
   def __init__(self):
       pass

   # 自定义一个方法执行linux命名
   def process(self, command_func, debug):
       # 即如果在debug为True情况下,数据源是公司测试的结果,统一存在一个files文件夹下的.out文件中
       if debug:
           # 打开数据源测试结果是否一致,便于修改
           info = open('files/disk.out', 'r', encoding='utf-8').read()
           pass
       else:
       # 接收command_func并传入命令执行
           info = command_func('df-h')
       # 通过下面的parse方法对数据处理一下
       ret = self.parse(info)
       return ret

   # 得到的数据多而咋,定义一个方法处理命令执行的结果
   def parse(self,info):
       pass

完整版:

bin/start.py

from src.plugins import PluginsManager
if __name__ == '__main__':
   # 第三种方法

   res = PluginsManager().execute()
   print(res)

 

conf/config.py

# (1)用户自定义配置文件
USER = 'root'

MODE = 'ssh'

# DEBUG为True代表是开发测试阶段,为False代表上线阶段
DEBUG = False
# 模仿django的中间件
PLUGIN_DICT = {
   'basc':'src.plugins.basic.Basic',
   'cpu':'src.plugins.cpu.Cpu',
   'disk':'src.plugins.disk.Disk',
   'memory':'src.plugins.memory.Memory',
}

lib/config/conf

# (3)导入自定义配置
from conf import config
# (4)导入全局配置
from . import global_settings

# (5)写__init__方法
class mySettings():
   # (6)集成用户自定义的配置和全局配置在这里吗
   def __init__(self):
       # (7)获取全局配置信息,顺序是先全局后自定义,这样自定义的属性可以覆盖掉全局的属性
       for k in dir(global_settings):
           if k.isupper():
               # 通过getattr获取到用户信息
               v = getattr(global_settings, k)
               # 通过setattr设置当前类的属性
               setattr(self, k, v)
       #(8)获取用户自定义信息
       for k in dir(config):
           if k.isupper():
               # 通过getattr获取到用户信息
               v = getattr(config,k)
               # 通过setattr设置当前类的属性
               setattr(self, k, v)


settings = mySettings()

lib/config/global

# (2)全局配置
EMAL_PORT = 25

src/plugins/_ _init _ _.py

import importlib


# 13 设置管理插件的类
class PluginsManager:
   def __init__(self):
       self.plugins_dict = settings.PLUGIN_DICT
       # 设置debug属性
       self.debug = settings.DEBUG

   # 管理配置文件插件,采集数据,会从conf/config.py这个配置文件里面读取中间件配置,
   # 循环导入模块进而获取中间件中的插件方法,process来获取数据信息
   def execute(self):
       # 循环读取配置
       response = {}
       for k, v in self.plugins_dict.items():
           '''
          k: basic
          v: src.plugins.basic.Basic
          '''
           # 循环导入配置,获取路径和方法名
           '''
          modul_path:src.plugins.basic
          class_name:Basic
          '''
           modul_path, class_name = v.rsplit('.', 1)
           # 此时modul_path是字符串,不是真的路径,无法直接导入
           # importlib下面有一个import_module方法能够导入字符串路径
           m = importlib.import_module(modul_path)
           # print(m)
           # print(type(m))
           '''
          打印结果
          <module 'src.plugins.basic' from 'G:\\sendcmdb\\src\\plugins\\basic.py'>
          <class 'module'>
          <module 'src.plugins.cpu' from 'G:\\sendcmdb\\src\\plugins\\cpu.py'>
          <class 'module'>
          <module 'src.plugins.disk' from 'G:\\sendcmdb\\src\\plugins\\disk.py'>
          <class 'module'>
          <module 'src.plugins.memory' from 'G:\\sendcmdb\\src\\plugins\\memory.py'>
          <class 'module'>
          '''
           # 反射获取类
           cls = getattr(m, class_name)
           # print(cls)
           '''
          打印结果:
          <class 'src.plugins.basic.Basic'>
          <class 'src.plugins.cpu.Cpu'>
          <class 'src.plugins.disk.Disk'>
          <class 'src.plugins.memory.Memory'>
          '''
           # 实例化类中,执行process方法(这就是鸭子方法,为什么都要设置相同的process方法)
           # 将command_func函数名传过来,即在process函数中执行command_func函数
           ret = cls().process(self.command_func,self.debug)
           # print(ret)
           '''
          # 打印结果:
          basic...
          cpu...
          disk...
          memory...
          '''
           # 需要对ret做一下处理,改为字典格式,新建一个response空字典在上面
           response[k] = ret
       return response

   # 定义一个函数,接收需要查询的命令,以及判断那种采集方式
   def command_func(self,cmd):
       # 判断如果MODE为agent则走第一套方案
       if settings.MODE == 'agent':
           import subprocess

           res = subprocess.getoutput(cmd)  # 否则走第二套
           return res
       else:
           import paramiko

           # 创建ssh对象
           ssh = paramiko.SSHClient()
           # 允许链接不在know_hosts文件中的主机
           ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
           # 链接服务器(虚拟机设置的地址)
           ssh.connect(hostname='10.0.0.200', port=22, username='root', password='123456')
           # 执行命令
           stdin, stdout, stderr = ssh.exec_command(cmd)
           # 获取命令结果
           result = stdout.read()
           ssh.close()
           return result

src/plugins/disk

# 11.采集磁盘信息的文件
class Disk:
   def __init__(self):
       pass

   # 自定义一个方法执行linux命名
   def process(self, command_func, debug):
       # 即如果在debug为True情况下,数据源是公司测试的结果,统一存在一个files文件夹下的.out文件中
       if debug:
           # 打开数据源测试结果是否一致,便于修改
           output = open('files/disk.out', 'r', encoding='utf-8').read()
           pass
       else:
       # 接收command_func并传入命令执行
       #     info = command_func('df-h')
           output = {
               'os_platform': command_func("uname").strip(),
               'os_version': str(command_func("cat /etc/issue"),encoding=('utf8')).strip().split('\n')[0],
               'hostname': command_func("hostname").strip(),
          }
       # 通过下面的parse方法对数据处理一下
       # ret = self.parse(info)
       # return ret
       print(output.get('os_version'))
       return output

   # 得到的数据多而咋,定义一个方法处理命令执行的结果
   def parse(self,info):
       pass

src/plugins/basic

# 9.服务器基础信息,包括服务器的主机名,操作系统等
class Basic:
   def __init__(self):
       pass

   def process(self, command_func,debug ):
       # 即如果在debug为True情况下,数据源是公司测试的结果,统一存在一个files文件夹下的.out文件中
       if debug:
           # 打开数据源测试结果是否一致,便于修改
           # info = open('files/disk.out', 'r', encoding='utf-8').read()
           pass
       else:
       # 接收command_func并传入命令执行
           info = command_func('')
       # 通过下面的parse方法对数据处理一下
       ret = self.parse(info)
       return ret

   # 得到的数据多而咋,定义一个方法处理命令执行的结果
   def parse(self, info):
       pass

src/plugins/cpu

# 10.采集cpu信息的文件
class Cpu:
   def __init__(self):
       pass

   def process(self, command_func,debug):
       # 即如果在debug为True情况下,数据源是公司测试的结果,统一存在一个files文件夹下的.out文件中
       if debug:
           # 打开数据源测试结果是否一致,便于修改
           info = open('files/disk.out', 'r', encoding='utf-8').read()
           pass
       else:
       # 接收command_func并传入命令执行
           info = command_func('cat /proc/cpuinfo')

       # 通过下面的parse方法对数据处理一下
       ret = self.parse(info)
       return ret

   # 得到的数据多而咋,定义一个方法处理命令执行的结果
   def parse(self,info):
       pass

 

  • 完善一下采集端代码

将agent方法和ssh方法的采集和发送设计成一个类

src/client

from src.plugins import PluginsManager
import requests
import json
from lib.config.conf import settings


# 定义一个采集方法,如果是agent方法就走Aegnt类
class Agent:
   # 定义一个方法实现收集信息并发送到服务端(API)
   def CollecAndPost(self):
       # 收集
       res = PluginsManager().execute()
       for k, v in res.items():
           print(k, v)
       # 发送(地址可以写在配置文件中,发送的数据可以直接用json参数)
       # requests.post('http://127.0.0.1:8000/api', data=json.dumps(res))
       requests.post(settings.APIURL, json=res)


class mySSH:
   def getHostnames(self):
       # 像API发送get请求获取数据库中的主机名
         # 像API发送get请求获取数据库中的主机名
       hostnames = requests.get(settings.APIURL)
       return hostnames

   def CollecAndPost(self):
       # 收集(在创建数据库的时候需要提前将主机名等信息录入,这就是hostname的来源)
       hostnames = self.getHostnames()
       for hostname in hostnames:
           res = PluginsManager(hostname=hostname).execute()
           #发送
           requests.post(settings.APIURL, json=res)

注意:init里面的采集方法中 PluginsManager类需要设置hostname等属性,这些属性都是通过get方法发送到API从数据库中取出来的

然后将两种方法整合到一个run函数,交由start.py文件启动

src/srcipt.py

from lib.config.conf import settings
from src.client import Agent,mySSH

def run():
   if __name__ == '__main__':
       # 判断如果收集方法为agent
       if settings.MODE == 'agent':
           obj = Agent()
       # 或者收集方法为ssh
       else:
           obj = mySSH()
       # 都会走CollecAndPost方法
       obj.CollecAndPost()
       # # print(settings.EMAL_PORT)

但是ssh方法中中控机采集信息会一个一个采集,若有一个堵塞,就会导致后面都停下等待,速度会比较慢

解决方案:

  • ssh方案的多线程采集

    • 了解线程和进程,协程的区别

    • 提高并发(IO密集型)的话,使用多线程

    • 计算密集型使用多进程

    • python2 多进程有 多线程没有

    • python3 多进程有 多线程有

示例:
# # 线程池
from concurrent.futures import ThreadPoolExecutor
# # 进程池
# from concurrent.futures import ProcessPoolExecutor
p = ThreadPoolExecutor(10)
import time


def test(i):
   time.sleep(1)
   print(i)


for i in range(100):
   p.submit(test,i)

根据上面的例子对src/client.py进行多线程处理

from src.plugins import PluginsManager
import requests
import json
from lib.config.conf import settings


# 定义一个采集方法,如果是agent方法就走Aegnt类
class Agent:
   # 定义一个方法实现收集信息并发送到服务端(API)
   def CollecAndPost(self):
       # 收集
       res = PluginsManager().execute()
       for k, v in res.items():
           print(k, v)
       # 发送(地址可以写在配置文件中,发送的数据可以直接用json参数)
       # requests.post('http://127.0.0.1:8000/api', data=json.dumps(res))
       requests.post(settings.APIURL, json=res)


class mySSH:
   def getHostnames(self):
       # 像API发送get请求获取数据库中的主机名
       hostnames = requests.get(settings.APIURL)
       return hostnames
   def task(self,hostname):
       res = PluginsManager(hostname=hostname).execute()
       # 发送
       requests.post(settings.APIURL, json=res)
   def CollecAndPost(self):
       from concurrent.futures import ThreadPoolExecutor
       p = ThreadPoolExecutor(10)
       # 收集(在创建数据库的时候需要提前将主机名等信息录入,这就是hostname的来源)
       hostnames = self.getHostnames()
       for hostname in hostnames:
           p.submit(self.task, hostname)
- 异常处理
> 增加代码的健壮性,增强代码的容错能力

traceback模块

通过important traceback模块可以准确捕获异常信息

   import traceback
   def execute(self):
       # 循环读取配置
       response = {}
       for k, v in self.plugins_dict.items():
          #设置响应状态码和信息
           res = {'status':None, 'data':None}
           try:
               modul_path, class_name = v.rsplit('.', 1)
               m = importlib.import_module(modul_path)    
               # 反射获取类
               cls = getattr(m, class_name)  
               ret = cls().process(self.command_func,self.debug)        
               res['status'] = 10000
               res['data'] = ret
               # 需要对ret做一下处理,改为字典格式,新建一个response空字典在上面
               response[k] = res
           except Exception as e:              
               res['status'] = 10001
               res['data'] = '错误信息:%s' %(traceback.format_exc())
               response[k] = res
       return response

3.服务端目录结构的设计

  • 服务端目录结构的设计 django中的app

    • api : 负责接收数据, 并且对比入库的

    • backend: 前端数据的展示

    • repository: 负责数据表的设计

posted @ 2020-03-27 00:56  Mr江  阅读(293)  评论(0编辑  收藏  举报