CMDB

Python的方向

1.web开发(Django,flask,tornado)
2.自动化运维(CMDB项目)
3.爬虫和数据分析
4.人工智能 机器学习 算法
5.自动化测试

CMDB项目

自动化运维
 运维:管理服务器 分为:基础运维,应用运维

为什么使用cmdb?
    -为了提高运维效率(实现自动化运维的第一步)
    -Excel管理资产过于混乱,不方便年底进行资产的审计,因此需要开发一套cmdb项目
 
为什么需要自动化运维?
    1.项目上线
        流程:产品经理调研(画出原型图)--> 定需求 -->三方会谈(产品经理,研发,大佬们) --> 定日期 -->测试项目 --> 最终上线 -->应用运维
        目前:把代码打包给运维,运维解压上线
        问题:随着机器数量的线性增加,运维的工作量也是线性增加,重复而无意义的劳动
        解决:
            1.写一个shell脚本,进行部署
            2.搞一个自动化代码上线系统
                必要条件:
                    服务器的各种信息(主机名,cpu,硬盘大小等)
    
    2.监控系统
        检测服务器的各种信息(硬盘是否满,cpu的使用率,内存的使用率,网站服务运行是否正常)
        问题:之前写简单的脚本,检测服务器的信息,比较麻烦
        解决:想将服务器的各种信息以图表的信息展示在web界面上(可视化)
            必要条件:服务器的各种信息(主机名,cpu,硬盘大小等)
                
    3.自动化装机系统
        问题:人工去装机,一台一台的装
        解决:搞一个装机系统,cobbler软件
               必要条件:服务器的各种信息(主机名,cpu等)
    
    4.Excel表格审计管理资产
    
cmdb实现的核心
    目标:收集服务器的信息(cpu,内存,网卡,硬盘等)
    
    实现方式:
        1.linux命令获取cpu,内存,网卡
        2.python执行linux的命令
            subprocess模块 getoutput函数
    
    4种实现方案:
        1.agent方式:
            实现方式:用subprocess模块执行linux命令安装agent脚本,执行完成以后用requests模块发送get,post请求给api(主动推送),api拿到数据以后进行数据清洗分析,分析完成入库,然后可以在web管理界面进行展示.
            缺点:每台服务器都要放置agent脚本
            优点:速度快
            使用场景:服务器较多的时候
         
        2.ssh类的方式:
            实现:中控机写paramiko模块脚本,登录上去,直接在服务器上执行命令,执行成功以后将命令结果返回给中控机.中控机将结果返回给api,api拿到数据以后进行数据清洗分析,分析完成入库,然后可以在web管理界面进行展示.        
            优点:无agent
            缺点:有一个中控机,速度慢
            使用场景:服务器比较少的时候
                
        3.slat-stack方式(python写的):
            实现:salt-master通过执行命令从ZeroMQ中拿到执行salt-minion放入其中的数据,将结果发送给api,pi拿到数据以后进行数据清洗分析,分析完成入库,然后可以在web管理界面进行展示.
            使用场景:公司已经使用salt-stack软件
            优点:开发成本低,速度快
            缺点:依赖saltstack软件
            linux安装salt-master:
                [root@db01 ~]# yum install salt-master
                
                配置文件
                [root@db01 ~]# vim /etc/salt/master
                    interface: 本机IP
                
                启动
                [root@db01 ~]# service salt-master start
           
               linux安装salt-minion:
                [root@db02 ~]# yum install salt-minion
                
                配置文件
                [root@db02 ~]# vim /etc/salt/minion
                    master: salt-master IP
                
                启动
                [root@db02 ~]# service salt-minion start
            
            授权:
            [root@db01 ~]# salt-key -L : 列出所有的minion主机 
            [root@db01 ~]# salt-key -A : 接受所有的主机
            
            发送命令:
            [root@db01 ~]# salt "主机名"  cmd.run "命令"
            [root@db01 ~]# salt "db02" cmd.run "ifconfig"
        
        4.puppet方式
            rubby写的   

CMDB项目代码分为三大部分

1.服务器数据采集

目标:
    实现上述三种方案,然后通过配置,可以任意切换方案

规划采集项目目录:
    bin:启动文件
    conf:配置文件
    lib:库文件或公共文件
    src/core:源代码
    test:测试文件

配置文件的管理
    参考django的高级配置文件
    django的全局配置文件:管理一些不常用的默认配置 比如:语言,email配置等
    目的:引入settings不仅可以点出来用户自定义的配置也可以点出来全局的配置
    config.py 用户自定义的配置
    global_settings.py 全局配置 
    settings.py 整合自定义配置和全局配置
    核心:setattr, getattr, dir 的用法
    代码:
        from conf import config
        from lib.config import global_settings

        class settings():
            def __init__(self):

                ##整合自定义配置文件
                for k in dir(config):
                    if k.isupper():
                        v = getattr(config,k)
                        setattr(self,k,v)

                   ##整合全局配置文件
                for k in dir(global_settings):
                    if k.isupper():
                        v = getattr(global_settings,k)
                        setattr(self,k,v)
        settings = settings()
  
采集代码
    实现高内聚低耦合
        -一个文件就负责干你这个文件该干的事情
    版本一:
        在start.py中编写业务逻辑
        缺点:扩产性差
        优点:上线快
   
    版本二:
        将业务逻辑的代码以插件的形式,写在src目录下,相对于版本一更加的清爽,业务逻辑更加清晰
        src下创建plugins(插件文件夹),该文件夹下包括:
        basic.py(用来采集基础的信息:主机名,ip信息等)
        board.py(收集主板信息:主板sn号)
        disk.py(硬盘的信息)
        cpu,py(cpu信息)
        nic.py(网卡信息)
        问题:无法随心所欲的备注
    
       版本三:
        可拔式插件采集信息
        
        参考django的中间件的写法
        config.py:

            PLUGINS_DICT = {
                'xxx' : 'src.plugins.xxx.Xxx',
                ......
            }
        
        
        src下plugins文件夹中
        __init__.py:
            def execute(self):
                # 1. 获取配置
                self.pluginSettings = settings.PLUGINS_DICT
                response = {}
                # 2. 循环执行
                for k, v in self.pluginSettings.items():
                    # k: basic  v: src.plugins.basic.Basic
                    module_name, class_name = v.rsplit('.', 1)
                    # 使用字符串路径导入模块 
                    m = importlib.import_module(module_name)
                    # 通过类名获取模块下面的类
                    cls = getattr(m, class_name)
                    res = cls().process()
                       response[k] = res
            return response
        
        插件代码冗余
            1.写一个基类,有一个通用的执行方法,然后所有插件继承基类
            2.将函数名作为参数传入一个函数
            salt的调用:
                    python2:
                        import salt.client
                        local = salt.client.LocalClient()
                        result = local.cmd('c2.salt.com', 'cmd.run', [cmd])

                    python3:
                        import subprocess
                        res_cmd = "salt '%s' cmd.run '%s'" % (self.hostname,cmd)
                        res = subprocess.getoutput(res_cmd)
                        return res
        错误信息的管理:
            容错(代码健壮性):
                import traceback
                traceback.format_exc()        
        
         post数据:
            使用request.body获取数据
资产的采集过程中遇到的问题?
技术问题:
    -Linux命令不熟 方案:查资料,问运维
    -唯一标识问题:
        我们之前用sn相当于电脑mac地址作为唯一标识,到数据库中去取数据
        问题:虚拟机和实体机共用一个sn,导致最终取出来的数据丢失
        解决:
            业务逻辑解决:
                -和产品商量,不采集虚拟机信息,但99%的公司,都是要采集的
            技术方案解决:
                -hostname(主机名)作为唯一标识
                问题:主机名可以随时被修改

                解决方案:
                    运维标准化流程:
                        a.手工录入服务器的机房,机架,第几层
                        b.提前对服务器分配主机名(主机名一定是唯一的)
                        c.第一次采集,需要运行一下采集资产的客户端(cmdbclient)
                            client端:
                                -将主机名保存到一个文件中
                        d.第n次采集,尽管主机名已经被开发更改,但我们永远以文件中的主机名为主
                    ps:生产过程中线上主机名一定要改回去
                       只要涉及到线上或生产环境中的东西千万不要动
解决唯一标识问题代码:
hostname = info['basic']['data']['hostname']
h = open(os.path.join(settings.BASEDIR, 'conf/cert'), 'r', encoding='utf-8').read()
if not h:
    with open(os.path.join(settings.BASEDIR, 'conf/cert'), 'w', encoding='utf-8') as f:
        f.write(hostname)
else:
    info['basic']['data']['hostname'] = h
client.py中,当使用ssh方法或者slat-stack方法给server发送info时,因为是要一台一台服务器连接,发送,所以我们需要用线程池/进程池完成并发处理.
python2:
    线程池:无
    进程池:有
python3:
    线程池:有
    进程池:有

多线程节省资源所以使用多线程
def run(self,hostname):
    info = PluginsManager(hostname=hostname).execute()
    self.post_data(info)
def collect(self):
    host_list = self.get_hosts()
    p = ThreadPoolExecutor(10)
    for hostname in host_list:
        p.submit(self.run,hostname)
__init__,py中的代码:
import importlib
import traceback
from lib.config.settings import settings

#插件管理类
class PluginsManager():
    def __init__(self,hostname=None):
        self.mode = settings.MODE
        self.hostname = hostname
        self.debug = settings.DEBUG
        if self.mode == 'ssh':
            self.ssh_port = settings.SSH_PORT
            self.ssh_username = settings.SSH_USERNAME
            self.ssh_pwd = settings.SSH_PWD
  def execute(self):
        #1.获取配置
        self.pluginSettings = settings.PLUGINS_DICT
        response =  {}
        #2.循环执行
        for k,v in self.pluginSettings.items():
            ret = {'status':1,'data':None}
            #k:basic v:src.plugins.basic.Basic
            try:
                module_name,class_name = v.rsplit('.',1)
                m = importlib.import_module(module_name)
                cls = getattr(m,class_name)
                res = cls().process(self.command,self.debug)
                ret['data'] = res
                response[k] = ret
            except Exception as e:
                ret['code'] = 2
                ret['data'] = "[%s] 模式下的主机 [%s] 采集[%s]出错,出错信息是:[%s]" % (self.mode, self.hostname if self.hostname else 'agent', k, traceback.format_exc())
                response[k] = ret
        return response

    def command(self,cmd):
        if self.mode == 'agent':
            return self.__agent(cmd)
        elif self.mode == 'ssh':
            return self.__ssh(cmd)
        elif self.mode == 'salt':
            return self.__salt(cmd)
        else:
            return '只支持采集的模式为:agent/ssh/salt 模式'

    #私有方法,外部的类无法调用
    def __agent(self,cmd):
        import subprocess
        res = subprocess.getoutput(cmd)
        return res

    def __ssh(self,cmd):
        import paramiko

        # 创建SSH对象
        ssh = paramiko.SSHClient()
        # 允许连接不在know_hosts文件中的主机
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        # 连接服务器
        ssh.connect(hostname=self.hostname, port=self.ssh_port, username=self.ssh_username, password=self.ssh_pwd)
        # 执行命令
        stdin, stdout, stderr = ssh.exec_command(cmd)
        # 获取命令结果
        result = stdout.read()
        # 关闭连接
        ssh.close()
        return result

    def __salt(self,cmd):
        import subprocess
        res_cmd = "salt '%s' cmd.run '%s'" % (self.hostname,cmd)
        res = subprocess.getoutput(res_cmd)
        return res

2.API获取数据并清洗入DB

数据库的设计:
    重点: 表和表之间的关系

    资产: 
        现在只收集服务器的信息
        将来回收集交换机还有路由器等网络设备

    目录结构:
        创建新app:python3 manage.py startapp backend
                python3 manage.py startapp repository
        api : 只负责数据的接收和清洗, 并入库
        backend : 负责后台数据的管理和记录
        repository: 负责数据模型的创建
API的验证:
    1.较简单的验证
    client.py
        import requests
        token = 'asdfghjkl'
        #将token值发送
        requests.get('https://127.0.0.1.8000/asset',headers={'Token':token})
    
    server.py
        server_token = 'asdfghjkl'
        #接受客户端的token值
        client_token = request.META.get('HTTP_TOKEN')
        #两者进行比较
        if server_token != client_token:
            return HttpResponse('非法请求')
    问题:黑客可以通过抓包获取到url和token
    
    2.加密验证
    client.py
        import requests,time,hashlib
        token = 'asdfghjkl'
        client_time = time.time()
        tmp = '%s|%s'%(client_time,token)
        m = hashlib.md5()
        m.update(bytes(tmp,encoding='utf-8'))
        time_token = m.hexdigest()
        #将token和时间都发送过去
        client_token = "%s|%s"%(time_token,client_time)
        requests.get('http://127.0.0.1:8000/asset',headers={'Token':client_token})
    
    server.py
           server_token = 'asdfghjkl'
        info = request.META.get('HTTP_TOKEN')
        client_token , client_time = info.split('|')
        #md5加密
        m = hashlib.md5()
        tmp = '%s|%s' %(client_time,server_token)
        m.update(bytes(tmp,encoding='utf-8'))
        server_ctoken = m.hexdigest()
        if client_token != server_ctoken:
            return HttpResponse('非法请求!')
    
    问题:每次请求都会产生一条请求,这样就会产生成千上万条请求,如果加密方式不变,黑客抓取其中一条一直进行请求,总会获取到数据 
        
    3.最终版本
    client.py
        import requests,time,hashlib
        token = 'asdfghjkl'
        client_time = time.time()
        tmp = '%s|%s'%(client_time,token)
        m = hashlib.md5()
        m.update(bytes(tmp,encoding='utf-8'))
        time_token = m.hexdigest()
        #将token和时间都发送过去
        client_token = "%s|%s"%(time_token,client_time)
        requests.get('http://127.0.0.1:8000/asset',headers={'Token':client_token})
    
    server.py
           key_record = {}
        def asset():
            #最终方案
            server_token = 'asdfghjkl'
            info = request.META.get('HTTP_TOKEN')
            client_token , client_time = info.split('|')

            # 1.超时时间验证
            client_time = float(client_time)
            server_time = time.time()
            if server_time - client_time >30:
                return HttpResponse('第一关超时了!')

            #2.md5加密验证
            m = hashlib.md5()
            tmp = '%s|%s' %(client_time,server_token)
            m.update(bytes(tmp,encoding='utf-8'))
            server_ctoken = m.hexdigest()
            if client_token != server_ctoken:
                return HttpResponse('第二关 md5编码错误!')

            #3.client_token 只能用一次 (可以使用redis数据库进行设置,将client_token设为k,client_time设为v,过期时间设为10秒)
            for k in list(key_record.keys()):
                v = key_record[k]
                if server_time > v:
                    del key_record[k]

            if client_token in key_record:
                return HttpResponse('第三关 已经访问过了')
            else:
                key_record[client_token] = client_time + 10
            return HttpResponse('重要的数据')     
数据清洗入库
以disk表为例:
    新的数据:
        {
            '0': {'slot': '0', 'pd_type': 'SAS', 'capacity': '279.396', 'model': 'SEAGATE ST300MM0006     LS08S0K2B5NV'}, 
            '1': {'slot': '1', 'pd_type': 'SAS', 'capacity': '279.396', 'model': 'SEAGATE ST300MM0006     LS08S0K2B5AH'}, 
            '2': {'slot': '2', 'pd_type': 'SATA', 'capacity': '476.939', 'model': 'S1SZNSAFA01085L     Samsung SSD 850 PRO 512GB               EXM01B6Q'}, 
            '3': {'slot': '3', 'pd_type': 'SAS', 'capacity': '476.939', 'model': 'S1AXNSAF912433K     Samsung SSD 840 PRO Series              DXM06B0Q'}, 
            '4': {'slot': '4', 'pd_type': 'SATA', 'capacity': '476.939', 'model': 'S1AXNSAF303909M     Samsung SSD 840 PRO Series              DXM05B0Q'}, 
            '5': {'slot': '5', 'pd_type': 'SATA', 'capacity': '476.939', 'model': 'S1AXNSAFB00549A     Samsung SSD 840 PRO Series              DXM06B0Q'}
        }
        new_disk_data = info['disk']['data']
        new_disk_slot = list(info['disk']['data'].keys())
    旧的数据:
        '''
        [
            {'slot':1, 'pd_type':'sas'},
            {'slot':2, 'pd_type':'sas'},
            ....
        ]
        '''
        通过server_obj从数据库中获取相应的disk表中的数据
          #old_disk_data = models.Disk.object.filter(server_obj = server_obj).all()
        old_disk_data = server_obj.disk.all()
        old_disk_solt = []
        for i in old_disk_data:
            old_disk_solt.append(item.solt)
            
    增加:
        add_slot = set(new_disk_solt).difference(set(old_disk_slot))
        if add_slot:
            change_log = []
            for slot in add_slot:
                disk_res = new_disk_data[slot]
                disk_res['server_obj'] = server_obj
                models.Disk.objects.create(**disk_res)
                
                #信息
                tmp = "增加硬盘 插槽是{slot}, 类型{pd_type}, 容量{capacity}, 型号{model}".format(**disk_res)
                change_log.append(tmp) 
            #列表转换为字符串
            content = ';'.join(change_log)
            #将信息内容存入资产变更表
            models.AssetRecord.objects.create(asset_obj=server_obj.asset, content=content)
    
    删除:
        del_solt = set(old_disk_slot).difference(set(new_disk_slot))
        if del_slot:
            # 到数据库中删除
            change_log = []
            for slot in del_slot:
                models.Disk.objects.filter(slot=slot, server_obj=server_obj).delete()
                tmp = "删除服务器%s 上的硬盘, 插槽%s" % (hostname, slot)
                change_log.append(tmp)
            
            #列表转换为字符串
            content = ';'.join(change_log)
            #将信息内容存入资产变更表
            models.AssetRecord.objects.create(asset_obj=server_obj.asset, content=content)
            
    更新:
        up_slot = set(old_disk_slot).intersection(set(new_disk_slot))
        if up_slot:
            change_log = []
            for slot in up_slot:
                '''
                {'slot': '4', 'pd_type': 'SATA', 'capacity': '476.939', 'model': 'Samsung SSD 840 PRO SeriesDXM05B0Q'}, 
                '''
                new_disk_row = new_disk_data[slot]
                
                '''
                {'slot':'4', 'pd_type':'sas', 'capacity':512, 'model':'Samsung SSD 840 PRO SeriesDXM05B0Q'}
                '''
                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: 4 ,sata, 476.939, Samsung
                    '''
                    #通过getattr方法拿到k对应的old_v
                    old_v = getattr(old_disk_row, k)
                    if str(new_v) != str(old_v):
                        #将old_disk_row的k值替换成new_v
                        setattr(old_disk_row, k, new_v)
                        tmp = "变更硬盘: 槽位 %s, %s 由 %s 变成了 %s" % (slot, key_map[k], old_v, new_v)
                        change_log.append(tmp)
                #old_disk_row直接进行保存
                old_disk_row.save()

            content = ';'.join(change_log)
            models.AssetRecord.objects.create(content=content, asset_obj=server_obj.asset)

3.web管理界面的展示

后台:
def server(request):
       return render(request, 'server.html')

def server_ajax(request):

    table_config = [
        {
            'field' : 'id', # 数据库中的一个字段
            'text'  : 'ID',    # 表头中显示的内容
            'display' : 1,  #控制显示
        },
        {
            'field': 'hostname',  # 数据库中的一个字段
            'text': '主机名',    # 表头中显示的内容
            'display' : 1     
        },
        {
            'field': 'sn',  # 数据库中的一个字段
            'text': 'sn号', # 表头中显示的内容
            'display':1
        },
           {
             'field': 'asset__business_unit__name',  # 数据库中的一个字段,可以连表查询
             'text': '产品线',  # 表头中显示的内容
             'display': 0   
        }
    ]
    
    filed_list = []
    for item in table_config:
        if item['display']:
            filed_list.append(item['field'])

    data_list = models.Server.objects.values(*filed_list)


    ret = {
        'data_list' : list(data_list),
        'table_config' : table_config
    }

    return HttpResponse(json.dumps(ret))

前端:server.html
<table>
    <thead id="mythead">
    
    </thead>

   <tbody id="mybody">
    
   </tbody>
</table>

<script>
    $.ajax({
        url : '/backend/server_ajax/',
        type: 'GET',
        dataType: 'json',   // 相当于 JSON.parse()
        success: function (msg) {
            // console.log(JSON.parse(msg)); 第一种转换方式
            console.log(msg);
            initThead(msg.table_config);
            initTbody(msg.data_list, msg.table_config);
        }
    });


    function initThead(table_config){
        /*
        table_config = [
            {
                'field' : 'id', # 数据库中的一个字段
                'text'  : 'ID',
                'display' : 1,
            },
        ]
         */
        var tr = document.createElement('tr'); // <tr></tr>

        $.each(table_config, function (k, v) {
            if(v['display']){
                var th = document.createElement('th'); // <th>dsads</th>
                th.innerHTML = v['text'];
                $(tr).append($(th));
            }

        });

        /*
            <tr>
                <th>dbsjada</th>
            </tr>
         */
        $('#mythead').append($(tr));
    }


    function initTbody(data_list, table_config) {

        /**
         * data_list = [{id: 1, hostname: "c1.com", sn: "gytrytrytr43543543"}]
         *
         * table_config = [
            {
                'field' : 'id', # 数据库中的一个字段
                'text'  : 'ID',
                'display' : 1,
            },
        ]
         */

        $.each(data_list, function (k, value) {
            var tr = document.createElement('tr');

            $.each(table_config, function (k1, item) {
                var td = document.createElement('td');
                td.innerHTML = value[item['field']];

                $(tr).append($(td));
            });

             $('#mybody').append($(tr));
        });
    }

</script>
前端界面框架:  x-admin  layui
前端图表:移动端:ANTV    PC端:HCharts ECharts
posted @ 2019-05-27 22:29  Zhuang_Z  阅读(313)  评论(0)    收藏  举报