一、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简介
公司:
开发
测试:功能测试 性能测试(高并发)
运维:上线 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