mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-25 05:22:36 +00:00
Compare commits
268 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a025930957 | ||
|
|
990c78e7cc | ||
|
|
0ef12906d3 | ||
|
|
61a37731ec | ||
|
|
d3217b6a67 | ||
|
|
04266cc20b | ||
|
|
4f36cf7dd1 | ||
|
|
490041587b | ||
|
|
3a3da94468 | ||
|
|
b7ad6cfe62 | ||
|
|
4463e7545d | ||
|
|
d0eafc8b8e | ||
|
|
8b98c20d68 | ||
|
|
caa5060ecd | ||
|
|
aabcf7f31c | ||
|
|
40d48cdfe4 | ||
|
|
8196537878 | ||
|
|
33a00f043b | ||
|
|
f235e20153 | ||
|
|
cf2455c084 | ||
|
|
fc1068a9dc | ||
|
|
35a0ca1875 | ||
|
|
56519354b6 | ||
|
|
78e4e13fb9 | ||
|
|
699b8d9980 | ||
|
|
ba9581801c | ||
|
|
0a5fdf4ea1 | ||
|
|
3849fa2b15 | ||
|
|
0952cbc7c6 | ||
|
|
bb06c39dd4 | ||
|
|
d60dc31443 | ||
|
|
76b3cd8edd | ||
|
|
638ba31694 | ||
|
|
c31b169cae | ||
|
|
fc167526ae | ||
|
|
55eff5eab9 | ||
|
|
f5a7f4e086 | ||
|
|
f7b0932cdd | ||
|
|
ba89ce8fb9 | ||
|
|
9d62deeabe | ||
|
|
459b41f327 | ||
|
|
3062e3f64a | ||
|
|
c1362ca4e2 | ||
|
|
9d24912ad9 | ||
|
|
db290609a8 | ||
|
|
4bc5eced6c | ||
|
|
b82a66c83d | ||
|
|
bf7079df9e | ||
|
|
f137c5740e | ||
|
|
ee47905966 | ||
|
|
f6cd193f9e | ||
|
|
a31775dd23 | ||
|
|
30ba1e5886 | ||
|
|
f97bfa7bf1 | ||
|
|
ace028fa7f | ||
|
|
69f6401e87 | ||
|
|
bd4d974df1 | ||
|
|
6e7446f530 | ||
|
|
afe9471aa2 | ||
|
|
4d56b84861 | ||
|
|
8fede58c64 | ||
|
|
370904212f | ||
|
|
ae03a5aeb7 | ||
|
|
24a38841dd | ||
|
|
bbc6156bd7 | ||
|
|
f387df41d7 | ||
|
|
ceb8b2f5b3 | ||
|
|
877781a6ca | ||
|
|
612d5efd1b | ||
|
|
7ea03801d0 | ||
|
|
ce8f4b4a48 | ||
|
|
15179d2450 | ||
|
|
9aae106970 | ||
|
|
c82044f6bc | ||
|
|
e4e6f59589 | ||
|
|
c8aa9d006f | ||
|
|
311538dcf8 | ||
|
|
324cf2469f | ||
|
|
01745ead1f | ||
|
|
4e705a52eb | ||
|
|
9bb58afee1 | ||
|
|
b45b33380c | ||
|
|
c86a036ac6 | ||
|
|
8694511d86 | ||
|
|
58c4a46f6e | ||
|
|
dfd26d88d4 | ||
|
|
dcf6959cff | ||
|
|
924affd978 | ||
|
|
ad6d233c11 | ||
|
|
d84ab1d215 | ||
|
|
a5fc04e0ce | ||
|
|
7f71513085 | ||
|
|
6004ef3f0d | ||
|
|
e76392a169 | ||
|
|
53f0b2e9b0 | ||
|
|
de79e36251 | ||
|
|
c84e984eae | ||
|
|
6d65c967b1 | ||
|
|
8199ea84f4 | ||
|
|
ce1b0da09d | ||
|
|
cd6bb848e9 | ||
|
|
34040fcd59 | ||
|
|
1969fb79fe | ||
|
|
904f64604b | ||
|
|
9b3509208d | ||
|
|
6700dc969f | ||
|
|
21714cc411 | ||
|
|
1a247d60e7 | ||
|
|
adf8b1f7aa | ||
|
|
69f640daa4 | ||
|
|
a43314f5be | ||
|
|
599e8a7e37 | ||
|
|
0e00451e1f | ||
|
|
866e5d2011 | ||
|
|
232674b1c1 | ||
|
|
ddf60d2512 | ||
|
|
3e6e0153cf | ||
|
|
6b984aac53 | ||
|
|
6d3ee8116e | ||
|
|
904a0f67dd | ||
|
|
81e1ce2688 | ||
|
|
7c422d2ed6 | ||
|
|
2a5129c481 | ||
|
|
28cdfca14b | ||
|
|
202aba048b | ||
|
|
3c2a4703bc | ||
|
|
4904aac2df | ||
|
|
303f88d6ee | ||
|
|
cccc74279d | ||
|
|
16db2abca5 | ||
|
|
859f2d9795 | ||
|
|
4fd9957bec | ||
|
|
0ac6e6ba2c | ||
|
|
785cc04126 | ||
|
|
f269eae774 | ||
|
|
6f19fcb702 | ||
|
|
b7b6218306 | ||
|
|
5cd809b48a | ||
|
|
5a1b894138 | ||
|
|
4d402617b6 | ||
|
|
666ef366e7 | ||
|
|
28d029a553 | ||
|
|
00763e986a | ||
|
|
f9a7cca478 | ||
|
|
d09b34e232 | ||
|
|
2737675c36 | ||
|
|
7591f40b2c | ||
|
|
c4af6fa72d | ||
|
|
19be7ac580 | ||
|
|
49404f763d | ||
|
|
87f2a67789 | ||
|
|
041edb6177 | ||
|
|
df2fad76c7 | ||
|
|
94020a8fbb | ||
|
|
cb1e19d28f | ||
|
|
0980dffb47 | ||
|
|
4051225ecb | ||
|
|
507518da04 | ||
|
|
d3bdbc0b81 | ||
|
|
3b56027edc | ||
|
|
8285610097 | ||
|
|
6acac9cb3d | ||
|
|
9d78f26807 | ||
|
|
9d53ba22e1 | ||
|
|
0d0cf04543 | ||
|
|
56a47b6ba3 | ||
|
|
1dbcf4e3ab | ||
|
|
95fcd60f64 | ||
|
|
178055eb57 | ||
|
|
01a101a710 | ||
|
|
eee6dd1436 | ||
|
|
27693c6288 | ||
|
|
55b55f6162 | ||
|
|
384cdfbc19 | ||
|
|
c8d007f9d7 | ||
|
|
d8069f47f2 | ||
|
|
d664018bd7 | ||
|
|
0f32e78891 | ||
|
|
6e061d2da5 | ||
|
|
b2717133ee | ||
|
|
2a0e68c58f | ||
|
|
176052e8e9 | ||
|
|
d026b31c9f | ||
|
|
50c1b3ed4a | ||
|
|
131e588d82 | ||
|
|
4bf0dfcf7b | ||
|
|
49a166552e | ||
|
|
0e1d3f93ff | ||
|
|
35403086ab | ||
|
|
2fde6cfe24 | ||
|
|
ed1dc7a984 | ||
|
|
9ab3f0441f | ||
|
|
d6567f0e57 | ||
|
|
84bd465b30 | ||
|
|
d0af8eba32 | ||
|
|
b2a8415f77 | ||
|
|
545d4fa691 | ||
|
|
560df5027a | ||
|
|
e3db7462f7 | ||
|
|
b55d137e7f | ||
|
|
7c10f8743f | ||
|
|
c5d1ed126e | ||
|
|
6b02cdfc37 | ||
|
|
340c615efe | ||
|
|
8e51f97dc7 | ||
|
|
76a08c9039 | ||
|
|
b9b8c35a81 | ||
|
|
e8fba2ec44 | ||
|
|
61df6f55b9 | ||
|
|
ef02b1f83a | ||
|
|
0798e3c466 | ||
|
|
a990098744 | ||
|
|
dab692c0eb | ||
|
|
8e93bfecb0 | ||
|
|
e5953e1932 | ||
|
|
67b21f0489 | ||
|
|
e15c9e6588 | ||
|
|
94d6525548 | ||
|
|
a14d6b298d | ||
|
|
e7725e6910 | ||
|
|
23bf2b0f8e | ||
|
|
ac9178cb93 | ||
|
|
7ff39259af | ||
|
|
b2aef87fdd | ||
|
|
e1f1bed9c9 | ||
|
|
08945f0a19 | ||
|
|
a1b80f5f0b | ||
|
|
7773c30240 | ||
|
|
231c907c64 | ||
|
|
db8882a2b9 | ||
|
|
e5285f312b | ||
|
|
332be54b46 | ||
|
|
fe7c3c29ad | ||
|
|
0e9ebed19d | ||
|
|
4a3327bc4b | ||
|
|
5d47bebb6b | ||
|
|
2ece3545ed | ||
|
|
a9a1bae805 | ||
|
|
f35c02b346 | ||
|
|
32df515f4b | ||
|
|
71750970b2 | ||
|
|
a85099ee60 | ||
|
|
a5b9b4e1d2 | ||
|
|
b3079a4a9b | ||
|
|
9a22874305 | ||
|
|
179018bf67 | ||
|
|
583214e91a | ||
|
|
fb44ef0986 | ||
|
|
90b77fdb08 | ||
|
|
a609f17078 | ||
|
|
068a280350 | ||
|
|
1293d72189 | ||
|
|
164c5ebabd | ||
|
|
b56d73ba9e | ||
|
|
dbdcdb722d | ||
|
|
517a27ea33 | ||
|
|
ab6c88823d | ||
|
|
1ff9f0eaa6 | ||
|
|
b95f8a7d6b | ||
|
|
29ff0efdc1 | ||
|
|
985bd6fc82 | ||
|
|
374039d287 | ||
|
|
c7ac93fcc1 | ||
|
|
ac7e3e7f97 | ||
|
|
4e0b25ae0f | ||
|
|
559f4d2f5c | ||
|
|
f40f6bc61e | ||
|
|
e9247dd578 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,6 +17,7 @@ dump.rdb
|
||||
.idea/
|
||||
db.sqlite3
|
||||
config.py
|
||||
config.yml
|
||||
*.log
|
||||
host_rsa_key
|
||||
*.bat
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@@ -6,13 +6,14 @@ RUN useradd jumpserver
|
||||
|
||||
COPY ./requirements /tmp/requirements
|
||||
|
||||
RUN yum -y install epel-release && cd /tmp/requirements && \
|
||||
yum -y install $(cat rpm_requirements.txt)
|
||||
|
||||
RUN cd /tmp/requirements && pip install -r requirements.txt
|
||||
RUN yum -y install epel-release && rpm -ivh https://repo.mysql.com/mysql57-community-release-el6.rpm
|
||||
RUN cd /tmp/requirements && yum -y install $(cat rpm_requirements.txt)
|
||||
RUN cd /tmp/requirements && pip install --upgrade pip setuptools && \
|
||||
pip install -i https://mirrors.aliyun.com/pypi/simple/ -r requirements.txt || pip install -r requirements.txt
|
||||
RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
|
||||
|
||||
COPY . /opt/jumpserver
|
||||
COPY config_docker.py /opt/jumpserver/config.py
|
||||
RUN echo > config.yml
|
||||
VOLUME /opt/jumpserver/data
|
||||
VOLUME /opt/jumpserver/logs
|
||||
|
||||
|
||||
189
README.md
189
README.md
@@ -1,4 +1,4 @@
|
||||
## Jumpserver
|
||||
## Jumpserver 多云环境下更好用的堡垒机
|
||||
|
||||
[](https://www.python.org/)
|
||||
[](https://www.djangoproject.com/)
|
||||
@@ -8,45 +8,200 @@
|
||||
|
||||
----
|
||||
|
||||
Jumpserver是全球首款完全开源的堡垒机,使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计系统。
|
||||
Jumpserver 是全球首款完全开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 的专业运维审计系统。
|
||||
|
||||
Jumpserver使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。
|
||||
Jumpserver 使用 Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。
|
||||
|
||||
Jumpserver采纳分布式架构,支持多机房跨区域部署,中心节点提供 API,各机房部署登录节点,可横向扩展、无并发限制。
|
||||
Jumpserver 采纳分布式架构,支持多机房跨区域部署,中心节点提供 API,各机房部署登录节点,可横向扩展、无并发限制。
|
||||
|
||||
改变世界,从一点点开始。
|
||||
|
||||
----
|
||||
- [English Version](https://github.com/jumpserver/jumpserver/blob/master/README_EN.md)
|
||||
|
||||
|
||||
### 功能
|
||||
----
|
||||
|
||||
<table class="subscription-level-table">
|
||||
<tr class="subscription-level-tr-border">
|
||||
<th style="background-color: #1ab394;color: #ffffff;" colspan="3">Jumpserver提供的堡垒机必备功能</th>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-first-td-background-style" rowspan="4">身份验证 Authentication</td>
|
||||
<td class="features-second-td-background-style" rowspan="3" >登录认证
|
||||
</td>
|
||||
<td class="features-third-td-background-style">资源统一登录和认证
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-third-td-background-style">LDAP认证
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-third-td-background-style">支持OpenID,实现单点登录
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-second-td-background-style">多因子认证
|
||||
</td>
|
||||
<td class="features-third-td-background-style">MFA(Google Authenticator)
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-first-td-background-style" rowspan="9">账号管理 Account</td>
|
||||
<td class="features-second-td-background-style" rowspan="2">集中账号管理
|
||||
</td>
|
||||
<td class="features-third-td-background-style">管理用户管理
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-third-td-background-style">系统用户管理
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-second-td-background-style" rowspan="4">统一密码管理
|
||||
</td>
|
||||
<td class="features-third-td-background-style">资产密码托管
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-third-td-background-style">自动生成密码
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-third-td-background-style">密码自动推送
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-third-td-background-style">密码过期设置
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-outline-td-background-style" rowspan="2">批量密码变更(X-PACK)
|
||||
</td>
|
||||
<td class="features-outline-td-background-style">定期批量修改密码
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-outline-td-background-style">生成随机密码
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-outline-td-background-style">多云环境的资产纳管(X-PACK)
|
||||
</td>
|
||||
<td class="features-outline-td-background-style">对私有云、公有云资产统一纳管
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-first-td-background-style" rowspan="8">授权控制 Authorization</td>
|
||||
<td class="features-second-td-background-style" rowspan="3">资产授权管理
|
||||
</td>
|
||||
<td class="features-third-td-background-style">资产树
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-third-td-background-style">资产或资产组灵活授权
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-third-td-background-style">节点内资产自动继承授权
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-outline-td-background-style">组织管理(X-PACK)
|
||||
</td>
|
||||
<td class="features-outline-td-background-style">实现多租户管理,权限隔离
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-second-td-background-style">多维度授权
|
||||
</td>
|
||||
<td class="features-third-td-background-style">可对用户、用户组或系统角色授权
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-second-td-background-style">指令限制
|
||||
</td>
|
||||
<td class="features-third-td-background-style">限制特权指令使用,支持黑白名单
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-second-td-background-style">统一文件传输
|
||||
</td>
|
||||
<td class="features-third-td-background-style">SFTP 文件上传/下载
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-second-td-background-style">文件管理
|
||||
</td>
|
||||
<td class="features-third-td-background-style">Web SFTP 文件管理
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-first-td-background-style" rowspan="6">安全审计 Audit</td>
|
||||
<td class="features-second-td-background-style" rowspan="2">会话管理
|
||||
</td>
|
||||
<td class="features-third-td-background-style">在线会话管理
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-third-td-background-style">历史会话管理
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-second-td-background-style" rowspan="2">录像管理
|
||||
</td>
|
||||
<td class="features-third-td-background-style">Linux 录像支持
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-third-td-background-style">Windows 录像支持
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-second-td-background-style">指令审计
|
||||
</td>
|
||||
<td class="features-third-td-background-style">指令记录
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="subscription-level-tr-border">
|
||||
<td class="features-second-td-background-style">文件传输审计
|
||||
</td>
|
||||
<td class="features-third-td-background-style">上传/下载记录审计
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||

|
||||
|
||||
### 开始使用
|
||||
----
|
||||
|
||||
快速开始文档 [Docker安装](http://docs.jumpserver.org/zh/docs/dockerinstall.html)
|
||||
- 快速开始文档 [Docker 安装](http://docs.jumpserver.org/zh/docs/dockerinstall.html)
|
||||
|
||||
一步一步安装文档 [详细部署](http://docs.jumpserver.org/zh/docs/step_by_step.html)
|
||||
- Step by Step 安装文档 [详细部署](http://docs.jumpserver.org/zh/docs/step_by_step.html)
|
||||
|
||||
也可以查看我们完整文档包括了使用和开发 [文档](http://docs.jumpserver.org)
|
||||
- 也可以查看我们完整文档 [文档](http://docs.jumpserver.org)
|
||||
|
||||
### Demo 和 截图
|
||||
### Demo、视频 和 截图
|
||||
----
|
||||
|
||||
我们提供了DEMO和截图可以让你快速了解Jumpserver
|
||||
我们提供了 Demo 、演示视频和截图可以让你快速了解 Jumpserver
|
||||
|
||||
[DEMO](http://demo.jumpserver.org)
|
||||
[截图](http://docs.jumpserver.org/zh/docs/snapshot.html)
|
||||
- [Demo](https://demo.jumpserver.org/auth/login/?next=/)
|
||||
- [视频](https://fit2cloud2-offline-installer.oss-cn-beijing.aliyuncs.com/tools/Jumpserver%20%E4%BB%8B%E7%BB%8Dv1.4.mp4)
|
||||
- [截图](http://docs.jumpserver.org/zh/docs/snapshot.html)
|
||||
|
||||
### SDK
|
||||
----
|
||||
|
||||
我们还编写了一些SDK,供你其它系统快速和Jumpserver APi交互,
|
||||
我们还编写了一些SDK,供你的其它系统快速和 Jumpserver API 交互
|
||||
|
||||
- [python](https://github.com/jumpserver/jumpserver-python-sdk) Jumpserver其它组件使用这个SDK完成交互
|
||||
- [java](https://github.com/KaiJunYan/jumpserver-java-sdk.git) 恺珺同学提供的Java版本的SDK
|
||||
- [Python](https://github.com/jumpserver/jumpserver-python-sdk) Jumpserver其它组件使用这个SDK完成交互
|
||||
- [Java](https://github.com/KaiJunYan/jumpserver-java-sdk.git) 恺珺同学提供的Java版本的SDK
|
||||
|
||||
|
||||
### License & Copyright
|
||||
Copyright (c) 2014-2018 Beijing Duizhan Tech, Inc., All rights reserved.
|
||||
Copyright (c) 2014-2019 飞致云 FIT2CLOUD, All rights reserved.
|
||||
|
||||
Licensed under The GNU General Public License version 2 (GPLv2) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
||||
|
||||
|
||||
58
README_EN.md
Normal file
58
README_EN.md
Normal file
@@ -0,0 +1,58 @@
|
||||
## Jumpserver
|
||||
|
||||
[](https://www.python.org/)
|
||||
[](https://www.djangoproject.com/)
|
||||
[](https://www.ansible.com/)
|
||||
[](http://www.paramiko.org/)
|
||||
|
||||
|
||||
----
|
||||
|
||||
- [中文版](https://github.com/jumpserver/jumpserver/blob/master/README_EN.md)
|
||||
|
||||
Jumpserver is the first fully open source bastion in the world, based on the GNU GPL v2.0 open source protocol. Jumpserver is a professional operation and maintenance audit system conforms to 4A specifications.
|
||||
|
||||
Jumpserver is developed using Python / Django, conforms to the Web 2.0 specification, and is equipped with the industry-leading Web Terminal solution which have beautiful interface and great user experience.
|
||||
|
||||
Jumpserver adopts a distributed architecture to support multi-branch deployment across multiple areas. The central node provides APIs, and login nodes are deployed in each branch. It can be scaled horizontally without concurrency restrictions.
|
||||
|
||||
Change the world, starting from little things.
|
||||
|
||||
----
|
||||
|
||||
### Features
|
||||
|
||||

|
||||
|
||||
### Start
|
||||
|
||||
Quick start [Docker Install](http://docs.jumpserver.org/zh/docs/dockerinstall.html)
|
||||
|
||||
Step by Step deployment. [Docs](http://docs.jumpserver.org/zh/docs/step_by_step.html)
|
||||
|
||||
Full documentation [Docs](http://docs.jumpserver.org)
|
||||
|
||||
### Demo、Video 和 Snapshot
|
||||
|
||||
We provide online demo, demo video and screenshots to get you started quickly.
|
||||
|
||||
[Demo](https://demo.jumpserver.org/auth/login/?next=/)
|
||||
[Video](https://fit2cloud2-offline-installer.oss-cn-beijing.aliyuncs.com/tools/Jumpserver%20%E4%BB%8B%E7%BB%8Dv1.4.mp4)
|
||||
[Snapshot](http://docs.jumpserver.org/zh/docs/snapshot.html)
|
||||
|
||||
### SDK
|
||||
|
||||
We provide the SDK for your other systems to quickly interact with the Jumpserver API.
|
||||
|
||||
- [Python](https://github.com/jumpserver/jumpserver-python-sdk) Jumpserver other components use this SDK to complete the interaction.
|
||||
- [Java](https://github.com/KaiJunYan/jumpserver-java-sdk.git) 恺珺同学提供的Java版本的SDK thanks to 恺珺 for provide Java SDK
|
||||
|
||||
|
||||
### License & Copyright
|
||||
Copyright (c) 2014-2019 Beijing Duizhan Tech, Inc., All rights reserved.
|
||||
|
||||
Licensed under The GNU General Public License version 2 (GPLv2) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
||||
|
||||
https://www.gnu.org/licenses/gpl-2.0.html
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
__version__ = "1.4.5"
|
||||
|
||||
@@ -5,3 +5,4 @@ from .system_user import *
|
||||
from .node import *
|
||||
from .domain import *
|
||||
from .cmd_filter import *
|
||||
from .asset_user import *
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import generics
|
||||
from rest_framework.response import Response
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
@@ -24,13 +25,14 @@ from common.utils import get_logger
|
||||
from ..hands import IsOrgAdmin
|
||||
from ..models import AdminUser, Asset
|
||||
from .. import serializers
|
||||
from ..tasks import test_admin_user_connectability_manual
|
||||
from ..tasks import test_admin_user_connectivity_manual
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'AdminUserViewSet', 'ReplaceNodesAdminUserApi',
|
||||
'AdminUserTestConnectiveApi', 'AdminUserAuthApi',
|
||||
'AdminUserAssetsListView',
|
||||
]
|
||||
|
||||
|
||||
@@ -81,12 +83,30 @@ class ReplaceNodesAdminUserApi(generics.UpdateAPIView):
|
||||
|
||||
class AdminUserTestConnectiveApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Test asset admin user connectivity
|
||||
Test asset admin user assets_connectivity
|
||||
"""
|
||||
queryset = AdminUser.objects.all()
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.TaskIDSerializer
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
admin_user = self.get_object()
|
||||
task = test_admin_user_connectability_manual.delay(admin_user)
|
||||
task = test_admin_user_connectivity_manual.delay(admin_user)
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
class AdminUserAssetsListView(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.AssetSimpleSerializer
|
||||
pagination_class = LimitOffsetPagination
|
||||
filter_fields = ("hostname", "ip")
|
||||
http_method_names = ['get']
|
||||
search_fields = filter_fields
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
return get_object_or_404(AdminUser, pk=pk)
|
||||
|
||||
def get_queryset(self):
|
||||
admin_user = self.get_object()
|
||||
return admin_user.get_related_assets()
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import uuid
|
||||
import random
|
||||
|
||||
from rest_framework import generics
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse_lazy
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Q
|
||||
|
||||
from common.mixins import IDInFilterMixin
|
||||
from common.utils import get_logger
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
|
||||
from ..const import CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX
|
||||
from ..models import Asset, AdminUser, Node
|
||||
from .. import serializers
|
||||
from ..tasks import update_asset_hardware_info_manual, \
|
||||
test_asset_connectability_manual
|
||||
test_asset_connectivity_manual
|
||||
from ..utils import LabelFilter
|
||||
|
||||
|
||||
@@ -25,7 +31,7 @@ logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'AssetViewSet', 'AssetListUpdateApi',
|
||||
'AssetRefreshHardwareApi', 'AssetAdminUserTestApi',
|
||||
'AssetGatewayApi'
|
||||
'AssetGatewayApi', 'AssetBulkUpdateSelectAPI'
|
||||
]
|
||||
|
||||
|
||||
@@ -41,40 +47,46 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet):
|
||||
pagination_class = LimitOffsetPagination
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
|
||||
def filter_node(self):
|
||||
def filter_node(self, queryset):
|
||||
node_id = self.request.query_params.get("node_id")
|
||||
if not node_id:
|
||||
return
|
||||
return queryset
|
||||
|
||||
node = get_object_or_404(Node, id=node_id)
|
||||
show_current_asset = self.request.query_params.get("show_current_asset") in ('1', 'true')
|
||||
|
||||
if node.is_root():
|
||||
if show_current_asset:
|
||||
self.queryset = self.queryset.filter(
|
||||
Q(nodes=node_id) | Q(nodes__isnull=True)
|
||||
)
|
||||
return
|
||||
if show_current_asset:
|
||||
self.queryset = self.queryset.filter(nodes=node)
|
||||
if node.is_root() and show_current_asset:
|
||||
queryset = queryset.filter(
|
||||
Q(nodes=node_id) | Q(nodes__isnull=True)
|
||||
)
|
||||
elif node.is_root() and not show_current_asset:
|
||||
pass
|
||||
elif not node.is_root() and show_current_asset:
|
||||
queryset = queryset.filter(nodes=node)
|
||||
else:
|
||||
self.queryset = self.queryset.filter(
|
||||
queryset = queryset.filter(
|
||||
nodes__key__regex='^{}(:[0-9]+)*$'.format(node.key),
|
||||
)
|
||||
return queryset
|
||||
|
||||
def filter_admin_user_id(self):
|
||||
def filter_admin_user_id(self, queryset):
|
||||
admin_user_id = self.request.query_params.get('admin_user_id')
|
||||
if admin_user_id:
|
||||
admin_user = get_object_or_404(AdminUser, id=admin_user_id)
|
||||
self.queryset = self.queryset.filter(admin_user=admin_user)
|
||||
if not admin_user_id:
|
||||
return queryset
|
||||
admin_user = get_object_or_404(AdminUser, id=admin_user_id)
|
||||
queryset = queryset.filter(admin_user=admin_user)
|
||||
return queryset
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
queryset = self.filter_node(queryset)
|
||||
queryset = self.filter_admin_user_id(queryset)
|
||||
return queryset
|
||||
|
||||
def get_queryset(self):
|
||||
self.queryset = super().get_queryset()\
|
||||
.prefetch_related('labels', 'nodes')\
|
||||
.select_related('admin_user')
|
||||
self.filter_admin_user_id()
|
||||
self.filter_node()
|
||||
return self.queryset.distinct()
|
||||
queryset = super().get_queryset().distinct()
|
||||
queryset = self.get_serializer_class().setup_eager_loading(queryset)
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView):
|
||||
@@ -86,6 +98,21 @@ class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
|
||||
class AssetBulkUpdateSelectAPI(APIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
assets_id = request.data.get('assets_id', '')
|
||||
if assets_id:
|
||||
spm = uuid.uuid4().hex
|
||||
key = CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX.format(spm)
|
||||
cache.set(key, assets_id, 300)
|
||||
url = reverse_lazy('assets:asset-bulk-update') + '?spm=%s' % spm
|
||||
return Response({'url': url})
|
||||
error = _('Please select assets that need to be updated')
|
||||
return Response({'error': error}, status=400)
|
||||
|
||||
|
||||
class AssetRefreshHardwareApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Refresh asset hardware info
|
||||
@@ -103,21 +130,23 @@ class AssetRefreshHardwareApi(generics.RetrieveAPIView):
|
||||
|
||||
class AssetAdminUserTestApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Test asset admin user connectivity
|
||||
Test asset admin user assets_connectivity
|
||||
"""
|
||||
queryset = Asset.objects.all()
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.TaskIDSerializer
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
asset_id = kwargs.get('pk')
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
task = test_asset_connectability_manual.delay(asset)
|
||||
task = test_asset_connectivity_manual.delay(asset)
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
class AssetGatewayApi(generics.RetrieveAPIView):
|
||||
queryset = Asset.objects.all()
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.GatewayWithAuthSerializer
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
asset_id = kwargs.get('pk')
|
||||
|
||||
101
apps/assets/api/asset_user.py
Normal file
101
apps/assets/api/asset_user.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import viewsets, status, generics
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
|
||||
from common.permissions import IsOrgAdminOrAppUser
|
||||
from common.utils import get_object_or_none, get_logger
|
||||
|
||||
from ..backends.multi import AssetUserManager
|
||||
from ..models import Asset
|
||||
from .. import serializers
|
||||
from ..tasks import test_asset_users_connectivity_manual
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AssetUserViewSet', 'AssetUserAuthInfoApi', 'AssetUserTestConnectiveApi',
|
||||
]
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AssetUserViewSet(viewsets.GenericViewSet):
|
||||
pagination_class = LimitOffsetPagination
|
||||
serializer_class = serializers.AssetUserSerializer
|
||||
permission_classes = (IsOrgAdminOrAppUser, )
|
||||
http_method_names = ['get', 'post']
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
def get_queryset(self):
|
||||
username = self.request.GET.get('username')
|
||||
asset_id = self.request.GET.get('asset_id')
|
||||
asset = get_object_or_none(Asset, pk=asset_id)
|
||||
queryset = AssetUserManager.filter(username=username, asset=asset)
|
||||
return queryset
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = sorted(
|
||||
queryset,
|
||||
key=lambda q: (q.asset.hostname, q.connectivity, q.username)
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetUserAuthInfoApi(generics.RetrieveAPIView):
|
||||
serializer_class = serializers.AssetUserAuthInfoSerializer
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance)
|
||||
status_code = status.HTTP_200_OK
|
||||
if not instance:
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
return Response(serializer.data, status=status_code)
|
||||
|
||||
def get_object(self):
|
||||
username = self.request.GET.get('username')
|
||||
asset_id = self.request.GET.get('asset_id')
|
||||
asset = get_object_or_none(Asset, pk=asset_id)
|
||||
try:
|
||||
instance = AssetUserManager.get(username, asset)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
return None
|
||||
else:
|
||||
return instance
|
||||
|
||||
|
||||
class AssetUserTestConnectiveApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Test asset users connective
|
||||
"""
|
||||
|
||||
def get_asset_users(self):
|
||||
username = self.request.GET.get('username')
|
||||
asset_id = self.request.GET.get('asset_id')
|
||||
asset = get_object_or_none(Asset, pk=asset_id)
|
||||
asset_users = AssetUserManager.filter(username=username, asset=asset)
|
||||
return asset_users
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
asset_users = self.get_asset_users()
|
||||
task = test_asset_users_connectivity_manual.delay(asset_users)
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
|
||||
@@ -51,9 +51,10 @@ class GatewayTestConnectionApi(SingleObjectMixin, APIView):
|
||||
model = Gateway
|
||||
object = None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object(Gateway.objects.all())
|
||||
ok, e = self.object.test_connective()
|
||||
local_port = self.request.data.get('port') or self.object.port
|
||||
ok, e = self.object.test_connective(local_port=local_port)
|
||||
if ok:
|
||||
return Response("ok")
|
||||
else:
|
||||
|
||||
@@ -17,15 +17,14 @@ from rest_framework import generics, mixins, viewsets
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.db.models import Count
|
||||
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from common.tree import TreeNodeSerializer
|
||||
from ..hands import IsOrgAdmin
|
||||
from ..models import Node
|
||||
from ..tasks import update_assets_hardware_info_util, test_asset_connectability_util
|
||||
from ..tasks import update_assets_hardware_info_util, test_asset_connectivity_util
|
||||
from .. import serializers
|
||||
|
||||
|
||||
@@ -34,7 +33,8 @@ __all__ = [
|
||||
'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi',
|
||||
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi',
|
||||
'NodeAddChildrenApi', 'RefreshNodeHardwareInfoApi',
|
||||
'TestNodeConnectiveApi'
|
||||
'TestNodeConnectiveApi', 'NodeListAsTreeApi',
|
||||
'NodeChildrenAsTreeApi', 'RefreshAssetsAmount',
|
||||
]
|
||||
|
||||
|
||||
@@ -61,45 +61,121 @@ class NodeViewSet(viewsets.ModelViewSet):
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
|
||||
class NodeListAsTreeApi(generics.ListAPIView):
|
||||
"""
|
||||
获取节点列表树
|
||||
[
|
||||
{
|
||||
"id": "",
|
||||
"name": "",
|
||||
"pId": "",
|
||||
"meta": ""
|
||||
}
|
||||
]
|
||||
"""
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = TreeNodeSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = [node.as_tree_node() for node in Node.objects.all()]
|
||||
return queryset
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
if self.request.query_params.get('refresh', '0') == '1':
|
||||
queryset = self.refresh_nodes(queryset)
|
||||
return queryset
|
||||
|
||||
@staticmethod
|
||||
def refresh_nodes(queryset):
|
||||
Node.expire_nodes_assets_amount()
|
||||
Node.expire_nodes_full_value()
|
||||
return queryset
|
||||
|
||||
|
||||
class NodeChildrenAsTreeApi(generics.ListAPIView):
|
||||
"""
|
||||
节点子节点作为树返回,
|
||||
[
|
||||
{
|
||||
"id": "",
|
||||
"name": "",
|
||||
"pId": "",
|
||||
"meta": ""
|
||||
}
|
||||
]
|
||||
|
||||
"""
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = TreeNodeSerializer
|
||||
node = None
|
||||
is_root = False
|
||||
|
||||
def get_queryset(self):
|
||||
node_key = self.request.query_params.get('key')
|
||||
if node_key:
|
||||
self.node = Node.objects.get(key=node_key)
|
||||
queryset = self.node.get_children(with_self=False)
|
||||
else:
|
||||
self.is_root = True
|
||||
self.node = Node.root()
|
||||
queryset = list(self.node.get_children(with_self=True))
|
||||
nodes_invalid = Node.objects.exclude(key__startswith=self.node.key)
|
||||
queryset.extend(list(nodes_invalid))
|
||||
queryset = [node.as_tree_node() for node in queryset]
|
||||
return queryset
|
||||
|
||||
def filter_assets(self, queryset):
|
||||
include_assets = self.request.query_params.get('assets', '0') == '1'
|
||||
if not include_assets:
|
||||
return queryset
|
||||
assets = self.node.get_assets()
|
||||
for asset in assets:
|
||||
queryset.append(asset.as_tree_node(self.node))
|
||||
return queryset
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = self.filter_assets(queryset)
|
||||
queryset = self.filter_refresh_nodes(queryset)
|
||||
return queryset
|
||||
|
||||
def filter_refresh_nodes(self, queryset):
|
||||
if self.request.query_params.get('refresh', '0') == '1':
|
||||
Node.expire_nodes_assets_amount()
|
||||
Node.expire_nodes_full_value()
|
||||
return queryset
|
||||
|
||||
|
||||
class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
|
||||
queryset = Node.objects.all()
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.NodeSerializer
|
||||
instance = None
|
||||
|
||||
def counter(self):
|
||||
values = [
|
||||
child.value[child.value.rfind(' '):]
|
||||
for child in self.get_object().get_children()
|
||||
if child.value.startswith("新节点 ")
|
||||
]
|
||||
values = [int(value) for value in values if value.strip().isdigit()]
|
||||
count = max(values)+1 if values else 1
|
||||
return count
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.list(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
if not request.data.get("value"):
|
||||
request.data["value"] = _("New node {}").format(self.counter())
|
||||
request.data["value"] = instance.get_next_child_preset_name()
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
value = request.data.get("value")
|
||||
_id = request.data.get('id') or None
|
||||
values = [child.value for child in instance.get_children()]
|
||||
if value in values:
|
||||
raise ValidationError(
|
||||
'The same level node name cannot be the same'
|
||||
)
|
||||
node = instance.create_child(value=value)
|
||||
return Response(
|
||||
{"id": node.id, "key": node.key, "value": node.value},
|
||||
status=201,
|
||||
)
|
||||
node = instance.create_child(value=value, _id=_id)
|
||||
return Response(self.serializer_class(instance=node).data, status=201)
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk') or self.request.query_params.get('id')
|
||||
if not pk:
|
||||
node = None
|
||||
node = Node.root()
|
||||
else:
|
||||
node = get_object_or_404(Node, pk=pk)
|
||||
return node
|
||||
@@ -107,7 +183,6 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
|
||||
def get_queryset(self):
|
||||
queryset = []
|
||||
query_all = self.request.query_params.get("all")
|
||||
query_assets = self.request.query_params.get('assets')
|
||||
node = self.get_object()
|
||||
|
||||
if node is None:
|
||||
@@ -120,23 +195,8 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
|
||||
else:
|
||||
children = node.get_children()
|
||||
queryset.extend(list(children))
|
||||
|
||||
if query_assets:
|
||||
assets = node.get_assets()
|
||||
for asset in assets:
|
||||
node_fake = Node()
|
||||
node_fake.assets__count = 0
|
||||
node_fake.id = asset.id
|
||||
node_fake.is_node = False
|
||||
node_fake.key = node.key + ':0'
|
||||
node_fake.value = asset.hostname
|
||||
queryset.append(node_fake)
|
||||
queryset = sorted(queryset, key=lambda x: x.is_node, reverse=True)
|
||||
return queryset
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
|
||||
class NodeAssetsApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
@@ -217,7 +277,7 @@ class RefreshNodeHardwareInfoApi(APIView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
node_id = kwargs.get('pk')
|
||||
node = get_object_or_404(self.model, id=node_id)
|
||||
assets = node.assets.all()
|
||||
assets = node.get_all_assets()
|
||||
# task_name = _("更新节点资产硬件信息: {}".format(node.name))
|
||||
task_name = _("Update node asset hardware information: {}").format(node.name)
|
||||
task = update_assets_hardware_info_util.delay(assets, task_name=task_name)
|
||||
@@ -231,8 +291,17 @@ class TestNodeConnectiveApi(APIView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
node_id = kwargs.get('pk')
|
||||
node = get_object_or_404(self.model, id=node_id)
|
||||
assets = node.assets.all()
|
||||
assets = node.get_all_assets()
|
||||
# task_name = _("测试节点下资产是否可连接: {}".format(node.name))
|
||||
task_name = _("Test if the assets under the node are connectable: {}".format(node.name))
|
||||
task = test_asset_connectability_util.delay(assets, task_name=task_name)
|
||||
task = test_asset_connectivity_util.delay(assets, task_name=task_name)
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
class RefreshAssetsAmount(APIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
model = Node
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.model.expire_nodes_assets_amount()
|
||||
return Response("Ok")
|
||||
|
||||
@@ -24,16 +24,16 @@ from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
|
||||
from ..models import SystemUser, Asset
|
||||
from .. import serializers
|
||||
from ..tasks import push_system_user_to_assets_manual, \
|
||||
test_system_user_connectability_manual, push_system_user_a_asset_manual, \
|
||||
test_system_user_connectability_a_asset
|
||||
test_system_user_connectivity_manual, push_system_user_a_asset_manual, \
|
||||
test_system_user_connectivity_a_asset
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'SystemUserViewSet', 'SystemUserAuthInfoApi',
|
||||
'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi',
|
||||
'SystemUserPushApi', 'SystemUserTestConnectiveApi',
|
||||
'SystemUserAssetsListView', 'SystemUserPushToAssetApi',
|
||||
'SystemUserTestAssetConnectabilityApi', 'SystemUserCommandFilterRuleListApi',
|
||||
'SystemUserTestAssetConnectivityApi', 'SystemUserCommandFilterRuleListApi',
|
||||
|
||||
]
|
||||
|
||||
@@ -68,6 +68,22 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
|
||||
return Response(status=204)
|
||||
|
||||
|
||||
class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Get system user with asset auth info
|
||||
"""
|
||||
queryset = SystemUser.objects.all()
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.SystemUserAuthSerializer
|
||||
|
||||
def get_object(self):
|
||||
instance = super().get_object()
|
||||
aid = self.kwargs.get('aid')
|
||||
asset = get_object_or_404(Asset, pk=aid)
|
||||
instance.load_specific_asset_auth(asset)
|
||||
return instance
|
||||
|
||||
|
||||
class SystemUserPushApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Push system user to cluster assets api
|
||||
@@ -93,15 +109,16 @@ class SystemUserTestConnectiveApi(generics.RetrieveAPIView):
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
system_user = self.get_object()
|
||||
task = test_system_user_connectability_manual.delay(system_user)
|
||||
task = test_system_user_connectivity_manual.delay(system_user)
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
class SystemUserAssetsListView(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.AssetSerializer
|
||||
serializer_class = serializers.AssetSimpleSerializer
|
||||
pagination_class = LimitOffsetPagination
|
||||
filter_fields = ("hostname", "ip")
|
||||
http_method_names = ['get']
|
||||
search_fields = filter_fields
|
||||
|
||||
def get_object(self):
|
||||
@@ -116,6 +133,7 @@ class SystemUserAssetsListView(generics.ListAPIView):
|
||||
class SystemUserPushToAssetApi(generics.RetrieveAPIView):
|
||||
queryset = SystemUser.objects.all()
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.TaskIDSerializer
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
system_user = self.get_object()
|
||||
@@ -125,15 +143,16 @@ class SystemUserPushToAssetApi(generics.RetrieveAPIView):
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
class SystemUserTestAssetConnectabilityApi(generics.RetrieveAPIView):
|
||||
class SystemUserTestAssetConnectivityApi(generics.RetrieveAPIView):
|
||||
queryset = SystemUser.objects.all()
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.TaskIDSerializer
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
system_user = self.get_object()
|
||||
asset_id = self.kwargs.get('aid')
|
||||
asset = get_object_or_404(Asset, id=asset_id)
|
||||
task = test_system_user_connectability_a_asset.delay(system_user, asset)
|
||||
task = test_system_user_connectivity_a_asset.delay(system_user, asset)
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
|
||||
60
apps/assets/backends/base.py
Normal file
60
apps/assets/backends/base.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
|
||||
from abc import abstractmethod
|
||||
|
||||
|
||||
class NotSupportError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BaseBackend:
|
||||
ObjectDoesNotExist = ObjectDoesNotExist
|
||||
MultipleObjectsReturned = MultipleObjectsReturned
|
||||
NotSupportError = NotSupportError
|
||||
MSG_NOT_EXIST = '{} Object matching query does not exist'
|
||||
MSG_MULTIPLE = '{} get() returned more than one object ' \
|
||||
'-- it returned {}!'
|
||||
|
||||
@classmethod
|
||||
def get(cls, username, asset):
|
||||
instances = cls.filter(username, asset)
|
||||
if len(instances) == 1:
|
||||
return instances[0]
|
||||
elif len(instances) == 0:
|
||||
cls.raise_does_not_exist(cls.__name__)
|
||||
else:
|
||||
cls.raise_multiple_return(cls.__name__, len(instances))
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def filter(cls, username=None, asset=None, latest=True):
|
||||
"""
|
||||
:param username: 用户名
|
||||
:param asset: <Asset>对象
|
||||
:param latest: 是否是最新记录
|
||||
:return: 元素为<AuthBook>的可迭代对象(<list> or <QuerySet>)
|
||||
"""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def create(cls, **kwargs):
|
||||
"""
|
||||
:param kwargs:
|
||||
{
|
||||
name, username, asset, comment, password, public_key, private_key,
|
||||
(org_id)
|
||||
}
|
||||
:return: <AuthBook>对象
|
||||
"""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def raise_does_not_exist(cls, name):
|
||||
raise cls.ObjectDoesNotExist(cls.MSG_NOT_EXIST.format(name))
|
||||
|
||||
@classmethod
|
||||
def raise_multiple_return(cls, name, length):
|
||||
raise cls.MultipleObjectsReturned(cls.MSG_MULTIPLE.format(name, length))
|
||||
2
apps/assets/backends/external/__init__.py
vendored
Normal file
2
apps/assets/backends/external/__init__.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
31
apps/assets/backends/external/db.py
vendored
Normal file
31
apps/assets/backends/external/db.py
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from assets.models import AuthBook
|
||||
|
||||
from ..base import BaseBackend
|
||||
|
||||
|
||||
class AuthBookBackend(BaseBackend):
|
||||
|
||||
@classmethod
|
||||
def filter(cls, username=None, asset=None, latest=True):
|
||||
queryset = AuthBook.objects.all()
|
||||
if username:
|
||||
queryset = queryset.filter(username=username)
|
||||
if asset:
|
||||
queryset = queryset.filter(asset=asset)
|
||||
if latest:
|
||||
queryset = queryset.latest_version()
|
||||
return queryset
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
auth_info = {
|
||||
'password': kwargs.pop('password', ''),
|
||||
'public_key': kwargs.pop('public_key', ''),
|
||||
'private_key': kwargs.pop('private_key', '')
|
||||
}
|
||||
obj = AuthBook.objects.create(**kwargs)
|
||||
obj.set_auth(**auth_info)
|
||||
return obj
|
||||
16
apps/assets/backends/external/utils.py
vendored
Normal file
16
apps/assets/backends/external/utils.py
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
# from django.conf import settings
|
||||
|
||||
from .db import AuthBookBackend
|
||||
# from .vault import VaultBackend
|
||||
|
||||
|
||||
def get_backend():
|
||||
default_backend = AuthBookBackend
|
||||
|
||||
# if settings.BACKEND_ASSET_USER_AUTH_VAULT:
|
||||
# return VaultBackend
|
||||
|
||||
return default_backend
|
||||
19
apps/assets/backends/external/vault.py
vendored
Normal file
19
apps/assets/backends/external/vault.py
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from ..base import BaseBackend
|
||||
|
||||
|
||||
class VaultBackend(BaseBackend):
|
||||
|
||||
@classmethod
|
||||
def get(cls, username, asset):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def filter(cls, username=None, asset=None, latest=True):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
pass
|
||||
4
apps/assets/backends/internal/__init__.py
Normal file
4
apps/assets/backends/internal/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
|
||||
38
apps/assets/backends/internal/admin_user.py
Normal file
38
apps/assets/backends/internal/admin_user.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from assets.models import Asset
|
||||
|
||||
from ..base import BaseBackend
|
||||
from .utils import construct_authbook_object
|
||||
|
||||
|
||||
class AdminUserBackend(BaseBackend):
|
||||
|
||||
@classmethod
|
||||
def filter(cls, username=None, asset=None, **kwargs):
|
||||
instances = cls.construct_authbook_objects(username, asset)
|
||||
return instances
|
||||
|
||||
@classmethod
|
||||
def _get_assets(cls, asset):
|
||||
if not asset:
|
||||
assets = Asset.objects.all().prefetch_related('admin_user')
|
||||
else:
|
||||
assets = [asset]
|
||||
return assets
|
||||
|
||||
@classmethod
|
||||
def construct_authbook_objects(cls, username, asset):
|
||||
instances = []
|
||||
assets = cls._get_assets(asset)
|
||||
for asset in assets:
|
||||
if username and asset.admin_user.username != username:
|
||||
continue
|
||||
instance = construct_authbook_object(asset.admin_user, asset)
|
||||
instances.append(instance)
|
||||
return instances
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
raise cls.NotSupportError("Not support create")
|
||||
32
apps/assets/backends/internal/asset_user.py
Normal file
32
apps/assets/backends/internal/asset_user.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from ..base import BaseBackend
|
||||
from .admin_user import AdminUserBackend
|
||||
from .system_user import SystemUserBackend
|
||||
|
||||
|
||||
class AssetUserBackend(BaseBackend):
|
||||
@classmethod
|
||||
def filter(cls, username=None, asset=None, **kwargs):
|
||||
admin_user_instances = AdminUserBackend.filter(username, asset)
|
||||
system_user_instances = SystemUserBackend.filter(username, asset)
|
||||
instances = cls._merge_instances(admin_user_instances, system_user_instances)
|
||||
return instances
|
||||
|
||||
@classmethod
|
||||
def _merge_instances(cls, admin_user_instances, system_user_instances):
|
||||
admin_user_instances_keyword_list = [
|
||||
{'username': instance.username, 'asset': instance.asset}
|
||||
for instance in admin_user_instances
|
||||
]
|
||||
instances = [
|
||||
instance for instance in system_user_instances
|
||||
if instance.keyword not in admin_user_instances_keyword_list
|
||||
]
|
||||
admin_user_instances.extend(instances)
|
||||
return admin_user_instances
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
raise cls.NotSupportError("Not support create")
|
||||
75
apps/assets/backends/internal/system_user.py
Normal file
75
apps/assets/backends/internal/system_user.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import itertools
|
||||
|
||||
from assets.models import Asset
|
||||
|
||||
from ..base import BaseBackend
|
||||
from .utils import construct_authbook_object
|
||||
|
||||
|
||||
class SystemUserBackend(BaseBackend):
|
||||
|
||||
@classmethod
|
||||
def filter(cls, username=None, asset=None, **kwargs):
|
||||
instances = cls.construct_authbook_objects(username, asset)
|
||||
return instances
|
||||
|
||||
@classmethod
|
||||
def _distinct_system_users_by_username(cls, system_users):
|
||||
system_users = sorted(
|
||||
system_users,
|
||||
key=lambda su: (su.username, su.priority, su.date_updated),
|
||||
reverse=True,
|
||||
)
|
||||
results = itertools.groupby(system_users, key=lambda su: su.username)
|
||||
system_users = [next(result[1]) for result in results]
|
||||
return system_users
|
||||
|
||||
@classmethod
|
||||
def _filter_system_users_by_username(cls, system_users, username):
|
||||
_system_users = cls._distinct_system_users_by_username(system_users)
|
||||
if username:
|
||||
_system_users = [su for su in _system_users if username == su.username]
|
||||
return _system_users
|
||||
|
||||
@classmethod
|
||||
def _construct_authbook_objects(cls, system_users, asset):
|
||||
instances = []
|
||||
for system_user in system_users:
|
||||
instance = construct_authbook_object(system_user, asset)
|
||||
instances.append(instance)
|
||||
return instances
|
||||
|
||||
@classmethod
|
||||
def _get_assets_with_system_users(cls, asset=None):
|
||||
"""
|
||||
{ 'asset': set(<SystemUser>, <SystemUser>, ...) }
|
||||
"""
|
||||
if not asset:
|
||||
_assets = Asset.objects.all().prefetch_related('systemuser_set')
|
||||
else:
|
||||
_assets = [asset]
|
||||
|
||||
assets = {asset: set(asset.systemuser_set.all()) for asset in _assets}
|
||||
return assets
|
||||
|
||||
@classmethod
|
||||
def construct_authbook_objects(cls, username, asset):
|
||||
"""
|
||||
:return: [<AuthBook>, <AuthBook>, ...]
|
||||
"""
|
||||
instances = []
|
||||
assets = cls._get_assets_with_system_users(asset)
|
||||
for _asset, _system_users in assets.items():
|
||||
_system_users = cls._filter_system_users_by_username(_system_users, username)
|
||||
_instances = cls._construct_authbook_objects(_system_users, _asset)
|
||||
instances.extend(_instances)
|
||||
return instances
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
raise Exception("Not support create")
|
||||
|
||||
|
||||
26
apps/assets/backends/internal/utils.py
Normal file
26
apps/assets/backends/internal/utils.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from assets.models import AuthBook
|
||||
|
||||
|
||||
def construct_authbook_object(asset_user, asset):
|
||||
"""
|
||||
作用: 将<AssetUser>对象构造成为<AuthBook>对象并返回
|
||||
|
||||
:param asset_user: <AdminUser>或<SystemUser>对象
|
||||
:param asset: <Asset>对象
|
||||
:return: <AuthBook>对象
|
||||
"""
|
||||
fields = [
|
||||
'id', 'name', 'username', 'comment', 'org_id',
|
||||
'_password', '_private_key', '_public_key',
|
||||
'date_created', 'date_updated', 'created_by'
|
||||
]
|
||||
|
||||
obj = AuthBook(asset=asset, version=0, is_latest=True)
|
||||
for field in fields:
|
||||
value = getattr(asset_user, field)
|
||||
setattr(obj, field, value)
|
||||
return obj
|
||||
|
||||
40
apps/assets/backends/multi.py
Normal file
40
apps/assets/backends/multi.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .base import BaseBackend
|
||||
|
||||
from .external.utils import get_backend
|
||||
from .internal.asset_user import AssetUserBackend
|
||||
|
||||
|
||||
class AssetUserManager(BaseBackend):
|
||||
"""
|
||||
资产用户管理器
|
||||
"""
|
||||
external_backend = get_backend()
|
||||
internal_backend = AssetUserBackend
|
||||
|
||||
@classmethod
|
||||
def filter(cls, username=None, asset=None, **kwargs):
|
||||
external_instance = list(cls.external_backend.filter(username, asset))
|
||||
internal_instance = list(cls.internal_backend.filter(username, asset))
|
||||
instances = cls._merge_instances(external_instance, internal_instance)
|
||||
return instances
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
instance = cls.external_backend.create(**kwargs)
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def _merge_instances(cls, external_instances, internal_instances):
|
||||
external_instances_keyword_list = [
|
||||
{'username': instance.username, 'asset': instance.asset}
|
||||
for instance in external_instances
|
||||
]
|
||||
instances = [
|
||||
instance for instance in internal_instances
|
||||
if instance.keyword not in external_instances_keyword_list
|
||||
]
|
||||
external_instances.extend(instances)
|
||||
return external_instances
|
||||
@@ -32,7 +32,22 @@ TEST_SYSTEM_USER_CONN_TASKS = [
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
ASSET_USER_CONN_CACHE_KEY = 'ASSET_USER_CONN_{}_{}'
|
||||
TEST_ASSET_USER_CONN_TASKS = [
|
||||
{
|
||||
"name": "ping",
|
||||
"action": {
|
||||
"module": "ping",
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
TASK_OPTIONS = {
|
||||
'timeout': 10,
|
||||
'forks': 10,
|
||||
}
|
||||
|
||||
CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX = '_KEY_ASSET_BULK_UPDATE_ID_{}'
|
||||
|
||||
|
||||
@@ -41,9 +41,6 @@ class AssetCreateForm(OrgModelForm):
|
||||
'nodes': _("Node"),
|
||||
}
|
||||
help_texts = {
|
||||
'hostname': '* required',
|
||||
'ip': '* required',
|
||||
'port': '* required',
|
||||
'admin_user': _(
|
||||
'root or other NOPASSWD sudo privilege user existed in asset,'
|
||||
'If asset is windows or other set any one, more see admin user left menu'
|
||||
@@ -80,10 +77,6 @@ class AssetUpdateForm(OrgModelForm):
|
||||
'nodes': _("Node"),
|
||||
}
|
||||
help_texts = {
|
||||
'hostname': '* required',
|
||||
'ip': '* required',
|
||||
'port': '* required',
|
||||
'cluster': '* required',
|
||||
'admin_user': _(
|
||||
'root or other NOPASSWD sudo privilege user existed in asset,'
|
||||
'If asset is windows or other set any one, more see admin user left menu'
|
||||
@@ -95,7 +88,7 @@ class AssetUpdateForm(OrgModelForm):
|
||||
|
||||
class AssetBulkUpdateForm(OrgModelForm):
|
||||
assets = forms.ModelMultipleChoiceField(
|
||||
required=True, help_text='* required',
|
||||
required=True,
|
||||
label=_('Select assets'), queryset=Asset.objects.all(),
|
||||
widget=forms.SelectMultiple(
|
||||
attrs={
|
||||
@@ -104,24 +97,12 @@ class AssetBulkUpdateForm(OrgModelForm):
|
||||
}
|
||||
)
|
||||
)
|
||||
port = forms.IntegerField(
|
||||
label=_('Port'), required=False, min_value=1, max_value=65535,
|
||||
)
|
||||
admin_user = forms.ModelChoiceField(
|
||||
required=False, queryset=AdminUser.objects,
|
||||
label=_("Admin user"),
|
||||
widget=forms.Select(
|
||||
attrs={
|
||||
'class': 'select2',
|
||||
'data-placeholder': _('Admin user')
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = [
|
||||
'assets', 'port', 'admin_user', 'labels', 'nodes', 'platform'
|
||||
'assets', 'port', 'admin_user', 'labels', 'platform',
|
||||
'protocol', 'domain',
|
||||
]
|
||||
widgets = {
|
||||
'labels': forms.SelectMultiple(
|
||||
@@ -132,6 +113,13 @@ class AssetBulkUpdateForm(OrgModelForm):
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# 重写其他字段为不再required
|
||||
for name, field in self.fields.items():
|
||||
if name != 'assets':
|
||||
field.required = False
|
||||
|
||||
def save(self, commit=True):
|
||||
changed_fields = []
|
||||
for field in self._meta.fields:
|
||||
|
||||
@@ -28,6 +28,15 @@ class DomainForm(forms.ModelForm):
|
||||
initial['assets'] = kwargs['instance'].assets.all()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# 前端渲染优化, 防止过多资产
|
||||
assets_field = self.fields.get('assets')
|
||||
if not self.data:
|
||||
instance = kwargs.get('instance')
|
||||
if instance:
|
||||
assets_field.queryset = instance.assets.all()
|
||||
else:
|
||||
assets_field.queryset = Asset.objects.none()
|
||||
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit=commit)
|
||||
assets = self.cleaned_data['assets']
|
||||
@@ -36,14 +45,12 @@ class DomainForm(forms.ModelForm):
|
||||
|
||||
|
||||
class GatewayForm(PasswordAndKeyAuthForm, OrgModelForm):
|
||||
protocol = forms.ChoiceField(
|
||||
choices=[Gateway.PROTOCOL_CHOICES[0]],
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
password_field = self.fields.get('password')
|
||||
password_field.help_text = _('Password should not contain special characters')
|
||||
protocol_field = self.fields.get('protocol')
|
||||
protocol_field.choices = [Gateway.PROTOCOL_CHOICES[0]]
|
||||
|
||||
def save(self, commit=True):
|
||||
# Because we define custom field, so we need rewrite :method: `save`
|
||||
@@ -59,11 +66,10 @@ class GatewayForm(PasswordAndKeyAuthForm, OrgModelForm):
|
||||
'name', 'ip', 'port', 'username', 'protocol', 'domain', 'password',
|
||||
'private_key_file', 'is_active', 'comment',
|
||||
]
|
||||
help_texts = {
|
||||
'protocol': _("SSH gateway support proxy SSH,RDP,VNC")
|
||||
}
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
|
||||
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
|
||||
}
|
||||
help_texts = {
|
||||
'name': '* required',
|
||||
'username': '* required',
|
||||
}
|
||||
|
||||
@@ -26,6 +26,15 @@ class LabelForm(forms.ModelForm):
|
||||
initial['assets'] = kwargs['instance'].assets.all()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# 前端渲染优化, 防止过多资产
|
||||
assets_field = self.fields.get('assets')
|
||||
if not self.data:
|
||||
instance = kwargs.get('instance')
|
||||
if instance:
|
||||
assets_field.queryset = instance.assets.all()
|
||||
else:
|
||||
assets_field.queryset = Asset.objects.none()
|
||||
|
||||
def save(self, commit=True):
|
||||
label = super().save(commit=commit)
|
||||
assets = self.cleaned_data['assets']
|
||||
|
||||
@@ -35,8 +35,12 @@ class PasswordAndKeyAuthForm(forms.ModelForm):
|
||||
if private_key_file:
|
||||
key_string = private_key_file.read()
|
||||
private_key_file.seek(0)
|
||||
key_string = key_string.decode()
|
||||
|
||||
if not validate_ssh_private_key(key_string, password):
|
||||
raise forms.ValidationError(_('Invalid private key'))
|
||||
msg = _('Invalid private key, Only support '
|
||||
'RSA/DSA format key')
|
||||
raise forms.ValidationError(msg)
|
||||
return private_key_file
|
||||
|
||||
def validate_password_key(self):
|
||||
@@ -80,10 +84,6 @@ class AdminUserForm(PasswordAndKeyAuthForm):
|
||||
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
|
||||
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
|
||||
}
|
||||
help_texts = {
|
||||
'name': '* required',
|
||||
'username': '* required',
|
||||
}
|
||||
|
||||
|
||||
class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
|
||||
@@ -99,8 +99,10 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
|
||||
auto_generate_key = self.cleaned_data.get('auto_generate_key', False)
|
||||
private_key, public_key = super().gen_keys()
|
||||
|
||||
if login_mode == SystemUser.MANUAL_LOGIN or \
|
||||
protocol in [SystemUser.RDP_PROTOCOL, SystemUser.TELNET_PROTOCOL]:
|
||||
if login_mode == SystemUser.LOGIN_MANUAL or \
|
||||
protocol in [SystemUser.PROTOCOL_RDP,
|
||||
SystemUser.PROTOCOL_TELNET,
|
||||
SystemUser.PROTOCOL_VNC]:
|
||||
system_user.auto_push = 0
|
||||
auto_generate_key = False
|
||||
system_user.save()
|
||||
@@ -120,17 +122,18 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
|
||||
if not self.instance and not auto_generate:
|
||||
super().validate_password_key()
|
||||
|
||||
def is_valid(self):
|
||||
validated = super().is_valid()
|
||||
username = self.cleaned_data.get('username')
|
||||
login_mode = self.cleaned_data.get('login_mode')
|
||||
if login_mode == SystemUser.AUTO_LOGIN and not username:
|
||||
self.add_error(
|
||||
"username", _('* Automatic login mode,'
|
||||
' must fill in the username.')
|
||||
)
|
||||
return False
|
||||
return validated
|
||||
def clean_username(self):
|
||||
username = self.data.get('username')
|
||||
login_mode = self.data.get('login_mode')
|
||||
protocol = self.data.get('protocol')
|
||||
|
||||
if username:
|
||||
return username
|
||||
if login_mode == SystemUser.LOGIN_AUTO and \
|
||||
protocol != SystemUser.PROTOCOL_VNC:
|
||||
msg = _('* Automatic login mode must fill in the username.')
|
||||
raise forms.ValidationError(msg)
|
||||
return username
|
||||
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
@@ -147,11 +150,10 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
|
||||
}),
|
||||
}
|
||||
help_texts = {
|
||||
'name': '* required',
|
||||
'username': '* required',
|
||||
'auto_push': _('Auto push system user to asset'),
|
||||
'priority': _('1-100, High level will be using login asset as default, '
|
||||
'if user was granted more than 2 system user'),
|
||||
'login_mode': _('If you choose manual login mode, you do not '
|
||||
'need to fill in the username and password.')
|
||||
'need to fill in the username and password.'),
|
||||
'sudo': _("Use comma split multi command, ex: /bin/whoami,/bin/ifconfig")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-28 10:16
|
||||
|
||||
import assets.models.asset
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('assets', '0002_auto_20180105_1807'), ('assets', '0003_auto_20180109_2331'), ('assets', '0004_auto_20180125_1218'), ('assets', '0005_auto_20180126_1637'), ('assets', '0006_auto_20180130_1502'), ('assets', '0007_auto_20180225_1815'), ('assets', '0008_auto_20180306_1804'), ('assets', '0009_auto_20180307_1212')]
|
||||
|
||||
dependencies = [
|
||||
('assets', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='adminuser',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Admin user'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='asset',
|
||||
options={'verbose_name': 'Asset'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='assetgroup',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Asset group'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='cluster',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Cluster'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='systemuser',
|
||||
options={'ordering': ['name'], 'verbose_name': 'System user'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='cluster',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='assetgroup',
|
||||
name='created_by',
|
||||
field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Label',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('value', models.CharField(max_length=128, verbose_name='Value')),
|
||||
('category', models.CharField(choices=[('S', 'System'), ('U', 'User')], default='U', max_length=128, verbose_name='Category')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
||||
('comment', models.TextField(blank=True, null=True, verbose_name='Comment')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'assets_label',
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='label',
|
||||
unique_together={('name', 'value')},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='labels',
|
||||
field=models.ManyToManyField(blank=True, related_name='assets', to='assets.Label', verbose_name='Labels'),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='cabinet_no',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='cabinet_pos',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='env',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='remote_card_ip',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='status',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='type',
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Node',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('key', models.CharField(max_length=64, unique=True, verbose_name='Key')),
|
||||
('value', models.CharField(max_length=128, verbose_name='Value')),
|
||||
('child_mark', models.IntegerField(default=0)),
|
||||
('date_create', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='groups',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='systemuser',
|
||||
name='cluster',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='admin_user',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.AdminUser', verbose_name='Admin user'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='protocol',
|
||||
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='nodes',
|
||||
field=models.ManyToManyField(default=assets.models.asset.default_node, related_name='assets', to='assets.Node', verbose_name='Nodes'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='nodes',
|
||||
field=models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Nodes'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='created_by',
|
||||
field=models.CharField(max_length=128, null=True, verbose_name='Created by'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='username',
|
||||
field=models.CharField(max_length=128, verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='platform',
|
||||
field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='created_by',
|
||||
field=models.CharField(max_length=128, null=True, verbose_name='Created by'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='username',
|
||||
field=models.CharField(max_length=128, verbose_name='Username'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,220 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-28 10:16
|
||||
|
||||
import assets.models.utils
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
# Functions from the following migrations need manual copying.
|
||||
# Move them and any dependencies into this file, then update the
|
||||
# RunPython operations to refer to the local versions:
|
||||
# assets.migrations.0017_auto_20180702_1415
|
||||
|
||||
def migrate_win_to_ssh_protocol(apps, schema_editor):
|
||||
asset_model = apps.get_model("assets", "Asset")
|
||||
db_alias = schema_editor.connection.alias
|
||||
asset_model.objects.using(db_alias).filter(platform__startswith='Win').update(protocol='rdp')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('assets', '0010_auto_20180307_1749'), ('assets', '0011_auto_20180326_0957'), ('assets', '0012_auto_20180404_1302'), ('assets', '0013_auto_20180411_1135'), ('assets', '0014_auto_20180427_1245'), ('assets', '0015_auto_20180510_1235'), ('assets', '0016_auto_20180511_1203'), ('assets', '0017_auto_20180702_1415'), ('assets', '0018_auto_20180807_1116'), ('assets', '0019_auto_20180816_1320')]
|
||||
|
||||
dependencies = [
|
||||
('assets', '0009_auto_20180307_1212'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='node',
|
||||
name='value',
|
||||
field=models.CharField(max_length=128, unique=True, verbose_name='Value'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Domain',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Gateway',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
|
||||
('username', models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
|
||||
('_password', models.CharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('_private_key', models.TextField(blank=True, max_length=4096, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key')),
|
||||
('_public_key', models.TextField(blank=True, max_length=4096, verbose_name='SSH public key')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True)),
|
||||
('date_updated', models.DateTimeField(auto_now=True)),
|
||||
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
|
||||
('ip', models.GenericIPAddressField(db_index=True, verbose_name='IP')),
|
||||
('port', models.IntegerField(default=22, verbose_name='Port')),
|
||||
('protocol', models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol')),
|
||||
('comment', models.CharField(blank=True, max_length=128, null=True, verbose_name='Comment')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
||||
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Domain', verbose_name='Domain')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='domain',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.Domain', verbose_name='Domain'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='assets',
|
||||
field=models.ManyToManyField(blank=True, to='assets.Asset', verbose_name='Assets'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='sudo',
|
||||
field=models.TextField(default='/bin/whoami', verbose_name='Sudo'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='username',
|
||||
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='username',
|
||||
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='username',
|
||||
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='username',
|
||||
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='node',
|
||||
name='value',
|
||||
field=models.CharField(max_length=128, verbose_name='Value'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='protocol',
|
||||
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=128, verbose_name='Protocol'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='login_mode',
|
||||
field=models.CharField(choices=[('auto', 'Automatic login'), ('manual', 'Manually login')], default='auto', max_length=10, verbose_name='Login mode'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='platform',
|
||||
field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Windows2016', 'Windows(2016)'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='protocol',
|
||||
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=16, verbose_name='Protocol'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=migrate_win_to_ssh_protocol,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='adminuser',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default=None, max_length=36, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default=None, max_length=36, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='domain',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default=None, max_length=36, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='gateway',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default=None, max_length=36, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='label',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default=None, max_length=36, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='node',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default=None, max_length=36, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default=None, max_length=36, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='name',
|
||||
field=models.CharField(max_length=128, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='hostname',
|
||||
field=models.CharField(max_length=128, verbose_name='Hostname'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='name',
|
||||
field=models.CharField(max_length=128, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='name',
|
||||
field=models.CharField(max_length=128, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='adminuser',
|
||||
unique_together={('name', 'org_id')},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='cpu_vcpus',
|
||||
field=models.IntegerField(null=True, verbose_name='CPU vcpus'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='asset',
|
||||
unique_together={('org_id', 'hostname')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='gateway',
|
||||
unique_together={('name', 'org_id')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='systemuser',
|
||||
unique_together={('name', 'org_id')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='label',
|
||||
unique_together={('name', 'value', 'org_id')},
|
||||
),
|
||||
]
|
||||
@@ -13,36 +13,36 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
|
||||
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
|
||||
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='domain',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
|
||||
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
|
||||
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='label',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
|
||||
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='node',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
|
||||
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
|
||||
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -16,7 +16,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='CommandFilter',
|
||||
fields=[
|
||||
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=64, verbose_name='Name')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
||||
@@ -32,7 +32,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='CommandFilterRule',
|
||||
fields=[
|
||||
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('type', models.CharField(choices=[('regex', 'Regex'), ('command', 'Command')], default='command', max_length=16, verbose_name='Type')),
|
||||
('priority', models.IntegerField(default=50, help_text='1-100, the lower will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')),
|
||||
|
||||
23
apps/assets/migrations/0024_auto_20181219_1614.py
Normal file
23
apps/assets/migrations/0024_auto_20181219_1614.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.1.4 on 2018-12-19 08:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0023_auto_20181016_1650'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='protocol',
|
||||
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)'), ('vnc', 'vnc')], default='ssh', max_length=128, verbose_name='Protocol'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='protocol',
|
||||
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)'), ('vnc', 'vnc')], default='ssh', max_length=16, verbose_name='Protocol'),
|
||||
),
|
||||
]
|
||||
21
apps/assets/migrations/0025_auto_20190221_1902.py
Normal file
21
apps/assets/migrations/0025_auto_20190221_1902.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-21 11:02
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0024_auto_20181219_1614'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='commandfilter',
|
||||
options={'verbose_name': 'Command filter'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='commandfilterrule',
|
||||
options={'ordering': ('-priority', 'action'), 'verbose_name': 'Command filter rule'},
|
||||
),
|
||||
]
|
||||
43
apps/assets/migrations/0026_auto_20190325_2035.py
Normal file
43
apps/assets/migrations/0026_auto_20190325_2035.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# Generated by Django 2.1.7 on 2019-03-25 12:35
|
||||
|
||||
import assets.models.utils
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0025_auto_20190221_1902'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AuthBook',
|
||||
fields=[
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('username', models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
|
||||
('_password', models.CharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('_private_key', models.TextField(blank=True, max_length=4096, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key')),
|
||||
('_public_key', models.TextField(blank=True, max_length=4096, verbose_name='SSH public key')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True)),
|
||||
('date_updated', models.DateTimeField(auto_now=True)),
|
||||
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
|
||||
('is_latest', models.BooleanField(default=False, verbose_name='Latest version')),
|
||||
('version', models.IntegerField(default=1, verbose_name='Version')),
|
||||
('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Asset', verbose_name='Asset')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'AuthBook',
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='node',
|
||||
options={'ordering': ['key'], 'verbose_name': 'Node'},
|
||||
),
|
||||
]
|
||||
@@ -7,3 +7,4 @@ from .node import *
|
||||
from .asset import *
|
||||
from .cmd_filter import *
|
||||
from .utils import *
|
||||
from .authbook import *
|
||||
|
||||
@@ -13,7 +13,6 @@ from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.cache import cache
|
||||
|
||||
from ..const import ASSET_ADMIN_CONN_CACHE_KEY
|
||||
from .user import AdminUser, SystemUser
|
||||
from orgs.mixins import OrgModelMixin, OrgManager
|
||||
|
||||
@@ -60,78 +59,65 @@ class Asset(OrgModelMixin):
|
||||
('Other', 'Other'),
|
||||
)
|
||||
|
||||
SSH_PROTOCOL = 'ssh'
|
||||
RDP_PROTOCOL = 'rdp'
|
||||
TELNET_PROTOCOL = 'telnet'
|
||||
PROTOCOL_SSH = 'ssh'
|
||||
PROTOCOL_RDP = 'rdp'
|
||||
PROTOCOL_TELNET = 'telnet'
|
||||
PROTOCOL_VNC = 'vnc'
|
||||
PROTOCOL_CHOICES = (
|
||||
(SSH_PROTOCOL, 'ssh'),
|
||||
(RDP_PROTOCOL, 'rdp'),
|
||||
(TELNET_PROTOCOL, 'telnet (beta)'),
|
||||
(PROTOCOL_SSH, 'ssh'),
|
||||
(PROTOCOL_RDP, 'rdp'),
|
||||
(PROTOCOL_TELNET, 'telnet (beta)'),
|
||||
(PROTOCOL_VNC, 'vnc'),
|
||||
)
|
||||
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True)
|
||||
hostname = models.CharField(max_length=128, verbose_name=_('Hostname'))
|
||||
protocol = models.CharField(max_length=128, default=SSH_PROTOCOL, choices=PROTOCOL_CHOICES, verbose_name=_('Protocol'))
|
||||
protocol = models.CharField(max_length=128, default=PROTOCOL_SSH, choices=PROTOCOL_CHOICES, verbose_name=_('Protocol'))
|
||||
port = models.IntegerField(default=22, verbose_name=_('Port'))
|
||||
platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform'))
|
||||
domain = models.ForeignKey("assets.Domain", null=True, blank=True,
|
||||
related_name='assets', verbose_name=_("Domain"),
|
||||
on_delete=models.SET_NULL)
|
||||
nodes = models.ManyToManyField('assets.Node', default=default_node,
|
||||
related_name='assets',
|
||||
verbose_name=_("Nodes"))
|
||||
domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), on_delete=models.SET_NULL)
|
||||
nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes"))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
|
||||
|
||||
# Auth
|
||||
admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT,
|
||||
null=True, verbose_name=_("Admin user"))
|
||||
admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, null=True, verbose_name=_("Admin user"))
|
||||
|
||||
# Some information
|
||||
public_ip = models.GenericIPAddressField(max_length=32, blank=True, null=True, verbose_name=_('Public IP'))
|
||||
number = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Asset number'))
|
||||
|
||||
# Collect
|
||||
vendor = models.CharField(max_length=64, null=True, blank=True,
|
||||
verbose_name=_('Vendor'))
|
||||
model = models.CharField(max_length=54, null=True, blank=True,
|
||||
verbose_name=_('Model'))
|
||||
sn = models.CharField(max_length=128, null=True, blank=True,
|
||||
verbose_name=_('Serial number'))
|
||||
vendor = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Vendor'))
|
||||
model = models.CharField(max_length=54, null=True, blank=True, verbose_name=_('Model'))
|
||||
sn = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Serial number'))
|
||||
|
||||
cpu_model = models.CharField(max_length=64, null=True, blank=True,
|
||||
verbose_name=_('CPU model'))
|
||||
cpu_model = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('CPU model'))
|
||||
cpu_count = models.IntegerField(null=True, verbose_name=_('CPU count'))
|
||||
cpu_cores = models.IntegerField(null=True, verbose_name=_('CPU cores'))
|
||||
cpu_vcpus = models.IntegerField(null=True, verbose_name=_('CPU vcpus'))
|
||||
memory = models.CharField(max_length=64, null=True, blank=True,
|
||||
verbose_name=_('Memory'))
|
||||
disk_total = models.CharField(max_length=1024, null=True, blank=True,
|
||||
verbose_name=_('Disk total'))
|
||||
disk_info = models.CharField(max_length=1024, null=True, blank=True,
|
||||
verbose_name=_('Disk info'))
|
||||
memory = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Memory'))
|
||||
disk_total = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk total'))
|
||||
disk_info = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk info'))
|
||||
|
||||
os = models.CharField(max_length=128, null=True, blank=True,
|
||||
verbose_name=_('OS'))
|
||||
os_version = models.CharField(max_length=16, null=True, blank=True,
|
||||
verbose_name=_('OS version'))
|
||||
os_arch = models.CharField(max_length=16, blank=True, null=True,
|
||||
verbose_name=_('OS arch'))
|
||||
hostname_raw = models.CharField(max_length=128, blank=True, null=True,
|
||||
verbose_name=_('Hostname raw'))
|
||||
os = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('OS'))
|
||||
os_version = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('OS version'))
|
||||
os_arch = models.CharField(max_length=16, blank=True, null=True, verbose_name=_('OS arch'))
|
||||
hostname_raw = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Hostname raw'))
|
||||
|
||||
labels = models.ManyToManyField('assets.Label', blank=True,
|
||||
related_name='assets',
|
||||
verbose_name=_("Labels"))
|
||||
created_by = models.CharField(max_length=32, null=True, blank=True,
|
||||
verbose_name=_('Created by'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, null=True,
|
||||
blank=True,
|
||||
verbose_name=_('Date created'))
|
||||
comment = models.TextField(max_length=128, default='', blank=True,
|
||||
verbose_name=_('Comment'))
|
||||
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
|
||||
created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created'))
|
||||
comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment'))
|
||||
|
||||
objects = OrgManager.from_queryset(AssetQuerySet)()
|
||||
CONNECTIVITY_CACHE_KEY = '_JMS_ASSET_CONNECTIVITY_{}'
|
||||
UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3)
|
||||
CONNECTIVITY_CHOICES = (
|
||||
(UNREACHABLE, _("Unreachable")),
|
||||
(REACHABLE, _('Reachable')),
|
||||
(UNKNOWN, _("Unknown")),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return '{0.hostname}({0.ip})'.format(self)
|
||||
@@ -167,7 +153,7 @@ class Asset(OrgModelMixin):
|
||||
nodes = []
|
||||
for node in self.get_nodes():
|
||||
_nodes = node.get_ancestor(with_self=True)
|
||||
_nodes.append(_nodes)
|
||||
nodes.append(_nodes)
|
||||
if flat:
|
||||
nodes = list(reduce(lambda x, y: set(x) | set(y), nodes))
|
||||
return nodes
|
||||
@@ -197,28 +183,21 @@ class Asset(OrgModelMixin):
|
||||
return ''
|
||||
|
||||
@property
|
||||
def is_connective(self):
|
||||
def connectivity(self):
|
||||
if not self.is_unixlike():
|
||||
return True
|
||||
val = cache.get(ASSET_ADMIN_CONN_CACHE_KEY.format(self.hostname))
|
||||
if val == 1:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return self.REACHABLE
|
||||
key = self.CONNECTIVITY_CACHE_KEY.format(str(self.id))
|
||||
cached = cache.get(key, None)
|
||||
return cached if cached is not None else self.UNKNOWN
|
||||
|
||||
def to_json(self):
|
||||
info = {
|
||||
'id': self.id,
|
||||
'hostname': self.hostname,
|
||||
'ip': self.ip,
|
||||
'port': self.port,
|
||||
}
|
||||
if self.domain and self.domain.gateway_set.all():
|
||||
info["gateways"] = [d.id for d in self.domain.gateway_set.all()]
|
||||
return info
|
||||
@connectivity.setter
|
||||
def connectivity(self, value):
|
||||
key = self.CONNECTIVITY_CACHE_KEY.format(str(self.id))
|
||||
cache.set(key, value, 3600*2)
|
||||
|
||||
def get_auth_info(self):
|
||||
if self.admin_user:
|
||||
self.admin_user.load_specific_asset_auth(self)
|
||||
return {
|
||||
'username': self.admin_user.username,
|
||||
'password': self.admin_user.password,
|
||||
@@ -236,15 +215,25 @@ class Asset(OrgModelMixin):
|
||||
fake_node.is_node = False
|
||||
return fake_node
|
||||
|
||||
def to_json(self):
|
||||
info = {
|
||||
'id': self.id,
|
||||
'hostname': self.hostname,
|
||||
'ip': self.ip,
|
||||
'port': self.port,
|
||||
}
|
||||
if self.domain and self.domain.gateway_set.all():
|
||||
info["gateways"] = [d.id for d in self.domain.gateway_set.all()]
|
||||
return info
|
||||
|
||||
def _to_secret_json(self):
|
||||
"""
|
||||
Ansible use it create inventory, First using asset user,
|
||||
otherwise using cluster admin user
|
||||
|
||||
Ansible use it create inventory
|
||||
Todo: May be move to ops implements it
|
||||
"""
|
||||
data = self.to_json()
|
||||
if self.admin_user:
|
||||
self.admin_user.load_specific_asset_auth(self)
|
||||
admin_user = self.admin_user
|
||||
data.update({
|
||||
'username': admin_user.username,
|
||||
@@ -255,6 +244,36 @@ class Asset(OrgModelMixin):
|
||||
})
|
||||
return data
|
||||
|
||||
def as_tree_node(self, parent_node):
|
||||
from common.tree import TreeNode
|
||||
icon_skin = 'file'
|
||||
if self.platform.lower() == 'windows':
|
||||
icon_skin = 'windows'
|
||||
elif self.platform.lower() == 'linux':
|
||||
icon_skin = 'linux'
|
||||
data = {
|
||||
'id': str(self.id),
|
||||
'name': self.hostname,
|
||||
'title': self.ip,
|
||||
'pId': parent_node.key,
|
||||
'isParent': False,
|
||||
'open': False,
|
||||
'iconSkin': icon_skin,
|
||||
'meta': {
|
||||
'type': 'asset',
|
||||
'asset': {
|
||||
'id': self.id,
|
||||
'hostname': self.hostname,
|
||||
'ip': self.ip,
|
||||
'port': self.port,
|
||||
'platform': self.platform,
|
||||
'protocol': self.protocol,
|
||||
}
|
||||
}
|
||||
}
|
||||
tree_node = TreeNode(**data)
|
||||
return tree_node
|
||||
|
||||
class Meta:
|
||||
unique_together = [('org_id', 'hostname')]
|
||||
verbose_name = _("Asset")
|
||||
|
||||
93
apps/assets/models/authbook.py
Normal file
93
apps/assets/models/authbook.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.cache import cache
|
||||
|
||||
from orgs.mixins import OrgManager
|
||||
|
||||
from .base import AssetUser
|
||||
from ..const import ASSET_USER_CONN_CACHE_KEY
|
||||
|
||||
__all__ = ['AuthBook']
|
||||
|
||||
|
||||
class AuthBookQuerySet(models.QuerySet):
|
||||
|
||||
def latest_version(self):
|
||||
return self.filter(is_latest=True)
|
||||
|
||||
|
||||
class AuthBookManager(OrgManager):
|
||||
pass
|
||||
|
||||
|
||||
class AuthBook(AssetUser):
|
||||
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset'))
|
||||
is_latest = models.BooleanField(default=False, verbose_name=_('Latest version'))
|
||||
version = models.IntegerField(default=1, verbose_name=_('Version'))
|
||||
|
||||
objects = AuthBookManager.from_queryset(AuthBookQuerySet)()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('AuthBook')
|
||||
|
||||
def _set_latest(self):
|
||||
self._remove_pre_obj_latest()
|
||||
self.is_latest = True
|
||||
self.save()
|
||||
|
||||
def _get_pre_obj(self):
|
||||
pre_obj = self.__class__.objects.filter(
|
||||
username=self.username, asset=self.asset).latest_version().first()
|
||||
return pre_obj
|
||||
|
||||
def _remove_pre_obj_latest(self):
|
||||
pre_obj = self._get_pre_obj()
|
||||
if pre_obj:
|
||||
pre_obj.is_latest = False
|
||||
pre_obj.save()
|
||||
|
||||
def _set_version(self):
|
||||
pre_obj = self._get_pre_obj()
|
||||
if pre_obj:
|
||||
self.version = pre_obj.version + 1
|
||||
else:
|
||||
self.version = 1
|
||||
self.save()
|
||||
|
||||
def set_version_and_latest(self):
|
||||
self._set_version()
|
||||
self._set_latest()
|
||||
|
||||
@property
|
||||
def _conn_cache_key(self):
|
||||
return ASSET_USER_CONN_CACHE_KEY.format(self.id, self.asset.id)
|
||||
|
||||
@property
|
||||
def connectivity(self):
|
||||
value = cache.get(self._conn_cache_key, self.UNKNOWN)
|
||||
return value
|
||||
|
||||
@connectivity.setter
|
||||
def connectivity(self, value):
|
||||
_connectivity = self.UNKNOWN
|
||||
|
||||
for host in value.get('dark', {}).keys():
|
||||
if host == self.asset.hostname:
|
||||
_connectivity = self.UNREACHABLE
|
||||
|
||||
for host in value.get('contacted', {}).keys():
|
||||
if host == self.asset.hostname:
|
||||
_connectivity = self.REACHABLE
|
||||
|
||||
cache.set(self._conn_cache_key, _connectivity, 3600)
|
||||
|
||||
@property
|
||||
def keyword(self):
|
||||
return {'username': self.username, 'asset': self.asset}
|
||||
|
||||
def __str__(self):
|
||||
return '{}@{}'.format(self.username, self.asset)
|
||||
|
||||
@@ -9,13 +9,17 @@ from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import get_signer, ssh_key_string_to_obj, ssh_key_gen
|
||||
from common.utils import (
|
||||
get_signer, ssh_key_string_to_obj, ssh_key_gen, get_logger
|
||||
)
|
||||
from common.validators import alphanumeric
|
||||
from orgs.mixins import OrgModelMixin
|
||||
from .utils import private_key_validator
|
||||
|
||||
signer = get_signer()
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class AssetUser(OrgModelMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
@@ -29,6 +33,13 @@ class AssetUser(OrgModelMixin):
|
||||
date_updated = models.DateTimeField(auto_now=True)
|
||||
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
|
||||
|
||||
UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3)
|
||||
CONNECTIVITY_CHOICES = (
|
||||
(UNREACHABLE, _("Unreachable")),
|
||||
(REACHABLE, _('Reachable')),
|
||||
(UNKNOWN, _("Unknown")),
|
||||
)
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
if self._password:
|
||||
@@ -38,8 +49,8 @@ class AssetUser(OrgModelMixin):
|
||||
|
||||
@password.setter
|
||||
def password(self, password_raw):
|
||||
raise AttributeError("Using set_auth do that")
|
||||
# self._password = signer.sign(password_raw)
|
||||
# raise AttributeError("Using set_auth do that")
|
||||
self._password = signer.sign(password_raw)
|
||||
|
||||
@property
|
||||
def private_key(self):
|
||||
@@ -48,8 +59,8 @@ class AssetUser(OrgModelMixin):
|
||||
|
||||
@private_key.setter
|
||||
def private_key(self, private_key_raw):
|
||||
raise AttributeError("Using set_auth do that")
|
||||
# self._private_key = signer.sign(private_key_raw)
|
||||
# raise AttributeError("Using set_auth do that")
|
||||
self._private_key = signer.sign(private_key_raw)
|
||||
|
||||
@property
|
||||
def private_key_obj(self):
|
||||
@@ -81,6 +92,11 @@ class AssetUser(OrgModelMixin):
|
||||
else:
|
||||
return None
|
||||
|
||||
@public_key.setter
|
||||
def public_key(self, public_key_raw):
|
||||
# raise AttributeError("Using set_auth do that")
|
||||
self._public_key = signer.sign(public_key_raw)
|
||||
|
||||
@property
|
||||
def public_key_obj(self):
|
||||
if self.public_key:
|
||||
@@ -108,6 +124,25 @@ class AssetUser(OrgModelMixin):
|
||||
def get_auth(self, asset=None):
|
||||
pass
|
||||
|
||||
def load_specific_asset_auth(self, asset):
|
||||
from ..backends.multi import AssetUserManager
|
||||
try:
|
||||
other = AssetUserManager.get(username=self.username, asset=asset)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
else:
|
||||
self._merge_auth(other)
|
||||
|
||||
def _merge_auth(self, other):
|
||||
if not other:
|
||||
return
|
||||
if other.password:
|
||||
self.password = other.password
|
||||
if other.public_key:
|
||||
self.public_key = other.public_key
|
||||
if other.private_key:
|
||||
self.private_key = other.private_key
|
||||
|
||||
def clear_auth(self):
|
||||
self._password = ''
|
||||
self._private_key = ''
|
||||
|
||||
@@ -27,6 +27,9 @@ class CommandFilter(OrgModelMixin):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Command filter")
|
||||
|
||||
|
||||
class CommandFilterRule(OrgModelMixin):
|
||||
TYPE_REGEX = 'regex'
|
||||
@@ -58,6 +61,7 @@ class CommandFilterRule(OrgModelMixin):
|
||||
|
||||
class Meta:
|
||||
ordering = ('-priority', 'action')
|
||||
verbose_name = _("Command filter rule")
|
||||
|
||||
@property
|
||||
def _pattern(self):
|
||||
|
||||
@@ -40,15 +40,15 @@ class Domain(OrgModelMixin):
|
||||
|
||||
|
||||
class Gateway(AssetUser):
|
||||
SSH_PROTOCOL = 'ssh'
|
||||
RDP_PROTOCOL = 'rdp'
|
||||
PROTOCOL_SSH = 'ssh'
|
||||
PROTOCOL_RDP = 'rdp'
|
||||
PROTOCOL_CHOICES = (
|
||||
(SSH_PROTOCOL, 'ssh'),
|
||||
(RDP_PROTOCOL, 'rdp'),
|
||||
(PROTOCOL_SSH, 'ssh'),
|
||||
(PROTOCOL_RDP, 'rdp'),
|
||||
)
|
||||
ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True)
|
||||
port = models.IntegerField(default=22, verbose_name=_('Port'))
|
||||
protocol = models.CharField(choices=PROTOCOL_CHOICES, max_length=16, default=SSH_PROTOCOL, verbose_name=_("Protocol"))
|
||||
protocol = models.CharField(choices=PROTOCOL_CHOICES, max_length=16, default=PROTOCOL_SSH, verbose_name=_("Protocol"))
|
||||
domain = models.ForeignKey(Domain, on_delete=models.CASCADE, verbose_name=_("Domain"))
|
||||
comment = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Comment"))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
||||
@@ -60,7 +60,9 @@ class Gateway(AssetUser):
|
||||
unique_together = [('name', 'org_id')]
|
||||
verbose_name = _("Gateway")
|
||||
|
||||
def test_connective(self):
|
||||
def test_connective(self, local_port=None):
|
||||
if local_port is None:
|
||||
local_port = self.port
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
proxy = paramiko.SSHClient()
|
||||
@@ -76,12 +78,11 @@ class Gateway(AssetUser):
|
||||
paramiko.SSHException) as e:
|
||||
return False, str(e)
|
||||
|
||||
sock = proxy.get_transport().open_channel(
|
||||
'direct-tcpip', ('127.0.0.1', self.port), ('127.0.0.1', 0)
|
||||
)
|
||||
|
||||
try:
|
||||
client.connect("127.0.0.1", port=self.port,
|
||||
sock = proxy.get_transport().open_channel(
|
||||
'direct-tcpip', ('127.0.0.1', local_port), ('127.0.0.1', 0)
|
||||
)
|
||||
client.connect("127.0.0.1", port=local_port,
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
key_filename=self.private_key_file,
|
||||
|
||||
@@ -5,6 +5,7 @@ import uuid
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ugettext
|
||||
from django.core.cache import cache
|
||||
|
||||
from orgs.mixins import OrgModelMixin
|
||||
@@ -22,10 +23,13 @@ class Node(OrgModelMixin):
|
||||
date_create = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
is_node = True
|
||||
_full_value_cache_key_prefix = '_NODE_VALUE_{}'
|
||||
_assets_amount = None
|
||||
_full_value_cache_key = '_NODE_VALUE_{}'
|
||||
_assets_amount_cache_key = '_NODE_ASSETS_AMOUNT_{}'
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Node")
|
||||
ordering = ['key']
|
||||
|
||||
def __str__(self):
|
||||
return self.full_value
|
||||
@@ -49,30 +53,65 @@ class Node(OrgModelMixin):
|
||||
def name(self):
|
||||
return self.value
|
||||
|
||||
@property
|
||||
def assets_amount(self):
|
||||
"""
|
||||
获取节点下所有资产数量速度太慢,所以需要重写,使用cache等方案
|
||||
:return:
|
||||
"""
|
||||
if self._assets_amount is not None:
|
||||
return self._assets_amount
|
||||
cache_key = self._assets_amount_cache_key.format(self.key)
|
||||
cached = cache.get(cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
assets_amount = self.get_all_assets().count()
|
||||
cache.set(cache_key, assets_amount, 3600)
|
||||
return assets_amount
|
||||
|
||||
@assets_amount.setter
|
||||
def assets_amount(self, value):
|
||||
self._assets_amount = value
|
||||
|
||||
def expire_assets_amount(self):
|
||||
ancestor_keys = self.get_ancestor_keys(with_self=True)
|
||||
cache_keys = [self._assets_amount_cache_key.format(k) for k in ancestor_keys]
|
||||
cache.delete_many(cache_keys)
|
||||
|
||||
@classmethod
|
||||
def expire_nodes_assets_amount(cls, nodes=None):
|
||||
if nodes:
|
||||
for node in nodes:
|
||||
node.expire_assets_amount()
|
||||
return
|
||||
key = cls._assets_amount_cache_key.format('*')
|
||||
cache.delete_pattern(key)
|
||||
|
||||
@property
|
||||
def full_value(self):
|
||||
key = self._full_value_cache_key_prefix.format(self.key)
|
||||
key = self._full_value_cache_key.format(self.key)
|
||||
cached = cache.get(key)
|
||||
if cached:
|
||||
return cached
|
||||
value = self.get_full_value()
|
||||
self.cache_full_value(value)
|
||||
return value
|
||||
|
||||
def get_full_value(self):
|
||||
# ancestor = [a.value for a in self.get_ancestor(with_self=True)]
|
||||
if self.is_root():
|
||||
return self.value
|
||||
parent_full_value = self.parent.full_value
|
||||
value = parent_full_value + ' / ' + self.value
|
||||
key = self._full_value_cache_key.format(self.key)
|
||||
cache.set(key, value, 3600)
|
||||
return value
|
||||
|
||||
def cache_full_value(self, value):
|
||||
key = self._full_value_cache_key_prefix.format(self.key)
|
||||
cache.set(key, value, 3600)
|
||||
|
||||
def expire_full_value(self):
|
||||
key = self._full_value_cache_key_prefix.format(self.key)
|
||||
key = self._full_value_cache_key.format(self.key)
|
||||
cache.delete_pattern(key+'*')
|
||||
|
||||
@classmethod
|
||||
def expire_nodes_full_value(cls, nodes=None):
|
||||
if nodes:
|
||||
for node in nodes:
|
||||
node.expire_full_value()
|
||||
return
|
||||
key = cls._full_value_cache_key.format('*')
|
||||
cache.delete_pattern(key+'*')
|
||||
|
||||
@property
|
||||
@@ -85,10 +124,21 @@ class Node(OrgModelMixin):
|
||||
self.save()
|
||||
return "{}:{}".format(self.key, mark)
|
||||
|
||||
def create_child(self, value):
|
||||
def get_next_child_preset_name(self):
|
||||
name = ugettext("New node")
|
||||
values = [
|
||||
child.value[child.value.rfind(' '):]
|
||||
for child in self.get_children()
|
||||
if child.value.startswith(name)
|
||||
]
|
||||
values = [int(value) for value in values if value.strip().isdigit()]
|
||||
count = max(values) + 1 if values else 1
|
||||
return '{} {}'.format(name, count)
|
||||
|
||||
def create_child(self, value, _id=None):
|
||||
with transaction.atomic():
|
||||
child_key = self.get_next_child_key()
|
||||
child = self.__class__.objects.create(key=child_key, value=value)
|
||||
child = self.__class__.objects.create(id=_id, key=child_key, value=value)
|
||||
return child
|
||||
|
||||
def get_children(self, with_self=False):
|
||||
@@ -98,7 +148,7 @@ class Node(OrgModelMixin):
|
||||
)
|
||||
|
||||
def get_all_children(self, with_self=False):
|
||||
pattern = r'^{0}$|^{0}:' if with_self else r'^{0}'
|
||||
pattern = r'^{0}$|^{0}:' if with_self else r'^{0}:'
|
||||
return self.__class__.objects.filter(
|
||||
key__regex=pattern.format(self.key)
|
||||
)
|
||||
@@ -134,7 +184,7 @@ class Node(OrgModelMixin):
|
||||
pattern = r'^{0}$|^{0}:'.format(self.key)
|
||||
args = []
|
||||
kwargs = {}
|
||||
if self.is_default_node():
|
||||
if self.is_root():
|
||||
args.append(Q(nodes__key__regex=pattern) | Q(nodes=None))
|
||||
else:
|
||||
kwargs['nodes__key__regex'] = pattern
|
||||
@@ -182,17 +232,18 @@ class Node(OrgModelMixin):
|
||||
child.save()
|
||||
self.save()
|
||||
|
||||
def get_ancestor(self, with_self=False):
|
||||
if self.is_root():
|
||||
root = self.__class__.root()
|
||||
return [root]
|
||||
_key = self.key.split(':')
|
||||
def get_ancestor_keys(self, with_self=False):
|
||||
parent_keys = []
|
||||
key_list = self.key.split(":")
|
||||
if not with_self:
|
||||
_key.pop()
|
||||
ancestor_keys = []
|
||||
for i in range(len(_key)):
|
||||
ancestor_keys.append(':'.join(_key))
|
||||
_key.pop()
|
||||
key_list.pop()
|
||||
for i in range(len(key_list)):
|
||||
parent_keys.append(":".join(key_list))
|
||||
key_list.pop()
|
||||
return parent_keys
|
||||
|
||||
def get_ancestor(self, with_self=False):
|
||||
ancestor_keys = self.get_ancestor_keys(with_self=with_self)
|
||||
ancestor = self.__class__.objects.filter(
|
||||
key__in=ancestor_keys
|
||||
).order_by('key')
|
||||
@@ -225,11 +276,28 @@ class Node(OrgModelMixin):
|
||||
@classmethod
|
||||
def default_node(cls):
|
||||
defaults = {'value': 'Default'}
|
||||
return cls.objects.get_or_create(defaults=defaults, key='1')
|
||||
obj, created = cls.objects.get_or_create(defaults=defaults, key='1')
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def get_tree_name_ref(cls):
|
||||
pass
|
||||
def as_tree_node(self):
|
||||
from common.tree import TreeNode
|
||||
from ..serializers import NodeSerializer
|
||||
name = '{} ({})'.format(self.value, self.assets_amount)
|
||||
node_serializer = NodeSerializer(instance=self)
|
||||
data = {
|
||||
'id': self.key,
|
||||
'name': name,
|
||||
'title': name,
|
||||
'pId': self.parent_key,
|
||||
'isParent': True,
|
||||
'open': self.is_root(),
|
||||
'meta': {
|
||||
'node': node_serializer.data,
|
||||
'type': 'node'
|
||||
}
|
||||
}
|
||||
tree_node = TreeNode(**data)
|
||||
return tree_node
|
||||
|
||||
@classmethod
|
||||
def generate_fake(cls, count=100):
|
||||
|
||||
@@ -14,7 +14,7 @@ from ..const import SYSTEM_USER_CONN_CACHE_KEY
|
||||
from .base import AssetUser
|
||||
|
||||
|
||||
__all__ = ['AdminUser', 'SystemUser',]
|
||||
__all__ = ['AdminUser', 'SystemUser']
|
||||
logger = logging.getLogger(__name__)
|
||||
signer = get_signer()
|
||||
|
||||
@@ -31,6 +31,7 @@ class AdminUser(AssetUser):
|
||||
become_method = models.CharField(choices=BECOME_METHOD_CHOICES, default='sudo', max_length=4)
|
||||
become_user = models.CharField(default='root', max_length=64)
|
||||
_become_pass = models.CharField(default='', max_length=128)
|
||||
CONNECTIVE_CACHE_KEY = '_JMS_ADMIN_USER_CONNECTIVE_{}'
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -67,6 +68,23 @@ class AdminUser(AssetUser):
|
||||
def assets_amount(self):
|
||||
return self.get_related_assets().count()
|
||||
|
||||
@property
|
||||
def connectivity(self):
|
||||
from .asset import Asset
|
||||
assets = self.get_related_assets().values_list('id', 'hostname', flat=True)
|
||||
data = {
|
||||
'unreachable': [],
|
||||
'reachable': [],
|
||||
}
|
||||
for asset_id, hostname in assets:
|
||||
key = Asset.CONNECTIVITY_CACHE_KEY.format(str(self.id))
|
||||
value = cache.get(key, Asset.UNKNOWN)
|
||||
if value == Asset.REACHABLE:
|
||||
data['reachable'].append(hostname)
|
||||
elif value == Asset.UNREACHABLE:
|
||||
data['unreachable'].append(hostname)
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
unique_together = [('name', 'org_id')]
|
||||
@@ -94,34 +112,36 @@ class AdminUser(AssetUser):
|
||||
|
||||
|
||||
class SystemUser(AssetUser):
|
||||
SSH_PROTOCOL = 'ssh'
|
||||
RDP_PROTOCOL = 'rdp'
|
||||
TELNET_PROTOCOL = 'telnet'
|
||||
PROTOCOL_SSH = 'ssh'
|
||||
PROTOCOL_RDP = 'rdp'
|
||||
PROTOCOL_TELNET = 'telnet'
|
||||
PROTOCOL_VNC = 'vnc'
|
||||
PROTOCOL_CHOICES = (
|
||||
(SSH_PROTOCOL, 'ssh'),
|
||||
(RDP_PROTOCOL, 'rdp'),
|
||||
(TELNET_PROTOCOL, 'telnet (beta)'),
|
||||
(PROTOCOL_SSH, 'ssh'),
|
||||
(PROTOCOL_RDP, 'rdp'),
|
||||
(PROTOCOL_TELNET, 'telnet (beta)'),
|
||||
(PROTOCOL_VNC, 'vnc'),
|
||||
)
|
||||
|
||||
AUTO_LOGIN = 'auto'
|
||||
MANUAL_LOGIN = 'manual'
|
||||
LOGIN_AUTO = 'auto'
|
||||
LOGIN_MANUAL = 'manual'
|
||||
LOGIN_MODE_CHOICES = (
|
||||
(AUTO_LOGIN, _('Automatic login')),
|
||||
(MANUAL_LOGIN, _('Manually login'))
|
||||
(LOGIN_AUTO, _('Automatic login')),
|
||||
(LOGIN_MANUAL, _('Manually login'))
|
||||
)
|
||||
|
||||
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
|
||||
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
|
||||
priority = models.IntegerField(default=20, verbose_name=_("Priority"),
|
||||
validators=[MinValueValidator(1), MaxValueValidator(100)])
|
||||
priority = models.IntegerField(default=20, verbose_name=_("Priority"), validators=[MinValueValidator(1), MaxValueValidator(100)])
|
||||
protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol'))
|
||||
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
|
||||
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
|
||||
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
|
||||
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=AUTO_LOGIN, max_length=10, verbose_name=_('Login mode'))
|
||||
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode'))
|
||||
cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True)
|
||||
|
||||
cache_key = "__SYSTEM_USER_CACHED_{}"
|
||||
SYSTEM_USER_CACHE_KEY = "__SYSTEM_USER_CACHED_{}"
|
||||
CONNECTIVE_CACHE_KEY = '_JMS_SYSTEM_USER_CONNECTIVE_{}'
|
||||
|
||||
def __str__(self):
|
||||
return '{0.name}({0.username})'.format(self)
|
||||
@@ -136,34 +156,61 @@ class SystemUser(AssetUser):
|
||||
'auto_push': self.auto_push,
|
||||
}
|
||||
|
||||
def get_assets(self):
|
||||
def get_related_assets(self):
|
||||
assets = set(self.assets.all())
|
||||
return assets
|
||||
|
||||
@property
|
||||
def assets_connective(self):
|
||||
_result = cache.get(SYSTEM_USER_CONN_CACHE_KEY.format(self.name), {})
|
||||
return _result
|
||||
def connectivity(self):
|
||||
cache_key = self.CONNECTIVE_CACHE_KEY.format(str(self.id))
|
||||
value = cache.get(cache_key, None)
|
||||
if not value or 'unreachable' not in value:
|
||||
return {'unreachable': [], 'reachable': []}
|
||||
else:
|
||||
return value
|
||||
|
||||
@connectivity.setter
|
||||
def connectivity(self, value):
|
||||
data = self.connectivity
|
||||
unreachable = data['unreachable']
|
||||
reachable = data['reachable']
|
||||
|
||||
for host in value.get('dark', {}).keys():
|
||||
if host not in unreachable:
|
||||
unreachable.append(host)
|
||||
if host in reachable:
|
||||
reachable.remove(host)
|
||||
for host in value.get('contacted'):
|
||||
if host not in reachable:
|
||||
reachable.append(host)
|
||||
if host in unreachable:
|
||||
unreachable.remove(host)
|
||||
cache_key = self.CONNECTIVE_CACHE_KEY.format(str(self.id))
|
||||
cache.set(cache_key, data, 3600)
|
||||
|
||||
@property
|
||||
def unreachable_assets(self):
|
||||
return list(self.assets_connective.get('dark', {}).keys())
|
||||
def assets_unreachable(self):
|
||||
return self.connectivity.get('unreachable')
|
||||
|
||||
@property
|
||||
def reachable_assets(self):
|
||||
return self.assets_connective.get('contacted', [])
|
||||
def assets_reachable(self):
|
||||
return self.connectivity.get('reachable')
|
||||
|
||||
@property
|
||||
def login_mode_display(self):
|
||||
return self.get_login_mode_display()
|
||||
|
||||
def is_need_push(self):
|
||||
if self.auto_push and self.protocol == self.__class__.SSH_PROTOCOL:
|
||||
if self.auto_push and self.protocol == self.PROTOCOL_SSH:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def set_cache(self):
|
||||
cache.set(self.cache_key.format(self.id), self, 3600)
|
||||
cache.set(self.SYSTEM_USER_CACHE_KEY.format(self.id), self, 3600)
|
||||
|
||||
def expire_cache(self):
|
||||
cache.delete(self.cache_key.format(self.id))
|
||||
cache.delete(self.SYSTEM_USER_CACHE_KEY.format(self.id))
|
||||
|
||||
@property
|
||||
def cmd_filter_rules(self):
|
||||
@@ -184,7 +231,7 @@ class SystemUser(AssetUser):
|
||||
|
||||
@classmethod
|
||||
def get_system_user_by_id_or_cached(cls, sid):
|
||||
cached = cache.get(cls.cache_key.format(sid))
|
||||
cached = cache.get(cls.SYSTEM_USER_CACHE_KEY.format(sid))
|
||||
if cached:
|
||||
return cached
|
||||
try:
|
||||
|
||||
@@ -8,3 +8,4 @@ from .system_user import *
|
||||
from .node import *
|
||||
from .domain import *
|
||||
from .cmd_filter import *
|
||||
from .asset_user import *
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
from django.core.cache import cache
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
|
||||
from ..models import Node, AdminUser
|
||||
from ..const import ADMIN_USER_CONN_CACHE_KEY
|
||||
|
||||
@@ -18,6 +20,7 @@ class AdminUserSerializer(serializers.ModelSerializer):
|
||||
reachable_amount = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
model = AdminUser
|
||||
fields = '__all__'
|
||||
|
||||
@@ -58,7 +61,7 @@ class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer):
|
||||
管理用户更新关联到的集群
|
||||
"""
|
||||
nodes = serializers.PrimaryKeyRelatedField(
|
||||
many=True, queryset = Node.objects.all()
|
||||
many=True, queryset=Node.objects.all()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -66,4 +69,5 @@ class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'nodes']
|
||||
|
||||
|
||||
|
||||
class TaskIDSerializer(serializers.Serializer):
|
||||
task = serializers.CharField(read_only=True)
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import serializers
|
||||
from rest_framework_bulk.serializers import BulkListSerializer
|
||||
|
||||
from common.mixins import BulkSerializerMixin
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
from ..models import Asset
|
||||
from .system_user import AssetSystemUserSerializer
|
||||
|
||||
__all__ = [
|
||||
'AssetSerializer', 'AssetGrantedSerializer', 'MyAssetGrantedSerializer',
|
||||
'AssetAsNodeSerializer', 'AssetSimpleSerializer',
|
||||
]
|
||||
|
||||
|
||||
@@ -18,18 +19,31 @@ class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer):
|
||||
"""
|
||||
class Meta:
|
||||
model = Asset
|
||||
list_serializer_class = BulkListSerializer
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = '__all__'
|
||||
validators = []
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset.prefetch_related('labels', 'nodes')\
|
||||
.select_related('admin_user')
|
||||
return queryset
|
||||
|
||||
def get_field_names(self, declared_fields, info):
|
||||
fields = super().get_field_names(declared_fields, info)
|
||||
fields.extend([
|
||||
'hardware_info', 'is_connective', 'org_name'
|
||||
'hardware_info', 'connectivity', 'org_name'
|
||||
])
|
||||
return fields
|
||||
|
||||
|
||||
class AssetAsNodeSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = ['id', 'hostname', 'ip', 'port', 'platform', 'protocol']
|
||||
|
||||
|
||||
class AssetGrantedSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
被授权资产的数据结构
|
||||
@@ -64,3 +78,9 @@ class MyAssetGrantedSerializer(AssetGrantedSerializer):
|
||||
"is_active", "system_users_join", "org_name",
|
||||
"os", "platform", "comment", "org_id", "protocol"
|
||||
)
|
||||
|
||||
|
||||
class AssetSimpleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = ['id', 'hostname', 'port', 'ip', 'connectivity']
|
||||
|
||||
65
apps/assets/serializers/asset_user.py
Normal file
65
apps/assets/serializers/asset_user.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..models import AuthBook
|
||||
from ..backends.multi import AssetUserManager
|
||||
|
||||
__all__ = [
|
||||
'AssetUserSerializer', 'AssetUserAuthInfoSerializer',
|
||||
]
|
||||
|
||||
|
||||
class AssetUserSerializer(serializers.ModelSerializer):
|
||||
|
||||
password = serializers.CharField(
|
||||
max_length=256, allow_blank=True, allow_null=True, write_only=True,
|
||||
required=False, help_text=_('Password')
|
||||
)
|
||||
public_key = serializers.CharField(
|
||||
max_length=4096, allow_blank=True, allow_null=True, write_only=True,
|
||||
required=False, help_text=_('Public key')
|
||||
)
|
||||
private_key = serializers.CharField(
|
||||
max_length=4096, allow_blank=True, allow_null=True, write_only=True,
|
||||
required=False, help_text=_('Private key')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = AuthBook
|
||||
read_only_fields = (
|
||||
'date_created', 'date_updated', 'created_by',
|
||||
'is_latest', 'version', 'connectivity',
|
||||
)
|
||||
fields = '__all__'
|
||||
extra_kwargs = {
|
||||
'username': {'required': True}
|
||||
}
|
||||
|
||||
def get_field_names(self, declared_fields, info):
|
||||
fields = super().get_field_names(declared_fields, info)
|
||||
fields = [f for f in fields if not f.startswith('_') and f != 'id']
|
||||
fields.extend(['connectivity'])
|
||||
return fields
|
||||
|
||||
def create(self, validated_data):
|
||||
kwargs = {
|
||||
'name': validated_data.get('name'),
|
||||
'username': validated_data.get('username'),
|
||||
'asset': validated_data.get('asset'),
|
||||
'comment': validated_data.get('comment', ''),
|
||||
'org_id': validated_data.get('org_id', ''),
|
||||
'password': validated_data.get('password'),
|
||||
'public_key': validated_data.get('public_key'),
|
||||
'private_key': validated_data.get('private_key')
|
||||
}
|
||||
instance = AssetUserManager.create(**kwargs)
|
||||
return instance
|
||||
|
||||
|
||||
class AssetUserAuthInfoSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = AuthBook
|
||||
fields = ['password', 'private_key', 'public_key']
|
||||
@@ -3,6 +3,7 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.fields import ChoiceDisplayField
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
from ..models import CommandFilter, CommandFilterRule, SystemUser
|
||||
|
||||
|
||||
@@ -12,6 +13,7 @@ class CommandFilterSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CommandFilter
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
@@ -21,3 +23,4 @@ class CommandFilterRuleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CommandFilterRule
|
||||
fields = '__all__'
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
|
||||
from ..models import Domain, Gateway
|
||||
|
||||
|
||||
@@ -12,6 +14,7 @@ class DomainSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Domain
|
||||
fields = '__all__'
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
|
||||
@staticmethod
|
||||
def get_asset_count(obj):
|
||||
@@ -25,6 +28,7 @@ class DomainSerializer(serializers.ModelSerializer):
|
||||
class GatewaySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Gateway
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = [
|
||||
'id', 'name', 'ip', 'port', 'protocol', 'username',
|
||||
'domain', 'is_active', 'date_created', 'date_updated',
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import serializers
|
||||
from rest_framework_bulk.serializers import BulkListSerializer
|
||||
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
|
||||
from ..models import Label
|
||||
|
||||
@@ -12,7 +13,7 @@ class LabelSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Label
|
||||
fields = '__all__'
|
||||
list_serializer_class = BulkListSerializer
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
|
||||
@staticmethod
|
||||
def get_asset_count(obj):
|
||||
|
||||
@@ -1,91 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from rest_framework import serializers
|
||||
from rest_framework_bulk.serializers import BulkListSerializer
|
||||
|
||||
from common.mixins import BulkSerializerMixin
|
||||
from ..models import Asset, Node
|
||||
from .asset import AssetGrantedSerializer
|
||||
|
||||
|
||||
__all__ = [
|
||||
'NodeSerializer', "NodeGrantedSerializer", "NodeAddChildrenSerializer",
|
||||
'NodeSerializer', "NodeAddChildrenSerializer",
|
||||
"NodeAssetsSerializer",
|
||||
]
|
||||
|
||||
|
||||
class NodeGrantedSerializer(BulkSerializerMixin, serializers.ModelSerializer):
|
||||
"""
|
||||
授权资产组
|
||||
"""
|
||||
assets_granted = AssetGrantedSerializer(many=True, read_only=True)
|
||||
assets_amount = serializers.SerializerMethodField()
|
||||
parent = serializers.SerializerMethodField()
|
||||
name = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Node
|
||||
fields = [
|
||||
'id', 'key', 'name', 'value', 'parent',
|
||||
'assets_granted', 'assets_amount', 'org_id',
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_assets_amount(obj):
|
||||
return len(obj.assets_granted)
|
||||
|
||||
@staticmethod
|
||||
def get_name(obj):
|
||||
return obj.name
|
||||
|
||||
@staticmethod
|
||||
def get_parent(obj):
|
||||
return obj.parent.id
|
||||
|
||||
|
||||
class NodeSerializer(serializers.ModelSerializer):
|
||||
assets_amount = serializers.SerializerMethodField()
|
||||
tree_id = serializers.SerializerMethodField()
|
||||
tree_parent = serializers.SerializerMethodField()
|
||||
assets_amount = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Node
|
||||
fields = [
|
||||
'id', 'key', 'value', 'assets_amount',
|
||||
'is_node', 'org_id', 'tree_id', 'tree_parent',
|
||||
'id', 'key', 'value', 'assets_amount', 'org_id',
|
||||
]
|
||||
read_only_fields = [
|
||||
'key', 'assets_amount', 'org_id',
|
||||
]
|
||||
list_serializer_class = BulkListSerializer
|
||||
|
||||
def validate(self, data):
|
||||
value = data.get('value')
|
||||
def validate_value(self, data):
|
||||
instance = self.instance if self.instance else Node.root()
|
||||
children = instance.parent.get_children().exclude(key=instance.key)
|
||||
values = [child.value for child in children]
|
||||
if value in values:
|
||||
if data in values:
|
||||
raise serializers.ValidationError(
|
||||
'The same level node name cannot be the same'
|
||||
)
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def get_assets_amount(obj):
|
||||
if hasattr(obj, 'assets_amount'):
|
||||
return obj.assets_amount
|
||||
return obj.get_all_assets().count()
|
||||
|
||||
@staticmethod
|
||||
def get_tree_id(obj):
|
||||
return obj.key
|
||||
|
||||
@staticmethod
|
||||
def get_tree_parent(obj):
|
||||
return obj.parent_key
|
||||
|
||||
def get_fields(self):
|
||||
fields = super().get_fields()
|
||||
field = fields["key"]
|
||||
field.required = False
|
||||
return fields
|
||||
|
||||
|
||||
class NodeAssetsSerializer(serializers.ModelSerializer):
|
||||
assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all())
|
||||
@@ -97,3 +43,4 @@ class NodeAssetsSerializer(serializers.ModelSerializer):
|
||||
|
||||
class NodeAddChildrenSerializer(serializers.Serializer):
|
||||
nodes = serializers.ListField()
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..models import SystemUser
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
|
||||
from ..models import SystemUser, Asset
|
||||
from .base import AuthSerializer
|
||||
|
||||
|
||||
@@ -17,21 +19,22 @@ class SystemUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
exclude = ('_password', '_private_key', '_public_key')
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
|
||||
def get_field_names(self, declared_fields, info):
|
||||
fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info)
|
||||
fields.extend([
|
||||
'get_login_mode_display',
|
||||
'login_mode_display',
|
||||
])
|
||||
return fields
|
||||
|
||||
@staticmethod
|
||||
def get_unreachable_assets(obj):
|
||||
return obj.unreachable_assets
|
||||
return obj.assets_unreachable
|
||||
|
||||
@staticmethod
|
||||
def get_reachable_assets(obj):
|
||||
return obj.reachable_assets
|
||||
return obj.assets_reachable
|
||||
|
||||
def get_unreachable_amount(self, obj):
|
||||
return len(self.get_unreachable_assets(obj))
|
||||
@@ -41,7 +44,7 @@ class SystemUserSerializer(serializers.ModelSerializer):
|
||||
|
||||
@staticmethod
|
||||
def get_assets_amount(obj):
|
||||
return len(obj.get_assets())
|
||||
return len(obj.get_related_assets())
|
||||
|
||||
|
||||
class SystemUserAuthSerializer(AuthSerializer):
|
||||
@@ -61,13 +64,19 @@ class AssetSystemUserSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
查看授权的资产系统用户的数据结构,这个和AssetSerializer不同,字段少
|
||||
"""
|
||||
actions = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
fields = (
|
||||
'id', 'name', 'username', 'priority',
|
||||
'protocol', 'comment', 'login_mode'
|
||||
'protocol', 'comment', 'login_mode', 'actions',
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_actions(obj):
|
||||
return [action.name for action in obj.actions]
|
||||
|
||||
|
||||
class SystemUserSimpleSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
@@ -75,4 +84,7 @@ class SystemUserSimpleSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
fields = ('id', 'name', 'username')
|
||||
fields = ('id', 'name', 'username')
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.dispatch import Signal
|
||||
|
||||
on_app_ready = Signal()
|
||||
@@ -1,13 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from collections import defaultdict
|
||||
from django.db.models.signals import post_save, m2m_changed
|
||||
from django.db.models.signals import post_save, m2m_changed, post_delete
|
||||
from django.dispatch import receiver
|
||||
|
||||
from common.utils import get_logger
|
||||
from .models import Asset, SystemUser, Node
|
||||
from .tasks import update_assets_hardware_info_util, \
|
||||
test_asset_connectability_util, push_system_user_to_assets
|
||||
from .models import Asset, SystemUser, Node, AuthBook
|
||||
from .tasks import (
|
||||
update_assets_hardware_info_util,
|
||||
test_asset_connectivity_util,
|
||||
push_system_user_to_assets
|
||||
)
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@@ -19,8 +22,8 @@ def update_asset_hardware_info_on_created(asset):
|
||||
|
||||
|
||||
def test_asset_conn_on_created(asset):
|
||||
logger.debug("Test asset `{}` connectability".format(asset))
|
||||
test_asset_connectability_util.delay([asset])
|
||||
logger.debug("Test asset `{}` connectivity".format(asset))
|
||||
test_asset_connectivity_util.delay([asset])
|
||||
|
||||
|
||||
def set_asset_root_node(asset):
|
||||
@@ -35,6 +38,17 @@ def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
|
||||
update_asset_hardware_info_on_created(instance)
|
||||
test_asset_conn_on_created(instance)
|
||||
|
||||
# 过期节点资产数量
|
||||
nodes = instance.nodes.all()
|
||||
Node.expire_nodes_assets_amount(nodes)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Asset, dispatch_uid="my_unique_identifier")
|
||||
def on_asset_delete(sender, instance=None, **kwargs):
|
||||
# 过期节点资产数量
|
||||
nodes = instance.nodes.all()
|
||||
Node.expire_nodes_assets_amount(nodes)
|
||||
|
||||
|
||||
@receiver(post_save, sender=SystemUser, dispatch_uid="my_unique_identifier")
|
||||
def on_system_user_update(sender, instance=None, created=True, **kwargs):
|
||||
@@ -63,10 +77,14 @@ def on_system_user_assets_change(sender, instance=None, **kwargs):
|
||||
|
||||
@receiver(m2m_changed, sender=Asset.nodes.through)
|
||||
def on_asset_node_changed(sender, instance=None, **kwargs):
|
||||
logger.debug("Asset nodes change signal received")
|
||||
if isinstance(instance, Asset):
|
||||
if kwargs['action'] == 'post_add':
|
||||
logger.debug("Asset node change signal received")
|
||||
if kwargs['action'] == 'pre_remove':
|
||||
nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
|
||||
Node.expire_nodes_assets_amount(nodes)
|
||||
if kwargs['action'] == 'post_add':
|
||||
nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
|
||||
Node.expire_nodes_assets_amount(nodes)
|
||||
system_users_assets = defaultdict(set)
|
||||
system_users = SystemUser.objects.filter(nodes__in=nodes)
|
||||
# 清理节点缓存
|
||||
@@ -79,9 +97,11 @@ def on_asset_node_changed(sender, instance=None, **kwargs):
|
||||
@receiver(m2m_changed, sender=Asset.nodes.through)
|
||||
def on_node_assets_changed(sender, instance=None, **kwargs):
|
||||
if isinstance(instance, Node):
|
||||
logger.debug("Node assets change signal {} received".format(instance))
|
||||
# 当节点和资产关系发生改变时,过期资产数量缓存
|
||||
instance.expire_assets_amount()
|
||||
assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
|
||||
if kwargs['action'] == 'post_add':
|
||||
logger.debug("Node assets change signal received")
|
||||
# 重新关联系统用户和资产的关系
|
||||
system_users = SystemUser.objects.filter(nodes=instance)
|
||||
for system_user in system_users:
|
||||
@@ -92,3 +112,10 @@ def on_node_assets_changed(sender, instance=None, **kwargs):
|
||||
def on_node_update_or_created(sender, instance=None, created=False, **kwargs):
|
||||
if instance and not created:
|
||||
instance.expire_full_value()
|
||||
|
||||
|
||||
@receiver(post_save, sender=AuthBook)
|
||||
def on_auth_book_created(sender, instance=None, created=False, **kwargs):
|
||||
if created:
|
||||
logger.debug('Receive create auth book object signal.')
|
||||
instance.set_version_and_latest()
|
||||
|
||||
@@ -4,15 +4,15 @@ import re
|
||||
import os
|
||||
|
||||
from celery import shared_task
|
||||
from ops.celery import app as celery_app
|
||||
from django.core.cache import cache
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.core.cache import cache
|
||||
|
||||
from common.utils import capacity_convert, \
|
||||
sum_capacity, encrypt_password, get_logger
|
||||
from ops.celery.utils import register_as_period_task, after_app_shutdown_clean, \
|
||||
after_app_ready_start
|
||||
from orgs.utils import set_to_root_org
|
||||
from common.utils import (
|
||||
capacity_convert, sum_capacity, encrypt_password, get_logger
|
||||
)
|
||||
from ops.celery.decorator import (
|
||||
register_as_period_task, after_app_shutdown_clean_periodic
|
||||
)
|
||||
|
||||
from .models import SystemUser, AdminUser, Asset
|
||||
from . import const
|
||||
@@ -23,7 +23,30 @@ TIMEOUT = 60
|
||||
logger = get_logger(__file__)
|
||||
CACHE_MAX_TIME = 60*60*2
|
||||
disk_pattern = re.compile(r'^hd|sd|xvd|vd')
|
||||
PERIOD_TASK = os.environ.get("PERIOD_TASK", "off")
|
||||
PERIOD_TASK = os.environ.get("PERIOD_TASK", "on")
|
||||
|
||||
|
||||
def check_asset_can_run_ansible(asset):
|
||||
if not asset.is_active:
|
||||
msg = _("Asset has been disabled, skipped: {}").format(asset)
|
||||
logger.info(msg)
|
||||
return False
|
||||
if not asset.support_ansible():
|
||||
msg = _("Asset may not be support ansible, skipped: {}").format(asset)
|
||||
logger.info(msg)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def clean_hosts(assets):
|
||||
clean_assets = []
|
||||
for asset in assets:
|
||||
if not check_asset_can_run_ansible(asset):
|
||||
continue
|
||||
clean_assets.append(asset)
|
||||
if not clean_assets:
|
||||
print(_("No assets matched, stop task"))
|
||||
return clean_assets
|
||||
|
||||
|
||||
@shared_task
|
||||
@@ -60,9 +83,12 @@ def set_assets_hardware_info(assets, result, **kwargs):
|
||||
___cpu_model = 'Unknown'
|
||||
___cpu_model = ___cpu_model[:64]
|
||||
___cpu_count = info.get('ansible_processor_count', 0)
|
||||
___cpu_cores = info.get('ansible_processor_cores', None) or len(info.get('ansible_processor', []))
|
||||
___cpu_cores = info.get('ansible_processor_cores', None) or \
|
||||
len(info.get('ansible_processor', []))
|
||||
___cpu_vcpus = info.get('ansible_processor_vcpus', 0)
|
||||
___memory = '%s %s' % capacity_convert('{} MB'.format(info.get('ansible_memtotal_mb')))
|
||||
___memory = '%s %s' % capacity_convert(
|
||||
'{} MB'.format(info.get('ansible_memtotal_mb'))
|
||||
)
|
||||
disk_info = {}
|
||||
for dev, dev_info in info.get('ansible_devices', {}).items():
|
||||
if disk_pattern.match(dev) and dev_info['removable'] == '0':
|
||||
@@ -96,19 +122,8 @@ def update_assets_hardware_info_util(assets, task_name=None):
|
||||
if task_name is None:
|
||||
task_name = _("Update some assets hardware info")
|
||||
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
|
||||
hosts = []
|
||||
for asset in assets:
|
||||
if not asset.is_active:
|
||||
msg = _("Asset has been disabled, skipped: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
if not asset.support_ansible():
|
||||
msg = _("Asset may not be support ansible, skipped: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
hosts.append(asset)
|
||||
hosts = clean_hosts(assets)
|
||||
if not hosts:
|
||||
logger.info(_("No assets matched, stop task"))
|
||||
return {}
|
||||
created_by = str(assets[0].org_id)
|
||||
task, created = update_or_create_ansible_task(
|
||||
@@ -116,8 +131,6 @@ def update_assets_hardware_info_util(assets, task_name=None):
|
||||
pattern='all', options=const.TASK_OPTIONS, run_as_admin=True,
|
||||
)
|
||||
result = task.run()
|
||||
# Todo: may be somewhere using
|
||||
# Manual run callback function
|
||||
set_assets_hardware_info(assets, result)
|
||||
return result
|
||||
|
||||
@@ -125,8 +138,7 @@ def update_assets_hardware_info_util(assets, task_name=None):
|
||||
@shared_task
|
||||
def update_asset_hardware_info_manual(asset):
|
||||
task_name = _("Update asset hardware info: {}").format(asset.hostname)
|
||||
# task_name = _("更新资产硬件信息")
|
||||
return update_assets_hardware_info_util(
|
||||
update_assets_hardware_info_util(
|
||||
[asset], task_name=task_name
|
||||
)
|
||||
|
||||
@@ -141,123 +153,18 @@ def update_assets_hardware_info_period():
|
||||
logger.debug("Period task disabled, update assets hardware info pass")
|
||||
return
|
||||
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
from orgs.models import Organization
|
||||
orgs = Organization.objects.all().values_list('id', flat=True)
|
||||
orgs.append('')
|
||||
task_name = _("Update assets hardware info period")
|
||||
# for org_id in orgs:
|
||||
# org_id = str(org_id)
|
||||
# hostname_list = [
|
||||
# asset for asset in Asset.objects.all()
|
||||
# if asset.is_active and asset.is_unixlike()
|
||||
# ]
|
||||
# tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
|
||||
#
|
||||
# # Only create, schedule by celery beat
|
||||
# update_or_create_ansible_task(
|
||||
# task_name, hosts=hostname_list, tasks=tasks, pattern='all',
|
||||
# options=const.TASK_OPTIONS, run_as_admin=True, created_by='System',
|
||||
# interval=60*60*24, is_periodic=True, callback=set_assets_hardware_info.name,
|
||||
# )
|
||||
|
||||
|
||||
## ADMIN USER CONNECTIVE ##
|
||||
|
||||
def set_admin_user_connectability_info(result, **kwargs):
|
||||
admin_user = kwargs.get("admin_user")
|
||||
task_name = kwargs.get("task_name")
|
||||
if admin_user is None and task_name is not None:
|
||||
admin_user = task_name.split(":")[-1]
|
||||
|
||||
raw, summary = result
|
||||
cache_key = const.ADMIN_USER_CONN_CACHE_KEY.format(admin_user)
|
||||
cache.set(cache_key, summary, CACHE_MAX_TIME)
|
||||
|
||||
for i in summary.get('contacted', []):
|
||||
asset_conn_cache_key = const.ASSET_ADMIN_CONN_CACHE_KEY.format(i)
|
||||
cache.set(asset_conn_cache_key, 1, CACHE_MAX_TIME)
|
||||
|
||||
for i, msg in summary.get('dark', {}).items():
|
||||
asset_conn_cache_key = const.ASSET_ADMIN_CONN_CACHE_KEY.format(i)
|
||||
cache.set(asset_conn_cache_key, 0, CACHE_MAX_TIME)
|
||||
logger.error(msg)
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_admin_user_connectability_util(admin_user, task_name):
|
||||
"""
|
||||
Test asset admin user can connect or not. Using ansible api do that
|
||||
:param admin_user:
|
||||
:param task_name:
|
||||
:return:
|
||||
"""
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
|
||||
assets = admin_user.get_related_assets()
|
||||
hosts = []
|
||||
for asset in assets:
|
||||
if not asset.is_active:
|
||||
msg = _("Asset has been disabled, skipped: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
if not asset.support_ansible():
|
||||
msg = _("Asset may not be support ansible, skipped: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
hosts.append(asset)
|
||||
if not hosts:
|
||||
logger.info(_("No assets matched, stop task"))
|
||||
return {}
|
||||
tasks = const.TEST_ADMIN_USER_CONN_TASKS
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True, created_by=admin_user.org_id,
|
||||
)
|
||||
result = task.run()
|
||||
set_admin_user_connectability_info(result, admin_user=admin_user.name)
|
||||
return result
|
||||
|
||||
|
||||
@shared_task
|
||||
@register_as_period_task(interval=3600)
|
||||
def test_admin_user_connectability_period():
|
||||
"""
|
||||
A period task that update the ansible task period
|
||||
"""
|
||||
admin_users = AdminUser.objects.all()
|
||||
for admin_user in admin_users:
|
||||
task_name = _("Test admin user connectability period: {}").format(admin_user.name)
|
||||
test_admin_user_connectability_util(admin_user, task_name)
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_admin_user_connectability_manual(admin_user):
|
||||
task_name = _("Test admin user connectability: {}").format(admin_user.name)
|
||||
# task_name = _("测试管理行号可连接性: {}").format(admin_user.name)
|
||||
return test_admin_user_connectability_util(admin_user, task_name)
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_asset_connectability_util(assets, task_name=None):
|
||||
def test_asset_connectivity_util(assets, task_name=None):
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
|
||||
if task_name is None:
|
||||
task_name = _("Test assets connectability")
|
||||
# task_name = _("测试资产可连接性")
|
||||
hosts = []
|
||||
for asset in assets:
|
||||
if not asset.is_active:
|
||||
msg = _("Asset has been disabled, skip: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
if not asset.support_ansible():
|
||||
msg = _("Asset may not be support ansible, skip: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
hosts.append(asset)
|
||||
task_name = _("Test assets connectivity")
|
||||
hosts = clean_hosts(assets)
|
||||
if not hosts:
|
||||
logger.info(_("No assets, task stop"))
|
||||
return {}
|
||||
tasks = const.TEST_ADMIN_USER_CONN_TASKS
|
||||
created_by = assets[0].org_id
|
||||
@@ -267,18 +174,20 @@ def test_asset_connectability_util(assets, task_name=None):
|
||||
)
|
||||
result = task.run()
|
||||
summary = result[1]
|
||||
for k in summary.get('dark'):
|
||||
cache.set(const.ASSET_ADMIN_CONN_CACHE_KEY.format(k), 0, CACHE_MAX_TIME)
|
||||
|
||||
for k in summary.get('contacted'):
|
||||
cache.set(const.ASSET_ADMIN_CONN_CACHE_KEY.format(k), 1, CACHE_MAX_TIME)
|
||||
for asset in assets:
|
||||
if asset.hostname in summary.get('dark', {}):
|
||||
asset.connectivity = asset.UNREACHABLE
|
||||
elif asset.hostname in summary.get('contacted', []):
|
||||
asset.connectivity = asset.REACHABLE
|
||||
else:
|
||||
asset.connectivity = asset.UNKNOWN
|
||||
return summary
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_asset_connectability_manual(asset):
|
||||
task_name = _("Test assets connectability: {}").format(asset)
|
||||
summary = test_asset_connectability_util([asset], task_name=task_name)
|
||||
def test_asset_connectivity_manual(asset):
|
||||
task_name = _("Test assets connectivity: {}").format(asset)
|
||||
summary = test_asset_connectivity_util([asset], task_name=task_name)
|
||||
|
||||
if summary.get('dark'):
|
||||
return False, summary['dark']
|
||||
@@ -286,21 +195,61 @@ def test_asset_connectability_manual(asset):
|
||||
return True, ""
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_admin_user_connectivity_util(admin_user, task_name):
|
||||
"""
|
||||
Test asset admin user can connect or not. Using ansible api do that
|
||||
:param admin_user:
|
||||
:param task_name:
|
||||
:return:
|
||||
"""
|
||||
assets = admin_user.get_related_assets()
|
||||
hosts = clean_hosts(assets)
|
||||
if not hosts:
|
||||
return {}
|
||||
summary = test_asset_connectivity_util(hosts, task_name)
|
||||
return summary
|
||||
|
||||
|
||||
@shared_task
|
||||
@register_as_period_task(interval=3600)
|
||||
def test_admin_user_connectivity_period():
|
||||
"""
|
||||
A period task that update the ansible task period
|
||||
"""
|
||||
if PERIOD_TASK != "on":
|
||||
logger.debug('Period task off, skip')
|
||||
return
|
||||
key = '_JMS_TEST_ADMIN_USER_CONNECTIVITY_PERIOD'
|
||||
prev_execute_time = cache.get(key)
|
||||
if prev_execute_time:
|
||||
logger.debug("Test admin user connectivity, less than 40 minutes, skip")
|
||||
return
|
||||
cache.set(key, 1, 60*40)
|
||||
admin_users = AdminUser.objects.all()
|
||||
for admin_user in admin_users:
|
||||
task_name = _("Test admin user connectivity period: {}").format(admin_user.name)
|
||||
test_admin_user_connectivity_util(admin_user, task_name)
|
||||
cache.set(key, 1, 60*40)
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_admin_user_connectivity_manual(admin_user):
|
||||
task_name = _("Test admin user connectivity: {}").format(admin_user.name)
|
||||
test_admin_user_connectivity_util(admin_user, task_name)
|
||||
return True
|
||||
|
||||
|
||||
## System user connective ##
|
||||
|
||||
@shared_task
|
||||
def set_system_user_connectablity_info(result, **kwargs):
|
||||
def set_system_user_connectivity_info(system_user, result):
|
||||
summary = result[1]
|
||||
task_name = kwargs.get("task_name")
|
||||
system_user = kwargs.get("system_user")
|
||||
if system_user is None:
|
||||
system_user = task_name.split(":")[-1]
|
||||
cache_key = const.SYSTEM_USER_CONN_CACHE_KEY.format(str(system_user.id))
|
||||
cache.set(cache_key, summary, CACHE_MAX_TIME)
|
||||
system_user.connectivity = summary
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_system_user_connectability_util(system_user, assets, task_name):
|
||||
def test_system_user_connectivity_util(system_user, assets, task_name):
|
||||
"""
|
||||
Test system cant connect his assets or not.
|
||||
:param system_user:
|
||||
@@ -309,56 +258,45 @@ def test_system_user_connectability_util(system_user, assets, task_name):
|
||||
:return:
|
||||
"""
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
hosts = []
|
||||
tasks = const.TEST_SYSTEM_USER_CONN_TASKS
|
||||
for asset in assets:
|
||||
if not asset.is_active:
|
||||
msg = _("Asset has been disabled, skip: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
if not asset.support_ansible():
|
||||
msg = _("Asset may not be support ansible, skip: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
hosts.append(asset)
|
||||
hosts = clean_hosts(assets)
|
||||
if not hosts:
|
||||
logger.info(_("No assets matched, stop task"))
|
||||
return {}
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name, hosts=hosts, tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS,
|
||||
run_as=system_user, created_by=system_user.org_id,
|
||||
run_as=system_user.username, created_by=system_user.org_id,
|
||||
)
|
||||
result = task.run()
|
||||
set_system_user_connectablity_info(result, system_user=system_user)
|
||||
set_system_user_connectivity_info(system_user, result)
|
||||
return result
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_system_user_connectability_manual(system_user):
|
||||
task_name = _("Test system user connectability: {}").format(system_user)
|
||||
assets = system_user.get_assets()
|
||||
return test_system_user_connectability_util(system_user, assets, task_name)
|
||||
def test_system_user_connectivity_manual(system_user):
|
||||
task_name = _("Test system user connectivity: {}").format(system_user)
|
||||
assets = system_user.get_related_assets()
|
||||
return test_system_user_connectivity_util(system_user, assets, task_name)
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_system_user_connectability_a_asset(system_user, asset):
|
||||
task_name = _("Test system user connectability: {} => {}").format(
|
||||
def test_system_user_connectivity_a_asset(system_user, asset):
|
||||
task_name = _("Test system user connectivity: {} => {}").format(
|
||||
system_user, asset
|
||||
)
|
||||
return test_system_user_connectability_util(system_user, [asset], task_name)
|
||||
return test_system_user_connectivity_util(system_user, [asset], task_name)
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_system_user_connectability_period():
|
||||
def test_system_user_connectivity_period():
|
||||
if PERIOD_TASK != "on":
|
||||
logger.debug("Period task disabled, test system user connectability pass")
|
||||
logger.debug("Period task disabled, test system user connectivity pass")
|
||||
return
|
||||
system_users = SystemUser.objects.all()
|
||||
for system_user in system_users:
|
||||
task_name = _("Test system user connectability period: {}").format(system_user)
|
||||
# task_name = _("定期测试系统用户可连接性: {}".format(system_user))
|
||||
test_system_user_connectability_util(system_user, task_name)
|
||||
task_name = _("Test system user connectivity period: {}").format(system_user)
|
||||
assets = system_user.get_related_assets()
|
||||
test_system_user_connectivity_util(system_user, assets, task_name)
|
||||
|
||||
|
||||
#### Push system user tasks ####
|
||||
@@ -380,6 +318,24 @@ def get_push_system_user_tasks(system_user):
|
||||
),
|
||||
}
|
||||
})
|
||||
tasks.extend([
|
||||
{
|
||||
'name': 'Check home dir exists',
|
||||
'action': {
|
||||
'module': 'stat',
|
||||
'args': 'path=/home/{}'.format(system_user.username)
|
||||
},
|
||||
'register': 'home_existed'
|
||||
},
|
||||
{
|
||||
'name': "Set home dir permission",
|
||||
'action': {
|
||||
'module': 'file',
|
||||
'args': "path=/home/{0} owner={0} group={0} mode=700".format(system_user.username)
|
||||
},
|
||||
'when': 'home_existed.stat.exists == true'
|
||||
}
|
||||
])
|
||||
if system_user.public_key:
|
||||
tasks.append({
|
||||
'name': 'Set {} authorized key'.format(system_user.username),
|
||||
@@ -391,6 +347,12 @@ def get_push_system_user_tasks(system_user):
|
||||
}
|
||||
})
|
||||
if system_user.sudo:
|
||||
sudo = system_user.sudo.replace('\r\n', '\n').replace('\r', '\n')
|
||||
sudo_list = sudo.split('\n')
|
||||
sudo_tmp = []
|
||||
for s in sudo_list:
|
||||
sudo_tmp.append(s.strip(','))
|
||||
sudo = ','.join(sudo_tmp)
|
||||
tasks.append({
|
||||
'name': 'Set {} sudo setting'.format(system_user.username),
|
||||
'action': {
|
||||
@@ -398,8 +360,7 @@ def get_push_system_user_tasks(system_user):
|
||||
'args': "dest=/etc/sudoers state=present regexp='^{0} ALL=' "
|
||||
"line='{0} ALL=(ALL) NOPASSWD: {1}' "
|
||||
"validate='visudo -cf %s'".format(
|
||||
system_user.username,
|
||||
system_user.sudo,
|
||||
system_user.username, sudo,
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -415,32 +376,23 @@ def push_system_user_util(system_user, assets, task_name):
|
||||
logger.info(msg)
|
||||
return
|
||||
|
||||
tasks = get_push_system_user_tasks(system_user)
|
||||
hosts = []
|
||||
for asset in assets:
|
||||
if not asset.is_active:
|
||||
msg = _("Asset has been disabled, skip: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
if not asset.support_ansible():
|
||||
msg = _("Asset may not be support ansible, skip: {}").format(asset)
|
||||
logger.info(msg)
|
||||
continue
|
||||
hosts.append(asset)
|
||||
hosts = clean_hosts(assets)
|
||||
if not hosts:
|
||||
logger.info(_("No assets matched, stop task"))
|
||||
return {}
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True,
|
||||
created_by=system_user.org_id,
|
||||
)
|
||||
return task.run()
|
||||
for host in hosts:
|
||||
system_user.load_specific_asset_auth(host)
|
||||
tasks = get_push_system_user_tasks(system_user)
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name=task_name, hosts=[host], tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True,
|
||||
created_by=system_user.org_id,
|
||||
)
|
||||
task.run()
|
||||
|
||||
|
||||
@shared_task
|
||||
def push_system_user_to_assets_manual(system_user):
|
||||
assets = system_user.get_assets()
|
||||
assets = system_user.get_related_assets()
|
||||
task_name = _("Push system users to assets: {}").format(system_user.name)
|
||||
return push_system_user_util(system_user, assets, task_name=task_name)
|
||||
|
||||
@@ -459,10 +411,59 @@ def push_system_user_to_assets(system_user, assets):
|
||||
return push_system_user_util(system_user, assets, task_name)
|
||||
|
||||
|
||||
@shared_task
|
||||
@after_app_shutdown_clean_periodic
|
||||
def test_system_user_connectability_period():
|
||||
pass
|
||||
|
||||
|
||||
@shared_task
|
||||
@after_app_shutdown_clean_periodic
|
||||
def test_admin_user_connectability_period():
|
||||
pass
|
||||
|
||||
|
||||
@shared_task
|
||||
def set_asset_user_connectivity_info(asset_user, result):
|
||||
summary = result[1]
|
||||
asset_user.connectivity = summary
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_asset_user_connectivity_util(asset_user, task_name):
|
||||
"""
|
||||
:param asset_user: <AuthBook>对象
|
||||
:param task_name:
|
||||
:return:
|
||||
"""
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
tasks = const.TEST_ASSET_USER_CONN_TASKS
|
||||
if not check_asset_can_run_ansible(asset_user.asset):
|
||||
return
|
||||
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name, hosts=[asset_user.asset], tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS,
|
||||
run_as=asset_user.username, created_by=asset_user.org_id
|
||||
)
|
||||
result = task.run()
|
||||
set_asset_user_connectivity_info(asset_user, result)
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_asset_users_connectivity_manual(asset_users):
|
||||
"""
|
||||
:param asset_users: <AuthBook>对象
|
||||
"""
|
||||
for asset_user in asset_users:
|
||||
task_name = _("Test asset user connectivity: {}").format(asset_user)
|
||||
test_asset_user_connectivity_util(asset_user, task_name)
|
||||
|
||||
|
||||
# @shared_task
|
||||
# @register_as_period_task(interval=3600)
|
||||
# @after_app_ready_start
|
||||
# # @after_app_shutdown_clean
|
||||
# @after_app_shutdown_clean_periodic
|
||||
# def push_system_user_period():
|
||||
# for system_user in SystemUser.objects.all():
|
||||
# push_system_user_related_nodes(system_user)
|
||||
|
||||
@@ -57,6 +57,10 @@
|
||||
<script>
|
||||
var zTree2, asset_table2 = 0;
|
||||
function initTable2() {
|
||||
if(asset_table2){
|
||||
return
|
||||
}
|
||||
|
||||
var options = {
|
||||
ele: $('#asset_list_modal_table'),
|
||||
ajax_url: '{% url "api-assets:asset-list" %}?show_current_asset=1',
|
||||
@@ -69,16 +73,16 @@ function initTable2() {
|
||||
return asset_table2
|
||||
}
|
||||
|
||||
function onSelected2(event, treeNode) {
|
||||
function onNodeSelected2(event, treeNode) {
|
||||
var url = asset_table2.ajax.url();
|
||||
url = setUrlParam(url, "node_id", treeNode.node_id);
|
||||
setCookie('node_selected', treeNode.id);
|
||||
url = setUrlParam(url, "node_id", treeNode.meta.node.id);
|
||||
asset_table2.ajax.url(url);
|
||||
asset_table2.ajax.reload();
|
||||
}
|
||||
|
||||
|
||||
function initTree2() {
|
||||
var url = '{% url 'api-assets:node-children-tree' %}?assets=0';
|
||||
var setting = {
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
@@ -89,33 +93,22 @@ function initTree2() {
|
||||
enable: true
|
||||
}
|
||||
},
|
||||
async: {
|
||||
enable: true,
|
||||
url: url,
|
||||
autoParam: ["id=key", "name=n", "level=lv"],
|
||||
type: 'get'
|
||||
},
|
||||
callback: {
|
||||
onSelected: onSelected2
|
||||
onSelected: onNodeSelected2
|
||||
}
|
||||
};
|
||||
|
||||
var zNodes = [];
|
||||
$.get("{% url 'api-assets:node-list' %}", function(data, status){
|
||||
$.each(data, function (index, value) {
|
||||
value["node_id"] = value["id"];
|
||||
value["id"] = value["tree_id"];
|
||||
value["pId"] = value["tree_parent"];
|
||||
{#value["open"] = true;#}
|
||||
if (value["key"] === "0") {
|
||||
value["open"] = true;
|
||||
}
|
||||
value["name"] = value["value"] + ' (' + value['assets_amount'] + ')';
|
||||
});
|
||||
zNodes = data;
|
||||
$.fn.zTree.init($("#assetTree2"), setting, zNodes);
|
||||
zTree2 = $.fn.zTree.getZTreeObj("assetTree2");
|
||||
var root = zTree2.getNodes()[0];
|
||||
zTree2.expandNode(root);
|
||||
});
|
||||
zTree2 = $.fn.zTree.init($("#assetTree2"), setting);
|
||||
}
|
||||
|
||||
|
||||
$(document).ready(function(){
|
||||
}).on('show.bs.modal', function () {
|
||||
initTable2();
|
||||
initTree2();
|
||||
})
|
||||
|
||||
28
apps/assets/templates/assets/_asset_user_auth_modal.html
Normal file
28
apps/assets/templates/assets/_asset_user_auth_modal.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends '_modal.html' %}
|
||||
{% load i18n %}
|
||||
{% block modal_id %}asset_user_auth_modal{% endblock %}
|
||||
{% block modal_title%}{% trans "Update asset user auth" %}{% endblock %}
|
||||
{% block modal_body %}
|
||||
<form class="form-horizontal" role="form" onkeydown="if(event.keyCode==13){ $('#btn_asset_user_auth_modal_confirm').trigger('click'); return false;}">
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">{% trans "Hostname" %}</label>
|
||||
<div class="col-sm-10">
|
||||
<p class="form-control-static" id="id_hostname_p"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">{% trans "Username" %}</label>
|
||||
<div class="col-sm-10">
|
||||
<p class="form-control-static" id="id_username_p"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">{% trans "Password" %}</label>
|
||||
<div class="col-sm-10">
|
||||
<input class="form-control" id="id_password" type="password" name="password" placeholder="{% trans 'Please input password' %}"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% block modal_confirm_id %}btn_asset_user_auth_modal_confirm{% endblock %}
|
||||
18
apps/assets/templates/assets/_gateway_test_modal.html
Normal file
18
apps/assets/templates/assets/_gateway_test_modal.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends '_modal.html' %}
|
||||
{% load i18n %}
|
||||
{% block modal_id %}gateway_test{% endblock %}
|
||||
{% block modal_title%}{% trans "Test gateway test connection" %}{% endblock %}
|
||||
{% block modal_body %}
|
||||
{% load bootstrap3 %}
|
||||
<form method="post" class="form-horizontal" action="" id="test_gateway_form" style="padding-top: 10px">
|
||||
<div class="form-group">
|
||||
<input id="gateway_id" name="gateway_id" hidden>
|
||||
<label for="port" class="col-sm-2 control-label">{% trans 'SSH Port' %}</label>
|
||||
<div class="col-sm-9" id="select2-container">
|
||||
<input id="ssh_test_port" name="port" class="form-control">
|
||||
<span class="help-block">{% trans 'If use nat, set the ssh real port' %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% block modal_confirm_id %}btn_gateway_test{% endblock %}
|
||||
@@ -103,15 +103,17 @@ var need_change_field_login_mode = [
|
||||
];
|
||||
|
||||
function protocolChange() {
|
||||
if ($(protocol_id + " option:selected").text() === 'rdp') {
|
||||
var protocol = $(protocol_id + " option:selected").text();
|
||||
if (protocol === 'rdp' || protocol === 'vnc') {
|
||||
$('.auth-fields').removeClass('hidden');
|
||||
$('#command-filter-block').addClass('hidden');
|
||||
$.each(need_change_field, function (index, value) {
|
||||
$(value).closest('.form-group').addClass('hidden')
|
||||
});
|
||||
}
|
||||
else if ($(protocol_id + " option:selected").text() === 'telnet (beta)') {
|
||||
else if (protocol === 'telnet (beta)') {
|
||||
$('.auth-fields').removeClass('hidden');
|
||||
$('#command-filter-block').removeClass('hidden');
|
||||
$.each(need_change_field, function (index, value) {
|
||||
$(value).closest('.form-group').addClass('hidden')
|
||||
});
|
||||
@@ -123,6 +125,7 @@ function protocolChange() {
|
||||
return
|
||||
}
|
||||
authFieldsDisplay();
|
||||
$('#command-filter-block').removeClass('hidden');
|
||||
$.each(need_change_field, function (index, value) {
|
||||
$(value).closest('.form-group').removeClass('hidden')
|
||||
});
|
||||
|
||||
@@ -45,13 +45,11 @@
|
||||
<table class="table table-striped table-bordered table-hover" id="asset_list_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
<input type="checkbox" id="check_all" class="ipt_check_all" >
|
||||
</th>
|
||||
<th>{% trans 'Hostname' %}</th>
|
||||
<th>{% trans 'IP' %}</th>
|
||||
<th>{% trans 'Port' %}</th>
|
||||
<th>{% trans 'Reachable' %}</th>
|
||||
<th>{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -86,39 +84,76 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'assets/_asset_user_auth_modal.html' %}
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
|
||||
function initTable() {
|
||||
var reachable = {{ admin_user.REACHABLE }};
|
||||
var unreachable = {{ admin_user.UNREACHABLE }};
|
||||
var options = {
|
||||
ele: $('#asset_list_table'),
|
||||
buttons: [],
|
||||
order: [],
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
{targets: 0, createdCell: function (td, cellData, rowData) {
|
||||
cellData = htmlEscape(cellData);
|
||||
var detail_btn = '<a href="{% url "assets:asset-detail" pk=DEFAULT_PK %}" data-aid="'+rowData.id+'">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
if (!cellData) {
|
||||
{targets: 3, createdCell: function (td, cellData) {
|
||||
if (cellData === unreachable) {
|
||||
$(td).html('<i class="fa fa-times text-danger"></i>')
|
||||
} else {
|
||||
} else if (cellData === reachable) {
|
||||
$(td).html('<i class="fa fa-check text-navy"></i>')
|
||||
}
|
||||
}}],
|
||||
ajax_url: '{% url "api-assets:asset-list" %}?admin_user_id={{ admin_user.id }}',
|
||||
} else {
|
||||
$(td).html('')
|
||||
}
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData, rowData) {
|
||||
var test_btn = ' <a class="btn btn-xs btn-info btn-test-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Test" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
|
||||
var update_auth_btn = ' <a class="btn btn-xs btn-primary btn-update-asset-user-auth" data-aid="{{ DEFAULT_PK }}" data-hostname="hostname777">{% trans "Update auth" %}</a>'.replace("{{ DEFAULT_PK }}", cellData).replace("hostname777", rowData.hostname);
|
||||
$(td).html(test_btn + update_auth_btn);
|
||||
}}
|
||||
],
|
||||
|
||||
ajax_url: '{% url "api-assets:admin-user-assets" pk=admin_user.id %}',
|
||||
columns: [
|
||||
{data: function(){return ""}}, {data: "hostname" }, {data: "ip" },
|
||||
{data: "port" }, {data: "is_connective" }],
|
||||
{data: "hostname" }, {data: "ip" },
|
||||
{data: "port" }, {data: "connectivity" }, {data: "id"}],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
jumpserver.initServerSideDataTable(options);
|
||||
}
|
||||
|
||||
function initAssetUserAuthModalForm(hostname, username){
|
||||
$('#id_hostname_p').html(hostname);
|
||||
$('#id_username_p').html(username);
|
||||
$('#id_password').parent().removeClass('has-error');
|
||||
$('#id_password').val('');
|
||||
}
|
||||
|
||||
var assetId ;
|
||||
|
||||
$(document).ready(function () {
|
||||
initTable();
|
||||
})
|
||||
.on('click', '.btn-test-asset', function () {
|
||||
var asset_id = $(this).data('uid');
|
||||
var the_url = "{% url 'api-assets:asset-alive-test' pk=DEFAULT_PK %}".replace('{{ DEFAULT_PK }}', asset_id);
|
||||
var success = function (data) {
|
||||
var task_id = data.task;
|
||||
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
|
||||
window.open(url, '', 'width=800,height=600,left=400,top=400')
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: 'GET',
|
||||
success: success,
|
||||
flash_message: false
|
||||
});
|
||||
})
|
||||
.on('click', '.btn-test-connective', function () {
|
||||
var the_url = "{% url 'api-assets:admin-user-connective' pk=admin_user.id %}";
|
||||
var success = function (data) {
|
||||
@@ -133,5 +168,38 @@ $(document).ready(function () {
|
||||
flash_message: false
|
||||
});
|
||||
})
|
||||
.on('click', '.btn-update-asset-user-auth', function() {
|
||||
assetId = $(this).data('aid');
|
||||
var hostname = $(this).data('hostname');
|
||||
var username = '{{ admin_user.username }}';
|
||||
initAssetUserAuthModalForm(hostname, username);
|
||||
$("#asset_user_auth_modal").modal();
|
||||
})
|
||||
.on('click', '#btn_asset_user_auth_modal_confirm', function(){
|
||||
var password = $('#id_password').val();
|
||||
if (password){
|
||||
var data = {
|
||||
'name': "{{ admin_user.username }}",
|
||||
'asset': assetId,
|
||||
'username': "{{ admin_user.username }}",
|
||||
'password': password
|
||||
};
|
||||
formSubmit({
|
||||
data: data,
|
||||
url: "{% url 'api-assets:asset-user-list' %}",
|
||||
method: 'POST',
|
||||
success: function () {
|
||||
toastr.success("{% trans 'Update successfully!' %}");
|
||||
},
|
||||
error: function () {
|
||||
toastr.error("{% trans 'Update failed!' %}");
|
||||
}
|
||||
});
|
||||
$("#asset_user_auth_modal").modal('hide');
|
||||
}
|
||||
else{
|
||||
$('#id_password').parent().addClass('has-error');
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -44,9 +44,10 @@ $(document).ready(function(){
|
||||
var options = {
|
||||
ele: $('#admin_user_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
{targets: 1, render: function (cellData, tp, rowData, meta) {
|
||||
cellData = htmlEscape(cellData);
|
||||
var detail_btn = '<a href="{% url "assets:admin-user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
return detail_btn.replace('{{ DEFAULT_PK }}', rowData.id);
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
var innerHtml = "";
|
||||
@@ -82,7 +83,6 @@ $(document).ready(function(){
|
||||
innerHtml = "<span class='text-danger'>" + num.toFixed(1) + "% </span>";
|
||||
}
|
||||
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
|
||||
|
||||
}},
|
||||
{targets: 8, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "assets:admin-user-update" pk=DEFAULT_PK %}" class="btn btn-xs m-l-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
@@ -90,8 +90,8 @@ $(document).ready(function(){
|
||||
$(td).html(update_btn + del_btn)
|
||||
}}],
|
||||
ajax_url: '{% url "api-assets:admin-user-list" %}',
|
||||
columns: [{data: function(){return ""}}, {data: "name" }, {data: "username" }, {data: "assets_amount" },
|
||||
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" }]
|
||||
columns: [{data: function(){return ""}}, {data: "name"}, {data: "username" }, {data: "assets_amount" },
|
||||
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment"}, {data: "id"}]
|
||||
};
|
||||
jumpserver.initServerSideDataTable(options)
|
||||
})
|
||||
|
||||
218
apps/assets/templates/assets/asset_asset_user_list.html
Normal file
218
apps/assets/templates/assets/asset_asset_user_list.html
Normal file
@@ -0,0 +1,218 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load common_tags %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block custom_head_css_js %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="panel-options">
|
||||
<ul class="nav nav-tabs">
|
||||
<li>
|
||||
<a href="{% url 'assets:asset-detail' pk=asset.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Asset detail' %}</a>
|
||||
</li>
|
||||
<li class="active">
|
||||
<a href="{% url 'assets:asset-user-list' pk=asset.id %}" class="text-center"><i class="fa fa-bar-chart-o"></i> {% trans 'Asset user list' %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-8" style="padding-left: 0;">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span style="float: left">{% trans 'Asset users of' %} <b>{{ asset.hostname }} </b></span>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-user">
|
||||
</ul>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<table class="table table-hover" id="asset_user_list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center"><input type="checkbox" class="ipt_check_all"></th>
|
||||
<th class="text-center">{% trans 'Username' %}</th>
|
||||
<th class="text-center">{% trans 'Password version' %}</th>
|
||||
<th class="text-center">{% trans 'Reachable' %}</th>
|
||||
<th class="text-center">{% trans 'Date updated' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4" style="padding-left: 0;padding-right: 0">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Quick modify' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
{% if asset.protocol == 'ssh' %}
|
||||
<tr class="no-borders-tr">
|
||||
<td>{% trans 'Test connective' %}:</td>
|
||||
<td>
|
||||
<span class="pull-right">
|
||||
<button type="button" class="btn btn-primary btn-xs" id="btn-bulk-test-connective" style="width: 54px">{% trans 'Test' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'assets/_asset_user_auth_modal.html' %}
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
function initAssetUserAuthModalForm(hostname){
|
||||
$('#id_hostname_p').html(hostname);
|
||||
$('#id_username_p').html(username);
|
||||
$('#id_password').parent().removeClass('has-error');
|
||||
$('#id_password').val('');
|
||||
}
|
||||
function initAssetUserTable() {
|
||||
var reachable = {{ asset.admin_user.REACHABLE }};
|
||||
var unreachable = {{ asset.admin_user.UNREACHABLE }};
|
||||
var options = {
|
||||
ele: $('#asset_user_list'),
|
||||
buttons: [],
|
||||
order: [],
|
||||
columnDefs: [
|
||||
{targets: 3, createdCell: function (td, cellData) {
|
||||
if (cellData === unreachable) {
|
||||
$(td).html('<i class="fa fa-times text-danger"></i>')
|
||||
} else if (cellData === reachable) {
|
||||
$(td).html('<i class="fa fa-check text-navy"></i>')
|
||||
} else {
|
||||
$(td).html('')
|
||||
}
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
$(td).html(cellData.slice(0, -6));
|
||||
}},
|
||||
{targets: 5, createdCell: function (td, cellData) {
|
||||
var update_auth_btn = ' <a class="btn btn-xs btn-primary btn-update-asset-user-auth" data-username="DEFAULT_USERNAME">{% trans "Update auth" %}</a>'.replace("DEFAULT_USERNAME", cellData);
|
||||
{% if asset.protocol == 'ssh' %}
|
||||
var test_btn = ' <a class="btn btn-xs btn-info btn-test-connective" data-username="DEFAULT_USERNAME">{% trans "Test" %}</a>'.replace("DEFAULT_USERNAME", cellData);
|
||||
$(td).html(test_btn + update_auth_btn);
|
||||
{% else %}
|
||||
$(td).html(update_auth_btn);
|
||||
{% endif %}
|
||||
{#var check_btn = ' <a class="btn btn-xs btn-info btn-check-asset-user-auth" data-username="DEFAULT_USERNAME">{% trans "Check auth" %}</a>'.replace("DEFAULT_USERNAME", cellData);#}
|
||||
|
||||
}}
|
||||
],
|
||||
ajax_url: '{% url "api-assets:asset-user-list" %}' + '?asset_id={{ asset.id }}',
|
||||
columns: [
|
||||
{data: function (){return ''}}, {data: "username" },
|
||||
{data: "version"}, {data: "connectivity"}, {data: "date_updated"},
|
||||
{data: "username", orderable: false}
|
||||
],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
jumpserver.initDataTable(options);
|
||||
}
|
||||
var username;
|
||||
$(document).ready(function () {
|
||||
initAssetUserTable();
|
||||
})
|
||||
{#.on('click', '.btn-check-asset-user-auth', function(){#}
|
||||
{# var username = $(this).data('username');#}
|
||||
{# var the_url = "{% url 'api-assets:asset-user-auth-info' %}" + '?asset_id={{ asset.id }}' + '&username=' + username;#}
|
||||
{# $.ajax({#}
|
||||
{# url: the_url,#}
|
||||
{# method: 'GET',#}
|
||||
{# success: function (data) {#}
|
||||
{# alert("Password: " + data.password);#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# })#}
|
||||
.on('click', '.btn-update-asset-user-auth', function() {
|
||||
username = $(this).data('username');
|
||||
var hostname = "{{ asset.hostname }}";
|
||||
initAssetUserAuthModalForm(hostname, username);
|
||||
$("#asset_user_auth_modal").modal();
|
||||
})
|
||||
.on('click', '#btn_asset_user_auth_modal_confirm', function(){
|
||||
var password = $('#id_password').val();
|
||||
if (password){
|
||||
var data = {
|
||||
'name': username,
|
||||
'asset': "{{ asset.id }}",
|
||||
'username': username,
|
||||
'password': password
|
||||
};
|
||||
formSubmit({
|
||||
data: data,
|
||||
url: "{% url 'api-assets:asset-user-list' %}",
|
||||
method: 'POST',
|
||||
success: function () {
|
||||
toastr.success("{% trans 'Update successfully!' %}");
|
||||
},
|
||||
error: function () {
|
||||
toastr.error("{% trans 'Update failed!' %}");
|
||||
}
|
||||
});
|
||||
$("#asset_user_auth_modal").modal('hide');
|
||||
}
|
||||
else{
|
||||
$('#id_password').parent().addClass('has-error');
|
||||
}
|
||||
})
|
||||
.on('click', '.btn-test-connective', function () {
|
||||
var username = $(this).data('username');
|
||||
var the_url = "{% url 'api-assets:asset-user-connective' %}" + "?asset_id={{ asset.id }}" + "&username=" + username;
|
||||
var success = function (data) {
|
||||
var task_id = data.task;
|
||||
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
|
||||
window.open(url, '', 'width=800,height=600,left=400,top=400')
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: 'GET',
|
||||
success: success,
|
||||
flash_message: false
|
||||
});
|
||||
})
|
||||
.on('click', '#btn-bulk-test-connective', function () {
|
||||
var the_url = "{% url 'api-assets:asset-user-connective' %}" + "?asset_id={{ asset.id }}";
|
||||
var success = function (data) {
|
||||
var task_id = data.task;
|
||||
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
|
||||
window.open(url, '', 'width=800,height=600,left=400,top=400')
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: 'GET',
|
||||
success: success,
|
||||
flash_message: false
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -95,9 +95,12 @@ $(document).ready(function () {
|
||||
if(protocol === 'rdp'){
|
||||
port = 3389;
|
||||
}
|
||||
if(protocol === 'telnet (beta)'){
|
||||
else if(protocol === 'telnet (beta)'){
|
||||
port = 23;
|
||||
}
|
||||
else if(protocol === 'vnc'){
|
||||
port = 5901;
|
||||
}
|
||||
$("#id_port").val(port);
|
||||
});
|
||||
})
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
<li class="active">
|
||||
<a href="{% url 'assets:asset-detail' pk=asset.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Asset detail' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'assets:asset-user-list' pk=asset.id %}" class="text-center"><i class="fa fa-bar-chart-o"></i> {% trans 'Asset user list' %} </a>
|
||||
</li>
|
||||
{% if user.is_superuser %}
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-default" href="{% url 'assets:asset-update' pk=asset.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
|
||||
@@ -32,7 +35,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-7" style="padding-left: 0">
|
||||
<div class="col-sm-8" style="padding-left: 0">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span class="label"><b>{{ asset.hostname }}</b></span>
|
||||
@@ -139,7 +142,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% if user.is_superuser or user.is_org_admin %}
|
||||
<div class="col-sm-5" style="padding-left: 0;padding-right: 0">
|
||||
<div class="col-sm-4" style="padding-left: 0;padding-right: 0">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Quick modify' %}
|
||||
|
||||
@@ -92,8 +92,7 @@
|
||||
<th class="text-center">{% trans 'Hostname' %}</th>
|
||||
<th class="text-center">{% trans 'IP' %}</th>
|
||||
<th class="text-center">{% trans 'Hardware' %}</th>
|
||||
<th class="text-center">{% trans 'Active' %}</th>
|
||||
{# <th class="text-center">{% trans 'Reachable' %}</th>#}
|
||||
<th class="text-center">{% trans 'Reachable' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -134,8 +133,11 @@
|
||||
<li id="menu_refresh_hardware_info" class="btn-refresh-hardware" tabindex="-1"><a><i class="fa fa-refresh"></i> {% trans 'Refresh node hardware info' %}</a></li>
|
||||
<li id="menu_test_connective" class="btn-test-connective" tabindex="-1"><a><i class="fa fa-chain"></i> {% trans 'Test node connective' %}</a></li>
|
||||
<li class="divider"></li>
|
||||
<li id="menu_refresh_assets_amount" class="btn-refresh-assets-amount" tabindex="-1"><a><i class="fa fa-refresh"></i> {% trans 'Refresh all node assets amount' %}</a></li>
|
||||
<li class="divider"></li>
|
||||
<li id="show_current_asset" class="btn-show-current-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-hand-o-up"></i> {% trans 'Display only current node assets' %}</a></li>
|
||||
<li id="show_all_asset" class="btn-show-all-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-th"></i> {% trans 'Displays all child node assets' %}</a></li>
|
||||
{# <li id="fresh_tree" class="btn-refresh-tree" tabindex="-1"><a><i class="fa fa-refresh"></i> {% trans 'Refresh' %}</a></li>#}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -147,11 +149,14 @@
|
||||
<script>
|
||||
var zTree, rMenu, asset_table, show = 0;
|
||||
var update_node_action = "";
|
||||
var current_node_id = null;
|
||||
var current_node = null;
|
||||
function initTable() {
|
||||
var options = {
|
||||
ele: $('#asset_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
cellData = htmlEscape(cellData);
|
||||
{% url 'assets:asset-detail' pk=DEFAULT_PK as the_url %}
|
||||
var detail_btn = '<a href="{{ the_url }}">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
@@ -160,10 +165,12 @@ function initTable() {
|
||||
$(td).html(rowData.hardware_info)
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
if (!cellData) {
|
||||
$(td).html('<i class="fa fa-times text-danger"></i>')
|
||||
if (cellData === 1){
|
||||
$(td).html('<i class="fa fa-circle text-navy"></i>')
|
||||
} else if (cellData === 0) {
|
||||
$(td).html('<i class="fa fa-circle text-danger"></i>')
|
||||
} else {
|
||||
$(td).html('<i class="fa fa-check text-navy"></i>')
|
||||
$(td).html('<i class="fa fa-circle text-warning"></i>')
|
||||
}
|
||||
}},
|
||||
|
||||
@@ -176,8 +183,8 @@ function initTable() {
|
||||
ajax_url: '{% url "api-assets:asset-list" %}',
|
||||
columns: [
|
||||
{data: "id"}, {data: "hostname" }, {data: "ip" },
|
||||
{data: "cpu_cores"}, {data: "is_active", orderable: false },
|
||||
{data: "id", orderable: false }
|
||||
{data: "cpu_cores", orderable: false},
|
||||
{data: "connectivity", orderable: false}, {data: "id", orderable: false }
|
||||
],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
@@ -191,18 +198,20 @@ function addTreeNode() {
|
||||
if (!parentNode){
|
||||
return
|
||||
}
|
||||
var url = "{% url 'api-assets:node-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", parentNode.node_id );
|
||||
var url = "{% url 'api-assets:node-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", parentNode.meta.node.id);
|
||||
$.post(url, {}, function (data, status){
|
||||
if (status === "success") {
|
||||
var newNode = {
|
||||
id: data["key"],
|
||||
name: data["value"],
|
||||
node_id: data["id"],
|
||||
pId: parentNode.id
|
||||
pId: parentNode.id,
|
||||
meta: {
|
||||
"node": data
|
||||
}
|
||||
};
|
||||
newNode.checked = zTree.getSelectedNodes()[0].checked;
|
||||
zTree.addNodes(parentNode, 0, newNode);
|
||||
var node = zTree.getNodeByParam('id', newNode.node_id, parentNode);
|
||||
var node = zTree.getNodeByParam('id', newNode.id, parentNode);
|
||||
zTree.editName(node);
|
||||
} else {
|
||||
alert("{% trans 'Create node failed' %}")
|
||||
@@ -218,10 +227,10 @@ function removeTreeNode() {
|
||||
}
|
||||
if (current_node.children && current_node.children.length > 0) {
|
||||
toastr.error("{% trans 'Have child node, cancel' %}");
|
||||
} else if (current_node.assets_amount !== 0) {
|
||||
} else if (current_node.meta.node.assets_amount !== 0) {
|
||||
toastr.error("{% trans 'Have assets, cancel' %}");
|
||||
} else {
|
||||
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.node_id );
|
||||
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
|
||||
$.ajax({
|
||||
url: url,
|
||||
method: "DELETE",
|
||||
@@ -238,8 +247,8 @@ function editTreeNode() {
|
||||
if (!current_node){
|
||||
return
|
||||
}
|
||||
if (current_node.value) {
|
||||
current_node.name = current_node.value;
|
||||
if (current_node) {
|
||||
current_node.name = current_node.meta.node.value;
|
||||
}
|
||||
zTree.editName(current_node);
|
||||
}
|
||||
@@ -281,7 +290,7 @@ function onBodyMouseDown(event){
|
||||
|
||||
|
||||
function onRename(event, treeId, treeNode, isCancel){
|
||||
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", treeNode.node_id);
|
||||
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
|
||||
var data = {"value": treeNode.name};
|
||||
if (isCancel){
|
||||
return
|
||||
@@ -291,13 +300,21 @@ function onRename(event, treeId, treeNode, isCancel){
|
||||
body: JSON.stringify(data),
|
||||
method: "PATCH",
|
||||
success_message: "{% trans 'Rename success' %}",
|
||||
fail_message: "{% trans 'Rename failed, do not change the root node name' %}"
|
||||
fail_message: "{% trans 'Rename failed, do not change the root node name' %}",
|
||||
success: function () {
|
||||
treeNode.name = treeNode.name + ' (' + treeNode.meta.node.assets_amount + ')'
|
||||
zTree.updateNode(treeNode);
|
||||
console.log("Success: " + treeNode.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onSelected(event, treeNode) {
|
||||
current_node = treeNode;
|
||||
current_node_id = treeNode.meta.node.id;
|
||||
zTree.expandNode(current_node, true);
|
||||
var url = asset_table.ajax.url();
|
||||
url = setUrlParam(url, "node_id", treeNode.node_id);
|
||||
url = setUrlParam(url, "node_id", current_node_id);
|
||||
url = setUrlParam(url, "show_current_asset", getCookie('show_current_asset'));
|
||||
setCookie('node_selected', treeNode.node_id);
|
||||
asset_table.ajax.url(url);
|
||||
@@ -305,6 +322,9 @@ function onSelected(event, treeNode) {
|
||||
}
|
||||
|
||||
function selectQueryNode() {
|
||||
// TODO: 是否应该添加
|
||||
// 暂时忽略之前选中的内容
|
||||
return
|
||||
var query_node_id = $.getUrlParam("node");
|
||||
var cookie_node_id = getCookie('node_selected');
|
||||
var node;
|
||||
@@ -329,10 +349,10 @@ function beforeDrag() {
|
||||
function beforeDrop(treeId, treeNodes, targetNode, moveType) {
|
||||
var treeNodesNames = [];
|
||||
$.each(treeNodes, function (index, value) {
|
||||
treeNodesNames.push(value.value);
|
||||
treeNodesNames.push(value.name);
|
||||
});
|
||||
|
||||
var msg = "你想移动节点: `" + treeNodesNames.join(",") + "` 到 `" + targetNode.value + "` 下吗?";
|
||||
var msg = "你想移动节点: `" + treeNodesNames.join(",") + "` 到 `" + targetNode.name + "` 下吗?";
|
||||
return confirm(msg);
|
||||
}
|
||||
|
||||
@@ -342,10 +362,10 @@ function onDrag(event, treeId, treeNodes) {
|
||||
function onDrop(event, treeId, treeNodes, targetNode, moveType) {
|
||||
var treeNodesIds = [];
|
||||
$.each(treeNodes, function (index, value) {
|
||||
treeNodesIds.push(value.node_id);
|
||||
treeNodesIds.push(value.meta.node.id);
|
||||
});
|
||||
|
||||
var the_url = "{% url 'api-assets:node-add-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", targetNode.node_id);
|
||||
var the_url = "{% url 'api-assets:node-add-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", targetNode.meta.node.id);
|
||||
var body = {nodes: treeNodesIds};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
@@ -355,6 +375,14 @@ function onDrop(event, treeId, treeNodes, targetNode, moveType) {
|
||||
}
|
||||
|
||||
function initTree() {
|
||||
if (zTree) {
|
||||
return
|
||||
}
|
||||
var url = '{% url 'api-assets:node-children-tree' %}?assets=0&all=';
|
||||
var showCurrentAsset = getCookie('show_current_asset');
|
||||
if (!showCurrentAsset) {
|
||||
url += '1'
|
||||
}
|
||||
var setting = {
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
@@ -365,6 +393,12 @@ function initTree() {
|
||||
enable: true
|
||||
}
|
||||
},
|
||||
async: {
|
||||
enable: true,
|
||||
url: url,
|
||||
autoParam: ["id=key", "name=n", "level=lv"],
|
||||
type: 'get'
|
||||
},
|
||||
edit: {
|
||||
enable: true,
|
||||
showRemoveBtn: false,
|
||||
@@ -387,26 +421,8 @@ function initTree() {
|
||||
};
|
||||
|
||||
var zNodes = [];
|
||||
$.get("{% url 'api-assets:node-list' %}", function(data, status){
|
||||
$.each(data, function (index, value) {
|
||||
value["node_id"] = value["id"];
|
||||
value["id"] = value["tree_id"];
|
||||
if (value["tree_id"] !== value["tree_parent"]){
|
||||
value["pId"] = value["tree_parent"];
|
||||
} else {
|
||||
value["isParent"] = true;
|
||||
}
|
||||
value["name"] = value["value"] + ' (' + value['assets_amount'] + ')';
|
||||
value['value'] = value['value'];
|
||||
});
|
||||
zNodes = data;
|
||||
$.fn.zTree.init($("#assetTree"), setting, zNodes);
|
||||
zTree = $.fn.zTree.getZTreeObj("assetTree");
|
||||
var root = zTree.getNodes()[0];
|
||||
zTree.expandNode(root);
|
||||
rMenu = $("#rMenu");
|
||||
selectQueryNode();
|
||||
});
|
||||
zTree = $.fn.zTree.init($("#assetTree"), setting, zNodes);
|
||||
rMenu = $("#rMenu");
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
@@ -443,20 +459,15 @@ $(document).ready(function(){
|
||||
.on('click', '.btn_export', function () {
|
||||
var $data_table = $('#asset_list_table').DataTable();
|
||||
var rows = $data_table.rows('.selected').data();
|
||||
var nodes = zTree.getSelectedNodes();
|
||||
var current_node;
|
||||
if (nodes && nodes.length === 1) {
|
||||
current_node = nodes[0];
|
||||
}
|
||||
|
||||
var assets = [];
|
||||
$.each(rows, function (index, obj) {
|
||||
assets.push(obj.id)
|
||||
});
|
||||
var _node_id = current_node ? current_node.node_id : null;
|
||||
$.ajax({
|
||||
url: "{% url "assets:asset-export" %}",
|
||||
method: 'POST',
|
||||
data: JSON.stringify({assets_id: assets, node_id: _node_id}),
|
||||
data: JSON.stringify({assets_id: assets, node_id: current_node_id}),
|
||||
dataType: "json",
|
||||
success: function (data, textStatus) {
|
||||
window.open(data.redirect)
|
||||
@@ -469,12 +480,8 @@ $(document).ready(function(){
|
||||
.on('click', '#btn_asset_import', function () {
|
||||
var $form = $('#fm_asset_import');
|
||||
var action = $form.attr("action");
|
||||
var nodes = zTree.getSelectedNodes();
|
||||
var current_node;
|
||||
if (nodes && nodes.length ===1 ){
|
||||
current_node = nodes[0];
|
||||
action = setUrlParam(action, 'node_id', current_node.node_id);
|
||||
{#action += "?node_id=" + current_node.node_id;#}
|
||||
if (current_node_id){
|
||||
action = setUrlParam(action, 'node_id', current_node_id);
|
||||
$form.attr("action", action)
|
||||
}
|
||||
$form.find('.help-block').remove();
|
||||
@@ -496,25 +503,14 @@ $(document).ready(function(){
|
||||
})
|
||||
.on('click', '.btn-create-asset', function () {
|
||||
var url = "{% url 'assets:asset-create' %}";
|
||||
var nodes = zTree.getSelectedNodes();
|
||||
var current_node;
|
||||
if (nodes && nodes.length ===1 ){
|
||||
current_node = nodes[0];
|
||||
url += "?node_id=" + current_node.node_id;
|
||||
if (current_node_id) {
|
||||
url += "?node_id=" + current_node_id;
|
||||
}
|
||||
window.open(url, '_self');
|
||||
})
|
||||
.on('click', '.btn-refresh-hardware', function () {
|
||||
var url = "{% url 'api-assets:node-refresh-hardware-info' pk=DEFAULT_PK %}";
|
||||
var nodes = zTree.getSelectedNodes();
|
||||
var current_node;
|
||||
if (nodes && nodes.length ===1 ){
|
||||
current_node = nodes[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
var the_url = url.replace("{{ DEFAULT_PK }}", current_node.node_id);
|
||||
var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
|
||||
function success(data) {
|
||||
rMenu.css({"visibility" : "hidden"});
|
||||
var task_id = data.task;
|
||||
@@ -531,15 +527,10 @@ $(document).ready(function(){
|
||||
})
|
||||
.on('click', '.btn-test-connective', function () {
|
||||
var url = "{% url 'api-assets:node-test-connective' pk=DEFAULT_PK %}";
|
||||
var nodes = zTree.getSelectedNodes();
|
||||
var current_node;
|
||||
if (nodes && nodes.length ===1 ){
|
||||
current_node = nodes[0];
|
||||
} else {
|
||||
if (!current_node_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var the_url = url.replace("{{ DEFAULT_PK }}", current_node.node_id);
|
||||
var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
|
||||
function success(data) {
|
||||
rMenu.css({"visibility" : "hidden"});
|
||||
var task_id = data.task;
|
||||
@@ -567,6 +558,19 @@ $(document).ready(function(){
|
||||
setCookie('show_current_asset', '');
|
||||
location.reload();
|
||||
})
|
||||
.on('click', '.btn-test-connective', function () {
|
||||
hideRMenu();
|
||||
|
||||
})
|
||||
.on('click', '#menu_refresh_assets_amount', function () {
|
||||
hideRMenu();
|
||||
var url = "{% url 'api-assets:refresh-assets-amount' %}";
|
||||
APIUpdateAttr({
|
||||
'url': url,
|
||||
'method': 'GET'
|
||||
});
|
||||
window.location.reload();
|
||||
})
|
||||
.on('click', '.btn_asset_delete', function () {
|
||||
var $this = $(this);
|
||||
var $data_table = $("#asset_list_table").DataTable();
|
||||
@@ -654,17 +658,28 @@ $(document).ready(function(){
|
||||
});
|
||||
}
|
||||
function doUpdate() {
|
||||
var id_list_string = id_list.join(',');
|
||||
var url = "{% url 'assets:asset-bulk-update' %}?assets_id=" + id_list_string;
|
||||
location.href = url
|
||||
var data = {
|
||||
'assets_id':id_list
|
||||
};
|
||||
function error(data) {
|
||||
toastr.error(JSON.parse(data).error)
|
||||
}
|
||||
function success(data) {
|
||||
location.href = data.url;
|
||||
}
|
||||
APIUpdateAttr({
|
||||
'url': "{% url 'api-assets:asset-bulk-update-select' %}",
|
||||
'method': 'POST',
|
||||
'body': JSON.stringify(data),
|
||||
'flash_message': false,
|
||||
'success': success,
|
||||
'error': error,
|
||||
})
|
||||
}
|
||||
|
||||
function doRemove() {
|
||||
var current_node;
|
||||
var nodes = zTree.getSelectedNodes();
|
||||
if (nodes && nodes.length === 1) {
|
||||
current_node = nodes[0]
|
||||
} else {
|
||||
if (!current_node_id) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -677,7 +692,7 @@ $(document).ready(function(){
|
||||
};
|
||||
|
||||
APIUpdateAttr({
|
||||
'url': '/api/assets/v1/nodes/' + current_node.node_id + '/assets/remove/',
|
||||
'url': '/api/assets/v1/nodes/' + current_node_id + '/assets/remove/',
|
||||
'method': 'PUT',
|
||||
'body': JSON.stringify(data),
|
||||
'success': success
|
||||
@@ -706,11 +721,7 @@ $(document).ready(function(){
|
||||
})
|
||||
.on('click', '#btn_asset_modal_confirm', function () {
|
||||
var assets_selected = asset_table2.selected;
|
||||
var current_node;
|
||||
var nodes = zTree.getSelectedNodes();
|
||||
if (nodes && nodes.length === 1) {
|
||||
current_node = nodes[0]
|
||||
} else {
|
||||
if (!current_node_id) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -722,9 +733,9 @@ $(document).ready(function(){
|
||||
|
||||
var url = '';
|
||||
if (update_node_action === "move") {
|
||||
url = "{% url 'api-assets:node-replace-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.node_id);
|
||||
url = "{% url 'api-assets:node-replace-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
|
||||
} else {
|
||||
url = "{% url 'api-assets:node-add-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.node_id);
|
||||
url = "{% url 'api-assets:node-add-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
|
||||
}
|
||||
|
||||
APIUpdateAttr({
|
||||
@@ -740,6 +751,7 @@ $(document).ready(function(){
|
||||
}).on('click', '#menu_asset_move', function () {
|
||||
update_node_action = "move"
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -40,6 +40,7 @@ function initTable() {
|
||||
ele: $('#cmd_filter_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
cellData = htmlEscape(cellData);
|
||||
var detail_btn = '<a href="{% url 'assets:cmd-filter-detail' pk=DEFAULT_PK %}">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
}},
|
||||
|
||||
@@ -24,14 +24,26 @@
|
||||
{% block custom_foot_js %}
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function () {
|
||||
console.log($.fn.select2.defaults);
|
||||
$('.select2').select2().off("select2:open");
|
||||
}).on('click', '.select2-selection__rendered', function (e) {
|
||||
e.preventDefault();
|
||||
$("#asset_list_modal").modal();
|
||||
initSelectedAssets2Table();
|
||||
})
|
||||
.on('click', '#btn_asset_modal_confirm', function () {
|
||||
var assets = asset_table2.selected;
|
||||
var options = [];
|
||||
$('#id_assets option').each(function (i, v) {
|
||||
options.push(v.value)
|
||||
});
|
||||
asset_table2.selected_rows.forEach(function (i) {
|
||||
var name = i.hostname + '(' + i.ip + ')';
|
||||
var option = new Option(name, i.id, false, true);
|
||||
|
||||
if (options.indexOf(i.id) === -1) {
|
||||
$('#id_assets').append(option).trigger('change');
|
||||
}
|
||||
});
|
||||
$('.select2').val(assets).trigger('change');
|
||||
$("#asset_list_modal").modal('hide');
|
||||
|
||||
|
||||
@@ -15,10 +15,16 @@
|
||||
<div class="panel-options">
|
||||
<ul class="nav nav-tabs">
|
||||
<li>
|
||||
<a href="{% url 'assets:domain-detail' pk=object.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
|
||||
<a href="{% url 'assets:domain-detail' pk=object.id %}"
|
||||
class="text-center"><i
|
||||
class="fa fa-laptop"></i> {% trans 'Detail' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="active">
|
||||
<a href="{% url 'assets:domain-detail' pk=object.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Gateway' %} </a>
|
||||
<a href="{% url 'assets:domain-detail' pk=object.id %}"
|
||||
class="text-center"><i
|
||||
class="fa fa-laptop"></i> {% trans 'Gateway' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -33,7 +39,8 @@
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<a class="dropdown-toggle"
|
||||
data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-user">
|
||||
@@ -45,13 +52,17 @@
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<div class="uc pull-left m-r-5">
|
||||
<a href="{% url 'assets:domain-gateway-create' pk=object.id %}" class="btn btn-sm btn-primary"> {% trans "Create gateway" %} </a>
|
||||
<a href="{% url 'assets:domain-gateway-create' pk=object.id %}"
|
||||
class="btn btn-sm btn-primary"> {% trans "Create gateway" %} </a>
|
||||
</div>
|
||||
<table class="table table-striped table-bordered table-hover " id="domain_list_table" >
|
||||
<table class="table table-striped table-bordered table-hover "
|
||||
id="domain_list_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
<input type="checkbox" id="check_all" class="ipt_check_all" >
|
||||
<input type="checkbox"
|
||||
id="check_all"
|
||||
class="ipt_check_all">
|
||||
</th>
|
||||
<th class="text-center">{% trans 'Name' %}</th>
|
||||
<th class="text-center">{% trans 'IP' %}</th>
|
||||
@@ -73,6 +84,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'assets/_gateway_test_modal.html' %}
|
||||
{% endblock %}
|
||||
{% block content_bottom_left %}{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
@@ -84,7 +96,7 @@ function initTable() {
|
||||
{targets: 7, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "assets:domain-gateway-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
var test_btn = '<a class="btn btn-xs btn-warning m-l-xs btn-test" data-uid="{{ DEFAULT_PK }}">{% trans "Test connection" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
var test_btn = '<a class="btn btn-xs btn-warning m-l-xs btn-test" data-uid="{{ DEFAULT_PK }}" data-port="PORT">{% trans "Test connection" %}</a>'.replace('{{ DEFAULT_PK }}', cellData).replace("PORT", rowData.port);
|
||||
if(rowData.protocol === 'rdp'){
|
||||
test_btn = '<a class="btn btn-xs btn-warning m-l-xs btn-test" disabled data-uid="{{ DEFAULT_PK }}">{% trans "Test connection" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
|
||||
}
|
||||
@@ -114,15 +126,21 @@ $(document).ready(function(){
|
||||
$data_table.ajax.reload();
|
||||
}, 3000);
|
||||
}).on('click', '.btn-test', function () {
|
||||
var $this = $(this);
|
||||
var uid = $this.data('uid');
|
||||
$("#ssh_test_port").val($(this).data('port'));
|
||||
$("#gateway_id").val($(this).data('uid'));
|
||||
$("#gateway_test").modal('show');
|
||||
|
||||
}).on('click', '#btn_gateway_test', function () {
|
||||
var data = $("#test_gateway_form").serializeObject();
|
||||
var uid = data.gateway_id;
|
||||
var the_url = '{% url "api-assets:test-gateway-connective" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
method: "GET",
|
||||
method: "POST",
|
||||
body: JSON.stringify({'port': parseInt(data.port)}),
|
||||
success_message: "{% trans 'Can be connected' %}",
|
||||
fail_message: "{% trans 'The connection fails' %}"
|
||||
})
|
||||
})
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -41,6 +41,7 @@ function initTable() {
|
||||
ele: $('#domain_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
cellData = htmlEscape(cellData);
|
||||
var detail_btn = '<a href="{% url "assets:domain-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
}},
|
||||
|
||||
@@ -32,9 +32,22 @@ $(document).ready(function () {
|
||||
}).on('click', '.select2-selection__rendered', function (e) {
|
||||
e.preventDefault();
|
||||
$("#asset_list_modal").modal();
|
||||
initSelectedAssets2Table();
|
||||
})
|
||||
.on('click', '#btn_asset_modal_confirm', function () {
|
||||
var assets = asset_table2.selected;
|
||||
var options = [];
|
||||
$('#id_assets option').each(function (i, v) {
|
||||
options.push(v.value)
|
||||
});
|
||||
asset_table2.selected_rows.forEach(function (i) {
|
||||
var name = i.hostname + '(' + i.ip + ')';
|
||||
var option = new Option(name, i.id, false, true);
|
||||
|
||||
if (options.indexOf(i.id) === -1) {
|
||||
$('#id_assets').append(option).trigger('change');
|
||||
}
|
||||
});
|
||||
$('#id_assets').val(assets).trigger('change');
|
||||
$("#asset_list_modal").modal('hide');
|
||||
})
|
||||
|
||||
@@ -30,6 +30,7 @@ function initTable() {
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
{# var detail_btn = '<a href="{% url "assets:label-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';#}
|
||||
cellData = htmlEscape(cellData);
|
||||
var detail_btn = '<a>' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
}},
|
||||
|
||||
@@ -132,35 +132,40 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'assets/_asset_user_auth_modal.html' %}
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
function initAssetsTable() {
|
||||
var unreachable = {{ system_user.unreachable_assets|safe }};
|
||||
var connectivity = {{ system_user.connectivity | safe }};
|
||||
var options = {
|
||||
ele: $('#system_user_list'),
|
||||
buttons: [],
|
||||
order: [],
|
||||
columnDefs: [
|
||||
{targets: 0, createdCell: function (td, cellData, rowData) {
|
||||
cellData = htmlEscape(cellData);
|
||||
var detail_btn = '<a href="{% url "assets:asset-detail" pk=DEFAULT_PK %}" data-aid="'+rowData.id+'">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
}},
|
||||
{targets: 3, createdCell: function (td, cellData) {
|
||||
if (unreachable.indexOf(cellData) >= 0) {
|
||||
if (connectivity.unreachable.indexOf(cellData) >= 0) {
|
||||
$(td).html('<i class="fa fa-times text-danger"></i>')
|
||||
} else {
|
||||
} else if (connectivity.reachable.indexOf(cellData) >= 0 ) {
|
||||
$(td).html('<i class="fa fa-check text-navy"></i>')
|
||||
}
|
||||
} else {
|
||||
$(td).html('')
|
||||
}
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
{targets: 4, createdCell: function (td, cellData, rowData) {
|
||||
var push_btn = '';
|
||||
{% if system_user.auto_push %}
|
||||
push_btn = '<a class="btn btn-xs btn-primary btn-push-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Push" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
|
||||
{% endif %}
|
||||
var test_btn = ' <a class="btn btn-xs btn-info btn-test-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Test" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
|
||||
{#var unbound_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-asset-unbound" data-uid="{{ DEFAULT_PK }}">{% trans "Unbound" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);#}
|
||||
$(td).html(push_btn + test_btn);
|
||||
var update_auth_btn = ' <a class="btn btn-xs btn-primary btn-update-asset-user-auth" data-aid="{{ DEFAULT_PK }}" data-hostname="hostname777">{% trans "Update auth" %}</a>'.replace("{{ DEFAULT_PK }}", cellData).replace("hostname777", rowData.hostname);
|
||||
$(td).html(push_btn + test_btn + update_auth_btn);
|
||||
}}
|
||||
],
|
||||
ajax_url: '{% url "api-assets:system-user-assets" pk=system_user.id %}',
|
||||
@@ -200,6 +205,15 @@ function updateSystemUserNode(nodes) {
|
||||
}
|
||||
jumpserver.nodes_selected = {};
|
||||
|
||||
function initAssetUserAuthModalForm(hostname, username){
|
||||
$('#id_hostname_p').html(hostname);
|
||||
$('#id_username_p').html(username);
|
||||
$('#id_password').parent().removeClass('has-error');
|
||||
$('#id_password').val('');
|
||||
}
|
||||
|
||||
var assetId;
|
||||
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2()
|
||||
.on('select2:select', function(evt) {
|
||||
@@ -313,6 +327,38 @@ $(document).ready(function () {
|
||||
error: error
|
||||
})
|
||||
})
|
||||
|
||||
.on('click', '.btn-update-asset-user-auth', function() {
|
||||
assetId = $(this).data('aid');
|
||||
var hostname = $(this).data('hostname');
|
||||
var username = '{{ system_user.username }}';
|
||||
initAssetUserAuthModalForm(hostname, username);
|
||||
$("#asset_user_auth_modal").modal();
|
||||
})
|
||||
.on('click', '#btn_asset_user_auth_modal_confirm', function(){
|
||||
var password = $('#id_password').val();
|
||||
if (password){
|
||||
var data = {
|
||||
'name': "{{ system_user.username }}",
|
||||
'asset': assetId,
|
||||
'username': "{{ system_user.username }}",
|
||||
'password': password
|
||||
};
|
||||
formSubmit({
|
||||
data: data,
|
||||
url: "{% url 'api-assets:asset-user-list' %}",
|
||||
method: 'POST',
|
||||
success: function () {
|
||||
toastr.success("{% trans 'Update successfully!' %}");
|
||||
},
|
||||
error: function () {
|
||||
toastr.error("{% trans 'Update failed!' %}");
|
||||
}
|
||||
});
|
||||
$("#asset_user_auth_modal").modal('hide');
|
||||
}
|
||||
else{
|
||||
$('#id_password').parent().addClass('has-error');
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -49,6 +49,7 @@ function initTable() {
|
||||
ele: $('#system_user_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
cellData = htmlEscape(cellData);
|
||||
var detail_btn = '<a href="{% url "assets:system-user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
}},
|
||||
@@ -95,7 +96,7 @@ function initTable() {
|
||||
}}],
|
||||
ajax_url: '{% url "api-assets:system-user-list" %}',
|
||||
columns: [
|
||||
{data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "get_login_mode_display"}, {data: "assets_amount" },
|
||||
{data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "login_mode_display"}, {data: "assets_amount" },
|
||||
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" }
|
||||
],
|
||||
op_html: $('#actions').html()
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<div class="file-manager ">
|
||||
<div id="assetTree" class="ztree">
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,6 +45,7 @@
|
||||
<th class="text-center">{% trans 'IP' %}</th>
|
||||
<th class="text-center">{% trans 'Active' %}</th>
|
||||
<th class="text-center">{% trans 'System users' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -62,16 +62,19 @@
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
var treeUrl = "{% url 'api-perms:my-nodes-assets-as-tree' %}?show_assets=0&cache_policy=1";
|
||||
var zTree, asset_table, show=0;
|
||||
var inited = false;
|
||||
var url;
|
||||
|
||||
|
||||
function initTable() {
|
||||
if (inited){
|
||||
return
|
||||
} else {
|
||||
inited = true;
|
||||
}
|
||||
url = "{% url 'api-perms:my-assets' %}";
|
||||
url = "{% url 'api-perms:my-assets' %}?cache_policy=1";
|
||||
var options = {
|
||||
ele: $('#user_assets_table'),
|
||||
columnDefs: [
|
||||
@@ -92,13 +95,18 @@ function initTable() {
|
||||
users.push(data.name);
|
||||
});
|
||||
$(td).html(users.join(', '))
|
||||
}},
|
||||
{targets: 5, createdCell: function (td, cellData) {
|
||||
var conn_btn = '<a href="{% url "luna-view" %}?login_to=' + cellData +'" class="btn btn-xs btn-primary">{% trans "Connect" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
|
||||
$(td).html(conn_btn)
|
||||
}}
|
||||
],
|
||||
ajax_url: url,
|
||||
columns: [
|
||||
{data: "id"}, {data: "hostname" }, {data: "ip" },
|
||||
{data: "is_active", orderable: false },
|
||||
{data: "system_users_granted", orderable: false}
|
||||
{data: "system_users_granted", orderable: false},
|
||||
{data: "id", orderable: false}
|
||||
]
|
||||
};
|
||||
asset_table = jumpserver.initServerSideDataTable(options);
|
||||
@@ -106,7 +114,7 @@ function initTable() {
|
||||
}
|
||||
|
||||
function onSelected(event, treeNode) {
|
||||
url = '{% url "api-perms:my-node-assets" node_id=DEFAULT_PK %}';
|
||||
url = '{% url "api-perms:my-node-assets" node_id=DEFAULT_PK %}?cache_policy=1';
|
||||
var node_id = treeNode.meta.node.id;
|
||||
url = url.replace("{{ DEFAULT_PK }}", node_id);
|
||||
setCookie('node_selected', treeNode.id);
|
||||
@@ -118,7 +126,7 @@ function initTree() {
|
||||
var setting = {
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
showLine: true
|
||||
showLine: true,
|
||||
},
|
||||
data: {
|
||||
simpleData: {
|
||||
@@ -131,10 +139,14 @@ function initTree() {
|
||||
};
|
||||
|
||||
var zNodes = [];
|
||||
$.get("{% url 'api-perms:my-nodes-assets-as-tree' %}?show_assets=0", function(data, status){
|
||||
$.get(treeUrl, function(data, status){
|
||||
zNodes = data;
|
||||
$.fn.zTree.init($("#assetTree"), setting, zNodes);
|
||||
zTree = $.fn.zTree.getZTreeObj("assetTree");
|
||||
rootNodeAddDom(zTree, function () {
|
||||
treeUrl = treeUrl.replace('cache_policy=1', 'cache_policy=2');
|
||||
initTree();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -159,7 +171,7 @@ $(document).ready(function () {
|
||||
'comment': "{% trans 'Comment' %}"
|
||||
{#'date_joined': "{% trans 'Date joined' %}",#}
|
||||
};
|
||||
$.each(data, function(index, value){
|
||||
$.each(data.results, function(index, value){
|
||||
if(value.id === asset_id){
|
||||
for(var i in desc){
|
||||
trs += "<tr class='no-borders-tr'>\n" +
|
||||
|
||||
@@ -17,6 +17,7 @@ router.register(r'nodes', api.NodeViewSet, 'node')
|
||||
router.register(r'domain', api.DomainViewSet, 'domain')
|
||||
router.register(r'gateway', api.GatewayViewSet, 'gateway')
|
||||
router.register(r'cmd-filter', api.CommandFilterViewSet, 'cmd-filter')
|
||||
router.register(r'asset-user', api.AssetUserViewSet, 'asset-user')
|
||||
|
||||
cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filter', lookup='filter')
|
||||
cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule')
|
||||
@@ -24,35 +25,49 @@ cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-r
|
||||
|
||||
urlpatterns = [
|
||||
path('assets-bulk/', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'),
|
||||
path('system-user/<uuid:pk>/auth-info/',
|
||||
api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
|
||||
path('system-user/<uuid:pk>/assets/',
|
||||
api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
|
||||
path('asset/update/select/',
|
||||
api.AssetBulkUpdateSelectAPI.as_view(), name='asset-bulk-update-select'),
|
||||
path('assets/<uuid:pk>/refresh/',
|
||||
api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'),
|
||||
path('assets/<uuid:pk>/alive/',
|
||||
api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'),
|
||||
path('assets/<uuid:pk>/gateway/',
|
||||
api.AssetGatewayApi.as_view(), name='asset-gateway'),
|
||||
|
||||
path('asset-user/auth-info/',
|
||||
api.AssetUserAuthInfoApi.as_view(), name='asset-user-auth-info'),
|
||||
path('asset-user/test-connective/',
|
||||
api.AssetUserTestConnectiveApi.as_view(), name='asset-user-connective'),
|
||||
|
||||
|
||||
path('admin-user/<uuid:pk>/nodes/',
|
||||
api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'),
|
||||
path('admin-user/<uuid:pk>/auth/',
|
||||
api.AdminUserAuthApi.as_view(), name='admin-user-auth'),
|
||||
path('admin-user/<uuid:pk>/connective/',
|
||||
api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'),
|
||||
path('admin-user/<uuid:pk>/assets/',
|
||||
api.AdminUserAssetsListView.as_view(), name='admin-user-assets'),
|
||||
|
||||
path('system-user/<uuid:pk>/auth-info/',
|
||||
api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
|
||||
path('system-user/<uuid:pk>/asset/<uuid:aid>/auth-info/',
|
||||
api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'),
|
||||
path('system-user/<uuid:pk>/assets/',
|
||||
api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
|
||||
path('system-user/<uuid:pk>/push/',
|
||||
api.SystemUserPushApi.as_view(), name='system-user-push'),
|
||||
path('system-user/<uuid:pk>/asset/<uuid:aid>/push/',
|
||||
api.SystemUserPushToAssetApi.as_view(), name='system-user-push-to-asset'),
|
||||
path('system-user/<uuid:pk>/asset/<uuid:aid>/test/',
|
||||
api.SystemUserTestAssetConnectabilityApi.as_view(), name='system-user-test-to-asset'),
|
||||
api.SystemUserTestAssetConnectivityApi.as_view(), name='system-user-test-to-asset'),
|
||||
path('system-user/<uuid:pk>/connective/',
|
||||
api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'),
|
||||
|
||||
path('system-user/<uuid:pk>/cmd-filter-rules/',
|
||||
api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'),
|
||||
|
||||
path('nodes/tree/', api.NodeListAsTreeApi.as_view(), name='node-tree'),
|
||||
path('nodes/children/tree/', api.NodeChildrenAsTreeApi.as_view(), name='node-children-tree'),
|
||||
path('nodes/<uuid:pk>/children/',
|
||||
api.NodeChildrenApi.as_view(), name='node-children'),
|
||||
path('nodes/children/', api.NodeChildrenApi.as_view(), name='node-children-2'),
|
||||
@@ -70,9 +85,12 @@ urlpatterns = [
|
||||
api.RefreshNodeHardwareInfoApi.as_view(), name='node-refresh-hardware-info'),
|
||||
path('nodes/<uuid:pk>/test-connective/',
|
||||
api.TestNodeConnectiveApi.as_view(), name='node-test-connective'),
|
||||
path('nodes/refresh-assets-amount/',
|
||||
api.RefreshAssetsAmount.as_view(), name='refresh-assets-amount'),
|
||||
|
||||
path('gateway/<uuid:pk>/test-connective/',
|
||||
api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
|
||||
|
||||
]
|
||||
|
||||
urlpatterns += router.urls + cmd_filter_router.urls
|
||||
|
||||
@@ -15,6 +15,8 @@ urlpatterns = [
|
||||
path('asset/<uuid:pk>/update/', views.AssetUpdateView.as_view(), name='asset-update'),
|
||||
path('asset/<uuid:pk>/delete/', views.AssetDeleteView.as_view(), name='asset-delete'),
|
||||
path('asset/update/', views.AssetBulkUpdateView.as_view(), name='asset-bulk-update'),
|
||||
# Asset user view
|
||||
path('asset/<uuid:pk>/asset-user/', views.AssetUserListView.as_view(), name='asset-user-list'),
|
||||
|
||||
# User asset view
|
||||
path('user-asset/', views.UserAssetListView.as_view(), name='user-asset-list'),
|
||||
|
||||
@@ -102,7 +102,7 @@ class AdminUserAssetsView(AdminUserRequiredMixin, SingleObjectMixin, ListView):
|
||||
'app': _('Assets'),
|
||||
'action': _('Admin user detail'),
|
||||
"total_amount": len(self.queryset),
|
||||
'unreachable_amount': len([asset for asset in self.queryset if asset.is_connective is False])
|
||||
'unreachable_amount': len([asset for asset in self.queryset if asset.connectivity is False])
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
@@ -28,13 +28,14 @@ from common.mixins import JSONResponseMixin
|
||||
from common.utils import get_object_or_none, get_logger
|
||||
from common.permissions import AdminUserRequiredMixin
|
||||
from common.const import create_success_msg, update_success_msg
|
||||
from ..const import CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX
|
||||
from orgs.utils import current_org
|
||||
from .. import forms
|
||||
from ..models import Asset, AdminUser, SystemUser, Label, Node, Domain
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AssetListView', 'AssetCreateView', 'AssetUpdateView',
|
||||
'AssetListView', 'AssetCreateView', 'AssetUpdateView', 'AssetUserListView',
|
||||
'UserAssetListView', 'AssetBulkUpdateView', 'AssetDetailView',
|
||||
'AssetDeleteView', 'AssetExportView', 'BulkImportAssetView',
|
||||
]
|
||||
@@ -56,6 +57,20 @@ class AssetListView(AdminUserRequiredMixin, TemplateView):
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AssetUserListView(AdminUserRequiredMixin, DetailView):
|
||||
model = Asset
|
||||
context_object_name = 'asset'
|
||||
template_name = 'assets/asset_asset_user_list.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Asset user list'),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class UserAssetListView(LoginRequiredMixin, TemplateView):
|
||||
template_name = 'assets/user_asset_list.html'
|
||||
|
||||
@@ -106,15 +121,12 @@ class AssetBulkUpdateView(AdminUserRequiredMixin, ListView):
|
||||
form = None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
assets_id = self.request.GET.get('assets_id', '')
|
||||
self.id_list = [i for i in assets_id.split(',')]
|
||||
|
||||
spm = request.GET.get('spm', '')
|
||||
assets_id = cache.get(CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX.format(spm))
|
||||
if kwargs.get('form'):
|
||||
self.form = kwargs['form']
|
||||
elif assets_id:
|
||||
self.form = self.form_class(
|
||||
initial={'assets': self.id_list}
|
||||
)
|
||||
self.form = self.form_class(initial={'assets': assets_id})
|
||||
else:
|
||||
self.form = self.form_class()
|
||||
return super().get(request, *args, **kwargs)
|
||||
@@ -216,7 +228,6 @@ class AssetExportView(LoginRequiredMixin, View):
|
||||
return HttpResponse('Json object not valid', status=400)
|
||||
|
||||
if not assets_id:
|
||||
print(node_id)
|
||||
node = get_object_or_none(Node, id=node_id) if node_id else Node.root()
|
||||
assets = node.get_all_assets()
|
||||
for asset in assets:
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from users.models import LoginLog
|
||||
@@ -13,6 +13,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='ftplog',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default=None, max_length=36, null=True),
|
||||
field=models.CharField(blank=True, db_index=True, default=None, max_length=36, null=True),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -13,6 +13,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='ftplog',
|
||||
name='org_id',
|
||||
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
|
||||
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -15,7 +15,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='OperateLog',
|
||||
fields=[
|
||||
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('user', models.CharField(max_length=128, verbose_name='User')),
|
||||
('action', models.CharField(choices=[('create', 'Create'), ('update', 'Update'), ('delete', 'Delete')], max_length=16, verbose_name='Action')),
|
||||
@@ -38,14 +38,4 @@ class Migration(migrations.Migration):
|
||||
('datetime', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserLoginLog',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
},
|
||||
bases=('users.loginlog',),
|
||||
),
|
||||
]
|
||||
|
||||
55
apps/audits/migrations/0005_auto_20190228_1715.py
Normal file
55
apps/audits/migrations/0005_auto_20190228_1715.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# Generated by Django 2.1.7 on 2019-02-28 09:15
|
||||
|
||||
from django.db import migrations, models, connection
|
||||
import django.utils.timezone
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('audits', '0004_operatelog_passwordchangelog_userloginlog'),
|
||||
('users', '0019_auto_20190304_1459'),
|
||||
]
|
||||
state_operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserLoginLog',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True,
|
||||
serialize=False)),
|
||||
('username',
|
||||
models.CharField(max_length=128, verbose_name='Username')),
|
||||
('type',
|
||||
models.CharField(choices=[('W', 'Web'), ('T', 'Terminal')],
|
||||
max_length=2, verbose_name='Login type')),
|
||||
('ip', models.GenericIPAddressField(verbose_name='Login ip')),
|
||||
('city', models.CharField(blank=True, max_length=254, null=True,
|
||||
verbose_name='Login city')),
|
||||
('user_agent',
|
||||
models.CharField(blank=True, max_length=254, null=True,
|
||||
verbose_name='User agent')),
|
||||
('mfa', models.SmallIntegerField(
|
||||
choices=[(0, 'Disabled'), (1, 'Enabled'), (2, '-')],
|
||||
default=2, verbose_name='MFA')),
|
||||
('reason', models.SmallIntegerField(
|
||||
choices=[(0, '-'), (1, 'Username/password check failed'),
|
||||
(2, 'MFA authentication failed'),
|
||||
(3, 'Username does not exist'),
|
||||
(4, 'Password expired')], default=0,
|
||||
verbose_name='Reason')),
|
||||
('status', models.BooleanField(
|
||||
choices=[(True, 'Success'), (False, 'Failed')],
|
||||
default=True, max_length=2, verbose_name='Status')),
|
||||
('datetime',
|
||||
models.DateTimeField(default=django.utils.timezone.now,
|
||||
verbose_name='Date login')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-datetime', 'username'],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.SeparateDatabaseAndState(state_operations=state_operations)
|
||||
]
|
||||
@@ -1,10 +1,11 @@
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import timezone
|
||||
|
||||
from orgs.mixins import OrgModelMixin
|
||||
from .hands import LoginLog
|
||||
|
||||
__all__ = [
|
||||
'FTPLog', 'OperateLog', 'PasswordChangeLog', 'UserLoginLog',
|
||||
@@ -55,6 +56,67 @@ class PasswordChangeLog(models.Model):
|
||||
return "{} change {}'s password".format(self.change_by, self.user)
|
||||
|
||||
|
||||
class UserLoginLog(LoginLog):
|
||||
class UserLoginLog(models.Model):
|
||||
LOGIN_TYPE_CHOICE = (
|
||||
('W', 'Web'),
|
||||
('T', 'Terminal'),
|
||||
)
|
||||
|
||||
MFA_DISABLED = 0
|
||||
MFA_ENABLED = 1
|
||||
MFA_UNKNOWN = 2
|
||||
|
||||
MFA_CHOICE = (
|
||||
(MFA_DISABLED, _('Disabled')),
|
||||
(MFA_ENABLED, _('Enabled')),
|
||||
(MFA_UNKNOWN, _('-')),
|
||||
)
|
||||
|
||||
REASON_NOTHING = 0
|
||||
REASON_PASSWORD = 1
|
||||
REASON_MFA = 2
|
||||
REASON_NOT_EXIST = 3
|
||||
REASON_PASSWORD_EXPIRED = 4
|
||||
|
||||
REASON_CHOICE = (
|
||||
(REASON_NOTHING, _('-')),
|
||||
(REASON_PASSWORD, _('Username/password check failed')),
|
||||
(REASON_MFA, _('MFA authentication failed')),
|
||||
(REASON_NOT_EXIST, _("Username does not exist")),
|
||||
(REASON_PASSWORD_EXPIRED, _("Password expired")),
|
||||
)
|
||||
|
||||
STATUS_CHOICE = (
|
||||
(True, _('Success')),
|
||||
(False, _('Failed'))
|
||||
)
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
username = models.CharField(max_length=128, verbose_name=_('Username'))
|
||||
type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, verbose_name=_('Login type'))
|
||||
ip = models.GenericIPAddressField(verbose_name=_('Login ip'))
|
||||
city = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('Login city'))
|
||||
user_agent = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('User agent'))
|
||||
mfa = models.SmallIntegerField(default=MFA_UNKNOWN, choices=MFA_CHOICE, verbose_name=_('MFA'))
|
||||
reason = models.SmallIntegerField(default=0, choices=REASON_CHOICE, verbose_name=_('Reason'))
|
||||
status = models.BooleanField(max_length=2, default=True, choices=STATUS_CHOICE, verbose_name=_('Status'))
|
||||
datetime = models.DateTimeField(default=timezone.now, verbose_name=_('Date login'))
|
||||
|
||||
@classmethod
|
||||
def get_login_logs(cls, date_form=None, date_to=None, user=None, keyword=None):
|
||||
login_logs = cls.objects.all()
|
||||
if date_form and date_to:
|
||||
login_logs = login_logs.filter(
|
||||
datetime__gt=date_form, datetime__lt=date_to
|
||||
)
|
||||
if user:
|
||||
login_logs = login_logs.filter(username=user)
|
||||
if keyword:
|
||||
login_logs = login_logs.filter(
|
||||
Q(ip__contains=keyword) |
|
||||
Q(city__contains=keyword) |
|
||||
Q(username__contains=keyword)
|
||||
)
|
||||
return login_logs
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
ordering = ['-datetime', 'username']
|
||||
|
||||
@@ -6,14 +6,17 @@ from django.dispatch import receiver
|
||||
from django.db import transaction
|
||||
|
||||
from jumpserver.utils import current_request
|
||||
from common.utils import get_request_ip
|
||||
from common.utils import get_request_ip, get_logger
|
||||
from users.models import User
|
||||
from .models import OperateLog, PasswordChangeLog
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
MODELS_NEED_RECORD = (
|
||||
'User', 'UserGroup', 'Asset', 'Node', 'AdminUser', 'SystemUser',
|
||||
'Domain', 'Gateway', 'Organization', 'AssetPermission',
|
||||
'Domain', 'Gateway', 'Organization', 'AssetPermission', 'CommandFilter',
|
||||
'CommandFilterRule', 'License', 'Setting', 'Account', 'SyncInstanceTask',
|
||||
)
|
||||
|
||||
|
||||
@@ -26,11 +29,16 @@ def create_operate_log(action, sender, resource):
|
||||
return
|
||||
resource_type = sender._meta.verbose_name
|
||||
remote_addr = get_request_ip(current_request)
|
||||
|
||||
data = {
|
||||
"user": str(user), 'action': action, 'resource_type': resource_type,
|
||||
'resource': str(resource), 'remote_addr': remote_addr,
|
||||
}
|
||||
with transaction.atomic():
|
||||
OperateLog.objects.create(
|
||||
user=user, action=action, resource_type=resource_type,
|
||||
resource=resource, remote_addr=remote_addr
|
||||
)
|
||||
try:
|
||||
OperateLog.objects.create(**data)
|
||||
except Exception as e:
|
||||
logger.error("Create operate log error: {}".format(e))
|
||||
|
||||
|
||||
@receiver(post_save, dispatch_uid="my_unique_identifier")
|
||||
|
||||
21
apps/audits/tasks.py
Normal file
21
apps/audits/tasks.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import datetime
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from celery import shared_task
|
||||
|
||||
from ops.celery.decorator import register_as_period_task
|
||||
from .models import UserLoginLog
|
||||
|
||||
|
||||
@register_as_period_task(interval=3600*24)
|
||||
@shared_task
|
||||
def clean_login_log_period():
|
||||
now = timezone.now()
|
||||
try:
|
||||
days = int(settings.LOGIN_LOG_KEEP_DAYS)
|
||||
except ValueError:
|
||||
days = 90
|
||||
expired_day = now - datetime.timedelta(days=days)
|
||||
UserLoginLog.objects.filter(datetime__lt=expired_day).delete()
|
||||
@@ -17,10 +17,10 @@
|
||||
<div class="form-group" id="date">
|
||||
<div class="input-daterange input-group" id="datepicker">
|
||||
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
|
||||
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" value="{{ date_from|date:'Y-m-d'}}">
|
||||
<input type="text" id="id_date_from" class="input-sm form-control" style="width: 100px;" name="date_from" value="{{ date_from|date:'Y-m-d'}}">
|
||||
{# <input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" >#}
|
||||
<span class="input-group-addon">to</span>
|
||||
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_to" value="{{ date_to|date:'Y-m-d'}}">
|
||||
<input type="text" id="id_date_to" class="input-sm form-control" style="width: 100px;" name="date_to" value="{{ date_to|date:'Y-m-d'}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
@@ -32,7 +32,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control input-sm" name="keyword" placeholder="{% trans 'Search' %}" value="{{ keyword }}">
|
||||
<input type="text" id="search" class="form-control input-sm" name="keyword" placeholder="{% trans 'Search' %}" value="{{ keyword }}">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<div class="input-group-btn">
|
||||
@@ -43,38 +43,57 @@
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% block table_container %}
|
||||
<table class="table table-striped table-bordered table-hover " id="login_log_table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">{% trans 'ID' %}</th>
|
||||
<th class="text-center">{% trans 'Username' %}</th>
|
||||
<th class="text-center">{% trans 'Type' %}</th>
|
||||
<th class="text-center">{% trans 'UA' %}</th>
|
||||
<th class="text-center">{% trans 'IP' %}</th>
|
||||
<th class="text-center">{% trans 'City' %}</th>
|
||||
<th class="text-center">{% trans 'MFA' %}</th>
|
||||
<th class="text-center">{% trans 'Reason' %}</th>
|
||||
<th class="text-center">{% trans 'Status' %}</th>
|
||||
<th class="text-center">{% trans 'Date' %}</th>
|
||||
</tr>
|
||||
<thead>
|
||||
|
||||
<tbody>
|
||||
{% for login_log in object_list %}
|
||||
<tr class="gradeX">
|
||||
<td class="text-center">{{ forloop.counter }}</td>
|
||||
<td class="text-center">{{ login_log.username }}</td>
|
||||
<td class="text-center">{{ login_log.get_type_display }}</td>
|
||||
<td class="text-center">
|
||||
<span href="javascript:void(0);" data-toggle="tooltips" title="{{ login_log.user_agent }}">{{ login_log.user_agent | truncatechars:20 }}</span>
|
||||
</td>
|
||||
<td class="text-center">{{ login_log.ip }}</td>
|
||||
<td class="text-center">{{ login_log.city }}</td>
|
||||
<td class="text-center">{{ login_log.get_mfa_display }}</td>
|
||||
<td class="text-center">{{ login_log.get_reason_display }}</td>
|
||||
<td class="text-center">{{ login_log.get_status_display }}</td>
|
||||
<td class="text-center">{{ login_log.datetime }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="actions" class="" style="margin-top: -20px">
|
||||
<div class="input-group">
|
||||
<select class="form-control m-b" style="width: auto" id="slct_bulk_update">
|
||||
<option value="export">{% trans 'Export' %}</option>
|
||||
</select>
|
||||
<div class="input-group-btn pull-left" style="padding-left: 5px;">
|
||||
<button id='btn_bulk_update' style="height: 32px;" class="btn btn-sm btn-primary btn_export">
|
||||
{% trans 'Submit' %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block table_head %}
|
||||
<th class="text-center">{% trans 'ID' %}</th>
|
||||
<th class="text-center">{% trans 'Username' %}</th>
|
||||
<th class="text-center">{% trans 'Type' %}</th>
|
||||
<th class="text-center">{% trans 'UA' %}</th>
|
||||
<th class="text-center">{% trans 'IP' %}</th>
|
||||
<th class="text-center">{% trans 'City' %}</th>
|
||||
<th class="text-center">{% trans 'MFA' %}</th>
|
||||
<th class="text-center">{% trans 'Reason' %}</th>
|
||||
<th class="text-center">{% trans 'Status' %}</th>
|
||||
<th class="text-center">{% trans 'Date' %}</th>
|
||||
{% endblock %}
|
||||
|
||||
{% block table_body %}
|
||||
{% for login_log in object_list %}
|
||||
<tr class="gradeX">
|
||||
<td class="text-center">{{ forloop.counter }}</td>
|
||||
<td class="text-center">{{ login_log.username }}</td>
|
||||
<td class="text-center">{{ login_log.get_type_display }}</td>
|
||||
<td class="text-center">
|
||||
<span href="javascript:void(0);" data-toggle="tooltips" title="{{ login_log.user_agent }}">{{ login_log.user_agent | truncatechars:20 }}</span>
|
||||
</td>
|
||||
<td class="text-center">{{ login_log.ip }}</td>
|
||||
<td class="text-center">{{ login_log.city }}</td>
|
||||
<td class="text-center">{{ login_log.get_mfa_display }}</td>
|
||||
<td class="text-center">{{ login_log.get_reason_display }}</td>
|
||||
<td class="text-center">{{ login_log.get_status_display }}</td>
|
||||
<td class="text-center">{{ login_log.datetime }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
|
||||
@@ -95,6 +114,29 @@
|
||||
width: 'auto'
|
||||
});
|
||||
})
|
||||
.on('click', '.btn_export', function () {
|
||||
var date_form = $('#id_date_from').val();
|
||||
var date_to = $('#id_date_to').val();
|
||||
var user = $('.select2 option:selected').val();
|
||||
var keyword = $('#search').val();
|
||||
$.ajax({
|
||||
url: "{% url "audits:login-log-export" %}",
|
||||
method: 'POST',
|
||||
data: JSON.stringify({
|
||||
'date_form':date_form,
|
||||
'date_to':date_to,
|
||||
'user':user,
|
||||
'keyword':keyword
|
||||
}),
|
||||
dataType: "json",
|
||||
success: function (data, textStatus) {
|
||||
window.open(data.redirect)
|
||||
},
|
||||
error: function () {
|
||||
toastr.error('Export failed');
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<select class="select2 form-control" name="asset">
|
||||
<select class="select2 form-control" name="action">
|
||||
<option value="">{% trans 'Action' %}</option>
|
||||
{% for k, v in actions.items %}
|
||||
<option value="{{ k }}" {% if k == action %} selected {% endif %}>{{ v }}</option>
|
||||
@@ -45,7 +45,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<select class="select2 form-control" name="system_user">
|
||||
<select class="select2 form-control" name="resource_type">
|
||||
<option value="">{% trans 'Resource Type' %}</option>
|
||||
{% for r in resource_type_list %}
|
||||
<option value="{{ r }}" {% if r == resource_type %} selected {% endif %}>{{ r }}</option>
|
||||
|
||||
@@ -14,4 +14,5 @@ urlpatterns = [
|
||||
path('operate-log/', views.OperateLogListView.as_view(), name='operate-log-list'),
|
||||
path('password-change-log/', views.PasswordChangeLogList.as_view(), name='password-change-log-list'),
|
||||
path('command-execution-log/', views.CommandExecutionListView.as_view(), name='command-execution-log-list'),
|
||||
path('login-log/export/', views.LoginLogExportView.as_view(), name='login-log-export'),
|
||||
]
|
||||
|
||||
22
apps/audits/utils.py
Normal file
22
apps/audits/utils.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import csv
|
||||
import codecs
|
||||
from django.http import HttpResponse
|
||||
|
||||
|
||||
def get_excel_response(filename):
|
||||
excel_response = HttpResponse(content_type='text/csv')
|
||||
excel_response[
|
||||
'Content-Disposition'] = 'attachment; filename="%s"' % filename
|
||||
excel_response.write(codecs.BOM_UTF8)
|
||||
return excel_response
|
||||
|
||||
|
||||
def write_content_to_excel(response, header=None, login_logs=None, fields=None):
|
||||
writer = csv.writer(response, dialect='excel', quoting=csv.QUOTE_MINIMAL)
|
||||
if header:
|
||||
writer.writerow(header)
|
||||
if login_logs:
|
||||
for log in login_logs:
|
||||
data = [getattr(log, field.name) for field in fields]
|
||||
writer.writerow(data)
|
||||
return response
|
||||
@@ -1,26 +1,43 @@
|
||||
import csv
|
||||
import json
|
||||
import uuid
|
||||
import codecs
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.generic import ListView
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Q
|
||||
|
||||
from audits.utils import get_excel_response, write_content_to_excel
|
||||
from common.mixins import DatetimeSearchMixin
|
||||
from common.permissions import AdminUserRequiredMixin
|
||||
|
||||
from orgs.utils import current_org
|
||||
from ops.views import CommandExecutionListView as UserCommandExecutionListView
|
||||
from users.models import User
|
||||
from .models import FTPLog, OperateLog, PasswordChangeLog, UserLoginLog
|
||||
|
||||
|
||||
def get_resource_type_list():
|
||||
from users.models import User, UserGroup
|
||||
from assets.models import Asset, Node, AdminUser, SystemUser, Domain, Gateway
|
||||
from assets.models import (
|
||||
Asset, Node, AdminUser, SystemUser, Domain, Gateway, CommandFilter,
|
||||
CommandFilterRule,
|
||||
)
|
||||
from orgs.models import Organization
|
||||
from perms.models import AssetPermission
|
||||
|
||||
models = [
|
||||
User, UserGroup, Asset, Node, AdminUser, SystemUser, Domain,
|
||||
Gateway, Organization, AssetPermission
|
||||
Gateway, Organization, AssetPermission, CommandFilter, CommandFilterRule
|
||||
]
|
||||
return [model._meta.verbose_name for model in models]
|
||||
|
||||
@@ -219,14 +236,49 @@ class CommandExecutionListView(UserCommandExecutionListView):
|
||||
return users
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update({
|
||||
'app': _('Audits'),
|
||||
'action': _('Command execution list'),
|
||||
'action': _('Command execution log'),
|
||||
'date_from': self.date_from,
|
||||
'date_to': self.date_to,
|
||||
'user_list': self.get_user_list(),
|
||||
'keyword': self.keyword,
|
||||
'user_id': self.user_id,
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
})
|
||||
return super().get_context_data(**context)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class LoginLogExportView(LoginRequiredMixin, View):
|
||||
|
||||
def get(self, request):
|
||||
fields = [
|
||||
field for field in UserLoginLog._meta.fields
|
||||
]
|
||||
filename = 'login-logs-{}.csv'.format(
|
||||
timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S')
|
||||
)
|
||||
excel_response = get_excel_response(filename)
|
||||
header = [field.verbose_name for field in fields]
|
||||
login_logs = cache.get(request.GET.get('spm', ''), [])
|
||||
|
||||
response = write_content_to_excel(excel_response, login_logs=login_logs,
|
||||
header=header, fields=fields)
|
||||
return response
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
date_form = json.loads(request.body).get('date_form', [])
|
||||
date_to = json.loads(request.body).get('date_to', [])
|
||||
user = json.loads(request.body).get('user', [])
|
||||
keyword = json.loads(request.body).get('keyword', [])
|
||||
|
||||
login_logs = UserLoginLog.get_login_logs(
|
||||
date_form=date_form, date_to=date_to, user=user, keyword=keyword)
|
||||
except ValueError:
|
||||
return HttpResponse('Json object not valid', status=400)
|
||||
spm = uuid.uuid4().hex
|
||||
cache.set(spm, login_logs, 300)
|
||||
url = reverse('audits:login-log-export') + '?spm=%s' % spm
|
||||
return JsonResponse({'redirect': url})
|
||||
4
apps/authentication/api/__init__.py
Normal file
4
apps/authentication/api/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .auth import *
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import uuid
|
||||
|
||||
from django.core.cache import cache
|
||||
@@ -14,16 +15,21 @@ from rest_framework.views import APIView
|
||||
from common.utils import get_logger, get_request_ip
|
||||
from common.permissions import IsOrgAdminOrAppUser
|
||||
from orgs.mixins import RootOrgViewMixin
|
||||
from ..serializers import UserSerializer
|
||||
from ..tasks import write_login_log_async
|
||||
from ..models import User, LoginLog
|
||||
from ..utils import check_user_valid, check_otp_code, \
|
||||
increase_login_failed_count, is_block_login, \
|
||||
clean_failed_count
|
||||
from ..hands import Asset, SystemUser
|
||||
from users.serializers import UserSerializer
|
||||
from users.models import User
|
||||
from assets.models import Asset, SystemUser
|
||||
from audits.models import UserLoginLog as LoginLog
|
||||
from users.utils import (
|
||||
check_user_valid, check_otp_code, increase_login_failed_count,
|
||||
is_block_login, clean_failed_count
|
||||
)
|
||||
|
||||
from ..signals import post_auth_success, post_auth_failed
|
||||
|
||||
logger = get_logger(__name__)
|
||||
__all__ = [
|
||||
'UserAuthApi', 'UserConnectionTokenApi', 'UserOtpAuthApi',
|
||||
]
|
||||
|
||||
|
||||
class UserAuthApi(RootOrgViewMixin, APIView):
|
||||
@@ -46,37 +52,22 @@ class UserAuthApi(RootOrgViewMixin, APIView):
|
||||
username = request.data.get('username', '')
|
||||
exist = User.objects.filter(username=username).first()
|
||||
reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST
|
||||
data = {
|
||||
'username': username,
|
||||
'mfa': LoginLog.MFA_UNKNOWN,
|
||||
'reason': reason,
|
||||
'status': False
|
||||
}
|
||||
self.write_login_log(request, data)
|
||||
self.send_auth_signal(success=False, username=username, reason=reason)
|
||||
increase_login_failed_count(username, ip)
|
||||
return Response({'msg': msg}, status=401)
|
||||
|
||||
if user.password_has_expired:
|
||||
data = {
|
||||
'username': user.username,
|
||||
'mfa': int(user.otp_enabled),
|
||||
'reason': LoginLog.REASON_PASSWORD_EXPIRED,
|
||||
'status': False
|
||||
}
|
||||
self.write_login_log(request, data)
|
||||
self.send_auth_signal(
|
||||
success=False, username=username,
|
||||
reason=LoginLog.REASON_PASSWORD_EXPIRED
|
||||
)
|
||||
msg = _("The user {} password has expired, please update.".format(
|
||||
user.username))
|
||||
logger.info(msg)
|
||||
return Response({'msg': msg}, status=401)
|
||||
|
||||
if not user.otp_enabled:
|
||||
data = {
|
||||
'username': user.username,
|
||||
'mfa': int(user.otp_enabled),
|
||||
'reason': LoginLog.REASON_NOTHING,
|
||||
'status': True
|
||||
}
|
||||
self.write_login_log(request, data)
|
||||
self.send_auth_signal(success=True, user=user)
|
||||
# 登陆成功,清除原来的缓存计数
|
||||
clean_failed_count(username, ip)
|
||||
token = user.create_bearer_token(request)
|
||||
@@ -91,7 +82,7 @@ class UserAuthApi(RootOrgViewMixin, APIView):
|
||||
'code': 101,
|
||||
'msg': _('Please carry seed value and '
|
||||
'conduct MFA secondary certification'),
|
||||
'otp_url': reverse('api-users:user-otp-auth'),
|
||||
'otp_url': reverse('api-auth:user-otp-auth'),
|
||||
'seed': seed,
|
||||
'user': self.serializer_class(user).data
|
||||
}, status=300
|
||||
@@ -108,22 +99,14 @@ class UserAuthApi(RootOrgViewMixin, APIView):
|
||||
)
|
||||
return user, msg
|
||||
|
||||
@staticmethod
|
||||
def write_login_log(request, data):
|
||||
login_ip = request.data.get('remote_addr', None)
|
||||
login_type = request.data.get('login_type', '')
|
||||
user_agent = request.data.get('HTTP_USER_AGENT', '')
|
||||
|
||||
if not login_ip:
|
||||
login_ip = get_request_ip(request)
|
||||
|
||||
tmp_data = {
|
||||
'ip': login_ip,
|
||||
'type': login_type,
|
||||
'user_agent': user_agent,
|
||||
}
|
||||
data.update(tmp_data)
|
||||
write_login_log_async.delay(**data)
|
||||
def send_auth_signal(self, success=True, user=None, username='', reason=''):
|
||||
if success:
|
||||
post_auth_success.send(sender=self.__class__, user=user, request=self.request)
|
||||
else:
|
||||
post_auth_failed.send(
|
||||
sender=self.__class__, username=username,
|
||||
request=self.request, reason=reason
|
||||
)
|
||||
|
||||
|
||||
class UserConnectionTokenApi(RootOrgViewMixin, APIView):
|
||||
@@ -167,29 +150,6 @@ class UserConnectionTokenApi(RootOrgViewMixin, APIView):
|
||||
return super().get_permissions()
|
||||
|
||||
|
||||
class UserToken(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def post(self, request):
|
||||
if not request.user.is_authenticated:
|
||||
username = request.data.get('username', '')
|
||||
email = request.data.get('email', '')
|
||||
password = request.data.get('password', '')
|
||||
public_key = request.data.get('public_key', '')
|
||||
|
||||
user, msg = check_user_valid(
|
||||
username=username, email=email,
|
||||
password=password, public_key=public_key)
|
||||
else:
|
||||
user = request.user
|
||||
msg = None
|
||||
if user:
|
||||
token = user.create_bearer_token(request)
|
||||
return Response({'Token': token, 'Keyword': 'Bearer'}, status=200)
|
||||
else:
|
||||
return Response({'error': msg}, status=406)
|
||||
|
||||
|
||||
class UserOtpAuthApi(RootOrgViewMixin, APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
serializer_class = UserSerializer
|
||||
@@ -197,52 +157,25 @@ class UserOtpAuthApi(RootOrgViewMixin, APIView):
|
||||
def post(self, request):
|
||||
otp_code = request.data.get('otp_code', '')
|
||||
seed = request.data.get('seed', '')
|
||||
|
||||
user = cache.get(seed, None)
|
||||
if not user:
|
||||
return Response(
|
||||
{'msg': _('Please verify the user name and password first')},
|
||||
status=401
|
||||
)
|
||||
|
||||
if not check_otp_code(user.otp_secret_key, otp_code):
|
||||
data = {
|
||||
'username': user.username,
|
||||
'mfa': int(user.otp_enabled),
|
||||
'reason': LoginLog.REASON_MFA,
|
||||
'status': False
|
||||
}
|
||||
self.write_login_log(request, data)
|
||||
self.send_auth_signal(success=False, username=user.username, reason=LoginLog.REASON_MFA)
|
||||
return Response({'msg': _('MFA certification failed')}, status=401)
|
||||
|
||||
data = {
|
||||
'username': user.username,
|
||||
'mfa': int(user.otp_enabled),
|
||||
'reason': LoginLog.REASON_NOTHING,
|
||||
'status': True
|
||||
}
|
||||
self.write_login_log(request, data)
|
||||
self.send_auth_signal(success=True, user=user)
|
||||
token = user.create_bearer_token(request)
|
||||
return Response(
|
||||
{
|
||||
'token': token,
|
||||
'user': self.serializer_class(user).data
|
||||
}
|
||||
)
|
||||
data = {'token': token, 'user': self.serializer_class(user).data}
|
||||
return Response(data)
|
||||
|
||||
@staticmethod
|
||||
def write_login_log(request, data):
|
||||
login_ip = request.data.get('remote_addr', None)
|
||||
login_type = request.data.get('login_type', '')
|
||||
user_agent = request.data.get('HTTP_USER_AGENT', '')
|
||||
|
||||
if not login_ip:
|
||||
login_ip = get_request_ip(request)
|
||||
|
||||
tmp_data = {
|
||||
'ip': login_ip,
|
||||
'type': login_type,
|
||||
'user_agent': user_agent
|
||||
}
|
||||
data.update(tmp_data)
|
||||
write_login_log_async.delay(**data)
|
||||
def send_auth_signal(self, success=True, user=None, username='', reason=''):
|
||||
if success:
|
||||
post_auth_success.send(sender=self.__class__, user=user, request=self.request)
|
||||
else:
|
||||
post_auth_failed.send(
|
||||
sender=self.__class__, username=username,
|
||||
request=self.request, reason=reason
|
||||
)
|
||||
2
apps/authentication/backends/__init__.py
Normal file
2
apps/authentication/backends/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
@@ -8,13 +8,13 @@ from django.core.cache import cache
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.six import text_type
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework import HTTP_HEADER_ENCODING
|
||||
from rest_framework import authentication, exceptions
|
||||
from rest_framework.authentication import CSRFCheck
|
||||
|
||||
from common.utils import get_object_or_none, make_signature, http_to_unixtime
|
||||
from .models import User, AccessKey, PrivateToken
|
||||
from ..models import AccessKey, PrivateToken
|
||||
|
||||
|
||||
def get_request_date_header(request):
|
||||
@@ -42,7 +42,6 @@ class AccessKeyAuthentication(authentication.BaseAuthentication):
|
||||
失败
|
||||
"""
|
||||
keyword = 'Sign'
|
||||
model = AccessKey
|
||||
|
||||
def authenticate(self, request):
|
||||
auth = authentication.get_authorization_header(request).split()
|
||||
@@ -109,7 +108,7 @@ class AccessKeyAuthentication(authentication.BaseAuthentication):
|
||||
|
||||
class AccessTokenAuthentication(authentication.BaseAuthentication):
|
||||
keyword = 'Bearer'
|
||||
model = User
|
||||
model = get_user_model()
|
||||
expiration = settings.TOKEN_EXPIRATION or 3600
|
||||
|
||||
def authenticate(self, request):
|
||||
@@ -133,10 +132,9 @@ class AccessTokenAuthentication(authentication.BaseAuthentication):
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
return self.authenticate_credentials(token)
|
||||
|
||||
@staticmethod
|
||||
def authenticate_credentials(token):
|
||||
def authenticate_credentials(self, token):
|
||||
user_id = cache.get(token)
|
||||
user = get_object_or_none(User, id=user_id)
|
||||
user = get_object_or_none(self.model, id=user_id)
|
||||
|
||||
if not user:
|
||||
msg = _('Invalid token or cache refreshed.')
|
||||
@@ -16,13 +16,13 @@ class LDAPAuthorizationBackend(LDAPBackend):
|
||||
"""
|
||||
|
||||
def authenticate(self, request=None, username=None, password=None, **kwargs):
|
||||
if password or self.settings.PERMIT_EMPTY_PASSWORD:
|
||||
ldap_user = LDAPUser(self, username=username.strip(), request=request)
|
||||
user = self.authenticate_ldap_user(ldap_user, password)
|
||||
else:
|
||||
logger.debug('Rejecting empty password for {}'.format(username))
|
||||
user = None
|
||||
|
||||
logger.info('Authentication LDAP backend')
|
||||
if not username:
|
||||
logger.info('Authenticate failed: username is None')
|
||||
return None
|
||||
ldap_user = LDAPUser(self, username=username.strip(), request=request)
|
||||
user = self.authenticate_ldap_user(ldap_user, password)
|
||||
logger.info('Authenticate user: {}'.format(user))
|
||||
return user
|
||||
|
||||
def get_user(self, user_id):
|
||||
@@ -88,7 +88,10 @@ class LDAPUser(_LDAPUser):
|
||||
def _populate_user_from_attributes(self):
|
||||
super()._populate_user_from_attributes()
|
||||
if not hasattr(self._user, 'email') or '@' not in self._user.email:
|
||||
email = '{}@{}'.format(self._user.username, settings.EMAIL_SUFFIX)
|
||||
if '@' not in self._user.username:
|
||||
email = '{}@{}'.format(self._user.username, settings.EMAIL_SUFFIX)
|
||||
else:
|
||||
email = self._user.username
|
||||
setattr(self._user, 'email', email)
|
||||
|
||||
|
||||
6
apps/authentication/backends/openid/__init__.py
Normal file
6
apps/authentication/backends/openid/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .backends import *
|
||||
from .middleware import *
|
||||
from .utils import *
|
||||
@@ -4,16 +4,19 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.conf import settings
|
||||
|
||||
from . import client
|
||||
from common.utils import get_logger
|
||||
from authentication.openid.models import OIDT_ACCESS_TOKEN
|
||||
from .utils import new_client
|
||||
from .models import OIDT_ACCESS_TOKEN
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
logger = get_logger(__file__)
|
||||
client = new_client()
|
||||
|
||||
BACKEND_OPENID_AUTH_CODE = \
|
||||
'authentication.openid.backends.OpenIDAuthorizationCodeBackend'
|
||||
|
||||
__all__ = [
|
||||
'OpenIDAuthorizationCodeBackend', 'OpenIDAuthorizationPasswordBackend',
|
||||
]
|
||||
|
||||
|
||||
class BaseOpenIDAuthorizationBackend(object):
|
||||
@@ -39,41 +42,41 @@ class BaseOpenIDAuthorizationBackend(object):
|
||||
class OpenIDAuthorizationCodeBackend(BaseOpenIDAuthorizationBackend):
|
||||
|
||||
def authenticate(self, request, **kwargs):
|
||||
logger.info('1.openid code backend')
|
||||
logger.info('Authentication OpenID code backend')
|
||||
|
||||
code = kwargs.get('code')
|
||||
redirect_uri = kwargs.get('redirect_uri')
|
||||
|
||||
if not code or not redirect_uri:
|
||||
logger.info('Authenticate failed: No code or No redirect uri')
|
||||
return None
|
||||
|
||||
try:
|
||||
oidt_profile = client.update_or_create_from_code(
|
||||
code=code,
|
||||
redirect_uri=redirect_uri
|
||||
)
|
||||
code=code, redirect_uri=redirect_uri
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
logger.info('Authenticate failed: get oidt_profile: {}'.format(e))
|
||||
|
||||
else:
|
||||
# Check openid user single logout or not with access_token
|
||||
request.session[OIDT_ACCESS_TOKEN] = oidt_profile.access_token
|
||||
|
||||
user = oidt_profile.user
|
||||
|
||||
logger.info('Authenticate success: user -> {}'.format(user))
|
||||
return user if self.user_can_authenticate(user) else None
|
||||
|
||||
|
||||
class OpenIDAuthorizationPasswordBackend(BaseOpenIDAuthorizationBackend):
|
||||
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
logger.info('2.openid password backend')
|
||||
logger.info('Authentication OpenID password backend')
|
||||
|
||||
if not settings.AUTH_OPENID:
|
||||
logger.info('Authenticate failed: AUTH_OPENID is False')
|
||||
return None
|
||||
|
||||
elif not username:
|
||||
logger.info('Authenticate failed: Not username')
|
||||
return None
|
||||
|
||||
try:
|
||||
@@ -82,9 +85,10 @@ class OpenIDAuthorizationPasswordBackend(BaseOpenIDAuthorizationBackend):
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
logger.info('Authenticate failed: get oidt_profile: {}'.format(e))
|
||||
|
||||
else:
|
||||
user = oidt_profile.user
|
||||
logger.info('Authenticate success: user -> {}'.format(user))
|
||||
return user if self.user_can_authenticate(user) else None
|
||||
|
||||
@@ -6,12 +6,13 @@ from django.contrib.auth import logout
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.contrib.auth import BACKEND_SESSION_KEY
|
||||
|
||||
from . import client
|
||||
from common.utils import get_logger
|
||||
from .backends import BACKEND_OPENID_AUTH_CODE
|
||||
from authentication.openid.models import OIDT_ACCESS_TOKEN
|
||||
from .utils import new_client
|
||||
from .models import OIDT_ACCESS_TOKEN
|
||||
|
||||
BACKEND_OPENID_AUTH_CODE = 'OpenIDAuthorizationCodeBackend'
|
||||
logger = get_logger(__file__)
|
||||
__all__ = ['OpenIDAuthenticationMiddleware']
|
||||
|
||||
|
||||
class OpenIDAuthenticationMiddleware(MiddlewareMixin):
|
||||
@@ -20,23 +21,25 @@ class OpenIDAuthenticationMiddleware(MiddlewareMixin):
|
||||
"""
|
||||
|
||||
def process_request(self, request):
|
||||
|
||||
# Don't need openid auth if AUTH_OPENID is False
|
||||
if not settings.AUTH_OPENID:
|
||||
logger.debug("Not settings.AUTH_OPENID")
|
||||
return
|
||||
|
||||
# Don't need check single logout if user not authenticated
|
||||
if not request.user.is_authenticated:
|
||||
logger.debug("User is not authenticated")
|
||||
return
|
||||
|
||||
elif request.session[BACKEND_SESSION_KEY] != BACKEND_OPENID_AUTH_CODE:
|
||||
elif not request.session[BACKEND_SESSION_KEY].endswith(
|
||||
BACKEND_OPENID_AUTH_CODE):
|
||||
logger.debug("BACKEND_SESSION_KEY is not BACKEND_OPENID_AUTH_CODE")
|
||||
return
|
||||
|
||||
# Check openid user single logout or not with access_token
|
||||
client = new_client()
|
||||
try:
|
||||
client.openid_connect_client.userinfo(
|
||||
token=request.session.get(OIDT_ACCESS_TOKEN))
|
||||
|
||||
token=request.session.get(OIDT_ACCESS_TOKEN)
|
||||
)
|
||||
except Exception as e:
|
||||
logout(request)
|
||||
logger.error(e)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user