CMDB项目
一 CMDB简介
https://www.wangt.cc/2021/03/cmdb%E9%A1%B9%E7%9B%AE/
1.1 什么是CMDB?
CMDB(资产管理系统)是所有运维工具的数据基础
1.2 CMDB包含的功能
用户管理,记录测试,开发,运维人员的用户表
业务线管理,需要记录业务的详情
项目管理,指定此项目用属于哪条业务线,以及项目详情
应用管理,指定此应用的开发人员,属于哪个项目,和代码地址,部署目录,部署集群,依赖的应用,软件等信息
主机管理,包括云主机,物理机,主机属于哪个集群,运行着哪些软件,主机管理员,连接哪些网络设备,云主机的资源池,存储等相关信息
主机变更管理,主机的一些信息变更,例如管理员,所属集群等信息更改,连接的网络变更等
网络设备管理,主要记录网络设备的详细信息,及网络设备连接的上级设备
IP管理,IP属于哪个主机,哪个网段, 是否被占用等
1.3 实现的四种方式
1.3.1 Agent实现方式
Agent方式,可以将服务器上面的Agent程序作定时任务,定时将资产信息提交到指定API录入数据库

其本质上就是在各个服务器上执行
subprocess.getoutput()命令,然后将每台机器上执行的结果,返回给主机API,然后主机API收到这些数据之后,放入到数据库中,最终通过web界面展现给用户
#linuximport subprocessimport reres = subprocess.getoutput("ifconfig")print(res)ip=re.findall('inet (.*?) netmask',res)print(ip)# windowsimport subprocessimport reres=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.pywith open('a.txt','a') as f:f.write('hello world')# 4 查看定时任务crontab -l
1.3.2 ssh实现方式 (基于Paramiko模块)
中控机通过Paramiko(py模块)登录到各个服务器上,然后执行命令的方式去获取各个服务器上的信息

优点:无Agent
缺点:速度慢
如果在服务器较少的情况下,可应用此方法
import paramikoimport 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方式

此方案本质上和第二种方案大致是差不多的流程,中控机发送命令给服务器执行。服务器将结果放入另一个队列中,中控机获取将服务信息发送到API进而录入数据库。
执行流程:
第一步: 由管理员录入资产(主机名,SN等信息),通过后台管理,录入数据库
第二步: salt-master从数据库获取未采集资产信息的服务器
第三步: salt-master发送命令给salt-minion执行
第四步: salt-master拿到执行结果
第五步: 将结果发送给API
第六步: API将其写入数据库解释:
salt-master可以理解为主人
salt-minion可以理解为奴隶
优点:快,开发成本低
缺点:依赖于第三方工具
salstack的安装和配置
1.安装和配置
master端:"""1. 安装salt-masteryum install salt-master2. 修改配置文件:/etc/salt/masterinterface: 0.0.0.0 # 表示Master的IP3. 启动service salt-master start"""slave端:"""1. 安装salt-minionyum install salt-minion2. 修改配置文件 /etc/salt/minionmaster: 10.211.55.4 # master的地址或master:- 10.211.55.4- 10.211.55.5random_master: Trueid: c2.salt.com # 客户端在salt-master中显示的唯一ID3. 启动service salt-minion start
2.授权
salt-key -L # 查看已授权和未授权的slavesalt-key -a salve_id # 接受指定id的salvesalt-key -r salve_id # 拒绝指定id的salvesalt-key -d salve_id # 删除指定id的salve
3.执行命令
在master服务器上对salve进行远程操作
salt 'c2.salt.com' cmd.run 'ifconfig'# 基于API的方式
import salt.clientlocal = 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.pyscript.py # 脚本文件client.py # 客户端类tests # 测试文件夹# 总结:bin,config,files,lib,src几个文件夹
2.2 仿django配置文件
custom_settings.py
# 用户配置PORT = 22USER = 'lqz'
global_settings.py
#### 全局配置PORT = 22USER = 'root'
config.py
from config import custom_settingsfrom . import global_settingsclass 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 tracebackfrom lib.conf.config import settingsimport importlibimport 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:passreturn response
src/plugins/cpu.py
#!/usr/bin/env python# -*- coding:utf-8 -*-import osfrom lib.conf.config import settingsclass Cpu(object):def __init__(self):pass@classmethoddef initial(cls):return cls()def process(self, command_func, debug):print('cpu print')
src/plugins/disk.py
import osfrom lib.conf.config import settingsclass Cpu(object):def __init__(self):pass@classmethoddef initial(cls):return cls()def process(self, command_func, debug):print('disk print')
2.4 冗余代码抽取
继承方式
把函数当参数传入函数中:
在src/plugins/init.py中写,__隐藏,调用execute的时候,把函数地址和命令传入
import tracebackfrom lib.conf.config import settingsimport importlibimport subprocess### 管理插件信息的类class PluginsManager(object):def __init__(self, hostname=None):self.plugins_dict = settings.PLUGINS_DICTself.hostname = hostname # 采集客户端的地址self.debug = settings.DEBUGif settings.MODE == 'ssh': # ssh方式才需要端口,用户名,密码,这些应该放到配置文件中self.port = settings.SSH_PORTself.name = settings.SSH_USERNAMEself.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'] = 10000ret['data'] = resexcept Exception as e:ret['status'] = 10001ret['data']= "[%s] 采集 [%s] 出错了, 错误信息是:%s" % (self.hostname if self.hostname else "Agent", k, str(traceback.format_exc()))response[k] = retreturn responsedef __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 resdef __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 resultdef __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@classmethoddef 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 bytesSystem InformationManufacturer: Parallels Software International Inc.Product Name: Parallels Virtual PlatformVersion: NoneSerial Number: Parallels-1A 1B CB 3B 64 66 4B 13 86 B0 86 FF 7E 2B 20 30UUID: 3BCB1B1A-6664-134B-86B0-86FF7E2B2030Wake-up Type: Power SwitchSKU Number: UndefinedFamily: Parallels VM'''key_map = {"Manufacturer" : 'manufacturer',"Product Name" : 'product_name',"Serial Number": 'sn'}result = {}data = res.strip().split('n')# 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__.pyimport tracebackfrom lib.conf.config import settingsimport importlibimport subprocess### 管理插件信息的类class PluginsManager(object):def __init__(self, hostname=None):self.plugins_dict = settings.PLUGINS_DICTself.hostname = hostnameself.debug = settings.DEBUGif settings.MODE == 'ssh':self.port = settings.SSH_PORTself.name = settings.SSH_USERNAMEself.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'] = 10000ret['data'] = resexcept Exception as e:ret['status'] = 10001ret['data']= "[%s] 采集 [%s] 出错了, 错误信息是:%s" % (self.hostname if self.hostname else "Agent", k, str(traceback.format_exc()))response[k] = retreturn responsedef __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 resdef __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 resultdef __cmd_salt(self, cmd):command = "salt %s cmd.run %s" % (self.hostname, cmd)res = subprocess.getoutput(command)return res
# basic.pyclass Basic(object):def __init__(self):pass@classmethoddef initial(cls):return cls()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': 'c2000.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
# board.pyimport osfrom lib.conf.config import settingsclass Board(object):def __init__(self):pass@classmethoddef 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('n'):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.pyimport osfrom lib.conf.config import settingsclass Cpu(object):def __init__(self):pass@classmethoddef 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('nn'):for row_line in item.split('n'):key, value = row_line.split(':')key = key.strip()if key == 'processor':response['cpu_count'] += 1elif key == 'physical id':cpu_physical_set.add(value)elif key == 'model name':if not response['cpu_model']:response['cpu_model'] = valueresponse['cpu_physical_count'] = len(cpu_physical_set)return response
#disk.pyimport reimport osfrom lib.conf.config import settingsclass Disk(object):def __init__(self):pass@classmethoddef 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("nnnn"):result.append(row_line)for item in result:temp_dict = {}for row in item.split('n'):if not row.strip():continueif len(row.split(':')) != 2:continuekey, 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_dictreturn response@staticmethoddef 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 valuereturn False
# memory.pyimport osfrom lib import convertfrom lib.conf.config import settingsclass Memory(object):def __init__(self):pass@classmethoddef 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:continueif item.startswith('#'):continuesegment = {}lines = item.split('nt')for line in lines:if not line.strip():continueif 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']] = segmentreturn ram_dict
#nic.py 网络接口控制器import osimport refrom lib.conf.config import settingsclass Nic(object):def __init__(self):pass@classmethoddef 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_infodef 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 + 'n' + cmd2)return ifacesdef which(self, exe):def _is_executable_file_or_link(exe):# check for os.X_OK doesn't suffice because directory may executablereturn (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 fullpathreturn exe# default path based on busybox's defaultdefault_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_pathreturn Nonedef _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 -= 8else:netmask += '{0:d}'.format(256 - (2 ** (8 - cidr_bits)))cidr_bits = 0return netmaskdef _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) & 0x55555555x = ((x >> 2) & 0x33333333) + (x & 0x33333333)x = ((x >> 4) + x) & 0x0f0f0f0fx += x >> 8x += x >> 16return x & 0x0000003fdef _interfaces_ip(self, out):'''Uses ip to return a dictionary of interfaces with various information abouteach (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, broadcastbased on the current set of cols'''brd = Noneif '/' in value: # we have a CIDR in this addressip, cidr = value.split('/') # pylint: disable=C0103else:ip = value # pylint: disable=C0103cidr = 32if 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('r?n\d').split(out)for group in groups:iface = Nonedata = dict()for line in group.splitlines():if ' ' not in line:continuematch = re.match(r'^d*:s+([w.-]+)(?:@)?([w.-]+)?:s+<(.+)>', line)if match:iface, parent, attrs = match.groups()if 'UP' in attrs.split(','):data['up'] = Trueelse:data['up'] = Falseif parent and parent in right_keys:data[parent] = parentcontinuecols = 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'] = ipaddraddr_obj['netmask'] = netmaskaddr_obj['broadcast'] = broadcastdata['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, brdelif type_.startswith('link'):data['hwaddr'] = valueif iface:if iface.startswith('pan') or iface.startswith('lo') or iface.startswith('v'):del iface, dataelse:ret[iface] = datadel iface, datareturn retdef 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.pydef convert_to_int(value,default=0):try:result = int(value)except Exception as e:result = defaultreturn resultdef convert_mb_to_gb(value,default=0):try:value = value.strip('MB')result = int(value)except Exception as e:result = defaultreturn result
# bin/start.pyfrom src.plugins import PluginsManagerif __name__ == '__main__':res=PluginsManager().execute()print(res)
注意
sudo dmidecode -t1
可以获取BIOS,系统,主板,处理器,内存,缓存等 序列号、电脑厂商、串口信息以及其它系统配件信息
https://ipcmen.com/dmidecode
sudo MegaCli -PDList -aALL
需要安装
2.7 异常处理
traceback使用
import tracebackdef test():try:a = "dsadsa"int(a)except Exception as e:print(traceback.format_exc())test()
src/plugins/init.py
import tracebackfrom lib.conf.config import settingsimport importlibimport subprocess### 管理插件信息的类class PluginsManager(object):def __init__(self, hostname=None):self.plugins_dict = settings.PLUGINS_DICTself.hostname = hostnameself.debug = settings.DEBUGif settings.MODE == 'ssh':self.port = settings.SSH_PORTself.name = settings.SSH_USERNAMEself.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'] = 10000ret['data'] = resexcept Exception as e:# hostname有值说明不是anget方案,是salstack或paramiko方案ret['status'] = 10001ret['data']= "[%s] 采集 [%s] 出错了, 错误信息是:%s" % (self.hostname if self.hostname else "Agent", k, str(traceback.format_exc()))response[k] = retreturn responsedef __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 resdef __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 resultdef __cmd_salt(self, cmd):command = "salt %s cmd.run %s" % (self.hostname, cmd)res = subprocess.getoutput(command)return res
2.8 把采集到的数据上传
客户端
## src/clientimport requestsfrom lib.conf.config import settingsfrom src.plugins import PluginsManagerimport osclass 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.comres = 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'] = resfor 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 ThreadPoolExecutorp = ThreadPoolExecutor(10)for hostname in hostnames:p.submit(self.run, hostname)
# src/script.pyfrom src.client import Agentfrom src.client import SSHSaltfrom lib.conf.config import settingsdef run():if settings.MODE == 'agent':obj = Agent()else:# 不管salt和paramiko方式,都需要从服务器获取客户端ip地址obj = SSHSalt()obj.collectAndPost()
# bin/start.pyfrom src.script import runif __name__ == '__main__':run()
服务端
2.9 唯一标识的问题
# 目标:将变更的信息通过程序的比对, 记录下来#第一天的时候:# 采集数据:{'status': 10000, 'data': {'os_platform': 'linux', 'os_version': 'CentOS release 6.6 (Final)nKernel r on an \m', 'hostname': 'c2.com'}}#API清洗的时候:因为是第一次, 数据库中并没有采集的数据数据入库:server:1000条id sn os_platform os_version disk_size1 dsadsa linux CentOS 250G........#第二天的时候(数据发生变化,应该比对):#采集数据:{'status': 10000, 'data': {'os_platform': 'linux', 'os_version': 'CentOS release 6.6 (Final)nKernel r 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.comres = 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 requestsfrom lib.conf.config import settingsfrom src.plugins import PluginsManagerimport osclass 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.comres = 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'] = resfor 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 requeststoken = "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 requeststoken = "dsabdshanbdjsanjdsanjds"import timeclient_time = time.time()tmp = "%s|%s" % (token, client_time)##### 加密import hashlibm = 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 modelsclass 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.nameclass 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.nameclass 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.nameclass IDC(models.Model):"""机房信息"""name = models.CharField('机房', max_length=32)floor = models.IntegerField('楼层', default=1)class Meta:verbose_name_plural = "机房表"def __str__(self):return self.nameclass Tag(models.Model):"""资产标签"""name = models.CharField('标签', max_length=32, unique=True)class Meta:verbose_name_plural = "标签表"def __str__(self):return self.nameclass 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.hostnameclass 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.slotclass 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.nameclass 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.slotclass 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 modelsadmin.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_listprint(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() ## objif 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更新: 交集'''#### 增加slotadd_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_inforecord_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)#### 删除slotdel_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 timeserver_time = time.time()if server_time - client_time > 10:return HttpResponse('第一关【超时了】')server_token = "dsabdshanbdjsanjdsanjdsa"tmp = "%s|%s" % (server_token, client_time)import hashlibm = 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中, 并且设置过期时间 10sreturn 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.pyimport xadminfrom repository import modelsclass 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 = 2data_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/
charset="utf-8">name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">layout 后台大布局 - Layui rel="stylesheet" href="/static/lib/layui/css/layui.css">{# #}class="layui-layout-body">- class="layui-layout layui-layout-admin">class="layui-header">class="layui-logo">layui 后台布局
- class="layui-nav layui-layout-left">
- class="layui-nav-item"> href="">控制台
- class="layui-nav-item"> href="">商品管理
- class="layui-nav-item"> href="">用户
- class="layui-nav-item">
href="javascript:;">其它系统- class="layui-nav-child">
- href="">邮件管理
- href="">消息管理
- href="">授权管理
- class="layui-nav layui-layout-right">
- class="layui-nav-item">
href="javascript:;">src="http://t.cn/RCzsdCq" class="layui-nav-img">
贤心- class="layui-nav-child">
- href="">基本资料
- href="">安全设置
- class="layui-nav-item"> href="">退了
class="layui-side layui-bg-black">class="layui-side-scroll">- class="layui-nav layui-nav-tree" lay-filter="test">
- class="layui-nav-item layui-nav-itemed">
class="" href="javascript:;">所有商品- class="layui-nav-child">
- href="javascript:;">列表一
- href="javascript:;">列表二
- href="javascript:;">列表三
- href="">超链接
- class="layui-nav-item">
href="javascript:;">解决方案- class="layui-nav-child">
- href="javascript:;">列表一
- href="javascript:;">列表二
- href="">超链接
- class="layui-nav-item"> href="">云市场
- class="layui-nav-item"> href="">发布商品
class="layui-body">style="padding: 15px;">id="container" style="max-width:800px;height:400px">class="layui-footer">? layui.com - 底部固定区域 //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'}}}]}});
echars
charset="utf-8">name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">layout 后台大布局 - Layui rel="stylesheet" href="/static/lib/layui/css/layui.css">class="layui-layout-body">- class="layui-layout layui-layout-admin">class="layui-header">class="layui-logo">layui 后台布局
- class="layui-nav layui-layout-left">
- class="layui-nav-item"> href="">控制台
- class="layui-nav-item"> href="">商品管理
- class="layui-nav-item"> href="">用户
- class="layui-nav-item">
href="javascript:;">其它系统- class="layui-nav-child">
- href="">邮件管理
- href="">消息管理
- href="">授权管理
- class="layui-nav layui-layout-right">
- class="layui-nav-item">
href="javascript:;">src="http://t.cn/RCzsdCq" class="layui-nav-img">
贤心- class="layui-nav-child">
- href="">基本资料
- href="">安全设置
- class="layui-nav-item"> href="">退了
class="layui-side layui-bg-black">class="layui-side-scroll">- class="layui-nav layui-nav-tree" lay-filter="test">
- class="layui-nav-item layui-nav-itemed">
class="" href="javascript:;">所有商品- class="layui-nav-child">
- href="javascript:;">列表一
- href="javascript:;">列表二
- href="javascript:;">列表三
- href="">超链接
- class="layui-nav-item">
href="javascript:;">解决方案- class="layui-nav-child">
- href="javascript:;">列表一
- href="javascript:;">列表二
- href="">超链接
- class="layui-nav-item"> href="">云市场
- class="layui-nav-item"> href="">发布商品
class="layui-body">style="padding: 15px;">id="main" style="width: 600px;height:400px;">class="layui-footer">? layui.com - 底部固定区域 //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);