一 CMDB简介
1.1 什么是CMDB?
CMDB(资产管理系统)是所有运维工具的数据基础
1.2 CMDB包含的功能
用户管理,记录测试,开发,运维人员的用户表
业务线管理,需要记录业务的详情
项目管理,指定此项目用属于哪条业务线,以及项目详情
应用管理,指定此应用的开发人员,属于哪个项目,和代码地址,部署目录,部署集群,依赖的应用,软件等信息
主机管理,包括云主机,物理机,主机属于哪个集群,运行着哪些软件,主机管理员,连接哪些网络设备,云主机的资源池,存储等相关信息
主机变更管理,主机的一些信息变更,例如管理员,所属集群等信息更改,连接的网络变更等
网络设备管理,主要记录网络设备的详细信息,及网络设备连接的上级设备
IP管理,IP属于哪个主机,哪个网段, 是否被占用等
1.3 实现的四种方式
1.3.1 Agent实现方式
Agent方式,可以将服务器上面的Agent程序作定时任务,定时将资产信息提交到指定API录入数据库
其本质上就是在各个服务器上执行
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模块)登录到各个服务器上,然后执行命令的方式去获取各个服务器上的信息
优点:无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方式
此方案本质上和第二种方案大致是差不多的流程,中控机发送命令给服务器执行。服务器将结果放入另一个队列中,中控机获取将服务信息发送到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(\'\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__.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)\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.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(\'\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.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(\'\n\n\'):
for row_line in item.split(\'\n\'):
key, value = row_line.split(\':\')
key = key.strip()
if key == \'processor\':
response[\'cpu_count\'] += 1
elif key == \'physical id\':
cpu_physical_set.add(value)
elif key == \'model name\':
if not response[\'cpu_model\']:
response[\'cpu_model\'] = value
response[\'cpu_physical_count\'] = len(cpu_physical_set)
return response
#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("\n\n\n\n"):
result.append(row_line)
for item in result:
temp_dict = {}
for row in item.split(\'\n\'):
if not row.strip():
continue
if len(row.split(\':\')) != 2:
continue
key, value = row.split(\':\')
name = self.mega_patter_match(key)
if name:
if key == \'Raw Size\':
raw_size = re.search(\'(\d+\.\d+)\', value.strip())
if raw_size:
temp_dict[name] = raw_size.group()
else:
raw_size = \'0\'
else:
temp_dict[name] = value.strip()
if temp_dict:
response[temp_dict[\'slot\']] = temp_dict
return response
@staticmethod
def mega_patter_match(needle):
grep_pattern = {\'Slot\': \'slot\', \'Raw Size\': \'capacity\', \'Inquiry\': \'model\', \'PD Type\': \'pd_type\'}
for key, value in grep_pattern.items():
if needle.startswith(key):
return value
return False
# 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(\'\n\t\')
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 + \'\n\' + 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(\'\r?\n\\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,系统,主板,处理器,内存,缓存等 序列号、电脑厂商、串口信息以及其它系统配件信息
sudo MegaCli -PDList -aALL
需要安装
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)\nKernel \r 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)\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.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-width: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="width: 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>