CMDB那些事儿 ------> 项目整理
一、CMDB项目所有笔记及总结
CMDB 项目笔记
1、为什么开发CMDB?
背景:社会调查发现,很多公司都是用Excel表以管理维护公司内的所有资产信息,当设备发生改变或是修改配置,就需要相应的
管理人员去实时的更新这个表,手动的添加上相关的详细变更信息;
但是这其中有个难题:资产变更时难以保证Excel表更正的正确性和实时性;这进而又引发出一个问题,就是信息交换的不便利性
和准确性,原因就在于更新的不及时和信息记录的随意
目标:为了解决上述考察的一系列问题,就需要开发一套自动化采集资产的工具,实现:资产自动采集更新,数据自动汇报,实时保存变更记录的功能。
最终目标:实现运维自动化
2、CMDB架构?
大致分为三个部分:
- 资产采集(资产采集)
- API(一是接受采集的数据保存入库;二是对外提供数据访问接口,供用户查询)
- 后台管理 可视化的操作界面
运维愿景:
1. 自动装机
2. 配置管理
3. 监控
4. 堡垒机
必备:资产管理
目前状况:
手动维护Excel表格
资产自动采集并汇报入库
CMDB - 配置管理数据库(资产管理)
如何实现自动采集?(4种方法)
利用subprocess模块和Linux基本命令,基于公司现使用机器批量管理程序的基础上,实现资产采集!
v = subprocess.getoutput('ls')
1. Agent
程序直接在服务器端执行
2. SSH类,paramiko,
pip3 install paramiko
3. saltstack(Python开发)
master
yum install salt-master
配置:1.1.1.1
service salt-master start
salve
yum install salt-minion
配置:
master: 1.1.1.1
service salt-master start
salve
yum install salt-minion
配置:
master: 1.1.1.1
service salt-master start
授权:
在母机上操作
salt-key -L 查看所有授权的子机
salt-key -A 添加所有子机
执行命令:
在master上执行: salt "*" cmd.run "ifconfig"
4. puppet(ruby)
资产管理获取数据方式(4种):到最后都是正则匹配获取有效信息,存于数据库
1、agent (机器多时适用,但对服务器的性能有损耗)
每台服务器上都有个获取当前服务器信息的脚本,每台机器定时自动执行并向接收端汇报信息。
获取信息方式:使用subprocess模块
import subprocess
import re
v = subprocess.getoutput("操作方法") 获取查询返回的字符串信息。然后利用正则的方法,去提取我们需要的字段信息
s = re.match("正则规则",v)
将结果发送给接收端 API (django框架接收)
发送需要注意三点:1、url,2、发送数据的格式,3、发送数据完成之后的返回值
import request 导入模块
v = request.post(url,data={"k1":v1,"k2":v2}) 以哪种方式发送数据,需要写入url地址及要发送的data数据,发送成功之后有返回值。
2、Paramiko ssh(机器少时适用,不影响服务器的性能,但是由于是基于网络发起的通信,耗时长)
在接收端和服务器端之间再有一台服务器作为中控机,用这台中控机向要管理的所有服务器进行远程管理,发送ssh命令获取信息。再把
获取到的所有信息发送给接收端。
需要下载模块 Paramiko 利用这个模块进行ssh远程通信。
3、SaltStack方式
他是通过master母机,管理所有的子机minion实现批量管理的功能。当母机发送一条命令时,所有的子机都会执行这条命令并把处理的结果
返回给母机。
要想实现管理,就需要给母机装上master协议,所有的子机装上minion协议,母机配置interface IP地址,子机配置母机master 的IP
地址,在分别重启服务。然后母机授权
(salt-key -L 查看已授权的子机,salt-key -A 为所有子机授权),否则无法实现管理。
母机测试代码(*代表所有):salt "*" cmd.run "ls" 查看本地能否得到返回信息。
这种模型,是以中控机作为母机,要管理的所有服务器作为子机。
母机执行py文件发送命令并接收返回值: v = subprocess.getoutput(salt "*" cmd.run "ls")
SaltStack执行原理:
母机与子机之间通信,中间存在一个消息队列,母机把操作发送到消息队列中,所有的子机去消息队列获取
这个操作然后执行对应的方法。执行结束之后,
所有的子机 会把处理完的结果全部放到一个临时存在的消息队列中,然后母机从这个临时队列中把所有的
子机返回的信息取出来。这样就算是完成了一次通信。
应用场景:机器比较多,公司已经在使用saltstack
4、puppet(ruby开发)
也是有一个中控机与所有的服务器通信,但是这种关联机制是又ruby开发的。服务器与中控机之间默认是每30分钟
通信一次,基于这个条件在遵循ruby规则写一个文件,去获取
每台机器的资产信息。但是还需要去学ruby。
应用场景:老牌公司使用puppet
目标:
兼容三种采集方式软件
高内聚:所有的功能都集合到一起自己文件内全部完成
低耦合:各模块之间无绝对关系,都是相对独立
导入模块的方法:【以字符串的形式导入模块】
传统模式:import 模块名
以字符串的形式导入模块
import importlib
m = importlib.import_module('字符串类型')
常看当前文件的所有方法(名称空间):dir()
os.environ()获取系统信息,字典类型(键值全是字符串),会列出当前所有的环境变量
加值的话,仅在当前py文件下的环境变量生效,运行结束之后就会消失,不会影响其他文件。
traceback 导入堆栈模块,用于程序出错时生成错误的详细信息
traceback.format_exc() ---> 获取详细的错误信息
配置文件:
配置信息名字一般都是大写
两种类型:
1、只支持用户定义:
2、支持用户定义的文件 和 默认内置的配置文件
就需要把这两类文件整合到一起,通过另一个文件创建一个类,导入这之下所有的方法,引用的时候直接引用这个类的对象即可。
客户端程序设计目录:
bin - 用于存放可执行文件
- start.py 开始程序
conf - 用于存放自定义文件
- settings.py 自定义的配置信息
lib - 库文件,用于存放一些内定的配置信息
- conf
- config.py 配置整合文件,通过该文件内的类,将自定义和内置配置全部获取到供全局使用
- global_settings.py 全局设置的文件(内置)
- convert.py 用于数据转换
src - 用于存放处理业务的文件
- plugins 用于存放处理各个模块的文件
- client.py 用于定义通过不同方式获取资产信息的方法
- script.py 定义执行方法的函数,通过配置文件判定获取资产方式。
设计注意点:
1、关于日志文件,一般是选取硬盘上的某个文件存放日志,而不是在程序中设计一个文件夹存放!
原因:日志会越来越多,每天都在生成。还不如找一个专门的位置存放日志妥当;代码要有精简性和可移植性,如果把日志
目录放在代码程序中,等后期日志多了,
要拷贝该程序到别的服务器上,岂不是说还要把庞大的日志信息也考走?
总结:
代码库一定要和这种未来要大量写入信息的文件 解耦分离!便于以后维护,移动和管理!!!
整个开发流程:
1. 目录
bin
config
lib
src
2. 配置文件
只支持用户定义:
settings.py
USER = 'root'
PWD = "sdfsdf"
支持用户定义
默认配置文件:
settings.py
USER = 'root'
PWD = "sdfsdf"
from lib.conf.config import settings
PS:
import os
os.environ['USER_SETTINGS'] = "config.settings"
3. 开发插件(可插拔)
公司采集资产有差别
默认采集:
basic
board
cpu
disk
memory
nic
定义插件:
class xxxxx(object):
def process(self):
return '1123123123'
写配置文件:
PLUGINS_DICT = {
...
'xxx': "xxx.xxx.xxx.xxxx",
...
}
执行命令:
- MegaCli
- dmidecode
4. 向API获取发送数据
host采集资产,发送到API
AGENT:
向API发送资产信息
SSH、SALT:
获取未采集的主机列表:[c1.com,c2.com]
for host in 主机列表:
host采集资产,发送到API
class Base(obj):
def post_asset(self,server_info):
向API发送资产信息
class Agent(Base):
def execute(self):
server_info = PluginManager().exec_plugin()
self.post_asset(server_info)
class SSHSALT(Base):
def get_host(self):
# 获取未采集的主机列表:
return [c1.com,c2.com]
def execute(self):
host_list = self.get_host()
for host in host_list:
server_info = PluginManager(host).exec_plugin()
self.post_asset(server_info)
5、后台管理
全插件
开发过程中遇到的难题(深坑)
Linux:Linux的操作命令不熟
API采集数据
1、唯一标识:标准化
机器型号,系统,软件,环境,主机名,放置机柜位置及该机器的标志位名字,
原因:采集资产汇报信息入库的时候,以哪个信息作为标识就是一个大问题
如果公司全是物理机,那么利用主板的SN码完全没问题,
但是如果是一台物理机上有多个虚拟机呢?那所有虚拟机返回的SN号就会完全一致,这样就导致信息的错误。
此时就需要人为的标准化信息:主机名不重复;通过主机名去做唯一标识,通过主机名去做判断。
标准化:
- 主机名不重复。
- 流程:
- 手动资产录入:机房,机柜,机柜位置,
- 装机时,需要将服务器的基本信息录入CMDB,此时已定主机名
- 资产采集:
主机名是唯一标识,依赖本地文件
步骤:
a. 装系统,初始化软件(CMDB),运行CMDB
- 通过命令获取主机名
- 写入本地指定文件
b. 将资产信息发送到API
c. 获取资产信息:
判定本地文件主机名是否与命令获取的主机名一致?
- 1、一致的话就更新
- 2、不一致按照本地文件写入
=======================================================================================
最终流程:
唯一标识
标准化:主机名不重复;流程标准化(装机同时,主机名也需要在cmdb中手动输入设置)
服务器资产采集(Agent):
a. 第一次:文件不存在,或内容为空;
采集资产:
- 从basic信息中获取主机名,然后写入文件
- 发送API
b. 第N次:采集资产,主机名:文件中获取
注意:规避了一点,主机名是从文件中获取的,如果主机名临时做了更改,对此台机器做资产采集
不会受影响;如果是需要对本机做正式的修改则需要去文件变更。
代码:
server_info = PluginManager().exec_plugin()
hostname = server_info['basic']['data']['hostname']
certname = open(settings.CERT_PATH,'r',encoding='utf-8').read().strip()
if not certname:
with open(settings.CERT_PATH,'w',encoding='utf-8') as f:
f.write(hostname)
else:
server_info['basic']['data']['hostname'] = certname
SSH或Salt:
中控机:先从API中获取未采集主机名列表:【c1.com 】
SSH和Salt不存在这种情况:这个架构执行初始是从中控机获取没有采集过的主机名(主机名跟IP是绑定的),而流程一开始主机名已经被录入cmdb了
中控机中高并发问题
2、线程池: 为提交采集信息的效率而实现高并发
目的:解决SSH或salt模式下,由原始的串行获取数据的方式,转变成多条任务同时执行获取数据;最终目标:节省时间,快速高效的完成任务
方式:结合线程和进程的知识,开启进/线程池,用以提高并发执行操作:线程、进程
线程池或进程池:20
Python2:
线程池:无
进程池:有
Python3:
线程池:有
进程池:有
代码举例:
import time
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor (导入进程或是线程池模块)
def task(i): #要执行的函数
time.sleep(1)
print(i)
p = ThreadPoolExecutor(10) #创建线程池对象,数字代表开启多少个线程
for row in range(100):
p.submit(task,row) #利用这个对象的 submit方法 触发多个任务,参数:第一个为要执行的函数,第二个为要执行函数传入的参数!
3、API验证
1、为什么要做API验证?
- 保证仅是有权限的人才能访问
- 访问过程中,保证数据安全,库不会被篡改
2、你是如何设计的?
- 与Tornado 中加密Cookie类似
- 创建动态KEY:md5(key+time)|time
- 为防止被黑客截获KEY,利用某人API,操作我后台数据,优化代码加以限制!
- 此处要:维护一个访问记录表(字典类型),仅当前10秒内访问的所有用户,超时相应用户会删除!
(解释:为什么要10s,各位看官,如果是每次访问生成的动态KEY我都记录的话,那到后期我这个表得大到什么地步!!!创建他的目的是优化访问权限,而不是增加累赘)
- 实现优化的鬼门三关(仅为娱乐)
第一关:时间 ------------> 超时无权限
第二关:算法规则 --------> 数据截获篡改,MD5算法值不一致,滚粗!
第三关:已访问记录 ------> 俗称人头关,首次访问会记录,同样的KEY再来一次,你当我算法是菜啊!
代码:
- 如果黑客截获KEY,他的网速比你快无比,这样是不是数据又被截获泄漏了?!怎么解决?--->数据加密!
这样他来访问,就是来拉高你的网速的!!!
3、数据加密方法:
- RSA(公钥私钥) ----> 由于内部有字节限制操作,生成公钥时需要指定bytes位数,同时也不清楚每次发送数据的大小,拘谨很大!
摒弃吧!我的哥,我们需要的是随心所遇的加密方法!
- AES(仿微信高级加密) ---> 通过一个16位字节格式(及倍数)的key,对数据进行加密(数据必须是要bytes)一定要注意,此种加密的数据长度必须是16个字节的倍数!
不够则需要在字节数据之后加值凑够倍数,解码的时候,再把凑数的值去掉,获取真正的数据!
代码:
参考武sir:http://www.cnblogs.com/wupeiqi/articles/6746744.html
数据存储问题
4、数据库设计
此处牵扯到项目三层(五层)架构设计的问题,及各个操作各司其职,互不干扰!
(高内聚,低耦合的原则,各个模块分离,各司其职,使用的话直接调用)
数据库访问层
- User:增,删除,修改
- ...
处理所有的数据库操作方法或业务!
业务处理层
- 用户:
- 授权:
- 。。。
处理所有的业务逻辑,
UI层 仅为展示数据
- html
- UI展示层
可视化操作,用于编写展示所有数据的方法
总结:业务分离解耦,业务变更直接修改所在模块的内部方法。其他逻辑处理层需要使用该模块下的方法,直接导入应用。
- 数据库表结构设计:
详见CMDB数据库表结构设计图
5、后台管理
- 序列化:
1. Django内置
# from django.core import serializers
# # 序列化queryset:[obj,obj,obj,obj]
# v = models.Server.objects.all()
# data = serializers.serialize("json", v)
2. JSON + 扩展
# json扩展:支持时间序列化
from datetime import datetime
from datetime import date
class JsonCustomEncoder(json.JSONEncoder):
def default(self, value):
if isinstance(value, datetime):
return value.strftime('%Y-%m-%d %H:%M:%S') #格式化的时间字符串
elif isinstance(value, date):
return value.strftime('%Y-%m-%d') #格式化的时间字符串
else:
return json.JSONEncoder.default(self, value)
v = models.Server.objects.values('id','hostname','create_at')
data = json.dumps(list(v),cls=JsonCustomEncoder)
RESTful API
面向资源编程:网络上任何东西当作资源
接口定义规则:一个url 对应一个视图函数,函数内部利用request.method的不同请求方式,做增删改查操作
request.method == "GET" 获取数据
request.method == "POST" 增加数据
request.method == "PUT" 更新数据
request.method == "DELETE" 删除数据
项目回顾:
1. 为什么开发CMDB?
Excel维护资产信息,资产变更时难以保证Excel表正确性;信息交换不方便
自动采集资产工具,目标:自动汇报,保存变更记录。
最终目标:实现运维自动化
2. CMDB架构?
- 资产采集(资产采集)
- API(接受数据保存入库,对外提供数据接口)
- 后台管理
3. 你负责做什么?
- 资产采集(资产采集)
三种方案:
- agent
- paramiko
- saltstack
提高扩展性,客户端配置是参考Django:配置,中间件(反射)设计的!
难题:错误堆栈信息
4. 有没有遇到难题(坑)?
Linux:Linux不太熟
唯一标识:大问题 ????????????????
二、各种图(意思理解了即好!):
1、client端资产采集原理草图:


2、整个项目设计草图:

3、服务端数据数据库表结构设计图:

进阶:

CMDB
CMDB简介
公司:
开发
测试:功能测试 性能测试(高并发)
运维:上线 copy--解压--运行 python manage.py run serve
DBA
提供机房服务的公司:兆维,世纪互联
运维人员工作:
装机------自动装机系统
配管系统(配置管理)(远程登录机器装软件,初始化操作) 代码部署
监控系统
堡垒机-----操作日志
上线
服务器资产管理问题:
之前使用Excel表格维护资产,依赖人为性操作,资产变更时,容易出错,且效率低。与其他部门进行信息交换时,没有
一个固定的数据形式。
目的:自动采集和汇报,保存变更记录
实现运维自动化
高内聚,低耦合
CMDB 资产管理数据库架构:
资产采集
API(接收数据,对外提供信息接口)
后台管理(数据显示)
资产采集的实现方案
1. agent模式
每一台服务器放一份agent程序,subprocess执行采集命令,requests提交数据
优点:简单,采集速度快
应用场景:机器多,性能要求降低
2. ssh模式
在服务器和API之间放置一台中控机 用ssh远程连接服务器 ,执行命令,获取结果,并发送给API
应用场景:机器少,性能要求高
优点:无agent 速度慢 ssh方式
例如:fabric ansible 封装了paramiko模块 批量执行命令

3. salt模式
saltstack(python写的)
在服务器和API之间放置一台中控机,中控机和服务器上分别安装saltstack,中控机上的salt执行命令获取资产信息
master
salve/minion
应用场景:已经用了saltstack 机器多 比ssh速度快
原理:
SaltStack 采用`C/S`模式,server端就是salt的master,client端就是minion,minion与master之间通过`ZeroMQ`消息队列通信。 minion上线后先与master端联系,把自己的`pub key`发过去,这时master端通过`salt-key -L`命令就会看到minion的key,接受该minion-key后,也就是master与minion已经互信。 master可以发送任何指令让minion执行了,salt有很多可执行模块,比如说cmd模块,在安装minion的时候已经自带了,它们通常位于你的python库中,`locate salt | grep /usr/`可以看到salt自带的所有东西。 这些模块是python写成的文件,里面会有好多函数,如cmd.run,当我们执行`salt '*' cmd.run 'uptime'`的时候,master下发任务匹配到的minion上去,minion执行模块函数,并返回结果。 master监听4505和4506端口,4505对应的是ZMQ的PUB system,用来发送消息,4506对应的是REP system是来接受消息的。 具体步骤如下 ``` 1、Salt stack的Master与Minion之间通过ZeroMq进行消息传递,使用了ZeroMq的发布-订阅模式,连接方式包括tcp,ipc 2、salt命令,将cmd.run ls命令从salt.client.LocalClient.cmd_cli发布到master,获取一个Jodid,根据jobid获取命令执行结果。 3、master接收到命令后,将要执行的命令发送给客户端minion。 4、minion从消息总线上接收到要处理的命令,交给minion._handle_aes处理 5、minion._handle_aes发起一个本地线程调用cmdmod执行ls命令。线程执行完ls后,调用minion._return_pub方法,将执行结果通过消息总线返回给master 6、master接收到客户端返回的结果,调用master._handle_aes方法,将结果写的文件中 7、salt.client.LocalClient.cmd_cli通过轮询获取Job执行结果,将结果输出到终端。 ``` #### saltstack 安装 [saltstack install](http://repo.saltstack.com/#rhel) #### 修改minion配置文件 ``` [root@linux-node2 ~]# vim /etc/salt/minion master: 192.168.56.11 [root@linux-node2 ~]# vim /etc/salt/minion master: 192.168.56.11 [root@linux-node1 pki]# pwd /etc/salt/pki [root@linux-node1 pki]# tree . ├── master │ ├── master.pem │ ├── master.pub │ ├── minions │ ├── minions_autosign │ ├── minions_denied │ ├── minions_pre │ │ ├── linux-node1.example.com │ │ └── linux-node2.example.com │ └── minions_rejected └── minion ├── minion_master.pub ├── minion.pem └── minion.pub [root@linux-node1 pki]# salt-key -A [root@linux-node1 pki]# tree . ├── master │ ├── master.pem │ ├── master.pub │ ├── minions │ │ ├── linux-node1.example.com │ │ └── linux-node2.example.com │ ├── minions_autosign │ ├── minions_denied │ ├── minions_pre │ └── minions_rejected └── minion ├── minion_master.pub ├── minion.pem └── minion.pub ``` #### 远程执行 ``` [root@linux-node1 pki]# salt "*" test.ping linux-node2.example.com: True linux-node1.example.com: True [root@linux-node1 pki]# salt "*" cmd.run 'w' linux-node1.example.com: 07:20:24 up 17:10, 1 user, load average: 0.00, 0.01, 0.05 USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT root pts/0 192.168.56.1 07:04 0.00s 0.30s 0.26s /usr/bin/python /usr/bin/salt * cmd.run w linux-node2.example.com: 08:26:25 up 22:40, 2 users, load average: 0.15, 0.05, 0.06 USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT root tty1 Sat09 13:12m 0.02s 0.02s -bash root pts/0 192.168.56.1 08:09 13:53 0.04s 0.04s -bash ``` #### 配置管理 ##### YAML - 缩进: - 两个空格 - 不能使用tab键 - 缩进代表层级关系 - 冒号: - key: value - 短横线代表list #### satate模块 ``` # vim /etc/salt/master file_roots: base: - /srv/salt # mkdir /srv/salt # mkdir /srv/salt # cd /srv/salt # mkdir web # cd web # pwd /srv/salt/web # vim apache.sls apache-install: pkg.installed: - names: - httpd - httpd-devel apache-service: service.running: - name: httpd - enable: True # salt '*' state.sls web.apache [root@linux-node2 salt]# cd /var/cache/salt/ [root@linux-node2 salt]# tree . `-- minion |-- extmods |-- files | `-- base | `-- web | `-- apache.sls |-- pkg_refresh `-- proc `-- 20160605081351939477 # cat /var/cache/salt/minion/files/base/web/apache.sls apache-install: pkg.installed: - names: - httpd - httpd-devel apache-service: service.running: - name: httpd - enable: True # ps -ef|grep yum root 34129 34103 1 08:13 ? 00:00:00 /usr/bin/python /usr/bin/yum --quiet check-update root 34204 34149 0 08:14 pts/1 00:00:00 grep --color=auto yum # cd /srv/salt/ # vim top.sls base: 'linux-node1.example.com': - web.apache 'linux-node2.example.com': - web.apache # salt '*' state.highstate test=True # salt '*' state.highstate # lsof -i:4505 -n COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME salt-mast 24739 root 13u IPv4 4637762 0t0 TCP *:4505 (LISTEN) salt-mast 24739 root 15u IPv4 4640421 0t0 TCP 192.168.56.11:4505->192.168.56.11:48344 (ESTABLISHED) salt-mast 24739 root 16u IPv4 4640542 0t0 TCP 192.168.56.11:4505->192.168.56.12:53039 (ESTABLISHED) salt-mini 25378 root 25u IPv4 4640888 0t0 TCP 192.168.56.11:48344->192.168.56.11:4505 (ESTABLISHED) ``` #### 数据系统 ##### Grains 静态数据 当minion启动时收集的minion本地相关信息

4. puppet(ruby)每30分钟连接一次master,执行一次ruby脚本
场景:公司现在在使用puppet
代码流程:
资产采集部分 采集资产subprocess, 兼容性(agent,ssh,salt), 正则或字符串方法(插件) 配置文件:默认配置和自定义配置 开发可插拔插件(每个公司采集的资产信息不同) 配置--路径--对应插件(中间件的设计模式) 插件-反射----init文件(从配置文件中获取插件信息)--pluginmanage (获取和执行插件) 给插件设置统一的方法process (返回对应信息) 解决兼容问题 方法一:设置基类(做扩展时麻烦) 方法二:给process传参 commond 在commod函数中先做判断 插件的构造方法执行之前自定制一些操作 @classmethod def initial(cls) ..... return cls() 错误堆栈信息:try except 测试模式:debug 向API发送数据 从API获取未采集资产
问题:
唯一标识:
周期:2-3个月,3个人 你负责做什么? 3处借鉴了Django源码的设计模式: 默认配置和自定义配置 中间件---插件做成可插拔的模式,增加采集资源的插件时,只要写一个类(命令+结果格式化) 在配置文件中写上路径,就可以采集资源的信息 用到了反射 遇到的难题:唯一标识 唯一标识 所有物理硬件上的标识不能作为唯一标识 主板SN号:虚拟机的SN号可能相同 IP地址会变 Mac地址不准确 标准化: --主机名不重复,作为唯一标识 --流程标准化 --资产录入,机房,机柜,机柜位置 --装机时,需要将服务信息录入CMDB --资产采集 最终流程:标准化:主机名不重复。流程标准化:装机同时,主机名在cmdb中设置 步骤: agent: a. 装系统,初始化软件cmdb,运行cmdb --通过命令获取主机名 --写入本地指定文件 b. 将资产信息发送到API c.获取资产信息 -本地文件主机名!= 命令获取的主机名(按照文件中的主机名) -本地文件住居明==命令获取的文件主机名 ssh/salt: 中控机:获取未采集主机名列表
线程池:
2 线程池 提高并发 python2: 进程池 python3:线程池 进程池 代码示例: from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor def task(i) print(i) p=ThreadPoolExecutor(10) for row in range(100) p.submit(task,row)
API验证:
为什么要做API验证?
数据传输过程中,保证数据不被篡改
如何设计?
--和Tornado中的加密Cookie类似
--客户端创建动态key md5(key+time)|time
--服务端 添加限制
-- 时间限制
-- 算法规则限制
-- 已访问记录 2s
ps :黑客窃取数据后,速度比正常汇报速度快,解决方法:数据加密 crypto模块 AES
#API验证 # 发令牌 静态, 隐患:易被他人截取 import requests key='ncjfsvnjsflbvfjslgbvhglbhfbh' response=requests.get('http://127.0.0.1:8000/api/asset/',headers={'openkey':key}) print(response.text) # 改良 动态令牌 隐患:易被他人截取 import time import requests import hashlib # ctime=time.time() key='vmkdsf;nvfglnbglbngjflbn' new_key='%s|%s'%(key,ctime) m=hashlib.md5() m.update(bytes(new_key,encoding='utf8')) md5_key=m.hexdigest() md5_time_key='%s|%s'%(md5_key,ctime) response=requests.get('http://127.0.0.1:8000/api/asset/',headers={'openkey':md5_time_key}) print(response.text) #解决方法 # ---记录已发送的md5_time_key # ---时间限制:将10s以外的排除 # 将两个限制结合起来 #隐患:黑客网速快 # 不管:搭建内网 # 管:数据加密
最终代码
def post_asset(self,server_info): #数据加密 server_info=json.dumps(server_info) server_info=self.xxxxxx(server_info) #API验证 ctime=time.time() key='vmkdsf;nvfglnbglbngjflbn' new_key='%s|%s'%(key,ctime) m=hashlib.md5() m.update(bytes(new_key,encoding='utf8')) md5_key=m.hexdigest() md5_time_key='%s|%s'%(md5_key,ctime) response=requests.get( url=settings.API, headers={'openkey':md5_time_key,'Content-Type':'application/json'}, data=server_info) # response = requests.get(settings.API,headers={'openkey': md5_time_key,},json=server_info) return response.text
def api_confirm(func): def wrapper(request): api_key_record = {} client_md5_time_key = request.META.get('HTTP_OPENKEY') client_md5_key, client_time = client_md5_time_key.split('|') client_time = float(client_time) server_time = time.time() # 第一关 排除超过10秒的请求 if server_time - client_time > 10: return HttpResponse('你网络太慢了吧,重新发') # 第二关:匹配MD5值 temp = "%s|%s" % (settings.AUTH_KEY, client_time,) m = hashlib.md5() m.update(bytes(temp, encoding='utf-8')) server_md5_key = m.hexdigest() if server_md5_key != client_md5_key: return HttpResponse('修改时间了吧,你还嫩点') # 将过期的MD5值记录删除 for k in list(api_key_record.keys()): v = api_key_record[k] if server_time > v: del api_key_record[k] # 第三关:检查此MD5值10秒之内是否访问过 if client_md5_time_key in api_key_record: return HttpResponse('是你,是你,就是你,heck') else: api_key_record[client_md5_time_key] = client_time + 10 return func(request) return wrapper
数据库设计

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) class Meta: verbose_name_plural = "用户表" def __str__(self): return self.name class AdminInfo(models.Model): """ 用户登陆相关信息 """ user_info = models.OneToOneField("UserProfile") username = models.CharField(u'用户名', max_length=64) password = models.CharField(u'密码', max_length=64) class Meta: verbose_name_plural = "管理员表" def __str__(self): return self.user_info.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 Asset(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') latest_date = models.DateField(null=True) create_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name_plural = "资产表" # def __str__(self): # return "%s-%s-%s" % (self.idc.name, self.cabinet_num, self.cabinet_order) class Server(models.Model): """ 服务器信息 """ asset = models.OneToOneField('Asset') 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 NetworkDevice(models.Model): asset = models.OneToOneField('Asset') management_ip = models.CharField('管理IP', max_length=64, blank=True, null=True) vlan_ip = models.CharField('VlanIP', max_length=64, blank=True, null=True) intranet_ip = models.CharField('内网IP', max_length=128, blank=True, null=True) sn = models.CharField('SN号', max_length=64, unique=True) manufacture = models.CharField(verbose_name=u'制造商', max_length=128, null=True, blank=True) model = models.CharField('型号', max_length=128, null=True, blank=True) port_num = models.SmallIntegerField('端口个数', null=True, blank=True) device_detail = models.CharField('设置详细配置', max_length=255, null=True, blank=True) class Meta: verbose_name_plural = "网络设备" class Disk(models.Model): """ 硬盘信息 """ slot = models.CharField('插槽位', max_length=8) model = models.CharField('磁盘型号', max_length=32) capacity = models.FloatField('磁盘容量GB') 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('Asset', 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('Asset', 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
数据展示层
<div>
<div class="search-list clearfix" style="position: relative">
<div class="search-btn col-md-offset-9 col-md-3" style="position: absolute;bottom: 1px;text-align: left" >
<input id="doSearch" type="button" class="btn btn-primary" value="搜索" />
</div>
<div class="search-item col-md-offset-2 col-md-10 clearfix" style="position: relative;height: 35px;">
<div style="position: absolute;left:0;width: 38px;">
<a type="button" class="btn btn-default add-search-condition">
<span class="glyphicon glyphicon-plus"></span>
</a>
</div>
<div class="input-group searchArea" style="position: absolute;left: 40px;right:300px;">
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="searchDefault">默认值</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
</ul>
</div>
<!-- /btn-group -->
<!-- <input type="text" class="form-control" aria-label="..."> -->
</div>
</div>
</div>
</div>
</head> <body> <div style="width: 700px;margin: 0 auto"> <div class="btn-group" role="group" aria-label="..." style="margin: 20px"> <button id="checkAll" type="button" class="btn btn-default">全选</button> <button id="checkReverse" type="button" class="btn btn-default">反选</button> <button id="checkCancel" type="button" class="btn btn-default">取消</button> <button id="inOutEditMode" type="button" class="btn btn-default">进入编辑模式</button> <a class="btn btn-default" href="#">添加</a> <button id="multiDel" type="button" class="btn btn-default">删除</button> <button id="refresh" type="button" class="btn btn-default">刷新</button> <button id="save" type="button" class="btn btn-default">保存</button> </div> <table class="table table-bordered table-striped"> <thead id="tbHead"> <tr> </tr> </thead> <tbody id="tbBody"> </tbody> </table> </div> </body> </html>
(function (jq) { var CREATE_SEARCH_CONDITION = true; var GLOBAL_DICT = {}; /* { 'device_type_choices': ( (1, '服务器'), (2, '交换机'), (3, '防火墙'), ) 'device_status_choices': ( (1, '上架'), (2, '在线'), (3, '离线'), (4, '下架'), ) } */ // 为字符串创建format方法,用于字符串格式化 String.prototype.format = function (args) { return this.replace(/\{(\w+)\}/g, function (s, i) { return args[i]; }); }; function getSearchCondition(){ var condition = {}; $('.search-list').find('input[type="text"],select').each(function(){ /* 获取所有搜索条件 */ var name = $(this).attr('name'); var value = $(this).val(); if(condition[name]){ condition[name].push(value); }else{ condition[name] = [value]; } }); return condition; } function initial(url) { // 执行一个函数, 获取当前搜索条件 var searchCondition = getSearchCondition(); console.log(searchCondition); $.ajax({ url: url, type: 'GET', // 获取数据 data: {condition: JSON.stringify(searchCondition)}, dataType: 'JSON', success: function (arg) { $.each(arg.global_dict,function(k,v){ GLOBAL_DICT[k] = v }); initTableHeader(arg.table_config); initTableBody(arg.server_list, arg.table_config); initSearch(arg.search_config); } }) } /* 初始化搜索条件 */ function initSearch(searchConfig){ if(searchConfig && CREATE_SEARCH_CONDITION){ CREATE_SEARCH_CONDITION = false; // 找打searchArea ul, $.each(searchConfig,function(k,v){ var li = document.createElement('li'); $(li).attr('search_type', v.search_type); $(li).attr('name', v.name); if(v.search_type == 'select'){ $(li).attr('global_name', v.global_name); } var a = document.createElement('a'); a.innerHTML = v.text; $(li).append(a); $('.searchArea ul').append(li); }); // 初始化默认搜索条件 // searchConfig[0],进行初始化 // 初始化默认选中值 $('.search-item .searchDefault').text(searchConfig[0].text); if(searchConfig[0].search_type == 'select'){ var sel = document.createElement('select'); $(sel).attr('class','form-control'); $.each(GLOBAL_DICT[searchConfig[0].global_name],function(k,v){ var op = document.createElement('option'); $(op).text(v[1]); $(op).val(v[0]); $(sel).append(op) }); $('.input-group').append(sel); }else{ // <input type="text" class="form-control" aria-label="..."> var inp = document.createElement('input'); $(inp).attr('name',searchConfig[0].name); $(inp).attr('type','text'); $(inp).attr('class','form-control'); $('.input-group').append(inp); } } } function initTableHeader(tableConfig) { /* [ {'q':'id','title':'ID'}, {'q':'hostname','title':'主机名'}, ] */ $('#tbHead').empty(); var tr = document.createElement('tr'); $.each(tableConfig, function (k, v) { if (v.display) { var tag = document.createElement('th'); tag.innerHTML = v.title; $(tr).append(tag); } }); $('#tbHead').append(tr); } function initTableBody(serverList, tableConfig) { /* serverList = [ {'id': 1, 'hostname':c2.com, create_at: xxxx-xx-xx-}, {'id': 1, 'hostname':c2.com, create_at: xxxx-xx-xx-}, {'id': 1, 'hostname':c2.com, create_at: xxxx-xx-xx-}, {'id': 1, 'hostname':c2.com, create_at: xxxx-xx-xx-}, ] */ $('#tbBody').empty(); $.each(serverList, function (k, row) { // row: {'id': 1, 'hostname':c2.com, create_at: xxxx-xx-xx-} /* <tr> <td>id</td> <td>hostn</td> <td>create</td> </tr> */ var tr = document.createElement('tr'); tr.setAttribute('nid',row.id); $.each(tableConfig, function (kk, rrow) { // kk: 1 rrow:{'q':'id','title':'ID'}, // rrow.q = "id" // kk: . rrow:{'q':'hostname','title':'主机名'},// rrow.q = "hostname" // kk: . rrow:{'q':'create_at','title':'创建时间'}, // rrow.q = "create_at" if (rrow.display) { var td = document.createElement('td'); /* 在td标签中添加内容 */ var newKwargs = {}; // {'n1':'1','n2':'123'} $.each(rrow.text.kwargs, function (kkk, vvv) { var av = vvv; if(vvv.substring(0,2) == '@@'){ var global_dict_key = vvv.substring(2,vvv.length); var nid = row[rrow.q]; $.each(GLOBAL_DICT[global_dict_key],function(gk,gv){ if(gv[0] == nid){ av = gv[1]; } }) } else if (vvv[0] == '@') { av = row[vvv.substring(1, vvv.length)]; } newKwargs[kkk] = av; }); var newText = rrow.text.tpl.format(newKwargs); td.innerHTML = newText; /* 在td标签中添加属性 */ $.each(rrow.attrs,function(atkey,atval){ // 如果@ if (atval[0] == '@') { td.setAttribute(atkey, row[atval.substring(1, atval.length)]); }else{ td.setAttribute(atkey,atval); } }); $(tr).append(td); } }); $('#tbBody').append(tr); }) } function trIntoEdit($tr){ $tr.find('td[edit-enable="true"]').each(function(){ // $(this) 每一个td var editType = $(this).attr('edit-type'); if(editType == 'select'){ // 生成下拉框:找到数据源 var deviceTypeChoices = GLOBAL_DICT[$(this).attr('global_key')]; // 生成select标签 var selectTag = document.createElement('select'); var origin = $(this).attr('origin'); $.each(deviceTypeChoices,function(k,v){ var option = document.createElement('option'); $(option).text(v[1]); $(option).val(v[0]); if(v[0] == origin){ // 默认选中原来的值 $(option).prop('selected',true); } $(selectTag).append(option); }); $(this).html(selectTag); // 显示默认值 }else{ // 获取原来td中的文本内容 var v1 = $(this).text(); // 创建input标签,并且内部设置值 var inp = document.createElement('input'); $(inp).val(v1); // 添加到td中 $(this).html(inp); } }); } function trOutEdit($tr){ $tr.find('td[edit-enable="true"]').each(function(){ // $(this) 每一个td var editType = $(this).attr('edit-type'); if(editType == 'select'){ var option = $(this).find('select')[0].selectedOptions; $(this).attr('new-origin',$(option).val()); $(this).html($(option).text()); }else{ var inputVal = $(this).find('input').val(); $(this).html(inputVal); } }); } jq.extend({ xx: function (url) { initial(url); // 所有checkbox绑定事件 $('#tbBody').on('click',':checkbox',function(){ // $(this) // checkbox标签 // 1. 检测是否已经被选中 if($('#inOutEditMode').hasClass('btn-warning')){ var $tr = $(this).parent().parent(); if($(this).prop('checked')){ // 进入编辑模式 trIntoEdit($tr); }else{ // 退出编辑模式 trOutEdit($tr); } } }); // 所有按钮绑定事件 $('#checkAll').click(function(){ if($('#inOutEditMode').hasClass('btn-warning')){ $('#tbBody').find(':checkbox').each(function(){ if(!$(this).prop('checked')){ var $tr = $(this).parent().parent(); trIntoEdit($tr); $(this).prop('checked',true); } }) }else{ $('#tbBody').find(':checkbox').prop('checked',true); } }); $('#checkReverse').click(function(){ if($('#inOutEditMode').hasClass('btn-warning')){ $('#tbBody').find(':checkbox').each(function(){ var $tr = $(this).parent().parent(); if($(this).prop('checked')){ trOutEdit($tr); $(this).prop('checked',false); }else{ trIntoEdit($tr); $(this).prop('checked',true); } }) }else{ $('#tbBody').find(':checkbox').each(function(){ var $tr = $(this).parent().parent(); if($(this).prop('checked')){ $(this).prop('checked',false); }else{ $(this).prop('checked',true); } }) } }); $('#checkCancel').click(function(){ if($('#inOutEditMode').hasClass('btn-warning')){ $('#tbBody').find(':checkbox').each(function(){ if($(this).prop('checked')){ var $tr = $(this).parent().parent(); trOutEdit($tr); $(this).prop('checked',false); } }) }else{ $('#tbBody').find(':checkbox').prop('checked',false); } }); $('#inOutEditMode').click(function(){ if($(this).hasClass('btn-warning')){ // 退出编辑模式 $(this).removeClass('btn-warning'); $(this).text('进入编辑模式'); $('#tbBody').find(':checkbox').each(function(){ if($(this).prop('checked')){ var $tr = $(this).parent().parent(); trOutEdit($tr); } }) }else{ // 进入编辑模式 $(this).addClass('btn-warning'); $(this).text('退出编辑模式'); $('#tbBody').find(':checkbox').each(function(){ if($(this).prop('checked')){ var $tr = $(this).parent().parent(); trIntoEdit($tr); } }) } }); $('#multiDel').click(function(){ // $('#tbBody').find(':checkbox') var idList = []; $('#tbBody').find(':checked').each(function(){ var v = $(this).val(); idList.push(v) }); $.ajax({ url: url, type: 'delete', data: JSON.stringify(idList), success:function(arg){ console.log(arg); } }) }); $('#refresh').click(function(){ initial(url) }); $('#save').click(function(){ if($('#inOutEditMode').hasClass('btn-warning')){ $('#tbBody').find(':checkbox').each(function(){ if($(this).prop('checked')){ var $tr = $(this).parent().parent(); trOutEdit($tr); } }) } var all_list = []; // 获取用户修改过的数据 $('#tbBody').children().each(function(){ // $(this) = tr var $tr= $(this); var nid= $tr.attr('nid'); var row_dict = {}; var flag = false; $tr.children().each(function(){ if($(this).attr('edit-enable')) { if($(this).attr('edit-type') == 'select'){ var newData = $(this).attr('new-origin'); var oldData = $(this).attr('origin'); if(newData){ if (newData != oldData) { var name = $(this).attr('name'); row_dict[name] = newData; flag = true; } } }else{ var newData = $(this).text(); var oldData = $(this).attr('origin'); if (newData != oldData) { var name = $(this).attr('name'); row_dict[name] = newData; flag = true; } } } }); if(flag){ row_dict['id'] = nid; } all_list.push(row_dict) }); // 通过Ajax提交后台 $.ajax({ url: url, type: 'PUT', data: JSON.stringify(all_list), success:function(arg){ console.log(arg); } }) }); $('.search-list').on('click','li',function(){ // 点击li执行函数 var wenben = $(this).text(); var searchType = $(this).attr('search_type'); var name = $(this).attr('name'); var globalName = $(this).attr('global_name'); // 把显示替换 $(this).parent().prev().find('.searchDefault').text(wenben); if(searchType == 'select'){ /* [ [1,‘文本’], [1,‘文本’], [1,‘文本’], ] */ var sel = document.createElement('select'); $(sel).attr('class','form-control'); $(sel).attr('name',name); $.each(GLOBAL_DICT[globalName],function(k,v){ var op = document.createElement('option'); $(op).text(v[1]); $(op).val(v[0]); $(sel).append(op); }); $(this).parent().parent().next().remove(); $(this).parent().parent().after(sel); }else{ var inp = document.createElement('input'); $(inp).attr('class','form-control'); $(inp).attr('name',name); $(inp).attr('type','text'); $(this).parent().parent().next().remove(); $(this).parent().parent().after(inp); } }); $('.search-list').on('click','.add-search-condition',function(){ // 拷贝的新一搜索项 var newSearchItem = $(this).parent().parent().clone(); $(newSearchItem).find('.add-search-condition span').removeClass('glyphicon-plus').addClass('glyphicon-minus'); $(newSearchItem).find('.add-search-condition').addClass('del-search-condition').removeClass('add-search-condition'); $('.search-list').append(newSearchItem); }); $('.search-list').on('click','.del-search-condition',function(){ $(this).parent().parent().remove(); }); $('#doSearch').click(function(){ initial(url); }) } }) })(jQuery);
table_config=[ {'q': None, 'title': '选择', 'display': True, 'text': { 'tpl': "<input type='checkbox' value='{n1}' />", 'kwargs': {'n1': '@id'}}, }, {'q':'id','title':'ID', 'display':False, 'text':{ 'tpl':"{n1}", 'kwargs':{'n1':'@id'} }}, {'q': 'hostname', 'title': '主机名', 'display': True, 'text': { 'tpl': "{n1}", 'kwargs': {'n1': '@hostname'} }, 'attrs':{'origin':'@hostname','name':'hostname','edit-enable':'true'}}, {'q': 'create_at', 'title': '创建时间', 'display': True, 'text': { 'tpl': "{n1}", 'kwargs': {'n1': '@create_at'} }}, {'q':None, 'title': '操作', 'display': True, 'text': { 'tpl': "<a href='/del?nid={nid}'>删除</a>", 'kwargs': {'nid': '@id'} }}, ] table_config1=[ {'q': None, 'title': '选择', 'display':True, 'text': { 'tpl': "<input type='checkbox' value='{n1}' />", 'kwargs': {'n1': '@id'}}, 'attrs':{'nid':'@id'} }, {'q':'id','title':'ID', 'display':False, 'text':{ 'tpl':"{n1}", 'kwargs':{'n1':'@id'} } }, {'q': 'business_unit_id__name', 'title': '业务线', 'display': True, 'text': { 'tpl': "{n1}", 'kwargs': {'n1': '@business_unit_id__name'} }, 'attrs':{'k1':'v1','k2':'@id'} }, {'q': 'device_type_id', 'title': '资产类型', 'display': True, 'text': { 'tpl': "{n1}", 'kwargs': {'n1': '@@device_type_choices'} }, 'attrs': {'k1': 'v1', 'nid': '@id','origin':'@device_type_id','name':'device_type_id', 'edit-enable':'true','edit-type':'select','global_key':'device_type_choices'}}, {'q': 'device_status_id', 'title': '状态', 'display': True, 'text': { 'tpl': "{n1}", 'kwargs': {'n1': '@@device_status_choices'} }, 'attrs': {'k1': 'v1', 'nid': '@id','origin':'@device_status_id','name':'device_status_id', 'edit-enable':'true','edit-type':'select','global_key':'device_status_choices'}}, {'q':None, 'title': '操作', 'display': True, 'text': { 'tpl': "<a href='/del?nid={nid}'>删除</a>", 'kwargs': {'nid': '@id'} },'attrs':{'k1':'v1','k2':'@id'} }, ] search_config = [ {'name': 'cabinet_num', 'text': '机柜号', 'search_type': 'input'}, {'name': 'device_type_id', 'text': '资产类型', 'search_type': 'select', 'global_name': 'device_type_choices'}, {'name': 'device_status_id', 'text': '资产状态', 'search_type': 'select', 'global_name': 'device_status_choices'}, ]
参考:http://www.cnblogs.com/nulige/p/6703160.html

浙公网安备 33010602011771号