CMDB项目

一 CMDB简介

1.1 什么是CMDB?

CMDB(资产管理系统)是所有运维工具的数据基础

1.2 CMDB包含的功能

用户管理,记录测试,开发,运维人员的用户表

业务线管理,需要记录业务的详情

项目管理,指定此项目用属于哪条业务线,以及项目详情

应用管理,指定此应用的开发人员,属于哪个项目,和代码地址,部署目录,部署集群,依赖的应用,软件等信息

主机管理,包括云主机,物理机,主机属于哪个集群,运行着哪些软件,主机管理员,连接哪些网络设备,云主机的资源池,存储等相关信息

主机变更管理,主机的一些信息变更,例如管理员,所属集群等信息更改,连接的网络变更等

网络设备管理,主要记录网络设备的详细信息,及网络设备连接的上级设备

IP管理,IP属于哪个主机,哪个网段, 是否被占用等

1.3 实现的四种方式

1.3.1 Agent实现方式

Agent方式,可以将服务器上面的Agent程序作定时任务,定时将资产信息提交到指定API录入数据库

image-20210312154829211

其本质上就是在各个服务器上执行subprocess.getoutput()命令,然后将每台机器上执行的结果,返回给主机API,然后主机API收到这些数据之后,放入到数据库中,最终通过web界面展现给用户

#linux
import subprocess
import re
res = subprocess.getoutput("ifconfig")
print(res)
ip=re.findall('inet (.*?) netmask',res)
print(ip)

# windows
import subprocess
import re
res=subprocess.getoutput('ipconfig')

print(res)
ip=re.findall('IPv4 地址 . . . . . . . . . . . . : (.*)',res)
print(ip)

优点:速度快
缺点:需要为每台服务器部署一个Agent程序

使用crontab定时执行python脚本

# 1 进入创建crontab定时任务
crontab -e 
# 2 写入任务(每分钟执行一次test.py)
* * * * * python3 test.py
# 3 编写test.py
with open('a.txt','a') as f:
    f.write('hello world')
    
# 4 查看定时任务
crontab -l 

1.3.2 ssh实现方式 (基于Paramiko模块)

中控机通过Paramiko(py模块)登录到各个服务器上,然后执行命令的方式去获取各个服务器上的信息

image-20210312155000231

优点:无Agent

缺点:速度慢

如果在服务器较少的情况下,可应用此方法

import paramiko
import re
#创建SSH对象
ssh = paramiko.SSHClient()

# 允许连接不在know_hosts文件中的主机
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy)

# 连接服务器
ssh.connect(hostname='101.133.225.166',port=22,username='root',password='')
# 执行命令
stdin,stdout,stderr = ssh.exec_command('ifconfig')

# 获取命令结果
result = stdout.read().decode('utf-8')

print(result)
ip=re.findall('inet (.*?) netmask',result)
print(ip)
# 关闭连接
ssh.close()

1.3.3 saltstack方式

image-20210312155144213

此方案本质上和第二种方案大致是差不多的流程,中控机发送命令给服务器执行。服务器将结果放入另一个队列中,中控机获取将服务信息发送到API进而录入数据库。

执行流程:
第一步: 由管理员录入资产(主机名,SN等信息),通过后台管理,录入数据库
第二步: salt-master从数据库获取未采集资产信息的服务器
第三步: salt-master发送命令给salt-minion执行
第四步: salt-master拿到执行结果
第五步: 将结果发送给API
第六步: API将其写入数据库

解释:
salt-master可以理解为主人
salt-minion可以理解为奴隶

优点:快,开发成本低

缺点:依赖于第三方工具

salstack的安装和配置

1.安装和配置

master端:
"""
1. 安装salt-master
    yum install salt-master
2. 修改配置文件:/etc/salt/master
    interface: 0.0.0.0    # 表示Master的IP 
3. 启动
    service salt-master start
"""
slave端:
"""
1. 安装salt-minion
    yum install salt-minion
2. 修改配置文件 /etc/salt/minion
    master: 10.211.55.4           # master的地址
    或
    master:
        - 10.211.55.4
        - 10.211.55.5
    random_master: True
    id: c2.salt.com                    # 客户端在salt-master中显示的唯一ID
3. 启动
    service salt-minion start

2.授权

salt-key -L                    # 查看已授权和未授权的slave
salt-key -a  salve_id      # 接受指定id的salve
salt-key -r  salve_id      # 拒绝指定id的salve
salt-key -d  salve_id      # 删除指定id的salve

3.执行命令

在master服务器上对salve进行远程操作

salt 'c2.salt.com' cmd.run  'ifconfig'
# 基于API的方式
import salt.client
local = salt.client.LocalClient()
result = local.cmd('c2.salt.com', 'cmd.run', ['ifconfig'])

1.3.4 Puppet(ruby语言开发)(了解)

每隔30分钟,通过RPC消息队列将执行的结果返回给用户

二 三种方案客户端编写

2.1 目录结构划分

autoclient               # 项目名
  -bin                   # 启动文件路径
  	-start.py            # 启动文件
	-config                # 配置文件路径
    -cert                # 私钥
    -custom_settings.py  # 用户自定义配置
	-files                 # 测试数据文件
    -board.out
    -cpuinfo.out
    -disk.out
    -memory.out
    -nic.out
  -lib                  # 库文件夹
    -conf               # 配置信息文件夹
    	-config.py        # 配置类
    	-global_settings.py # 全局常量配置
    -convert.py          # 公共方法
  -src                  # 源文件
    -plugins             # 插件
      -__init__.py       # 初始化文件
      -basic.py          
      -board.py
      -cpu.py
      -disk.py
      -memory.py
      -nic.py
    script.py           # 脚本文件
    client.py           # 客户端类
tests                   # 测试文件夹


# 总结:bin,config,files,lib,src几个文件夹

2.2 仿django配置文件

custom_settings.py

# 用户配置
PORT = 22
USER = 'lqz'

global_settings.py

#### 全局配置

PORT = 22
USER = 'root'

config.py

from config import custom_settings
from . import global_settings

class Settings():
    def __init__(self):

        #### 全局配置
        for key in dir(global_settings):
            if key.isupper():
                #### 获取key所对应的值
                v = getattr(global_settings, key)
                #### 设置key以及值到当前的setting对象
                setattr(self, key, v)

        #### 自定制配置
        for key in dir(custom_settings):
            if key.isupper():
                 #### 获取key所对应的值
                 v = getattr(custom_settings, key)
                 #### 设置key以及值到当前的setting对象
                 setattr(self, key, v)

settings = Settings()

2.3 可插拔式配置

custom_settings.py

### 可插拔式的采集,注释掉某个就不会执行
PLUGINS_DICT = {
    'basic':'src.plugins.basic.Basic',
    'board':'src.plugins.board.Board',
    'cpu':'src.plugins.cpu.Cpu',
    'disk':'src.plugins.disk.Disk',
    'nic':'src.plugins.nic.Nic',
    'memory':'src.plugins.memory.Memory',
}

src/plugins/__init__.py

import traceback

from lib.conf.config import settings
import importlib
import subprocess
### 管理插件信息的类
class PluginsManager(object):

    def __init__(self, hostname=None):
        pass

    ### 读取配置文件中的pluginsdict, 并执行对应模块中的process方法
    def execute(self):
        response = {}
        for k, v in self.plugins_dict.items():
            ret = {"status":None, 'data':None}
            '''
            k:  board,...
            v:  src.plugins.board.Board   字符串
            '''
            try:
                # 1. 导入模块路径
                moudle_path, class_name = v.rsplit('.', 1)
                # 2. 导入这个路径
                moudle_name = importlib.import_module(moudle_path)
                # 3. 导入对应模块下的类
                classobj = getattr(moudle_name, class_name)
                # 4. 执行类下面对应的process方法
                res = classobj().process()
            except Exception as e:
								pass
        return response

src/plugins/cpu.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import os
from lib.conf.config import settings

class Cpu(object):
    def __init__(self):
        pass

    @classmethod
    def initial(cls):
        return cls()

    def process(self, command_func, debug):
        print('cpu print')

src/plugins/disk.py

import os
from lib.conf.config import settings

class Cpu(object):
    def __init__(self):
        pass

    @classmethod
    def initial(cls):
        return cls()

    def process(self, command_func, debug):
        print('disk print')

2.4 冗余代码抽取

继承方式

把函数当参数传入函数中:

在src/plugins/init.py中写,__隐藏,调用execute的时候,把函数地址和命令传入

import traceback

from lib.conf.config import settings
import importlib
import subprocess
### 管理插件信息的类
class PluginsManager(object):

    def __init__(self, hostname=None):
        self.plugins_dict = settings.PLUGINS_DICT
        self.hostname = hostname  # 采集客户端的地址
        self.debug = settings.DEBUG
        if settings.MODE == 'ssh': # ssh方式才需要端口,用户名,密码,这些应该放到配置文件中
            self.port = settings.SSH_PORT
            self.name = settings.SSH_USERNAME
            self.pwd  = settings.SSH_PASSWORD

    ### 读取配置文件中的pluginsdict, 并执行对应模块中的process方法
    def execute(self):
        response = {}
        for k, v in self.plugins_dict.items():
            ret = {"status":None, 'data':None}
            '''
            k:  board,...
            v:  src.plugins.board.Board   字符串
            '''
            try:
                # 1. 导入模块路径
                moudle_path, class_name = v.rsplit('.', 1)
                # 2. 导入这个路径
                moudle_name = importlib.import_module(moudle_path)
                # 3. 导入对应模块下的类
                classobj = getattr(moudle_name, class_name)
                # 4. 执行类下面对应的process方法
                res = classobj().process(self.__cmd_run, self.debug)
                ret['status'] = 10000
                ret['data'] = res
            except Exception as e:
                ret['status'] = 10001
                ret['data']=  "[%s] 采集 [%s] 出错了, 错误信息是:%s" % (self.hostname if self.hostname else "Agent", k, str(traceback.format_exc()))
            response[k] = ret
        return response


    def __cmd_run(self, cmd):
        if settings.MODE == 'agent':
            return self.__cmd_agent(cmd)
        elif settings.MODE == 'ssh':
            return self.__cmd_ssh(cmd)
        elif settings.MODE == 'salt':
            return self.__cmd_salt(cmd)
        else:
            print("只支持的模式有:agent/ssh/salt")

    def __cmd_agent(self, cmd):
        res = subprocess.getoutput(cmd)
        return res

    def __cmd_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.port, username=self.name, password=self.pwd)
        # 执行命令
        stdin, stdout, stderr = ssh.exec_command(cmd)
        # 获取命令结果
        result = stdout.read()
        # 关闭连接
        ssh.close()
        return result

    def __cmd_salt(self, cmd):
        command = "salt %s cmd.run %s" % (self.hostname, cmd)
        res = subprocess.getoutput(command)
        return res

在cpu.py disk.py中编写

class Cpu(object):
    def __init__(self):
        pass

    @classmethod
    def initial(cls):
        return cls()

    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)

2.5 解析数据(以主板为例)

# sudo dmidecode -t1 https://ipcmen.com/dmidecode
# 可以获取BIOS,系统,主板,处理器,内存,缓存等 序列号、电脑厂商、串口信息以及其它系统配件信息
res  = '''
SMBIOS 2.7 present.

Handle 0x0001, DMI type 1, 27 bytes
System Information
	Manufacturer: Parallels Software International Inc.
	Product Name: Parallels Virtual Platform
	Version: None
	Serial Number: Parallels-1A 1B CB 3B 64 66 4B 13 86 B0 86 FF 7E 2B 20 30
	UUID: 3BCB1B1A-6664-134B-86B0-86FF7E2B2030
	Wake-up Type: Power Switch
	SKU Number: Undefined
	Family: Parallels VM
'''

key_map = {
    "Manufacturer" : 'manufacturer',
    "Product Name" : 'product_name',
    "Serial Number": 'sn'
}

result = {}
data = res.strip().split('
')
# print(data)
for k in data:
    v = (k.strip().split(':'))
    if len(v) == 2:
        if v[0] in key_map:
           result[key_map[v[0]]]  = v[1].strip()

print(result)

'''
result = {
    'manufacturer' : 'Parallels Software International Inc.'   ,
    'product_name' : 'Parallels Virtual Platform',
    'sn' : 'Parallels-1A 1B CB 3B 64 66 4B 13 86 B0 86 FF 7E 2B 20 30'
}
'''

2.6 代码整合

plugins
  -__init__.py
  -basic.py
  -board.py
  -cpu.py
  -disk.py
  -memory.py
  -nic.py
#__init__.py
import traceback

from lib.conf.config import settings
import importlib
import subprocess
### 管理插件信息的类
class PluginsManager(object):

    def __init__(self, hostname=None):
        self.plugins_dict = settings.PLUGINS_DICT
        self.hostname = hostname
        self.debug = settings.DEBUG
        if settings.MODE == 'ssh':
            self.port = settings.SSH_PORT
            self.name = settings.SSH_USERNAME
            self.pwd  = settings.SSH_PASSWORD

    ### 读取配置文件中的pluginsdict, 并执行对应模块中的process方法
    def execute(self):
        response = {}
        for k, v in self.plugins_dict.items():
            ret = {"status":None, 'data':None}
            '''
            k:  board,...
            v:  src.plugins.board.Board   字符串
            '''
            try:
                # 1. 导入模块路径
                moudle_path, class_name = v.rsplit('.', 1)
                # 2. 导入这个路径
                moudle_name = importlib.import_module(moudle_path)
                # 3. 导入对应模块下的类
                classobj = getattr(moudle_name, class_name)
                # 4. 执行类下面对应的process方法
                res = classobj().process(self.__cmd_run, self.debug)
                ret['status'] = 10000
                ret['data'] = res
            except Exception as e:
                ret['status'] = 10001
                ret['data']=  "[%s] 采集 [%s] 出错了, 错误信息是:%s" % (self.hostname if self.hostname else "Agent", k, str(traceback.format_exc()))
            response[k] = ret
        return response


    def __cmd_run(self, cmd):
        if settings.MODE == 'agent':
            return self.__cmd_agent(cmd)
        elif settings.MODE == 'ssh':
            return self.__cmd_ssh(cmd)
        elif settings.MODE == 'salt':
            return self.__cmd_salt(cmd)
        else:
            print("只支持的模式有:agent/ssh/salt")

    def __cmd_agent(self, cmd):
        res = subprocess.getoutput(cmd)
        return res

    def __cmd_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.port, username=self.name, password=self.pwd)
        # 执行命令
        stdin, stdout, stderr = ssh.exec_command(cmd)
        # 获取命令结果
        result = stdout.read()
        # 关闭连接
        ssh.close()
        return result

    def __cmd_salt(self, cmd):
        command = "salt %s cmd.run %s" % (self.hostname, cmd)
        res = subprocess.getoutput(command)
        return res
# basic.py

class Basic(object):
    def __init__(self):
        pass

    @classmethod
    def initial(cls):
        return cls()

    def process(self, command_func, debug):
        if debug:
            output = {
                'os_platform': "linux",
                'os_version': "CentOS release 6.6 (Final)
Kernel 
 on an m",
                'hostname': 'c2000.com'
            }
        else:
            output = {
                'os_platform': command_func("uname").strip(),
                'os_version': command_func("cat /etc/issue").strip().split('
')[0],
                'hostname': command_func("hostname").strip(),
            }


        return output
# board.py
import os
from lib.conf.config import settings


class Board(object):
    def __init__(self):
        pass

    @classmethod
    def initial(cls):
        return cls()

    def process(self, command_func, debug):
        if debug:
            output = open(os.path.join(settings.BASEDIR, 'files/board.out'), 'r', encoding='utf-8').read()
        else:
            output = command_func("sudo dmidecode -t1")
        return self.parse(output)

    def parse(self, content):

        result = {}
        key_map = {
            'Manufacturer': 'manufacturer',
            'Product Name': 'model',
            'Serial Number': 'sn',
        }

        for item in content.split('
'):
            row_data = item.strip().split(':')

            if len(row_data) == 2:
                if row_data[0] in key_map:
                    result[key_map[row_data[0]]] = row_data[1].strip() if row_data[1] else row_data[1]

        return result
# cpu.py
import os
from lib.conf.config import settings

class Cpu(object):
    def __init__(self):
        pass

    @classmethod
    def initial(cls):
        return cls()

    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('

'):
            for row_line in item.split('
'):
                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
#disk.py
import re
import os
from lib.conf.config import settings


class Disk(object):
    def __init__(self):
        pass

    @classmethod
    def initial(cls):
        return cls()

    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("sudo MegaCli  -PDList -aALL")
        return self.parse(output)

    def parse(self, content):
        """
        解析shell命令返回结果
        :param content: shell 命令结果
        :return:解析后的结果
        """
        response = {}
        result = []
        for row_line in content.split("



"):
            result.append(row_line)
        for item in result:
            temp_dict = {}
            for row in item.split('
'):
                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
# memory.py
import os
from lib import convert
from lib.conf.config import settings


class Memory(object):
    def __init__(self):
        pass

    @classmethod
    def initial(cls):
        return cls()

    def process(self, command_func, debug):
        if debug:
            output = open(os.path.join(settings.BASEDIR, 'files/memory.out'), 'r', encoding='utf-8').read()
        else:
            output = command_func("sudo dmidecode  -q -t 17 2>/dev/null")

        return self.parse(output)

    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('
	')
            for line in lines:
                if not line.strip():
                    continue
                if len(line.split(':')):
                    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
#nic.py 网络接口控制器
import os
import re
from lib.conf.config import settings

class Nic(object):
    def __init__(self):
        pass

    @classmethod
    def initial(cls):
        return cls()

    def process(self, command_func, debug):
        if debug:
            output = open(os.path.join(settings.BASEDIR, 'files/nic.out'), 'r', encoding='utf-8').read()
            interfaces_info = self._interfaces_ip(output)
        else:
            interfaces_info = self.linux_interfaces(command_func)

        self.standard(interfaces_info)

        return interfaces_info

    def linux_interfaces(self, command_func):
        '''
        Obtain interface information for *NIX/BSD variants
        '''
        ifaces = dict()
        ip_path = 'ip'
        if ip_path:
            cmd1 = command_func('sudo {0} link show'.format(ip_path))
            cmd2 = command_func('sudo {0} addr show'.format(ip_path))
            ifaces = self._interfaces_ip(cmd1 + '
' + cmd2)
        return ifaces

    def which(self, exe):
        def _is_executable_file_or_link(exe):
            # check for os.X_OK doesn't suffice because directory may executable
            return (os.access(exe, os.X_OK) and
                    (os.path.isfile(exe) or os.path.islink(exe)))

        if exe:
            if _is_executable_file_or_link(exe):
                # executable in cwd or fullpath
                return exe

            # default path based on busybox's default
            default_path = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin'
            search_path = os.environ.get('PATH', default_path)
            path_ext = os.environ.get('PATHEXT', '.EXE')
            ext_list = path_ext.split(';')

            search_path = search_path.split(os.pathsep)
            if True:
                # Add any dirs in the default_path which are not in search_path. If
                # there was no PATH variable found in os.environ, then this will be
                # a no-op. This ensures that all dirs in the default_path are
                # searched, which lets salt.utils.which() work well when invoked by
                # salt-call running from cron (which, depending on platform, may
                # have a severely limited PATH).
                search_path.extend(
                    [
                        x for x in default_path.split(os.pathsep)
                        if x not in search_path
                    ]
                )
            for path in search_path:
                full_path = os.path.join(path, exe)
                if _is_executable_file_or_link(full_path):
                    return full_path

        return None

    def _number_of_set_bits_to_ipv4_netmask(self, set_bits):  # pylint: disable=C0103
        '''
        Returns an IPv4 netmask from the integer representation of that mask.

        Ex. 0xffffff00 -> '255.255.255.0'
        '''
        return self.cidr_to_ipv4_netmask(self._number_of_set_bits(set_bits))

    def cidr_to_ipv4_netmask(self, cidr_bits):
        '''
        Returns an IPv4 netmask
        '''
        try:
            cidr_bits = int(cidr_bits)
            if not 1 <= cidr_bits <= 32:
                return ''
        except ValueError:
            return ''

        netmask = ''
        for idx in range(4):
            if idx:
                netmask += '.'
            if cidr_bits >= 8:
                netmask += '255'
                cidr_bits -= 8
            else:
                netmask += '{0:d}'.format(256 - (2 ** (8 - cidr_bits)))
                cidr_bits = 0
        return netmask

    def _number_of_set_bits(self, x):
        '''
        Returns the number of bits that are set in a 32bit int
        '''
        # Taken from http://stackoverflow.com/a/4912729. Many thanks!
        x -= (x >> 1) & 0x55555555
        x = ((x >> 2) & 0x33333333) + (x & 0x33333333)
        x = ((x >> 4) + x) & 0x0f0f0f0f
        x += x >> 8
        x += x >> 16
        return x & 0x0000003f

    def _interfaces_ip(self, out):
        '''
        Uses ip to return a dictionary of interfaces with various information about
        each (up/down state, ip address, netmask, and hwaddr)
        '''
        ret = dict()
        right_keys = ['name', 'hwaddr', 'up', 'netmask', 'ipaddrs']

        def parse_network(value, cols):
            '''
            Return a tuple of ip, netmask, broadcast
            based on the current set of cols
            '''
            brd = None
            if '/' in value:  # we have a CIDR in this address
                ip, cidr = value.split('/')  # pylint: disable=C0103
            else:
                ip = value  # pylint: disable=C0103
                cidr = 32

            if type_ == 'inet':
                mask = self.cidr_to_ipv4_netmask(int(cidr))
                if 'brd' in cols:
                    brd = cols[cols.index('brd') + 1]
            return (ip, mask, brd)

        groups = re.compile('
?
\d').split(out)
        for group in groups:
            iface = None
            data = dict()

            for line in group.splitlines():
                if ' ' not in line:
                    continue
                match = re.match(r'^d*:s+([w.-]+)(?:@)?([w.-]+)?:s+<(.+)>', line)
                if match:
                    iface, parent, attrs = match.groups()
                    if 'UP' in attrs.split(','):
                        data['up'] = True
                    else:
                        data['up'] = False
                    if parent and parent in right_keys:
                        data[parent] = parent
                    continue

                cols = line.split()
                if len(cols) >= 2:
                    type_, value = tuple(cols[0:2])

                    iflabel = cols[-1:][0]
                    if type_ in ('inet',):
                        if 'secondary' not in cols:
                            ipaddr, netmask, broadcast = parse_network(value, cols)
                            if type_ == 'inet':
                                if 'inet' not in data:
                                    data['inet'] = list()
                                addr_obj = dict()
                                addr_obj['address'] = ipaddr
                                addr_obj['netmask'] = netmask
                                addr_obj['broadcast'] = broadcast
                                data['inet'].append(addr_obj)
                        else:
                            if 'secondary' not in data:
                                data['secondary'] = list()
                            ip_, mask, brd = parse_network(value, cols)
                            data['secondary'].append({
                                'type': type_,
                                'address': ip_,
                                'netmask': mask,
                                'broadcast': brd,
                            })
                            del ip_, mask, brd
                    elif type_.startswith('link'):
                        data['hwaddr'] = value
            if iface:
                if iface.startswith('pan') or iface.startswith('lo') or iface.startswith('v'):
                    del iface, data
                else:
                    ret[iface] = data
                    del iface, data
        return ret

    def standard(self, interfaces_info):

        for key, value in interfaces_info.items():
            ipaddrs = set()
            netmask = set()
            if not 'inet' in value:
                value['ipaddrs'] = ''
                value['netmask'] = ''
            else:
                for item in value['inet']:
                    ipaddrs.add(item['address'])
                    netmask.add(item['netmask'])
                value['ipaddrs'] = '/'.join(ipaddrs)
                value['netmask'] = '/'.join(netmask)
                del value['inet']
# lib/convert.py  
def convert_to_int(value,default=0):

    try:
        result = int(value)
    except Exception as e:
        result = default

    return result

def convert_mb_to_gb(value,default=0):

    try:
        value = value.strip('MB')
        result = int(value)
    except Exception as e:
        result = default

    return result
# bin/start.py
from src.plugins import PluginsManager
if __name__ == '__main__':
    res=PluginsManager().execute()
    print(res)

注意

sudo dmidecode -t1

可以获取BIOS,系统,主板,处理器,内存,缓存等 序列号、电脑厂商、串口信息以及其它系统配件信息

https://ipcmen.com/dmidecode

sudo MegaCli -PDList -aALL

需要安装

https://www.cnblogs.com/xth0331/p/9655593.html

2.7 异常处理

traceback使用

import traceback
def test():
    try:
        a = "dsadsa"
        int(a)
    except Exception as e:
        print(traceback.format_exc())

test()

src/plugins/init.py

import traceback

from lib.conf.config import settings
import importlib
import subprocess
### 管理插件信息的类
class PluginsManager(object):

    def __init__(self, hostname=None):
        self.plugins_dict = settings.PLUGINS_DICT
        self.hostname = hostname
        self.debug = settings.DEBUG
        if settings.MODE == 'ssh':
            self.port = settings.SSH_PORT
            self.name = settings.SSH_USERNAME
            self.pwd  = settings.SSH_PASSWORD

    ### 读取配置文件中的pluginsdict, 并执行对应模块中的process方法
    def execute(self):
        response = {}
        for k, v in self.plugins_dict.items():
            ret = {"status":None, 'data':None}
            '''
            k:  board,...
            v:  src.plugins.board.Board   字符串
            '''
            try:
                # 1. 导入模块路径
                moudle_path, class_name = v.rsplit('.', 1)
                # 2. 导入这个路径
                moudle_name = importlib.import_module(moudle_path)
                # 3. 导入对应模块下的类
                classobj = getattr(moudle_name, class_name)
                # 4. 执行类下面对应的process方法
                res = classobj().process(self.__cmd_run, self.debug)
                ret['status'] = 10000
                ret['data'] = res
            except Exception as e:
              	# hostname有值说明不是anget方案,是salstack或paramiko方案
                ret['status'] = 10001
                ret['data']=  "[%s] 采集 [%s] 出错了, 错误信息是:%s" % (self.hostname if self.hostname else "Agent", k, str(traceback.format_exc()))
            response[k] = ret
        return response


    def __cmd_run(self, cmd):
        if settings.MODE == 'agent':
            return self.__cmd_agent(cmd)
        elif settings.MODE == 'ssh':
            return self.__cmd_ssh(cmd)
        elif settings.MODE == 'salt':
            return self.__cmd_salt(cmd)
        else:
            print("只支持的模式有:agent/ssh/salt")

    def __cmd_agent(self, cmd):
        res = subprocess.getoutput(cmd)
        return res

    def __cmd_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.port, username=self.name, password=self.pwd)
        # 执行命令
        stdin, stdout, stderr = ssh.exec_command(cmd)
        # 获取命令结果
        result = stdout.read()
        # 关闭连接
        ssh.close()
        return result

    def __cmd_salt(self, cmd):
        command = "salt %s cmd.run %s" % (self.hostname, cmd)
        res = subprocess.getoutput(command)
        return res

2.8 把采集到的数据上传

客户端

##  src/client
import requests
from lib.conf.config import settings
from src.plugins import PluginsManager
import os
class Base():
    def post_data(self, server_info):
        requests.post(settings.API_URL, json=server_info)

class Agent(Base):
    ### 收集数据并发送
    def collectAndPost(self):
        server_info = PluginsManager().execute()

        hostname = server_info['basic']['data']['hostname']  ### c10000.com
        res = open(os.path.join(settings.BASEDIR, 'config/cert'), 'r', encoding='utf-8').read()

        if not res.strip():
            #### 第一次采集, 将采集的hostname写入到一个文件中
            with open(os.path.join(settings.BASEDIR, 'config/cert'), 'w', encoding='utf-8') as fp:
                fp.write(hostname)
        else:
            #### 第二次采集的时候, 永远以第一次文件中保存的主机名为标准
            server_info['basic']['data']['hostname'] = res


        for k, v in server_info.items():
            print(k, v)
        # requests.post(settings.API_URL, data=json.dumps(res))
        ### Content-Type':"application/json"
        self.post_data(server_info)


class SSHSalt(Base):
    def get_hostnames(self):
        hostnames = requests.get(settings.API_URL)
        return ['c1.com', 'c2.com']

    def run(self, hostname):
        server_info = PluginsManager(hostname).execute()
        self.post_data(server_info)

    def collectAndPost(self):
        hostnames = self.get_hostnames()
        ### 单线程执行, 循环速度比较慢
        # for hostname in hostnames:
        #     server_info = PluginsManager(hostname).execute()
        #     self.post_data(server_info)

        ### 线程池的方式采集数据
        from concurrent.futures import ThreadPoolExecutor
        p = ThreadPoolExecutor(10)
        for hostname in hostnames:
            p.submit(self.run, hostname)
# src/script.py
from src.client import Agent
from src.client import SSHSalt
from lib.conf.config import settings

def run():
    if settings.MODE == 'agent': 
        obj = Agent()
    else:# 不管salt和paramiko方式,都需要从服务器获取客户端ip地址
        obj = SSHSalt()
    obj.collectAndPost()
# bin/start.py
from src.script import run
if __name__ == '__main__':
    run()

服务端

2.9 唯一标识的问题

	
# 目标:将变更的信息通过程序的比对, 记录下来
	#第一天的时候:
	# 采集数据:
		{'status': 10000, 'data': {'os_platform': 'linux', 'os_version': 'CentOS release 6.6 (Final)
Kernel 
 on an \m', 'hostname': 'c2.com'}}
			
	#API清洗的时候:
		因为是第一次, 数据库中并没有采集的数据
		数据入库:
				server:1000条
					id    sn        os_platform   os_version    disk_size
					1     dsadsa       linux        CentOS        250G
					........		
	#第二天的时候(数据发生变化,应该比对):
			#采集数据:	
				{'status': 10000, 'data': {'os_platform': 'linux', 'os_version': 'CentOS release 6.6 (Final)
Kernel 
 on an \m', 'hostname': 'c2.com'}}
		
				{'status': 10000, 'data': {'0': {'slot': '0', 'pd_type': 'SAS', 'capacity': '300G', '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': 'SATA', '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'}}}

     # API清洗的时候:
				应该在新的POST数据中选取一个 唯一 的字段, 然后到数据库中作为where条件, 获取到对应的数据
				
			问题是  应该选取谁?
				选取的是 sn 序列号(mac地址) 作为唯一的字段
			用sn遇到的问题:
				虚拟机和实体机共用一个sn, 导致数据不准确
				
# 解决的方案:
				a. 如果公司不需要采集虚拟机的信息, 使用sn没有问题
				b. 采用 hostname 作为唯一标识
					- 是允许开发可以临时修改主机名的
        	-实现方案:
        		-1. 给这些服务器分配唯一的主机名
          	-2 将分配好的主机名录入到后台管理的DBserver表中
            -3. 将采集的client客户端代码, 运行一次
            -4 然后将得到的主机名地址保存到一个文件中
            第一天:
            1. 给这些服务器分配唯一的主机名
            2. 将分配好的主机名录入到后台管理的DBserver表中
            3. 将采集的client客户端代码, 运行一次,
            然后将得到的主机名地址保存到一个文件中
            第二天:
            hostname = server_info['basic']['data']['hostname']  ### c10000.com
            res = open(os.path.join(settings.BASEDIR, 'config/cert'), 'r', encoding='utf-8').read()
            if not res.strip():
              #### 第一次采集, 将采集的hostname写入到一个文件中
              with open(os.path.join(settings.BASEDIR, 'config/cert'), 'w', encoding='utf-8') as fp:
                fp.write(hostname)
                else:
                  #### 第二次采集的时候, 永远以第一次文件中保存的主机名为标准
                  server_info['basic']['data']['hostname'] = res

代码实现

# src/client.py
# angent方案:第一次运行时取主机名,写到文件中,以后永远用主机名
# ssh和salt方案,不需要此操作,因为一旦主机名改了,就连接不上了
import requests
from lib.conf.config import settings
from src.plugins import PluginsManager
import os
class Base():
    def post_data(self, server_info):
        requests.post(settings.API_URL, json=server_info)

class Agent(Base):
    ### 收集数据并发送
    def collectAndPost(self):
        server_info = PluginsManager().execute()

        hostname = server_info['basic']['data']['hostname']  ### c10000.com
        res = open(os.path.join(settings.BASEDIR, 'config/cert'), 'r', encoding='utf-8').read()

        if not res.strip():
            #### 第一次采集, 将采集的hostname写入到一个文件中
            with open(os.path.join(settings.BASEDIR, 'config/cert'), 'w', encoding='utf-8') as fp:
                fp.write(hostname)
        else:
            #### 第二次采集的时候, 永远以第一次文件中保存的主机名为标准
            server_info['basic']['data']['hostname'] = res


        for k, v in server_info.items():
            print(k, v)
        # requests.post(settings.API_URL, data=json.dumps(res))
        ### Content-Type':"application/json"
        self.post_data(server_info)


class SSHSalt(Base):
 	pass

2.10 API的验证

第一种方式

# 客户端:

#### 第一种方式
import  requests
token = "dsabdshanbdjsanjdsanjds"
#### 切记, 进行token验证的时候, 一定是将token写在http的请求头中
res = requests.get("http://127.0.0.1:8000/getInfo/", headers = {"token":token})
print(res.text)

# 服务端:

token = request.META.get('HTTP_TOKEN')
server_token = "dsabdshanbdjsanjdsanjdsa"
if token != server_token:
return HttpResponse('token值是错误的!')

第二种方式

## 客户端:	
import  requests
token = "dsabdshanbdjsanjdsanjds"

import time
client_time = time.time()
tmp = "%s|%s" % (token, client_time)

##### 加密
import hashlib
m = hashlib.md5()
m.update(bytes(tmp, encoding='utf8'))
res = m.hexdigest()
client_md5_token = "%s|%s" % (res, client_time)

#### 切记, 进行token验证的时候, 一定是将token写在http的请求头中
data = requests.get("http://127.0.0.1:8000/getInfo/", headers = {"token":client_md5_token})
print(data.text)

			
# 服务端:
server_token = "dsabdshanbdjsanjdsanjds"
server_time = time.time()
client_md5_header = request.META.get('HTTP_TOKEN')
client_md5_token, client_time = client_md5_header.split('|')
client_time = float(client_time)

if server_time - client_time > 10:
  return HttpResponse(' 时间太久了.....')

tmp = "%s|%s" % (server_token, client_time)
m = hashlib.md5()
m.update(bytes(tmp, encoding='utf-8'))
server_md5_token = m.hexdigest()

if server_md5_token != client_md5_token:
  return HttpResponse('修改了token')

三 服务端编写

3.1 后台表结构

Disk表:

NIC表:

Memory表:

Server表:机器位置信息,在哪个机房,机房基层,机柜位置,部署时间这些属性手动录入

跟上面三个表是一对多

IDC表:机房表,跟Server是一对多

BusinessUnit表:业务线(产品线)表,跟server是一对多

Tag表:标签表,跟Server是多对多

UserInfo表:用户表,分产品线表是多对多

UserGroup表:用户组表,跟用户多对多

AssetRecord表:资产变更记录表,server跟AssetRecord是一对多

ErrorLog表:错误日志表,server跟errorlog是一对多

from django.db import models

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)
    password = models.CharField(u'密码', max_length=64)

    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')
    manager = models.ForeignKey('UserGroup', verbose_name='系统管理员', related_name='m')

    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 Tag(models.Model):
    """
    资产标签
    """
    name = models.CharField('标签', max_length=32, unique=True)

    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)
    business_unit = models.ForeignKey('BusinessUnit', verbose_name='属于的业务线', null=True, blank=True)

    tag = models.ManyToManyField('Tag')

    hostname = models.CharField('主机名',max_length=128, unique=True)
    sn = models.CharField('SN号', max_length=64, db_index=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')

    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')


    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')


    class Meta:
        verbose_name_plural = "内存表"

    def __str__(self):
        return self.slot

class AssetRecord(models.Model):
    """
    资产变更记录,creator为空时,表示是资产汇报的数据。
    """
    asset_obj = models.ForeignKey('Server', related_name='ar')
    content = models.TextField(null=True)# 新增硬盘
    creator = models.ForeignKey('UserProfile', null=True, blank=True) #
    create_at = models.DateTimeField(auto_now_add=True)


    class Meta:
        verbose_name_plural = "资产记录表"

    def __str__(self):
        return "%s-%s-%s" % (self.asset_obj.idc.name, self.asset_obj.cabinet_num, self.asset_obj.cabinet_order)


class ErrorLog(models.Model):
    """
    错误日志,如:agent采集数据错误 或 运行错误
    """
    asset_obj = models.ForeignKey('Server', null=True, blank=True)
    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
      
# admin管理
from repository import models
admin.site.register(models.Server)
admin.site.register(models.UserProfile)
admin.site.register(models.UserGroup)
admin.site.register(models.BusinessUnit)
admin.site.register(models.IDC)
admin.site.register(models.Tag)
admin.site.register(models.Disk)
admin.site.register(models.Memory)
admin.site.register(models.NIC)
admin.site.register(models.AssetRecord)
admin.site.register(models.ErrorLog)

# 录入信息
# 录入三条业务线:互娱部,新闻部,云计算部
# 录入用户组:A组,B组,C组
# 录入管理员:张三,李四,王五
# 录入Server数据:服务器,上架,机柜号13,机柜中序号32,IDC机房,业务线,标签,主机名(c2.com)
# 录入IDC机房:世纪互联,神州
# 录入标签:web,db,cache

3.2 资产清洗录入(以硬盘为例)

# 新增:new-old
# 删除:old-new
# 更新:交集

# 差集
new_slot_list={0,1,2}
old_slot_list={0,1}
# 差集
res=new_slot_list-old_slot_list
print(res)
# 或者
res=new_slot_list.difference(old_slot_list)
print(res)

# 交集
print(new_slot_list & old_slot_list)
# 或者
print(new_slot_list.intersection(old_slot_list))
def getInfo(request):

    if request.method == 'POST':
        data = request.body
        # print(data)
        data = json.loads(data)

        #### 通过主机名获取老的数据对应的记录
        hostname = data['basic']['data']['hostname']

        old_server_info = models.Server.objects.filter(hostname=hostname).first() ## obj

        if not old_server_info:
            return HttpResponse('资产不存在')


        #### 以分析disk硬盘数据为例, 进行比对分析

        #### 如果采集出错的话, 记录错误的信息
        if data['disk']['status'] != 10000:
            models.ErrorLog.objects.create(asset_obj=old_server_info, title = "%s 采集硬盘出错了" % (hostname), content=data['disk']['data'])

        '''
            {
                '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'}, 
            }
        '''
        new_disk_info = data['disk']['data']

        '''
            [
                obj(slot:0, pd_type:SAS,......),
                obj(slot:1, pd_type:SATA,......),
                ....
            ]
        '''
        old_disk_info = models.Disk.objects.filter(server_obj=old_server_info).all() ## []

        new_slot_list = list(new_disk_info.keys())
        old_slot_list = []
        for obj in old_disk_info:
            old_slot_list.append(obj.slot)

        '''
        new_slot_list = [0,2]
        old_slot_list = [0,1]
        新增: new_slot_list - old_slot_list = 2   
        删除: old_slot_list - new_slot_list = 1
        更新: 交集
        '''
        #### 增加slot
        add_slot_list = set(new_slot_list).difference(set(old_slot_list))

        if add_slot_list:
            record_list = []
            for slot in add_slot_list:
                # {'slot': '0', 'pd_type': 'SAS', 'capacity': '279.396', 'model': 'SEAGATE ST300MM0006     LS08S0K2B5NV'}
                disk_res = new_disk_info[slot]
                tmp = "添加插槽是:{slot}, 磁盘类型是:{pd_type}, 磁盘容量是:{capacity}, 磁盘的型号:{model}".format(**disk_res)
                disk_res['server_obj'] = old_server_info
                record_list.append(tmp)
                models.Disk.objects.create(**disk_res)

            ### 将变更新的信息添加到变更记录表中
            record_str = ";".join(record_list)
            models.AssetRecord.objects.create(asset_obj=old_server_info, content=record_str)

        #### 删除slot
        del_slot_list = set(old_slot_list).difference(set(new_slot_list))
        if del_slot_list:
            record_str = "删除的槽位是:%s" % (";".join(del_slot_list))
            models.Disk.objects.filter(slot__in=del_slot_list, server_obj=old_server_info).delete()
            models.AssetRecord.objects.create(asset_obj=old_server_info, content=record_str)


        #### 更新硬盘数据
        up_solt_list = set(new_slot_list).intersection(set(old_slot_list))
        if up_solt_list:
            record_list = []
            for slot in up_solt_list:
                ## 新的:'0': {'slot': '0', 'pd_type': 'SAS', 'capacity': '500G', 'model': 'SEAGATE ST300MM0006     LS08S0K2B5NV'}
                new_disk_row = new_disk_info[slot]
                ### 老的:obj(slot:0, pd_type:SAS,.....)
                old_disk_row = models.Disk.objects.filter(slot=slot, server_obj=old_server_info).first()
                for k, new_v in new_disk_row.items():
                    '''
                    k:      slot, pd_type, capacity,...
                    new_v:   0     SAS       279.396,....
                    '''
                    ### 利用反射
                    ### 1. 先从老的数据中心获取老的数据
                    old_v = getattr(old_disk_row, k)
                    ### 2. 判断老的数据和新的数据是否相同
                    if new_v != old_v:
                        tmp = "槽位%s, %s由原来的%s变成了%s" % (slot, k, old_v, new_v)
                        record_list.append(tmp)
                        ### 3. 将新的数据设置回到老的数据行对象中
                        setattr(old_disk_row, k, new_v)
                ### 4. 调用save, 保存
                old_disk_row.save()

            if record_list:
                models.AssetRecord.objects.create(asset_obj=old_server_info, content=";".join(record_list))


        return HttpResponse('ok')
    else:
        ### 第一种方式的判断
        # if token != server_token:
        #     return HttpResponse('token值是错误的!')

        ### 连接数据库获取主机名列表
        token = request.META.get('HTTP_TOKEN')
        client_md5_token, client_time = token.split('|')

        client_time = float(client_time)

        import time
        server_time = time.time()
        if server_time - client_time > 10:
            return HttpResponse('第一关【超时了】')

        server_token = "dsabdshanbdjsanjdsanjdsa"

        tmp = "%s|%s" % (server_token, client_time)
        import hashlib
        m = hashlib.md5()
        m.update(bytes(tmp, encoding='utf8'))
        server_md5_token = m.hexdigest()
        if server_md5_token != client_md5_token:
            return HttpResponse('第二关【数据被修改过了】')


        #### 第三关, 连接redis

        ### 第一次来的时候, 先去redis中判断, client_md5_token 是否在redis中,
        ### 如果在redis中, 则代表已经访问过了, return 回去
        ### 如果不在redis中, 则第一次访问, 添加到redis中, 并且设置过期时间 10s


        return HttpResponse('非常重要的数据')

3.3 前后端混合开发之layui

https://www.layui.com/doc/element/layout.html#adminhttps://www.layui.com/doc/element/layout.html#admin

3.4 前后端混合开发之xadmin

# adminx.py
import xadmin

from repository import models


class DiskAdmin(object):
    list_display = ['id','slot' ,'model','capacity','pd_type','server_obj']

    search_fields = ['id', 'slot' ,'model','capacity','pd_type']
    # list_editable = ['name' ,'email','phone','mobile']
    # list_filter = ['name' ,'email','phone','mobile']
    # list_filter = ['oid','user' ,'odate','oisPay','ototal','oadress']

class ServerAdmin(object):
    list_display = ['id', 'device_type_id', 'device_status_id', 'idc', 'business_unit', 'hostname', 'create_at']

    show_detail_fields = ['hostname']
    # search_fields = ['id', 'slot', 'model', 'capacity', 'pd_type']
    # data_charts = {
    #     "user_count": {'title': u"服务器分布", "x-field": "idc", "y-field": ("business_unit",),},
    #     # "avg_count": {'title': u"Avg Report", "x-field": "date", "y-field": ('avg_count',), "order": ('date',)}
    # }
    # list_per_page = 2
    data_charts = {
        "host_service_type_counts": {
            'title': '部门机器使用情况',
            'x-field': "business_unit",
            'y-field': ("business_unit"),
            'option': {
                "series": {"bars": {"align": "center", "barWidth": 0.8, "show": True}},
                "xaxis": {"aggregate": "count", "mode": "categories"}
            },
        },
        "host_idc_counts": {
            'title': '机房统计',
            'x-field': "idc",
            'y-field': ("idc",),
            'option': {
                "series": {"bars": {"align": "center", "barWidth": 0.3, "show": True}},
                "xaxis": {"aggregate": "count", "mode": "categories"}
            }
        }
    }


class IDCAdmin(object):
    list_display = ['id', 'name', 'floor']

    show_detail_fields = ['name']
    # search_fields = ['id', 'slot', 'model', 'capacity', 'pd_type']


xadmin.site.register(models.Disk,DiskAdmin)
xadmin.site.register(models.Server,ServerAdmin)
xadmin.site.register(models.IDC,IDCAdmin)

3.5 前后端分离之vue-admin

# 介绍地址
https://panjiachen.github.io/vue-element-admin-site/zh/guide/
  
# 集成版本(高级版本)
https://github.com/PanJiaChen/vue-element-admin
# 演示地址
https://github.com/PanJiaChen/vue-element-admin/blob/master/README.zh-CN.md
  
# 基础版本
https://github.com/PanJiaChen/vue-admin-template
  
# 桌面版
https://github.com/PanJiaChen/electron-vue-admin
  

3.6 图表展示

Highchars

https://www.highcharts.com.cn/

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>layout 后台大布局 - Layui</title>
    <link rel="stylesheet" href="/static/lib/layui/css/layui.css">
    <script src="http://cdn.highcharts.com.cn/highcharts/highcharts.js"></script>
{#    <script src="/static/js/hichars.js"></script>#}
</head>
<body class="layui-layout-body">
<div class="layui-layout layui-layout-admin">
    <div class="layui-header">
        <div class="layui-logo">layui 后台布局</div>
        <!-- 头部区域(可配合layui已有的水平导航) -->
        <ul class="layui-nav layui-layout-left">
            <li class="layui-nav-item"><a href="">控制台</a></li>
            <li class="layui-nav-item"><a href="">商品管理</a></li>
            <li class="layui-nav-item"><a href="">用户</a></li>
            <li class="layui-nav-item">
                <a href="javascript:;">其它系统</a>
                <dl class="layui-nav-child">
                    <dd><a href="">邮件管理</a></dd>
                    <dd><a href="">消息管理</a></dd>
                    <dd><a href="">授权管理</a></dd>
                </dl>
            </li>
        </ul>
        <ul class="layui-nav layui-layout-right">
            <li class="layui-nav-item">
                <a href="javascript:;">
                    <img src="http://t.cn/RCzsdCq" class="layui-nav-img">
                    贤心
                </a>
                <dl class="layui-nav-child">
                    <dd><a href="">基本资料</a></dd>
                    <dd><a href="">安全设置</a></dd>
                </dl>
            </li>
            <li class="layui-nav-item"><a href="">退了</a></li>
        </ul>
    </div>

    <div class="layui-side layui-bg-black">
        <div class="layui-side-scroll">
            <!-- 左侧导航区域(可配合layui已有的垂直导航) -->
            <ul class="layui-nav layui-nav-tree" lay-filter="test">
                <li class="layui-nav-item layui-nav-itemed">
                    <a class="" href="javascript:;">所有商品</a>
                    <dl class="layui-nav-child">
                        <dd><a href="javascript:;">列表一</a></dd>
                        <dd><a href="javascript:;">列表二</a></dd>
                        <dd><a href="javascript:;">列表三</a></dd>
                        <dd><a href="">超链接</a></dd>
                    </dl>
                </li>
                <li class="layui-nav-item">
                    <a href="javascript:;">解决方案</a>
                    <dl class="layui-nav-child">
                        <dd><a href="javascript:;">列表一</a></dd>
                        <dd><a href="javascript:;">列表二</a></dd>
                        <dd><a href="">超链接</a></dd>
                    </dl>
                </li>
                <li class="layui-nav-item"><a href="">云市场</a></li>
                <li class="layui-nav-item"><a href="">发布商品</a></li>
            </ul>
        </div>
    </div>

    <div class="layui-body">
        <!-- 内容主体区域 -->
        <div style="padding: 15px;">
          <div id="container" style="max-800px;height:400px"></div>


        </div>
    </div>

    <div class="layui-footer">
        <!-- 底部固定区域 -->
        © layui.com - 底部固定区域
    </div>
</div>
<script src="/static/lib/layui/layui.js"></script>
<script>
    //JavaScript代码区域
    layui.use('element', function () {
        var element = layui.element;

    });

    var chart = Highcharts.chart('container', {
   title: {
      text: '用户活跃量'
   },
   yAxis: {
      title: {
         text: '用户人数'
      }
   },
   legend: {
      layout: 'vertical',
      align: 'right',
      verticalAlign: 'middle'
   },
   plotOptions: {
      series: {
         label: {
            connectorAllowed: false
         },
         pointStart: 1
      }
   },
   series: [{
      name: '用户登录系统人数',
      data: [10, 20, 14, 30, 55, 77, 99, 12]
   },],
   responsive: {
      rules: [{
         condition: {
            maxWidth: 500
         },
         chartOptions: {
            legend: {
               layout: 'horizontal',
               align: 'center',
               verticalAlign: 'bottom'
            }
         }
      }]
   }
});
</script>
</body>
</html>

echars

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>layout 后台大布局 - Layui</title>
    <link rel="stylesheet" href="/static/lib/layui/css/layui.css">
    <script src="https://cdn.jsdelivr.net/npm/echarts@5.0.2/dist/echarts.min.js"></script>
</head>
<body class="layui-layout-body">
<div class="layui-layout layui-layout-admin">
    <div class="layui-header">
        <div class="layui-logo">layui 后台布局</div>
        <!-- 头部区域(可配合layui已有的水平导航) -->
        <ul class="layui-nav layui-layout-left">
            <li class="layui-nav-item"><a href="">控制台</a></li>
            <li class="layui-nav-item"><a href="">商品管理</a></li>
            <li class="layui-nav-item"><a href="">用户</a></li>
            <li class="layui-nav-item">
                <a href="javascript:;">其它系统</a>
                <dl class="layui-nav-child">
                    <dd><a href="">邮件管理</a></dd>
                    <dd><a href="">消息管理</a></dd>
                    <dd><a href="">授权管理</a></dd>
                </dl>
            </li>
        </ul>
        <ul class="layui-nav layui-layout-right">
            <li class="layui-nav-item">
                <a href="javascript:;">
                    <img src="http://t.cn/RCzsdCq" class="layui-nav-img">
                    贤心
                </a>
                <dl class="layui-nav-child">
                    <dd><a href="">基本资料</a></dd>
                    <dd><a href="">安全设置</a></dd>
                </dl>
            </li>
            <li class="layui-nav-item"><a href="">退了</a></li>
        </ul>
    </div>

    <div class="layui-side layui-bg-black">
        <div class="layui-side-scroll">
            <!-- 左侧导航区域(可配合layui已有的垂直导航) -->
            <ul class="layui-nav layui-nav-tree" lay-filter="test">
                <li class="layui-nav-item layui-nav-itemed">
                    <a class="" href="javascript:;">所有商品</a>
                    <dl class="layui-nav-child">
                        <dd><a href="javascript:;">列表一</a></dd>
                        <dd><a href="javascript:;">列表二</a></dd>
                        <dd><a href="javascript:;">列表三</a></dd>
                        <dd><a href="">超链接</a></dd>
                    </dl>
                </li>
                <li class="layui-nav-item">
                    <a href="javascript:;">解决方案</a>
                    <dl class="layui-nav-child">
                        <dd><a href="javascript:;">列表一</a></dd>
                        <dd><a href="javascript:;">列表二</a></dd>
                        <dd><a href="">超链接</a></dd>
                    </dl>
                </li>
                <li class="layui-nav-item"><a href="">云市场</a></li>
                <li class="layui-nav-item"><a href="">发布商品</a></li>
            </ul>
        </div>
    </div>

    <div class="layui-body">
        <!-- 内容主体区域 -->
        <div style="padding: 15px;">
            <div id="main" style=" 600px;height:400px;"></div>


        </div>
    </div>

    <div class="layui-footer">
        <!-- 底部固定区域 -->
        © layui.com - 底部固定区域
    </div>
</div>
<script src="/static/lib/layui/layui.js"></script>
<script>
    //JavaScript代码区域
    layui.use('element', function () {
        var element = layui.element;

    });
    var myChart = echarts.init(document.getElementById('main'));
    option = {
        xAxis: {
            type: 'category',
            data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
        },
        yAxis: {
            type: 'value'
        },
        series: [{
            data: [150, 230, 224, 218, 135, 147, 260],
            type: 'line'
        }]
    };
    myChart.setOption(option);

</script>
</body>
</html>
原文地址:https://www.cnblogs.com/liuqingzheng/p/14527292.html