资产采集的实现方案
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速度快
原理:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 SaltStack 采用`C/S`模式,server端就是salt的master,client端就是minion,minion与master之间通过`ZeroMQ`消息队列通信。 2 3 minion上线后先与master端联系,把自己的`pub key`发过去,这时master端通过`salt-key -L`命令就会看到minion的key,接受该minion-key后,也就是master与minion已经互信。 4 5 master可以发送任何指令让minion执行了,salt有很多可执行模块,比如说cmd模块,在安装minion的时候已经自带了,它们通常位于你的python库中,`locate salt | grep /usr/`可以看到salt自带的所有东西。 6 7 这些模块是python写成的文件,里面会有好多函数,如cmd.run,当我们执行`salt '*' cmd.run 'uptime'`的时候,master下发任务匹配到的minion上去,minion执行模块函数,并返回结果。 8 9 master监听4505和4506端口,4505对应的是ZMQ的PUB system,用来发送消息,4506对应的是REP system是来接受消息的。 10 具体步骤如下 11 12 ``` 13 1、Salt stack的Master与Minion之间通过ZeroMq进行消息传递,使用了ZeroMq的发布-订阅模式,连接方式包括tcp,ipc 14 2、salt命令,将cmd.run ls命令从salt.client.LocalClient.cmd_cli发布到master,获取一个Jodid,根据jobid获取命令执行结果。 15 3、master接收到命令后,将要执行的命令发送给客户端minion。 16 4、minion从消息总线上接收到要处理的命令,交给minion._handle_aes处理 17 5、minion._handle_aes发起一个本地线程调用cmdmod执行ls命令。线程执行完ls后,调用minion._return_pub方法,将执行结果通过消息总线返回给master 18 6、master接收到客户端返回的结果,调用master._handle_aes方法,将结果写的文件中 19 7、salt.client.LocalClient.cmd_cli通过轮询获取Job执行结果,将结果输出到终端。 20 ``` 21 22 #### saltstack 安装 23 24 [saltstack install](http://repo.saltstack.com/#rhel) 25 26 #### 修改minion配置文件 27 ``` 28 [root@linux-node2 ~]# vim /etc/salt/minion 29 master: 192.168.56.11 30 [root@linux-node2 ~]# vim /etc/salt/minion 31 master: 192.168.56.11 32 [root@linux-node1 pki]# pwd 33 /etc/salt/pki 34 [root@linux-node1 pki]# tree 35 . 36 ├── master 37 │ ├── master.pem 38 │ ├── master.pub 39 │ ├── minions 40 │ ├── minions_autosign 41 │ ├── minions_denied 42 │ ├── minions_pre 43 │ │ ├── linux-node1.example.com 44 │ │ └── linux-node2.example.com 45 │ └── minions_rejected 46 └── minion 47 ├── minion_master.pub 48 ├── minion.pem 49 └── minion.pub 50 [root@linux-node1 pki]# salt-key -A 51 [root@linux-node1 pki]# tree 52 . 53 ├── master 54 │ ├── master.pem 55 │ ├── master.pub 56 │ ├── minions 57 │ │ ├── linux-node1.example.com 58 │ │ └── linux-node2.example.com 59 │ ├── minions_autosign 60 │ ├── minions_denied 61 │ ├── minions_pre 62 │ └── minions_rejected 63 └── minion 64 ├── minion_master.pub 65 ├── minion.pem 66 └── minion.pub 67 ``` 68 69 #### 远程执行 70 ``` 71 [root@linux-node1 pki]# salt "*" test.ping 72 linux-node2.example.com: 73 True 74 linux-node1.example.com: 75 True 76 [root@linux-node1 pki]# salt "*" cmd.run 'w' 77 linux-node1.example.com: 78 07:20:24 up 17:10, 1 user, load average: 0.00, 0.01, 0.05 79 USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT 80 root pts/0 192.168.56.1 07:04 0.00s 0.30s 0.26s /usr/bin/python /usr/bin/salt * cmd.run w 81 linux-node2.example.com: 82 08:26:25 up 22:40, 2 users, load average: 0.15, 0.05, 0.06 83 USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT 84 root tty1 Sat09 13:12m 0.02s 0.02s -bash 85 root pts/0 192.168.56.1 08:09 13:53 0.04s 0.04s -bash 86 ``` 87 88 #### 配置管理 89 ##### YAML 90 91 - 缩进: 92 - 两个空格 93 - 不能使用tab键 94 - 缩进代表层级关系 95 96 - 冒号: 97 - key: value 98 99 - 短横线代表list 100 101 #### satate模块 102 ``` 103 # vim /etc/salt/master 104 file_roots: 105 base: 106 - /srv/salt 107 # mkdir /srv/salt 108 # mkdir /srv/salt 109 # cd /srv/salt 110 # mkdir web 111 # cd web 112 # pwd 113 /srv/salt/web 114 # vim apache.sls 115 apache-install: 116 pkg.installed: 117 - names: 118 - httpd 119 - httpd-devel 120 121 apache-service: 122 service.running: 123 - name: httpd 124 - enable: True 125 126 # salt '*' state.sls web.apache 127 [root@linux-node2 salt]# cd /var/cache/salt/ 128 [root@linux-node2 salt]# tree 129 . 130 `-- minion 131 |-- extmods 132 |-- files 133 | `-- base 134 | `-- web 135 | `-- apache.sls 136 |-- pkg_refresh 137 `-- proc 138 `-- 20160605081351939477 139 # cat /var/cache/salt/minion/files/base/web/apache.sls 140 apache-install: 141 pkg.installed: 142 - names: 143 - httpd 144 - httpd-devel 145 146 apache-service: 147 service.running: 148 - name: httpd 149 - enable: True 150 # ps -ef|grep yum 151 root 34129 34103 1 08:13 ? 00:00:00 /usr/bin/python /usr/bin/yum --quiet check-update 152 root 34204 34149 0 08:14 pts/1 00:00:00 grep --color=auto yum 153 154 # cd /srv/salt/ 155 # vim top.sls 156 base: 157 'linux-node1.example.com': 158 - web.apache 159 'linux-node2.example.com': 160 - web.apache 161 # salt '*' state.highstate test=True 162 # salt '*' state.highstate 163 164 # lsof -i:4505 -n 165 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME 166 salt-mast 24739 root 13u IPv4 4637762 0t0 TCP *:4505 (LISTEN) 167 salt-mast 24739 root 15u IPv4 4640421 0t0 TCP 192.168.56.11:4505->192.168.56.11:48344 (ESTABLISHED) 168 salt-mast 24739 root 16u IPv4 4640542 0t0 TCP 192.168.56.11:4505->192.168.56.12:53039 (ESTABLISHED) 169 salt-mini 25378 root 25u IPv4 4640888 0t0 TCP 192.168.56.11:48344->192.168.56.11:4505 (ESTABLISHED) 170 ``` 171 172 #### 数据系统 173 174 ##### Grains 175 静态数据 当minion启动时收集的minion本地相关信息
4. puppet(ruby)每30分钟连接一次master,执行一次ruby脚本
场景:公司现在在使用puppet
代码流程:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
资产采集部分 采集资产subprocess, 兼容性(agent,ssh,salt), 正则或字符串方法(插件) 1 配置文件:默认配置和自定义配置 2 开发可插拔插件(每个公司采集的资产信息不同) 配置--路径--对应插件(中间件的设计模式) 插件-反射----init文件(从配置文件中获取插件信息)--pluginmanage (获取和执行插件) 给插件设置统一的方法process (返回对应信息) 3 解决兼容问题 方法一:设置基类(做扩展时麻烦) 方法二:给process传参 commond 在commod函数中先做判断 4 插件的构造方法执行之前自定制一些操作 @classmethod def initial(cls) ..... return cls() 错误堆栈信息:try except 测试模式:debug 5 向API发送数据 从API获取未采集资产
问题:
唯一标识:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
周期:2-3个月,3个人 你负责做什么? 3处借鉴了Django源码的设计模式: 1 默认配置和自定义配置 2 中间件---插件做成可插拔的模式,增加采集资源的插件时,只要写一个类(命令+结果格式化) 在配置文件中写上路径,就可以采集资源的信息 用到了反射 遇到的难题:唯一标识 1 唯一标识 所有物理硬件上的标识不能作为唯一标识 主板SN号:虚拟机的SN号可能相同 IP地址会变 Mac地址不准确 标准化: --主机名不重复,作为唯一标识 --流程标准化 --资产录入,机房,机柜,机柜位置 --装机时,需要将服务信息录入CMDB --资产采集 最终流程:标准化:主机名不重复。流程标准化:装机同时,主机名在cmdb中设置 步骤: agent: a. 装系统,初始化软件cmdb,运行cmdb --通过命令获取主机名 --写入本地指定文件 b. 将资产信息发送到API c.获取资产信息 -本地文件主机名!= 命令获取的主机名(按照文件中的主机名) -本地文件住居明==命令获取的文件主机名 ssh/salt: 中控机:获取未采集主机名列表
线程池:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 2 线程池 2 提高并发 3 python2: 进程池 4 python3:线程池 进程池 5 代码示例: 6 from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor 7 8 def task(i) 9 print(i) 10 11 p=ThreadPoolExecutor(10) 12 for row in range(100) 13 p.submit(task,row)
API验证:
为什么要做API验证?
数据传输过程中,保证数据不被篡改
如何设计?
--和Tornado中的加密Cookie类似
--客户端创建动态key md5(key+time)|time
--服务端 添加限制
-- 时间限制
-- 算法规则限制
-- 已访问记录 2s
ps :黑客窃取数据后,速度比正常汇报速度快,解决方法:数据加密 crypto模块 AES
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #API验证 2 # 发令牌 静态, 隐患:易被他人截取 3 4 import requests 5 key='ncjfsvnjsflbvfjslgbvhglbhfbh' 6 response=requests.get('http://127.0.0.1:8000/api/asset/',headers={'openkey':key}) 7 print(response.text) 8 9 # 改良 动态令牌 隐患:易被他人截取 10 import time 11 import requests 12 import hashlib 13 # 14 ctime=time.time() 15 key='vmkdsf;nvfglnbglbngjflbn' 16 new_key='%s|%s'%(key,ctime) 17 18 m=hashlib.md5() 19 m.update(bytes(new_key,encoding='utf8')) 20 md5_key=m.hexdigest() 21 22 md5_time_key='%s|%s'%(md5_key,ctime) 23 response=requests.get('http://127.0.0.1:8000/api/asset/',headers={'openkey':md5_time_key}) 24 print(response.text) 25 26 #解决方法 27 # ---记录已发送的md5_time_key 28 # ---时间限制:将10s以外的排除 29 # 将两个限制结合起来 30 31 #隐患:黑客网速快 32 # 不管:搭建内网 33 # 管:数据加密
最终代码
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 def post_asset(self,server_info): 2 #数据加密 3 server_info=json.dumps(server_info) 4 server_info=self.xxxxxx(server_info) 5 #API验证 6 ctime=time.time() 7 key='vmkdsf;nvfglnbglbngjflbn' 8 new_key='%s|%s'%(key,ctime) 9 10 m=hashlib.md5() 11 m.update(bytes(new_key,encoding='utf8')) 12 md5_key=m.hexdigest() 13 14 md5_time_key='%s|%s'%(md5_key,ctime) 15 16 response=requests.get( 17 url=settings.API, 18 headers={'openkey':md5_time_key,'Content-Type':'application/json'}, 19 data=server_info) 20 # response = requests.get(settings.API,headers={'openkey': md5_time_key,},json=server_info) 21 return response.text
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 def api_confirm(func): 2 def wrapper(request): 3 api_key_record = {} 4 client_md5_time_key = request.META.get('HTTP_OPENKEY') 5 client_md5_key, client_time = client_md5_time_key.split('|') 6 client_time = float(client_time) 7 server_time = time.time() 8 9 # 第一关 排除超过10秒的请求 10 if server_time - client_time > 10: 11 return HttpResponse('你网络太慢了吧,重新发') 12 # 第二关:匹配MD5值 13 14 temp = "%s|%s" % (settings.AUTH_KEY, client_time,) 15 m = hashlib.md5() 16 m.update(bytes(temp, encoding='utf-8')) 17 server_md5_key = m.hexdigest() 18 if server_md5_key != client_md5_key: 19 return HttpResponse('修改时间了吧,你还嫩点') 20 21 # 将过期的MD5值记录删除 22 for k in list(api_key_record.keys()): 23 v = api_key_record[k] 24 if server_time > v: 25 del api_key_record[k] 26 27 # 第三关:检查此MD5值10秒之内是否访问过 28 if client_md5_time_key in api_key_record: 29 return HttpResponse('是你,是你,就是你,heck') 30 else: 31 api_key_record[client_md5_time_key] = client_time + 10 32 return func(request) 33 return wrapper
数据库设计
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 from django.db import models 2 3 4 class UserProfile(models.Model): 5 """ 6 用户信息 7 """ 8 name = models.CharField(u'姓名', max_length=32) 9 email = models.EmailField(u'邮箱') 10 phone = models.CharField(u'座机', max_length=32) 11 mobile = models.CharField(u'手机', max_length=32) 12 13 class Meta: 14 verbose_name_plural = "用户表" 15 16 def __str__(self): 17 return self.name 18 19 20 class AdminInfo(models.Model): 21 """ 22 用户登陆相关信息 23 """ 24 user_info = models.OneToOneField("UserProfile") 25 username = models.CharField(u'用户名', max_length=64) 26 password = models.CharField(u'密码', max_length=64) 27 28 class Meta: 29 verbose_name_plural = "管理员表" 30 31 def __str__(self): 32 return self.user_info.name 33 34 35 class UserGroup(models.Model): 36 """ 37 用户组 38 """ 39 name = models.CharField(max_length=32, unique=True) 40 users = models.ManyToManyField('UserProfile') 41 42 class Meta: 43 verbose_name_plural = "用户组表" 44 45 def __str__(self): 46 return self.name 47 48 49 class BusinessUnit(models.Model): 50 """ 51 业务线 52 """ 53 name = models.CharField('业务线', max_length=64, unique=True) 54 contact = models.ForeignKey('UserGroup', verbose_name='业务联系人', related_name='c') 55 manager = models.ForeignKey('UserGroup', verbose_name='系统管理员', related_name='m') 56 57 class Meta: 58 verbose_name_plural = "业务线表" 59 60 def __str__(self): 61 return self.name 62 63 64 class IDC(models.Model): 65 """ 66 机房信息 67 """ 68 name = models.CharField('机房', max_length=32) 69 floor = models.IntegerField('楼层', default=1) 70 71 class Meta: 72 verbose_name_plural = "机房表" 73 74 def __str__(self): 75 return self.name 76 77 78 class Tag(models.Model): 79 """ 80 资产标签 81 """ 82 name = models.CharField('标签', max_length=32, unique=True) 83 84 class Meta: 85 verbose_name_plural = "标签表" 86 87 def __str__(self): 88 return self.name 89 90 91 class Asset(models.Model): 92 """ 93 资产信息表,所有资产公共信息(交换机,服务器,防火墙等) 94 """ 95 device_type_choices = ( 96 (1, '服务器'), 97 (2, '交换机'), 98 (3, '防火墙'), 99 ) 100 device_status_choices = ( 101 (1, '上架'), 102 (2, '在线'), 103 (3, '离线'), 104 (4, '下架'), 105 ) 106 107 device_type_id = models.IntegerField(choices=device_type_choices, default=1) 108 device_status_id = models.IntegerField(choices=device_status_choices, default=1) 109 110 cabinet_num = models.CharField('机柜号', max_length=30, null=True, blank=True) 111 cabinet_order = models.CharField('机柜中序号', max_length=30, null=True, blank=True) 112 113 idc = models.ForeignKey('IDC', verbose_name='IDC机房', null=True, blank=True) 114 business_unit = models.ForeignKey('BusinessUnit', verbose_name='属于的业务线', null=True, blank=True) 115 116 tag = models.ManyToManyField('Tag') 117 118 latest_date = models.DateField(null=True) 119 create_at = models.DateTimeField(auto_now_add=True) 120 121 class Meta: 122 verbose_name_plural = "资产表" 123 124 # def __str__(self): 125 # return "%s-%s-%s" % (self.idc.name, self.cabinet_num, self.cabinet_order) 126 127 128 class Server(models.Model): 129 """ 130 服务器信息 131 """ 132 asset = models.OneToOneField('Asset') 133 134 hostname = models.CharField(max_length=128, unique=True) 135 sn = models.CharField('SN号', max_length=64, db_index=True) 136 manufacturer = models.CharField(verbose_name='制造商', max_length=64, null=True, blank=True) 137 model = models.CharField('型号', max_length=64, null=True, blank=True) 138 139 manage_ip = models.GenericIPAddressField('管理IP', null=True, blank=True) 140 141 os_platform = models.CharField('系统', max_length=16, null=True, blank=True) 142 os_version = models.CharField('系统版本', max_length=16, null=True, blank=True) 143 144 cpu_count = models.IntegerField('CPU个数', null=True, blank=True) 145 cpu_physical_count = models.IntegerField('CPU物理个数', null=True, blank=True) 146 cpu_model = models.CharField('CPU型号', max_length=128, null=True, blank=True) 147 148 create_at = models.DateTimeField(auto_now_add=True, blank=True) 149 150 class Meta: 151 verbose_name_plural = "服务器表" 152 153 def __str__(self): 154 return self.hostname 155 156 157 class NetworkDevice(models.Model): 158 asset = models.OneToOneField('Asset') 159 management_ip = models.CharField('管理IP', max_length=64, blank=True, null=True) 160 vlan_ip = models.CharField('VlanIP', max_length=64, blank=True, null=True) 161 intranet_ip = models.CharField('内网IP', max_length=128, blank=True, null=True) 162 sn = models.CharField('SN号', max_length=64, unique=True) 163 manufacture = models.CharField(verbose_name=u'制造商', max_length=128, null=True, blank=True) 164 model = models.CharField('型号', max_length=128, null=True, blank=True) 165 port_num = models.SmallIntegerField('端口个数', null=True, blank=True) 166 device_detail = models.CharField('设置详细配置', max_length=255, null=True, blank=True) 167 168 class Meta: 169 verbose_name_plural = "网络设备" 170 171 172 class Disk(models.Model): 173 """ 174 硬盘信息 175 """ 176 slot = models.CharField('插槽位', max_length=8) 177 model = models.CharField('磁盘型号', max_length=32) 178 capacity = models.FloatField('磁盘容量GB') 179 pd_type = models.CharField('磁盘类型', max_length=32) 180 server_obj = models.ForeignKey('Server',related_name='disk') 181 182 class Meta: 183 verbose_name_plural = "硬盘表" 184 185 def __str__(self): 186 return self.slot 187 188 189 class NIC(models.Model): 190 """ 191 网卡信息 192 """ 193 name = models.CharField('网卡名称', max_length=128) 194 hwaddr = models.CharField('网卡mac地址', max_length=64) 195 netmask = models.CharField(max_length=64) 196 ipaddrs = models.CharField('ip地址', max_length=256) 197 up = models.BooleanField(default=False) 198 server_obj = models.ForeignKey('Server',related_name='nic') 199 200 201 class Meta: 202 verbose_name_plural = "网卡表" 203 204 def __str__(self): 205 return self.name 206 207 208 class Memory(models.Model): 209 """ 210 内存信息 211 """ 212 slot = models.CharField('插槽位', max_length=32) 213 manufacturer = models.CharField('制造商', max_length=32, null=True, blank=True) 214 model = models.CharField('型号', max_length=64) 215 capacity = models.FloatField('容量', null=True, blank=True) 216 sn = models.CharField('内存SN号', max_length=64, null=True, blank=True) 217 speed = models.CharField('速度', max_length=16, null=True, blank=True) 218 219 server_obj = models.ForeignKey('Server',related_name='memory') 220 221 222 class Meta: 223 verbose_name_plural = "内存表" 224 225 def __str__(self): 226 return self.slot 227 228 229 class AssetRecord(models.Model): 230 """ 231 资产变更记录,creator为空时,表示是资产汇报的数据。 232 """ 233 asset_obj = models.ForeignKey('Asset', related_name='ar') 234 content = models.TextField(null=True)# 新增硬盘 235 creator = models.ForeignKey('UserProfile', null=True, blank=True) 236 create_at = models.DateTimeField(auto_now_add=True) 237 238 239 class Meta: 240 verbose_name_plural = "资产记录表" 241 242 def __str__(self): 243 return "%s-%s-%s" % (self.asset_obj.idc.name, self.asset_obj.cabinet_num, self.asset_obj.cabinet_order) 244 245 246 class ErrorLog(models.Model): 247 """ 248 错误日志,如:agent采集数据错误 或 运行错误 249 """ 250 asset_obj = models.ForeignKey('Asset', null=True, blank=True) 251 title = models.CharField(max_length=16) 252 content = models.TextField() 253 create_at = models.DateTimeField(auto_now_add=True) 254 255 class Meta: 256 verbose_name_plural = "错误日志表" 257 258 def __str__(self): 259 return self.title
数据展示层
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 <div> 2 <div class="search-list clearfix" style="position: relative"> 3 4 <div class="search-btn col-md-offset-9 col-md-3" style="position: absolute;bottom: 1px;text-align: left" > 5 <input id="doSearch" type="button" class="btn btn-primary" value="搜索" /> 6 </div> 7 8 <div class="search-item col-md-offset-2 col-md-10 clearfix" style="position: relative;height: 35px;"> 9 <div style="position: absolute;left:0; 38px;"> 10 <a type="button" class="btn btn-default add-search-condition"> 11 <span class="glyphicon glyphicon-plus"></span> 12 </a> 13 </div> 14 <div class="input-group searchArea" style="position: absolute;left: 40px;right:300px;"> 15 <div class="input-group-btn"> 16 <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> 17 <span class="searchDefault">默认值</span> 18 <span class="caret"></span> 19 </button> 20 <ul class="dropdown-menu"> 21 22 </ul> 23 </div> 24 <!-- /btn-group --> 25 <!-- <input type="text" class="form-control" aria-label="..."> --> 26 27 </div> 28 </div> 29 30 </div> 31 </div>
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 </head> 2 <body> 3 <div style=" 700px;margin: 0 auto"> 4 <div class="btn-group" role="group" aria-label="..." style="margin: 20px"> 5 <button id="checkAll" type="button" class="btn btn-default">全选</button> 6 <button id="checkReverse" type="button" class="btn btn-default">反选</button> 7 <button id="checkCancel" type="button" class="btn btn-default">取消</button> 8 <button id="inOutEditMode" type="button" class="btn btn-default">进入编辑模式</button> 9 <a class="btn btn-default" href="#">添加</a> 10 <button id="multiDel" type="button" class="btn btn-default">删除</button> 11 <button id="refresh" type="button" class="btn btn-default">刷新</button> 12 <button id="save" type="button" class="btn btn-default">保存</button> 13 </div> 14 <table class="table table-bordered table-striped"> 15 <thead id="tbHead"> 16 <tr> 17 18 </tr> 19 </thead> 20 <tbody id="tbBody"> 21 22 </tbody> 23 </table> 24 </div> 25 </body> 26 </html>
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 (function (jq) { 2 var CREATE_SEARCH_CONDITION = true; 3 var GLOBAL_DICT = {}; 4 /* 5 { 6 'device_type_choices': ( 7 (1, '服务器'), 8 (2, '交换机'), 9 (3, '防火墙'), 10 ) 11 'device_status_choices': ( 12 (1, '上架'), 13 (2, '在线'), 14 (3, '离线'), 15 (4, '下架'), 16 ) 17 } 18 */ 19 20 // 为字符串创建format方法,用于字符串格式化 21 String.prototype.format = function (args) { 22 return this.replace(/{(w+)}/g, function (s, i) { 23 return args[i]; 24 }); 25 }; 26 27 function getSearchCondition(){ 28 var condition = {}; 29 $('.search-list').find('input[type="text"],select').each(function(){ 30 31 /* 获取所有搜索条件 */ 32 var name = $(this).attr('name'); 33 var value = $(this).val(); 34 if(condition[name]){ 35 condition[name].push(value); 36 }else{ 37 condition[name] = [value]; 38 } 39 40 }); 41 return condition; 42 } 43 44 function initial(url) { 45 // 执行一个函数, 获取当前搜索条件 46 var searchCondition = getSearchCondition(); 47 console.log(searchCondition); 48 $.ajax({ 49 url: url, 50 type: 'GET', // 获取数据 51 data: {condition: JSON.stringify(searchCondition)}, 52 dataType: 'JSON', 53 success: function (arg) { 54 $.each(arg.global_dict,function(k,v){ 55 GLOBAL_DICT[k] = v 56 }); 57 initTableHeader(arg.table_config); 58 initTableBody(arg.server_list, arg.table_config); 59 initSearch(arg.search_config); 60 } 61 }) 62 } 63 64 /* 65 初始化搜索条件 66 */ 67 function initSearch(searchConfig){ 68 if(searchConfig && CREATE_SEARCH_CONDITION){ 69 70 CREATE_SEARCH_CONDITION = false; 71 // 找打searchArea ul, 72 $.each(searchConfig,function(k,v){ 73 var li = document.createElement('li'); 74 $(li).attr('search_type', v.search_type); 75 $(li).attr('name', v.name); 76 if(v.search_type == 'select'){ 77 $(li).attr('global_name', v.global_name); 78 } 79 80 var a = document.createElement('a'); 81 a.innerHTML = v.text; 82 $(li).append(a); 83 $('.searchArea ul').append(li); 84 }); 85 86 // 初始化默认搜索条件 87 // searchConfig[0],进行初始化 88 // 初始化默认选中值 89 $('.search-item .searchDefault').text(searchConfig[0].text); 90 if(searchConfig[0].search_type == 'select'){ 91 var sel = document.createElement('select'); 92 $(sel).attr('class','form-control'); 93 $.each(GLOBAL_DICT[searchConfig[0].global_name],function(k,v){ 94 var op = document.createElement('option'); 95 $(op).text(v[1]); 96 $(op).val(v[0]); 97 $(sel).append(op) 98 }); 99 $('.input-group').append(sel); 100 }else{ 101 // <input type="text" class="form-control" aria-label="..."> 102 var inp = document.createElement('input'); 103 $(inp).attr('name',searchConfig[0].name); 104 $(inp).attr('type','text'); 105 $(inp).attr('class','form-control'); 106 $('.input-group').append(inp); 107 } 108 109 110 } 111 } 112 113 function initTableHeader(tableConfig) { 114 /* 115 [ 116 {'q':'id','title':'ID'}, 117 {'q':'hostname','title':'主机名'}, 118 ] 119 */ 120 $('#tbHead').empty(); 121 var tr = document.createElement('tr'); 122 $.each(tableConfig, function (k, v) { 123 if (v.display) { 124 var tag = document.createElement('th'); 125 tag.innerHTML = v.title; 126 $(tr).append(tag); 127 } 128 }); 129 $('#tbHead').append(tr); 130 } 131 132 function initTableBody(serverList, tableConfig) { 133 /* 134 serverList = [ 135 {'id': 1, 'hostname':c2.com, create_at: xxxx-xx-xx-}, 136 {'id': 1, 'hostname':c2.com, create_at: xxxx-xx-xx-}, 137 {'id': 1, 'hostname':c2.com, create_at: xxxx-xx-xx-}, 138 {'id': 1, 'hostname':c2.com, create_at: xxxx-xx-xx-}, 139 ] 140 */ 141 $('#tbBody').empty(); 142 $.each(serverList, function (k, row) { 143 // row: {'id': 1, 'hostname':c2.com, create_at: xxxx-xx-xx-} 144 /* 145 <tr> 146 <td>id</td> 147 <td>hostn</td> 148 <td>create</td> 149 </tr> 150 */ 151 var tr = document.createElement('tr'); 152 tr.setAttribute('nid',row.id); 153 $.each(tableConfig, function (kk, rrow) { 154 // kk: 1 rrow:{'q':'id','title':'ID'}, // rrow.q = "id" 155 // kk: . rrow:{'q':'hostname','title':'主机名'},// rrow.q = "hostname" 156 // kk: . rrow:{'q':'create_at','title':'创建时间'}, // rrow.q = "create_at" 157 if (rrow.display) { 158 var td = document.createElement('td'); 159 160 /* 在td标签中添加内容 */ 161 var newKwargs = {}; // {'n1':'1','n2':'123'} 162 $.each(rrow.text.kwargs, function (kkk, vvv) { 163 var av = vvv; 164 if(vvv.substring(0,2) == '@@'){ 165 var global_dict_key = vvv.substring(2,vvv.length); 166 var nid = row[rrow.q]; 167 $.each(GLOBAL_DICT[global_dict_key],function(gk,gv){ 168 if(gv[0] == nid){ 169 av = gv[1]; 170 } 171 }) 172 } 173 else if (vvv[0] == '@') { 174 av = row[vvv.substring(1, vvv.length)]; 175 } 176 newKwargs[kkk] = av; 177 }); 178 var newText = rrow.text.tpl.format(newKwargs); 179 td.innerHTML = newText; 180 181 /* 在td标签中添加属性 */ 182 $.each(rrow.attrs,function(atkey,atval){ 183 // 如果@ 184 if (atval[0] == '@') { 185 td.setAttribute(atkey, row[atval.substring(1, atval.length)]); 186 }else{ 187 td.setAttribute(atkey,atval); 188 } 189 }); 190 191 $(tr).append(td); 192 } 193 }); 194 $('#tbBody').append(tr); 195 196 }) 197 } 198 199 function trIntoEdit($tr){ 200 $tr.find('td[edit-enable="true"]').each(function(){ 201 // $(this) 每一个td 202 var editType = $(this).attr('edit-type'); 203 if(editType == 'select'){ 204 // 生成下拉框:找到数据源 205 var deviceTypeChoices = GLOBAL_DICT[$(this).attr('global_key')]; 206 207 // 生成select标签 208 var selectTag = document.createElement('select'); 209 var origin = $(this).attr('origin'); 210 211 $.each(deviceTypeChoices,function(k,v){ 212 var option = document.createElement('option'); 213 $(option).text(v[1]); 214 $(option).val(v[0]); 215 if(v[0] == origin){ 216 // 默认选中原来的值 217 $(option).prop('selected',true); 218 } 219 $(selectTag).append(option); 220 }); 221 222 $(this).html(selectTag); 223 // 显示默认值 224 }else{ 225 // 获取原来td中的文本内容 226 var v1 = $(this).text(); 227 // 创建input标签,并且内部设置值 228 var inp = document.createElement('input'); 229 $(inp).val(v1); 230 // 添加到td中 231 $(this).html(inp); 232 } 233 234 235 }); 236 } 237 function trOutEdit($tr){ 238 $tr.find('td[edit-enable="true"]').each(function(){ 239 // $(this) 每一个td 240 var editType = $(this).attr('edit-type'); 241 if(editType == 'select'){ 242 var option = $(this).find('select')[0].selectedOptions; 243 $(this).attr('new-origin',$(option).val()); 244 $(this).html($(option).text()); 245 }else{ 246 var inputVal = $(this).find('input').val(); 247 $(this).html(inputVal); 248 } 249 250 }); 251 } 252 jq.extend({ 253 xx: function (url) { 254 initial(url); 255 256 // 所有checkbox绑定事件 257 $('#tbBody').on('click',':checkbox',function(){ 258 // $(this) // checkbox标签 259 // 1. 检测是否已经被选中 260 if($('#inOutEditMode').hasClass('btn-warning')){ 261 var $tr = $(this).parent().parent(); 262 if($(this).prop('checked')){ 263 // 进入编辑模式 264 trIntoEdit($tr); 265 }else{ 266 // 退出编辑模式 267 trOutEdit($tr); 268 } 269 } 270 }); 271 272 // 所有按钮绑定事件 273 $('#checkAll').click(function(){ 274 if($('#inOutEditMode').hasClass('btn-warning')){ 275 $('#tbBody').find(':checkbox').each(function(){ 276 if(!$(this).prop('checked')){ 277 var $tr = $(this).parent().parent(); 278 trIntoEdit($tr); 279 $(this).prop('checked',true); 280 } 281 }) 282 }else{ 283 $('#tbBody').find(':checkbox').prop('checked',true); 284 } 285 286 }); 287 288 $('#checkReverse').click(function(){ 289 if($('#inOutEditMode').hasClass('btn-warning')){ 290 $('#tbBody').find(':checkbox').each(function(){ 291 var $tr = $(this).parent().parent(); 292 if($(this).prop('checked')){ 293 trOutEdit($tr); 294 $(this).prop('checked',false); 295 }else{ 296 trIntoEdit($tr); 297 $(this).prop('checked',true); 298 } 299 }) 300 }else{ 301 $('#tbBody').find(':checkbox').each(function(){ 302 var $tr = $(this).parent().parent(); 303 if($(this).prop('checked')){ 304 $(this).prop('checked',false); 305 }else{ 306 $(this).prop('checked',true); 307 } 308 }) 309 } 310 }); 311 312 $('#checkCancel').click(function(){ 313 if($('#inOutEditMode').hasClass('btn-warning')){ 314 $('#tbBody').find(':checkbox').each(function(){ 315 if($(this).prop('checked')){ 316 var $tr = $(this).parent().parent(); 317 trOutEdit($tr); 318 $(this).prop('checked',false); 319 } 320 }) 321 }else{ 322 $('#tbBody').find(':checkbox').prop('checked',false); 323 } 324 }); 325 326 $('#inOutEditMode').click(function(){ 327 if($(this).hasClass('btn-warning')){ 328 // 退出编辑模式 329 $(this).removeClass('btn-warning'); 330 $(this).text('进入编辑模式'); 331 $('#tbBody').find(':checkbox').each(function(){ 332 if($(this).prop('checked')){ 333 var $tr = $(this).parent().parent(); 334 trOutEdit($tr); 335 } 336 }) 337 }else{ 338 // 进入编辑模式 339 $(this).addClass('btn-warning'); 340 $(this).text('退出编辑模式'); 341 342 $('#tbBody').find(':checkbox').each(function(){ 343 if($(this).prop('checked')){ 344 var $tr = $(this).parent().parent(); 345 trIntoEdit($tr); 346 } 347 }) 348 } 349 }); 350 351 $('#multiDel').click(function(){ 352 // $('#tbBody').find(':checkbox') 353 var idList = []; 354 $('#tbBody').find(':checked').each(function(){ 355 var v = $(this).val(); 356 idList.push(v) 357 }); 358 359 $.ajax({ 360 url: url, 361 type: 'delete', 362 data: JSON.stringify(idList), 363 success:function(arg){ 364 console.log(arg); 365 } 366 }) 367 368 }); 369 370 $('#refresh').click(function(){ 371 initial(url) 372 }); 373 374 $('#save').click(function(){ 375 if($('#inOutEditMode').hasClass('btn-warning')){ 376 377 $('#tbBody').find(':checkbox').each(function(){ 378 if($(this).prop('checked')){ 379 var $tr = $(this).parent().parent(); 380 trOutEdit($tr); 381 } 382 }) 383 384 } 385 386 var all_list = []; 387 // 获取用户修改过的数据 388 $('#tbBody').children().each(function(){ 389 // $(this) = tr 390 var $tr= $(this); 391 var nid= $tr.attr('nid'); 392 var row_dict = {}; 393 var flag = false; 394 $tr.children().each(function(){ 395 if($(this).attr('edit-enable')) { 396 if($(this).attr('edit-type') == 'select'){ 397 var newData = $(this).attr('new-origin'); 398 var oldData = $(this).attr('origin'); 399 if(newData){ 400 if (newData != oldData) { 401 var name = $(this).attr('name'); 402 row_dict[name] = newData; 403 flag = true; 404 } 405 } 406 407 }else{ 408 var newData = $(this).text(); 409 var oldData = $(this).attr('origin'); 410 if (newData != oldData) { 411 var name = $(this).attr('name'); 412 row_dict[name] = newData; 413 flag = true; 414 } 415 } 416 417 } 418 419 }); 420 if(flag){ 421 row_dict['id'] = nid; 422 } 423 all_list.push(row_dict) 424 425 426 }); 427 428 // 通过Ajax提交后台 429 $.ajax({ 430 url: url, 431 type: 'PUT', 432 data: JSON.stringify(all_list), 433 success:function(arg){ 434 console.log(arg); 435 } 436 }) 437 }); 438 439 $('.search-list').on('click','li',function(){ 440 // 点击li执行函数 441 var wenben = $(this).text(); 442 var searchType = $(this).attr('search_type'); 443 var name = $(this).attr('name'); 444 var globalName = $(this).attr('global_name'); 445 446 // 把显示替换 447 $(this).parent().prev().find('.searchDefault').text(wenben); 448 449 450 if(searchType == 'select'){ 451 /* 452 [ 453 [1,‘文本’], 454 [1,‘文本’], 455 [1,‘文本’], 456 ] 457 */ 458 var sel = document.createElement('select'); 459 $(sel).attr('class','form-control'); 460 $(sel).attr('name',name); 461 $.each(GLOBAL_DICT[globalName],function(k,v){ 462 var op = document.createElement('option'); 463 $(op).text(v[1]); 464 $(op).val(v[0]); 465 $(sel).append(op); 466 }); 467 $(this).parent().parent().next().remove(); 468 $(this).parent().parent().after(sel); 469 }else{ 470 var inp = document.createElement('input'); 471 $(inp).attr('class','form-control'); 472 $(inp).attr('name',name); 473 $(inp).attr('type','text'); 474 $(this).parent().parent().next().remove(); 475 $(this).parent().parent().after(inp); 476 } 477 478 }); 479 480 $('.search-list').on('click','.add-search-condition',function(){ 481 // 拷贝的新一搜索项 482 var newSearchItem = $(this).parent().parent().clone(); 483 $(newSearchItem).find('.add-search-condition span').removeClass('glyphicon-plus').addClass('glyphicon-minus'); 484 $(newSearchItem).find('.add-search-condition').addClass('del-search-condition').removeClass('add-search-condition'); 485 $('.search-list').append(newSearchItem); 486 487 }); 488 489 $('.search-list').on('click','.del-search-condition',function(){ 490 $(this).parent().parent().remove(); 491 }); 492 493 $('#doSearch').click(function(){ 494 initial(url); 495 }) 496 } 497 }) 498 })(jQuery);
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 table_config=[ 2 {'q': None, 'title': '选择', 3 'display': True, 4 'text': { 5 'tpl': "<input type='checkbox' value='{n1}' />", 6 'kwargs': {'n1': '@id'}}, 7 }, 8 {'q':'id','title':'ID', 9 'display':False, 10 'text':{ 11 'tpl':"{n1}", 12 'kwargs':{'n1':'@id'} 13 }}, 14 {'q': 'hostname', 'title': '主机名', 15 'display': True, 16 'text': { 17 'tpl': "{n1}", 18 'kwargs': {'n1': '@hostname'} 19 }, 20 'attrs':{'origin':'@hostname','name':'hostname','edit-enable':'true'}}, 21 {'q': 'create_at', 'title': '创建时间', 22 'display': True, 23 'text': { 24 'tpl': "{n1}", 25 'kwargs': {'n1': '@create_at'} 26 }}, 27 {'q':None, 'title': '操作', 28 'display': True, 29 'text': { 30 'tpl': "<a href='/del?nid={nid}'>删除</a>", 31 'kwargs': {'nid': '@id'} 32 }}, 33 ] 34 table_config1=[ 35 {'q': None, 'title': '选择', 36 'display':True, 37 'text': { 38 'tpl': "<input type='checkbox' value='{n1}' />", 39 'kwargs': {'n1': '@id'}}, 40 'attrs':{'nid':'@id'} 41 }, 42 {'q':'id','title':'ID', 43 'display':False, 44 'text':{ 45 'tpl':"{n1}", 46 'kwargs':{'n1':'@id'} 47 } 48 }, 49 {'q': 'business_unit_id__name', 'title': '业务线', 50 'display': True, 51 'text': { 52 'tpl': "{n1}", 53 'kwargs': {'n1': '@business_unit_id__name'} 54 }, 55 'attrs':{'k1':'v1','k2':'@id'} 56 }, 57 {'q': 'device_type_id', 'title': '资产类型', 58 'display': True, 59 'text': { 60 'tpl': "{n1}", 61 'kwargs': {'n1': '@@device_type_choices'} 62 }, 63 'attrs': {'k1': 'v1', 'nid': '@id','origin':'@device_type_id','name':'device_type_id', 64 'edit-enable':'true','edit-type':'select','global_key':'device_type_choices'}}, 65 {'q': 'device_status_id', 'title': '状态', 66 'display': True, 67 'text': { 68 'tpl': "{n1}", 69 'kwargs': {'n1': '@@device_status_choices'} 70 }, 71 'attrs': {'k1': 'v1', 'nid': '@id','origin':'@device_status_id','name':'device_status_id', 72 'edit-enable':'true','edit-type':'select','global_key':'device_status_choices'}}, 73 {'q':None, 'title': '操作', 74 'display': True, 75 'text': { 76 'tpl': "<a href='/del?nid={nid}'>删除</a>", 77 'kwargs': {'nid': '@id'} 78 },'attrs':{'k1':'v1','k2':'@id'} 79 }, 80 ] 81 search_config = [ 82 {'name': 'cabinet_num', 'text': '机柜号', 'search_type': 'input'}, 83 {'name': 'device_type_id', 'text': '资产类型', 'search_type': 'select', 'global_name': 'device_type_choices'}, 84 {'name': 'device_status_id', 'text': '资产状态', 'search_type': 'select', 'global_name': 'device_status_choices'}, 85 ]
参考:http://www.cnblogs.com/nulige/p/6703160.html