CMDB_Agent_ssh版本分析

  • 2020 年 3 月 28 日
  • 筆記

CMDB_Agent+ssh版本+server端

CMDB_Agent版本

CMDB概念

CMDB: Configure Manage DataBase  中文:配置管理数据库。  主要的作用是:收集服务器的基础信息(包括:服务器的主机名,ip,操作系统版本,磁盘,CPU等信息),将来提供给子系统(代码发布,工单系统等)数据  

CMDB_Agent介绍

其本质上就是在各个服务器上执行subprocess.getoutput()命令,然后将每台机器上执行的结果,返回给主机API,然后主机API收到这些数据之后,放入到数据库中,最终通过web界面展现给用户  优点:速度快  缺点:需要为每台服务器步数一个Agent的程序  

agent方案

将待采集的服务器看成一个agent,然后再服务器上使用python的subprocess模块执行linux相关的命令,然后分析得到的结果,将分析得到的结果通过requests模块发送给API,API获取到数据之后,进行二次比对数据,最后将比对的结果存入到数据库中,最后django起一个webserver从数据库中将数据获取出来,供用户查看  

ssh类方案

在中控机服务器上安装一个模块叫paramiko模块,通过这个模块登录到带采集的服务器上,然后执行相关的linux命令,最后返回执行的结果,将分析得到的结果通过requests模块发送给API,API获取到数据之后,进行二次比对数据,最后将比对的结果存入到数据库中,最后django起一个webserver从数据库中将数据获取出来,供用户查看  

相比较

agent方案  优点:不需要额外的增加中控机。  缺点:每新增一台服务器,就需要额外部署agent脚本。使用场景是:服务器多的情况 (1000台以上)    ssh方案  优点:不需要额外的部署脚本。  缺点:速度比较慢。使用场景是:服务器少  (1000台往下)  

client端

架构目录

bin-start.py 启动文件

from src.srcipt import run    if __name__ == '__main__':      run()    

conf-config.py 自定义配置文件

模仿Django的setting,常用的配置写在这里面。不常用的写在global_settings.py中。

加载顺寻:先加载全局的。再加载局部的

USER = 'root'  MODE = 'agent'    DEBUG = True  # True:代表是开发测试阶段  False:代表是上现阶段    import os  BASEDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))      PLUGINS_DICT = {      'basic': 'src.plugins.basic.Basic',      'cpu': 'src.plugins.cpu.Cpu',      'disk': 'src.plugins.disk.Disk',      # 'memory': 'src.plugins.memory.Memory',  }    APIURL = 'http://127.0.0.1:8000/api/'  

files 开发测试的文件

DEBUT=True时为测试阶段,用files的测试数据

lib-config-global_settings.py 全局配置的文件

pass  

lib-config-conf.py 读取配置的文件

全局配置放在前面先加载,自定义配置的放在后面后加载。自定义配置了就用自定义的(覆盖),没有配置久用全局的

from conf import config  from . import global_settings      class mySettings():        def __init__(self):            # print('aa:', dir(global_settings))          # print('bb:', dir(config))          # 全局配置          for k in dir(global_settings):                if k.isupper():                  v = getattr(global_settings, k)                  setattr(self, k, v)            # 自定义配置          for k in dir(config):              if k.isupper():                  v = getattr(config, k)                  setattr(self, k, v)      settings = mySettings()    

src-plugins-init.py 核心文件

from lib.config.conf import settings  import importlib      class PluginsManager():        def __init__(self, hostname=None):          self.plugins_dict = settings.PLUGINS_DICT          self.debug = settings.DEBUG          self.hostname = hostname        # 1.采集数据      def execute(self):          response = {}          for k, v in self.plugins_dict.items():              '''              k: basic              v: src.plugins.basic.Basic              '''              res = {'status':None, 'data':None}              try:                  # 2.循环导入(字符串路径)                  moudle_path, class_name = v.rsplit('.', 1)  # ['src.plugins.basic','Basic']                  # 用importlib.import_module()导入字符串路径                  m = importlib.import_module(moudle_path)                    # 3.导入类                  cls = getattr(m, class_name)                  # 循环执行鸭子类型的process方法,command_func函数的内存地址传过去,把debug传过去                  ret = cls().process(self.command_func, self.debug)                    res['status'] = 10000                  res['data'] = ret                    response[k] = res              except Exception as e:                  import traceback                  res['status'] = 10001                  res['data'] = '错误信息:%s'%(traceback.format_exc())                  response[k] = res          return response        # 真正的连接,执行命令,返回结果的函数。命令变成参数      def command_func(self, cmd):          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=self.hostname, port=22, username='root', password='123456')                # 执行命令              stdin, stdout, stderr = ssh.exec_command(cmd)              # 获取命令结果              result = stdout.read()                # 关闭连接              ssh.close()              return result    

src-plugins-basic.py 查看硬件信息

from conf import config        class Basic(object):        def process(self, command_func, debug):          if debug:              output = {                  'os_platform': "linux",                  'os_version': "CentOS release 6.6 (Final)nKernel r on an m",                  'hostname': 'c1.com'              }          else:              output = {                  'os_platform': command_func("uname").strip(),                  'os_version': command_func("cat /etc/issue").strip().split('n')[0],                  'hostname': command_func("hostname").strip(),              }          return output  

src-plugins-cpu.py 查看cpu属性

import os  from lib.config.conf import settings    class Cpu():      def __init__(self):          pass        def process(self, command_func, debug):          if debug:              output = open(os.path.join(settings.BASEDIR, 'files/cpuinfo.out'), 'r', encoding='utf-8').read()          else:              output = command_func("cat /proc/cpuinfo")          return self.parse(output)        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('nn'):              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  

src-plugins-disk.py 查看磁盘信息

# 采集磁盘信息  from lib.config.conf import settings  import os  import re    class Disk(object):      def __init__(self):          pass        def process(self, command_func, debug):          if debug:              output = open(os.path.join(settings.BASEDIR, 'files/disk.out'), 'r', encoding='utf-8').read()          else:              output = command_func('MegaCli -PDList -aALL')  # radi 卡 磁盘阵列            return self.parse(output)  # 调用过滤的函数          # 过滤函数,对字符串的处理过滤      def parse(self, content):          """          解析shell命令返回结果          :param content: shell 命令结果          :return:解析后的结果          """          response = {}          result = []          for row_line in content.split("nnnn"):              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  

server端

架构目录

服务端目录结构的设计  django的app    - api : 负责接收数据, 并且对比入库的  - backend:  前端数据的展示  - repository: 负责数据表的设计  

配置

数据库,App注册等省略  

repository-models.py 表设计

from django.db import models      # Create your models here.    class UserProfile(models.Model):      """      用户信息      """      name = models.CharField(u'姓名', max_length=32)      email = models.EmailField(u'邮箱')      phone = models.CharField(u'座机', max_length=32)      mobile = models.CharField(u'手机', max_length=32)        class Meta:          verbose_name_plural = "用户表"        def __str__(self):          return self.name      class UserGroup(models.Model):      """      用户组      """      name = models.CharField(max_length=32, unique=True)      users = models.ManyToManyField('UserProfile')        class Meta:          verbose_name_plural = "用户组表"        def __str__(self):          return self.name      class BusinessUnit(models.Model):      """      业务线      """      name = models.CharField('业务线', max_length=64, unique=True)      contact = models.ForeignKey('UserGroup', verbose_name='业务联系人', related_name='c', on_delete=models.CASCADE)        class Meta:          verbose_name_plural = "业务线表"        def __str__(self):          return self.name      class IDC(models.Model):      """      机房信息      """      name = models.CharField('机房', max_length=32)      floor = models.IntegerField('楼层', default=1)        class Meta:          verbose_name_plural = "机房表"        def __str__(self):          return self.name      class Server(models.Model):      """      服务器信息      """      device_type_choices = (          (1, '服务器'),          (2, '交换机'),          (3, '防火墙'),      )      device_status_choices = (          (1, '上架'),          (2, '在线'),          (3, '离线'),          (4, '下架'),      )        device_type_id = models.IntegerField('服务器类型', choices=device_type_choices, default=1)      device_status_id = models.IntegerField('服务器状态', choices=device_status_choices, default=1)        cabinet_num = models.CharField('机柜号', max_length=30, null=True, blank=True)      cabinet_order = models.CharField('机柜中序号', max_length=30, null=True, blank=True)        idc = models.ForeignKey('IDC', verbose_name='IDC机房', null=True, blank=True, on_delete=models.CASCADE)      business_unit = models.ForeignKey('BusinessUnit', verbose_name='属于的业务线', null=True, blank=True,                                        on_delete=models.CASCADE)        hostname = models.CharField('主机名', max_length=128, unique=True)      sn = models.CharField('SN号', max_length=64, db_index=True, blank=True)      manufacturer = models.CharField(verbose_name='制造商', max_length=64, null=True, blank=True)      model = models.CharField('型号', max_length=64, null=True, blank=True)        manage_ip = models.GenericIPAddressField('管理IP', null=True, blank=True)        os_platform = models.CharField('系统', max_length=16, null=True, blank=True)      os_version = models.CharField('系统版本', max_length=16, null=True, blank=True)        cpu_count = models.IntegerField('CPU个数', null=True, blank=True)      cpu_physical_count = models.IntegerField('CPU物理个数', null=True, blank=True)      cpu_model = models.CharField('CPU型号', max_length=128, null=True, blank=True)        create_at = models.DateTimeField(auto_now_add=True, blank=True)        class Meta:          verbose_name_plural = "服务器表"        def __str__(self):          return self.hostname      class Disk(models.Model):      """      硬盘信息      """      slot = models.CharField('插槽位', max_length=8)      model = models.CharField('磁盘型号', max_length=32)      capacity = models.CharField('磁盘容量GB', max_length=32)      pd_type = models.CharField('磁盘类型', max_length=32)        server_obj = models.ForeignKey('Server', related_name='disk', on_delete=models.CASCADE)        class Meta:          verbose_name_plural = "硬盘表"        def __str__(self):          return self.slot      class NIC(models.Model):      """      网卡信息      """      name = models.CharField('网卡名称', max_length=128)      hwaddr = models.CharField('网卡mac地址', max_length=64)      netmask = models.CharField(max_length=64)      ipaddrs = models.CharField('ip地址', max_length=256)      up = models.BooleanField(default=False)        server_obj = models.ForeignKey('Server', related_name='nic', on_delete=models.CASCADE)        class Meta:          verbose_name_plural = "网卡表"        def __str__(self):          return self.name      class Memory(models.Model):      """      内存信息      """      slot = models.CharField('插槽位', max_length=32)      manufacturer = models.CharField('制造商', max_length=32, null=True, blank=True)      model = models.CharField('型号', max_length=64)      capacity = models.FloatField('容量', null=True, blank=True)      sn = models.CharField('内存SN号', max_length=64, null=True, blank=True)      speed = models.CharField('速度', max_length=16, null=True, blank=True)        server_obj = models.ForeignKey('Server', related_name='memory', on_delete=models.CASCADE)        class Meta:          verbose_name_plural = "内存表"        def __str__(self):          return self.slot      class ErrorLog(models.Model):      """      错误日志,如:agent采集数据错误 或 运行错误      """      server_obj = models.ForeignKey('Server', null=True, blank=True, on_delete=models.CASCADE)      title = models.CharField(max_length=16)      content = models.TextField()      create_at = models.DateTimeField(auto_now_add=True)        class Meta:          verbose_name_plural = "错误日志表"        def __str__(self):          return self.title    

Api-views.py 数据处理

这里只对磁盘信息做了处理

	数据处理:  	'''          老的槽位信息: [1,2,6]          新的槽位信息:[1,2,3,4,5]          处理:增加3,4,5槽位                删除6槽位                检测1,2槽位有无更新磁盘信息            下面的所有事情:分析磁盘的信息与老信息              1.增加了那些槽位,              2.删除了那些槽位              3.更新了那些槽位,记录变更的日志          '''  
from django.shortcuts import render,HttpResponse  import json    from repository import models    def asset(request):      if request.method == 'POST':          info = json.loads(request.body)          hostname = info['basic']['data']['hostname']  # c1.com          # 每一个服务器的对象,这里固定为c1.com服务器          server_obj = models.Server.objects.filter(hostname=hostname).first()          if not server_obj:              return HttpResponse('服务器为录入')            # 磁盘数据状态码为例          status = info['disk']['status']  # 状态码            if status != 10000:              # 添加错误信息              models.ErrorLog.objects.create(title='错误信息', content=info['disk']['data'], server_obj=server_obj)              return HttpResponse('采集出错!')            '''          老的槽位信息: [1,2,6]          新的槽位信息:[1,2,3,4,5]          处理:增加3,4,5槽位                删除6槽位                检测1,2槽位有无更新磁盘信息            下面的所有事情:分析磁盘的信息与老信息              1.增加了那些槽位,              2.删除了那些槽位              3.更新了那些槽位,记录变更的日志          '''          new_disk_info = info['disk']['data']  # 新的磁盘信息          print(new_disk_info)          old_disk_info = models.Disk.objects.filter(server_obj=server_obj).all()  # 老的磁盘信息            # 集合去重          new_slot = set(new_disk_info.keys())            old_slot = []            for obj in old_disk_info:              old_slot.append(obj.slot)            old_slot = set(old_slot)          print(new_slot)          print(old_slot)            # 增加的槽位数据          add_slot = new_slot.difference(old_slot)          if add_slot:              for slot in add_slot:                  ### {'slot': '3', 'pd_type': 'SATA', 'capacity': '476.939', 'model': 'S1AXNSAF912433K     Samsung SSD 840 PRO Series              DXM06B0Q'},                  add_disk_info = new_disk_info[slot]                  add_disk_info['server_obj'] = server_obj                    #### 可以将增加的变更 {2,3,4,5} 数据记录到变更日志表中                  models.Disk.objects.create(**add_disk_info)            # 删除的槽位数据          del_slot = old_slot.difference(new_slot)            if del_slot:              models.Disk.objects.filter(server_obj=server_obj, slot__in=del_slot).delete()              ### 将删除的槽位数据记录到变更日志表中            # 更新的槽位数据          up_slot = new_slot.intersection(old_slot)          if up_slot:              for slot in up_slot:                  ## {'slot': '0', 'pd_type': 'SATA', 'capacity': '279.396', 'model': 'SEAGATE ST300MM0006     LS08S0K2B5NV'}                  new_disk_row = new_disk_info[slot]                  ## obj(slot:0, pd_type:sas, capacity:234,...)                  old_disk_row = models.Disk.objects.filter(slot=slot, server_obj=server_obj).first()                    for k, new_v in new_disk_row.items():                      '''                      k: slot, pd_type, capacity, model...                      new_v:  0, SATA,  279 , ...                      '''                      # 利用反射获取                      old_v = getattr(old_disk_row, k)                        if new_v != old_v:                          # 记录变更日志,利用反射添加                          setattr(old_disk_row, k, new_v)                  old_disk_row.save()          print(info)          return HttpResponse('ok')      else:          print('get')          print(request.body)          return ['c1.com','a2']