Compare commits

...

162 Commits
1.0.0 ... 1.3.0

Author SHA1 Message Date
老广
84003b777c Merge pull request #1283 from jumpserver/dev
更改版本号
2018-05-02 10:54:38 +08:00
老广
1edfb1cec4 [Update] 修改版本号 (#1282) 2018-05-02 10:52:57 +08:00
老广
9fa6b3e387 [Update] 暂时隐藏rdp的session,等待windows录像播放 (#1276) (#1278) 2018-04-27 21:23:18 +08:00
老广
c0bbda9769 [Update] 暂时隐藏rdp的session,等待windows录像播放 (#1276) 2018-04-27 15:38:13 +08:00
老广
d7a32120ba Merge pull request #1274 from jumpserver/dev
Merge with dev
2018-04-27 12:44:00 +08:00
老广
55096f9ad5 Bugfix perm asset not active (#1273)
* [Bugfix] 修复资产禁用了还可以登录的bug
2018-04-27 11:41:47 +08:00
老广
494cd760d7 Trans change (#1272)
* [Update] 更新授权规则生效日期文案
2018-04-27 11:33:28 +08:00
老广
6f494ef09c 校验用户名长度和特殊字符 (#1271)
* [Bugfix] 修复系统用户用户名校验问题

* [Update] 增加长度限制
2018-04-27 11:27:16 +08:00
wojiushixiaobai
e476cab2a1 更新自动升级脚本 (#1250) 2018-04-27 11:01:52 +08:00
老广
cc67fcb53b Merge pull request #1269 from jumpserver/dev
Dev
2018-04-26 21:02:00 +08:00
老广
b074bd8fbd Merge pull request #1267 from jumpserver/bugfix_user
[Bugfix] 修复首次登录条款问题及引导页面MFA配置问题
2018-04-26 21:00:20 +08:00
老广
627582233b Merge pull request #1268 from jumpserver/bufix_for_perm_tree
[Bugfix] 解决上次引入的bug
2018-04-26 20:59:05 +08:00
ibuler
5103dab72e [Bugfix] 解决上次引入的bug 2018-04-26 20:48:32 +08:00
老广
43c13355f2 Merge pull request #1266 from jumpserver/dev
Dev
2018-04-26 19:54:42 +08:00
老广
59eb1f8e3e Merge pull request #1265 from jumpserver/bufix_for_perm_tree
[Bugfix] 修复授权树列表和资产树列表不同的bug
2018-04-26 19:53:39 +08:00
ibuler
16aa42a861 [Bugfix] 修复授权树列表和资产树列表不同的bug 2018-04-26 19:51:32 +08:00
BaiJiangjie
0962a16b22 [Bugfix] 修复首次登录条款问题及引导页面MFA配置问题 2018-04-26 19:49:09 +08:00
老广
787be3ff7a Merge pull request #1248 from jumpserver/dev
Bugfix 修复搜索命令的bug
2018-04-25 17:55:39 +08:00
ibuler
d5debc375e [Bugfix] 修复命令搜索异常bug 2018-04-25 17:48:44 +08:00
ibuler
40a0c4597b Merge remote-tracking branch 'github/dev' into dev 2018-04-25 15:15:36 +08:00
老广
5c17b1a7f7 Merge pull request #1240 from jumpserver/dev
首次登陆免验证码,首次登录信息变更
2018-04-25 10:00:56 +08:00
BaiJiangjie
ea2863a51b [Update] 创建用户、资产成功的提示信息可关闭 2018-04-24 22:10:52 +08:00
BaiJiangjie
102e1ca97c [Bugfix] 创建资产,资产系统置上,RDP:3389 2018-04-24 18:29:14 +08:00
BaiJiangjie
2823d02763 [Bugfix] 修复资产列表导入,资产id问题 2018-04-24 16:15:04 +08:00
BaiJiangjie
c37414045b [Bugfix] 修改用户列表导出,默认导出全部用户 2018-04-24 15:16:05 +08:00
BaiJiangjie
784bec42ff [Update] 修改用户登录,首次登录不需要验证码,登录失败时需要验证码校验 2018-04-24 13:00:36 +08:00
ibuler
9a3d0732bc [Update] 更新授权代码 2018-04-24 11:07:09 +08:00
ibuler
20a7247b16 Merge remote-tracking branch 'github/dev' into dev 2018-04-24 10:52:31 +08:00
ibuler
df60981eb4 Merge branch 'dev' of bitbucket.org:jumpserver/core into dev 2018-04-24 10:51:30 +08:00
老广
7aa2bb06e8 Merge pull request #1228 from wojiushixiaobai/dev
添加升级脚本
2018-04-24 10:41:23 +08:00
liuzheng712
536be1175a feat: replay api update
make a v2 session replay api, get the json response
2018-04-24 08:53:30 +08:00
BaiJiangjie
b5fc76d6a5 [Update] 修改用户首次登录页 2018-04-23 21:04:46 +08:00
ibuler
941dd627e3 [Update] 更新session列表 2018-04-23 11:44:09 +08:00
ibuler
8389c85054 [Update] 更改sesion表结构 2018-04-23 11:40:30 +08:00
ibuler
02ca8c3139 [Update] 更新session api 2018-04-23 11:32:46 +08:00
wojiushixiaobai
abd20f31b8 更新升级脚本 2018-04-22 22:37:51 +08:00
wojiushixiaobai
5c7acae018 更改版本号 2018-04-22 20:18:10 +08:00
wojiushixiaobai
1c623f71e0 添加升级脚本 2018-04-22 19:22:55 +08:00
BaiJiangjie
5b52b907c0 [Update] 创建用户,更新用户,添加MFA设置选项 2018-04-20 16:15:45 +08:00
BaiJiangjie
8447c6f487 [Update] 修改coco文案OTP改为MFA 2018-04-20 11:51:32 +08:00
老广
e865484a56 Merge pull request #1224 from jumpserver/dev
Dev
2018-04-20 11:36:16 +08:00
BaiJiangjie
ad6e22cd42 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2018-04-20 11:24:21 +08:00
BaiJiangjie
7a219e1710 [Update] OTP文案修改为MFA 2018-04-20 11:23:31 +08:00
老广
a0a8419c5e Merge pull request #1221 from jumpserver/dev
[Bugfix] 修复用户错误storage的bug
2018-04-19 18:27:49 +08:00
ibuler
0c24310510 [Bugfix] 修复用户错误storage的bug 2018-04-19 18:26:59 +08:00
老广
967491fba5 Merge pull request #1220 from jumpserver/dev
[Update] 修改用户详情,自己更改自己otp,提示小写
2018-04-19 17:56:41 +08:00
ibuler
9ac7f26c74 [Update] 修改用户详情,自己更改自己otp,提示小写 2018-04-19 17:55:40 +08:00
老广
910f3cdddc Merge pull request #1219 from jumpserver/dev
[Bugfix] 修复用户登录缓存设置问题
2018-04-19 17:22:04 +08:00
BaiJiangjie
f73fe1f315 [Bugfix] 修复用户登录缓存设置问题 2018-04-19 17:20:53 +08:00
老广
28acc6cc63 Merge pull request #1217 from jumpserver/dev
[Bugfix] 修复解密None的bug
2018-04-19 16:39:27 +08:00
ibuler
763cf0d981 [Bugfix] 修复解密None的bug 2018-04-19 16:35:38 +08:00
老广
611289a5ec Merge pull request #1214 from jumpserver/dev
支持二次认证
2018-04-19 12:16:25 +08:00
ibuler
95a8bf0988 Merge remote-tracking branch 'github/dev' into dev 2018-04-19 11:46:07 +08:00
ibuler
947f7d206a [Bugfix] 修复生成system user的问题 2018-04-19 11:16:52 +08:00
BaiJiangjie
12c8cf6b76 [Update] 添加OTP认证功能 2018-04-19 11:13:11 +08:00
BaiJiangjie
33bc73aba7 [Update] 添加OTP认证功能 2018-04-19 10:49:47 +08:00
老广
53c532a6ad Merge pull request #1213 from jumpserver/dev
更新资产选择
2018-04-19 10:49:01 +08:00
ibuler
035dd16b36 [Bugfix] 修复授权详情中选择用户或资产的bug 2018-04-19 10:34:17 +08:00
BaiJiangjie
f450accbf8 [Merge] with dev 2018-04-18 12:49:17 +08:00
BaiJiangjie
0bbfc7433d [Feature] 支持otp 2018-04-18 12:48:07 +08:00
ibuler
48e8785725 [Update] 修改users otp secret key 2018-04-18 12:46:25 +08:00
ibuler
b90d3306c5 [Bugfix] 去掉gateway name连接 2018-04-17 12:01:00 +08:00
ibuler
7f7d634c38 Merge remote-tracking branch 'github/master' into dev 2018-04-16 17:08:01 +08:00
老广
45b13abed3 Merge pull request #1204 from jumpserver/bugfix_user_assets
[Bugfix] 修复用户看不到我的资产的bug
2018-04-16 17:04:29 +08:00
ibuler
72cd7a3be2 [Bugfix] 修复用户看不到我的资产的bug 2018-04-16 17:01:25 +08:00
ibuler
3ccd54680e [Bugfix] 修改授权详情快速Node更改失效的bug 2018-04-14 12:18:15 +08:00
ibuler
071d14c639 [Update] 修改资产获取select 2018-04-13 21:26:10 +08:00
ibuler
823e879432 [Update] 更新节点移动交互 2018-04-13 15:48:10 +08:00
ibuler
739932b005 [Update] 更新资产导入 2018-04-12 18:50:43 +08:00
ibuler
24f144fdc3 [Bugfix] 修复导入资产时url地址问题 2018-04-12 18:17:42 +08:00
ibuler
967800391e [Update] 更新api 2018-04-12 17:31:37 +08:00
老广
3ccb6637d7 Merge pull request #1197 from jumpserver/dev
[Update] 更新迁移脚本
2018-04-12 17:03:39 +08:00
ibuler
8dfdefd428 [Update] 更新迁移脚本 2018-04-12 17:02:04 +08:00
老广
ab2c58b626 Merge pull request #1194 from jumpserver/dev
[Bugfix] 修复资产重复的bug
2018-04-12 11:08:14 +08:00
ibuler
ee4f5a8194 [Bugfix] 修复资产重复的bug 2018-04-12 11:07:40 +08:00
老广
084a76b215 Merge pull request #1193 from jumpserver/dev
更改表结构
2018-04-12 10:17:53 +08:00
ibuler
2398e9acbd 更改表截稿 2018-04-12 10:16:38 +08:00
老广
5ad8b3cc70 Merge pull request #1191 from jumpserver/dev
[Update] 更改版本号
2018-04-12 09:51:54 +08:00
ibuler
7d14e1f248 [Update] 更改版本号 2018-04-12 09:49:44 +08:00
老广
819f8f469d Merge pull request #1187 from jumpserver/dev
[Bugfix] 修复一个脚本的bug
2018-04-11 17:02:02 +08:00
ibuler
a31b7a8800 [Update] 2018-04-11 16:59:07 +08:00
老广
24bdaecab4 Merge pull request #1185 from jumpserver/dev
授权规则优化,支持细颗粒授权
2018-04-11 15:25:02 +08:00
ibuler
8b3b517bab [Update] 修改授权规则详情列表页面 2018-04-11 15:24:12 +08:00
ibuler
7fc2ef00ee [Update] 修改资产api获取的bug 2018-04-11 12:45:04 +08:00
ibuler
cbd6c3ee69 [Update] 添加迁移脚本 2018-04-11 12:23:35 +08:00
ibuler
3835adafb8 [Update] 修改bug 2018-04-11 12:13:49 +08:00
ibuler
bbaa35c773 [Update] 修改Perms 2018-04-11 11:34:15 +08:00
ibuler
0fa8287811 Merge branch 'dev' into perms 2018-04-11 10:25:18 +08:00
ibuler
78f4e5a89a [Update] 修改用户Opt 2018-04-10 21:04:56 +08:00
ibuler
3193c5549d Merge remote-tracking branch 'github/dev' into dev 2018-04-10 21:02:36 +08:00
ibuler
ed71e7d2d9 [Update] 修改用户Opt 2018-04-10 21:02:07 +08:00
ibuler
33c299566a [Update] 修复bug 2018-04-10 20:45:01 +08:00
ibuler
84634eb8c0 [Update] 修改Permr认证 2018-04-10 20:29:06 +08:00
ibuler
a4ff2181c5 [Update] 修改用户view的api 2018-04-10 09:41:06 +08:00
ibuler
fffa0def9e [Update] 修改api和view 2018-04-08 20:02:40 +08:00
ibuler
d0ede246e7 [Update] 修改授权 2018-04-08 00:16:37 +08:00
BaiJiangJie
b8b78ffeb2 Merge pull request #1162 from BaiJiangJie/dev
创建用户发送邮件
2018-04-06 15:50:50 +08:00
老广
d2d10b59ac Merge pull request #1175 from jumpserver/dev
支持sftp
2018-04-06 13:30:05 +08:00
ibuler
cb8e59edf2 [Update] 修改文案 2018-04-06 13:23:15 +08:00
ibuler
1c0d783eec [update] 添加Migrations 2018-04-06 11:36:47 +08:00
ibuler
4fa72400be Merge remote-tracking branch 'github/dev' into dev 2018-04-06 11:35:33 +08:00
ibuler
37c0062fae [Update] 添加审计模块 2018-04-06 11:27:52 +08:00
BaiJiangjie
8504c3d2fd 修复资产列表导出问题 2018-04-04 19:38:32 +08:00
老广
2d10e13057 Merge pull request #1171 from jumpserver/dev
Dev
2018-04-04 14:05:16 +08:00
ibuler
1d7ba3e204 [Bugfix] 修复小bug 2018-04-04 13:03:36 +08:00
ibuler
d966e22cf9 [Update] 修改小bug 2018-04-04 13:01:57 +08:00
ibuler
6a88fd2d60 [Update] 修改api权限 2018-04-04 08:47:02 +08:00
BaiJiangjie
b63999f385 创建用户发送邮件 2018-04-03 16:28:58 +08:00
老广
82d06351e7 Merge pull request #1158 from jumpserver/dev
bugfix for celery log path
2018-04-03 14:50:49 +08:00
ibuler
aa8bece724 Merge branch 'ansible' into dev 2018-04-03 14:49:58 +08:00
老广
241bdff7c8 Merge pull request #1156 from jumpserver/dev
Dev
2018-04-03 12:18:41 +08:00
ibuler
168335a381 [Update] 更新内容 2018-04-03 12:14:58 +08:00
ibuler
121726b731 Merge remote-tracking branch 'github/dev' into dev 2018-04-03 10:44:35 +08:00
ibuler
0b812a03c6 [Bugfix] 修复 ApiUpdateAttr的bug 2018-04-03 10:36:11 +08:00
ibuler
79ae6efafb [Bugfix] 修复没有Log path引起的bug 2018-04-02 19:14:42 +08:00
老广
d2f108eeec Merge pull request #1154 from BaiJiangJie/master
终端管理用户显示,资产导入IP字段空格,资产导入domain字段保存
2018-04-02 19:03:51 +08:00
BaiJiangJie
c48beb10af Merge pull request #1 from BaiJiangJie/dev
终端管理用户,资产列表IP字段,domain保存对象
2018-04-02 18:52:52 +08:00
老广
ea9264ec49 Merge pull request #1153 from BaiJiangJie/dev
修复导入资产列表IP字段前后有空格问题,导入资产列表时domain保存为对象
2018-04-02 18:43:33 +08:00
BaiJiangjie
5b65ed8a19 修复导入资产列表IP字段前后有空格问题,导入资产列表时doamin保存为对象 2018-04-02 18:32:24 +08:00
ibuler
951ac252fe [Merge] 更新ansible功能 2018-04-02 17:45:17 +08:00
ibuler
24c4e1df50 [Update] 资产,系统用户,管理用户等支持查看日志 2018-04-02 16:55:39 +08:00
ibuler
d247e49b70 [Update] 修改celery位置 2018-04-02 15:54:49 +08:00
ibuler
a4c843ff13 [Update] 迁移celery到ops 2018-04-02 13:19:31 +08:00
BaiJiangjie
ec6103448e 使终端管理的应用用户不显示在用户组里 2018-04-02 12:02:06 +08:00
老广
0ca14463cd Merge pull request #1147 from jumpserver/dev
支持网域功能
2018-04-02 10:59:55 +08:00
ibuler
df80e8047a [Update] 修改日志 2018-04-01 23:45:37 +08:00
ibuler
09fc2776df [Update] Support history view 2018-03-30 22:03:43 +08:00
ibuler
e4c2affb5f Merge branch 'rev' into dev 2018-03-29 15:07:49 +08:00
ibuler
6fae4d5dee [Bugfix] for some commit 2018-03-29 15:06:35 +08:00
ibuler
0c80e3e815 [rev] for api commit 2018-03-29 15:02:09 +08:00
ibuler
d32f070b5c [Update] 修改Inverntoy,增加更多属性 2018-03-28 19:37:29 +08:00
ibuler
b959f1f68b Merge branch 'bugfix_ip_lookup' into dev 2018-03-28 16:47:44 +08:00
ibuler
e1cab35db0 [Bugfix] 修改获取城市 2018-03-28 16:47:36 +08:00
ibuler
8014cc48b6 [Update] 修改settings和缩进 2018-03-28 15:47:20 +08:00
ibuler
829f57e2d7 [Bugfix] 修复小bug 2018-03-28 14:35:27 +08:00
ibuler
5f0b4a4b63 [Update] 修改一些翻译 2018-03-28 11:28:40 +08:00
ibuler
c5af4d47eb Merge branch 'some_auth_api' into dev 2018-03-27 18:37:04 +08:00
ibuler
c37bfb682a [Update] 添加设置认证api和创建用户时可以不选择组 2018-03-27 18:34:41 +08:00
ibuler
3aaea6cc31 Merge branch 'bugfix_useradd' into dev 2018-03-27 17:48:35 +08:00
ibuler
f85e5b6f75 [Bugfix] 修改添加用户的bug 2018-03-27 17:47:53 +08:00
ibuler
d598571dc1 [Update] 添加一个log 2018-03-27 16:51:27 +08:00
ibuler
e873be95d5 [Update] 导入到当前node 2018-03-26 16:16:18 +08:00
ibuler
dbaa4ab502 [Update] 修改资产导入的事务问题 2018-03-26 15:55:31 +08:00
ibuler
ac1e319cd9 [Merge] merge with vpc 2018-03-26 09:56:25 +08:00
ibuler
a39424ac09 [Update] 更新jms脚本 2018-03-25 21:48:41 +08:00
ibuler
75319b99ae [Update] 更新ap请求 2018-03-25 21:47:29 +08:00
ibuler
7f4f67aa8d [Update] 支持网域 2018-03-23 19:46:46 +08:00
ibuler
fe1862120f [Update] 删掉集群等等 2018-03-21 18:13:16 +08:00
ibuler
759760e7d9 [Update] 服务器可以生成用户密钥 2018-03-21 15:22:10 +08:00
老广
15b74da57c Merge pull request #1100 from jumpserver/dev
Dev
2018-03-21 11:59:05 +08:00
ibuler
6f29cf5ddd [Update] 修改quickstart 2018-03-21 09:26:14 +08:00
ibuler
0bba840e4d Merge branch 'pubkey' into dev 2018-03-19 16:24:59 +08:00
ibuler
2156e0f51a [Update] 修改jms 2018-03-19 16:24:50 +08:00
ibuler
2fc9c04228 [Bugfix] 修复更新系统用户后关联节点丢失的问题 2018-03-19 15:32:02 +08:00
ibuler
d8e614c54d [Update] 管理脚本 2018-03-19 11:26:51 +08:00
ibuler
2ab26e25cc [Update] 改为supervisor启动 2018-03-16 10:43:21 +08:00
ibuler
f195b309d4 [Bugfix] 全选后编辑checkbox问题 2018-03-16 10:42:53 +08:00
ibuler
5e41c5cadc [Update] 删掉没用的脚本 2018-03-15 18:14:43 +08:00
ibuler
d92d09bd80 [Update] 更新说明 2018-03-15 09:34:19 +08:00
liuzheng
227804b7ab Update step_by_step.rst 2018-03-15 07:33:56 +08:00
liuzheng
eeae989c06 Update step_by_step.rst 2018-03-15 07:30:32 +08:00
liuzheng
c67a9eb845 Update quickstart.rst 2018-03-15 07:27:30 +08:00
192 changed files with 6933 additions and 2469 deletions

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
__version__ = "0.5.0" __version__ = "1.3.0"

View File

@@ -3,3 +3,4 @@ from .asset import *
from .label import * from .label import *
from .system_user import * from .system_user import *
from .node import * from .node import *
from .domain import *

View File

@@ -28,7 +28,8 @@ from ..tasks import test_admin_user_connectability_manual
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = [
'AdminUserViewSet', 'ReplaceNodesAdminUserApi', 'AdminUserTestConnectiveApi' 'AdminUserViewSet', 'ReplaceNodesAdminUserApi',
'AdminUserTestConnectiveApi', 'AdminUserAuthApi',
] ]
@@ -41,6 +42,12 @@ class AdminUserViewSet(IDInFilterMixin, BulkModelViewSet):
permission_classes = (IsSuperUser,) permission_classes = (IsSuperUser,)
class AdminUserAuthApi(generics.UpdateAPIView):
queryset = AdminUser.objects.all()
serializer_class = serializers.AdminUserAuthSerializer
permission_classes = (IsSuperUser,)
class ReplaceNodesAdminUserApi(generics.UpdateAPIView): class ReplaceNodesAdminUserApi(generics.UpdateAPIView):
queryset = AdminUser.objects.all() queryset = AdminUser.objects.all()
serializer_class = serializers.ReplaceNodeAdminUserSerializer serializer_class = serializers.ReplaceNodeAdminUserSerializer
@@ -72,5 +79,5 @@ class AdminUserTestConnectiveApi(generics.RetrieveAPIView):
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
admin_user = self.get_object() admin_user = self.get_object()
test_admin_user_connectability_manual.delay(admin_user) task = test_admin_user_connectability_manual.delay(admin_user)
return Response({"msg": "Task created"}) return Response({"task": task.id})

View File

@@ -50,7 +50,9 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet):
if node_id: if node_id:
node = get_object_or_404(Node, id=node_id) node = get_object_or_404(Node, id=node_id)
if not node.is_root(): if not node.is_root():
queryset = queryset.filter(nodes__key__startswith=node.key).distinct() queryset = queryset.filter(
nodes__key__regex='{}(:[0-9]+)*$'.format(node.key),
).distinct()
return queryset return queryset
@@ -87,12 +89,8 @@ class AssetRefreshHardwareApi(generics.RetrieveAPIView):
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
asset_id = kwargs.get('pk') asset_id = kwargs.get('pk')
asset = get_object_or_404(Asset, pk=asset_id) asset = get_object_or_404(Asset, pk=asset_id)
summary = update_asset_hardware_info_manual(asset)[1] task = update_asset_hardware_info_manual.delay(asset)
logger.debug("Refresh summary: {}".format(summary)) return Response({"task": task.id})
if summary.get('dark'):
return Response(summary['dark'].values(), status=501)
else:
return Response({"msg": "ok"})
class AssetAdminUserTestApi(generics.RetrieveAPIView): class AssetAdminUserTestApi(generics.RetrieveAPIView):
@@ -105,8 +103,5 @@ class AssetAdminUserTestApi(generics.RetrieveAPIView):
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
asset_id = kwargs.get('pk') asset_id = kwargs.get('pk')
asset = get_object_or_404(Asset, pk=asset_id) asset = get_object_or_404(Asset, pk=asset_id)
ok, msg = test_asset_connectability_manual(asset) task = test_asset_connectability_manual.delay(asset)
if ok: return Response({"task": task.id})
return Response({"msg": "pong"})
else:
return Response({"error": msg}, status=502)

55
apps/assets/api/domain.py Normal file
View File

@@ -0,0 +1,55 @@
# ~*~ coding: utf-8 ~*~
from rest_framework_bulk import BulkModelViewSet
from rest_framework.views import APIView, Response
from rest_framework.generics import RetrieveAPIView
from django.views.generic.detail import SingleObjectMixin
from common.utils import get_logger
from ..hands import IsSuperUser, IsSuperUserOrAppUser
from ..models import Domain, Gateway
from ..utils import test_gateway_connectability
from .. import serializers
logger = get_logger(__file__)
__all__ = ['DomainViewSet', 'GatewayViewSet', "GatewayTestConnectionApi"]
class DomainViewSet(BulkModelViewSet):
queryset = Domain.objects.all()
permission_classes = (IsSuperUser,)
serializer_class = serializers.DomainSerializer
def get_serializer_class(self):
if self.request.query_params.get('gateway'):
return serializers.DomainWithGatewaySerializer
return super().get_serializer_class()
def get_permissions(self):
if self.request.query_params.get('gateway'):
self.permission_classes = (IsSuperUserOrAppUser,)
return super().get_permissions()
class GatewayViewSet(BulkModelViewSet):
filter_fields = ("domain",)
search_fields = filter_fields
queryset = Gateway.objects.all()
permission_classes = (IsSuperUser,)
serializer_class = serializers.GatewaySerializer
class GatewayTestConnectionApi(SingleObjectMixin, APIView):
permission_classes = (IsSuperUser,)
model = Gateway
object = None
def get(self, request, *args, **kwargs):
self.object = self.get_object(Gateway.objects.all())
ok, e = test_gateway_connectability(self.object)
if ok:
return Response("ok")
else:
return Response({"failed": e}, status=404)

View File

@@ -30,7 +30,9 @@ from .. import serializers
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = [
'NodeViewSet', 'NodeChildrenApi', 'NodeViewSet', 'NodeChildrenApi',
'NodeAssetsApi', 'NodeWithAssetsApi',
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeAddAssetsApi', 'NodeRemoveAssetsApi',
'NodeReplaceAssetsApi',
'NodeAddChildrenApi', 'RefreshNodeHardwareInfoApi', 'NodeAddChildrenApi', 'RefreshNodeHardwareInfoApi',
'TestNodeConnectiveApi' 'TestNodeConnectiveApi'
] ]
@@ -47,6 +49,34 @@ class NodeViewSet(BulkModelViewSet):
serializer.save() serializer.save()
class NodeWithAssetsApi(generics.ListAPIView):
permission_classes = (IsSuperUser,)
serializers = serializers.NodeSerializer
def get_node(self):
pk = self.kwargs.get('pk') or self.request.query_params.get('node')
if not pk:
node = Node.root()
else:
node = get_object_or_404(Node, pk)
return node
def get_queryset(self):
queryset = []
node = self.get_node()
children = node.get_children()
assets = node.get_assets()
queryset.extend(list(children))
for asset in assets:
node = Node()
node.id = asset.id
node.parent = node.id
node.value = asset.hostname
queryset.append(node)
return queryset
class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView): class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
queryset = Node.objects.all() queryset = Node.objects.all()
permission_classes = (IsSuperUser,) permission_classes = (IsSuperUser,)
@@ -69,14 +99,55 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
status=201, status=201,
) )
def get(self, request, *args, **kwargs): def get_object(self):
instance = self.get_object() pk = self.kwargs.get('pk') or self.request.query_params.get('id')
if self.request.query_params.get("all"): if not pk:
children = instance.get_all_children() node = Node.root()
else: else:
children = instance.get_children() node = get_object_or_404(Node, pk=pk)
response = [{"id": node.id, "key": node.key, "value": node.value} for node in children] return node
return Response(response, status=200)
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 == Node.root():
queryset.append(node)
if query_all:
children = node.get_all_children()
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.id = asset.id
node_fake.parent = node
node_fake.value = asset.hostname
node_fake.is_node = False
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 = (IsSuperUser,)
serializer_class = serializers.AssetSerializer
def get_queryset(self):
node_id = self.kwargs.get('pk')
query_all = self.request.query_params.get('all')
instance = get_object_or_404(Node, pk=node_id)
if query_all:
return instance.get_all_assets()
else:
return instance.get_assets()
class NodeAddChildrenApi(generics.UpdateAPIView): class NodeAddChildrenApi(generics.UpdateAPIView):
@@ -122,6 +193,19 @@ class NodeRemoveAssetsApi(generics.UpdateAPIView):
instance.assets.remove(*tuple(assets)) instance.assets.remove(*tuple(assets))
class NodeReplaceAssetsApi(generics.UpdateAPIView):
serializer_class = serializers.NodeAssetsSerializer
queryset = Node.objects.all()
permission_classes = (IsSuperUser,)
instance = None
def perform_update(self, serializer):
assets = serializer.validated_data.get('assets')
instance = self.get_object()
for asset in assets:
asset.nodes.set([instance])
class RefreshNodeHardwareInfoApi(APIView): class RefreshNodeHardwareInfoApi(APIView):
permission_classes = (IsSuperUser,) permission_classes = (IsSuperUser,)
model = Node model = Node
@@ -130,10 +214,9 @@ class RefreshNodeHardwareInfoApi(APIView):
node_id = kwargs.get('pk') node_id = kwargs.get('pk')
node = get_object_or_404(self.model, id=node_id) node = get_object_or_404(self.model, id=node_id)
assets = node.assets.all() assets = node.assets.all()
# task_name = _("Refresh node assets hardware info: {}".format(node.name))
task_name = _("更新节点资产硬件信息: {}".format(node.name)) task_name = _("更新节点资产硬件信息: {}".format(node.name))
update_assets_hardware_info_util.delay(assets, task_name=task_name) task = update_assets_hardware_info_util.delay(assets, task_name=task_name)
return Response({"msg": "Task created"}) return Response({"task": task.id})
class TestNodeConnectiveApi(APIView): class TestNodeConnectiveApi(APIView):
@@ -145,6 +228,5 @@ class TestNodeConnectiveApi(APIView):
node = get_object_or_404(self.model, id=node_id) node = get_object_or_404(self.model, id=node_id)
assets = node.assets.all() assets = node.assets.all()
task_name = _("测试节点下资产是否可连接: {}".format(node.name)) task_name = _("测试节点下资产是否可连接: {}".format(node.name))
test_asset_connectability_util.delay(assets, task_name=task_name) task = test_asset_connectability_util.delay(assets, task_name=task_name)
return Response({"msg": "Task created"}) return Response({"task": task.id})

View File

@@ -48,15 +48,6 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateAPIView):
permission_classes = (IsSuperUserOrAppUser,) permission_classes = (IsSuperUserOrAppUser,)
serializer_class = serializers.SystemUserAuthSerializer serializer_class = serializers.SystemUserAuthSerializer
def update(self, request, *args, **kwargs):
password = request.data.pop("password", None)
private_key = request.data.pop("private_key", None)
instance = self.get_object()
if password or private_key:
instance.set_auth(password=password, private_key=private_key)
return super().update(request, *args, **kwargs)
class SystemUserPushApi(generics.RetrieveAPIView): class SystemUserPushApi(generics.RetrieveAPIView):
""" """
@@ -67,8 +58,8 @@ class SystemUserPushApi(generics.RetrieveAPIView):
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
system_user = self.get_object() system_user = self.get_object()
push_system_user_to_assets_manual.delay(system_user) task = push_system_user_to_assets_manual.delay(system_user)
return Response({"msg": "Task created"}) return Response({"task": task.id})
class SystemUserTestConnectiveApi(generics.RetrieveAPIView): class SystemUserTestConnectiveApi(generics.RetrieveAPIView):
@@ -80,5 +71,5 @@ class SystemUserTestConnectiveApi(generics.RetrieveAPIView):
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
system_user = self.get_object() system_user = self.get_object()
test_system_user_connectability_manual.delay(system_user) task = test_system_user_connectability_manual.delay(system_user)
return Response({"msg": "Task created"}) return Response({"task": task.id})

View File

@@ -3,3 +3,4 @@
from .asset import * from .asset import *
from .label import * from .label import *
from .user import * from .user import *
from .domain import *

View File

@@ -16,6 +16,7 @@ class AssetCreateForm(forms.ModelForm):
fields = [ fields = [
'hostname', 'ip', 'public_ip', 'port', 'comment', 'hostname', 'ip', 'public_ip', 'port', 'comment',
'nodes', 'is_active', 'admin_user', 'labels', 'platform', 'nodes', 'is_active', 'admin_user', 'labels', 'platform',
'domain',
] ]
widgets = { widgets = {
@@ -26,9 +27,15 @@ class AssetCreateForm(forms.ModelForm):
'class': 'select2', 'data-placeholder': _('Admin user') 'class': 'select2', 'data-placeholder': _('Admin user')
}), }),
'labels': forms.SelectMultiple(attrs={ 'labels': forms.SelectMultiple(attrs={
'class': 'select2', 'data-placeholder': _('Labels') 'class': 'select2', 'data-placeholder': _('Label')
}), }),
'port': forms.TextInput(), 'port': forms.TextInput(),
'domain': forms.Select(attrs={
'class': 'select2', 'data-placeholder': _('Domain')
}),
}
labels = {
'nodes': _("Node"),
} }
help_texts = { help_texts = {
'hostname': '* required', 'hostname': '* required',
@@ -38,7 +45,8 @@ class AssetCreateForm(forms.ModelForm):
'root or other NOPASSWD sudo privilege user existed in asset,' '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' 'If asset is windows or other set any one, more see admin user left menu'
), ),
'platform': _("* required Must set exact system platform, Windows, Linux ...") 'platform': _("* required Must set exact system platform, Windows, Linux ..."),
'domain': _("If your have some network not connect with each other, you can set domain")
} }
@@ -48,18 +56,25 @@ class AssetUpdateForm(forms.ModelForm):
fields = [ fields = [
'hostname', 'ip', 'port', 'nodes', 'is_active', 'platform', 'hostname', 'ip', 'port', 'nodes', 'is_active', 'platform',
'public_ip', 'number', 'comment', 'admin_user', 'labels', 'public_ip', 'number', 'comment', 'admin_user', 'labels',
'domain',
] ]
widgets = { widgets = {
'nodes': forms.SelectMultiple(attrs={ 'nodes': forms.SelectMultiple(attrs={
'class': 'select2', 'data-placeholder': _('Nodes') 'class': 'select2', 'data-placeholder': _('Node')
}), }),
'admin_user': forms.Select(attrs={ 'admin_user': forms.Select(attrs={
'class': 'select2', 'data-placeholder': _('Admin user') 'class': 'select2', 'data-placeholder': _('Admin user')
}), }),
'labels': forms.SelectMultiple(attrs={ 'labels': forms.SelectMultiple(attrs={
'class': 'select2', 'data-placeholder': _('Labels') 'class': 'select2', 'data-placeholder': _('Label')
}), }),
'port': forms.TextInput(), 'port': forms.TextInput(),
'domain': forms.Select(attrs={
'class': 'select2', 'data-placeholder': _('Domain')
}),
}
labels = {
'nodes': _("Node"),
} }
help_texts = { help_texts = {
'hostname': '* required', 'hostname': '* required',
@@ -70,7 +85,8 @@ class AssetUpdateForm(forms.ModelForm):
'root or other NOPASSWD sudo privilege user existed in asset,' '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' 'If asset is windows or other set any one, more see admin user left menu'
), ),
'platform': _("* required Must set exact system platform, Windows, Linux ...") 'platform': _("* required Must set exact system platform, Windows, Linux ..."),
'domain': _("If your have some network not connect with each other, you can set domain")
} }
@@ -106,10 +122,10 @@ class AssetBulkUpdateForm(forms.ModelForm):
] ]
widgets = { widgets = {
'labels': forms.SelectMultiple( 'labels': forms.SelectMultiple(
attrs={'class': 'select2', 'data-placeholder': _('Select labels')} attrs={'class': 'select2', 'data-placeholder': _('Label')}
), ),
'nodes': forms.SelectMultiple( 'nodes': forms.SelectMultiple(
attrs={'class': 'select2', 'data-placeholder': _('Select nodes')} attrs={'class': 'select2', 'data-placeholder': _('Node')}
), ),
} }

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
#
from django import forms
from django.utils.translation import gettext_lazy as _
from ..models import Domain, Asset, Gateway
from .user import PasswordAndKeyAuthForm
__all__ = ['DomainForm', 'GatewayForm']
class DomainForm(forms.ModelForm):
assets = forms.ModelMultipleChoiceField(
queryset=Asset.objects.all(), label=_('Asset'), required=False,
widget=forms.SelectMultiple(
attrs={'class': 'select2', 'data-placeholder': _('Select assets')}
)
)
class Meta:
model = Domain
fields = ['name', 'comment', 'assets']
def __init__(self, *args, **kwargs):
if kwargs.get('instance', None):
initial = kwargs.get('initial', {})
initial['assets'] = kwargs['instance'].assets.all()
super().__init__(*args, **kwargs)
def save(self, commit=True):
instance = super().save(commit=commit)
assets = self.cleaned_data['assets']
instance.assets.set(assets)
return instance
class GatewayForm(PasswordAndKeyAuthForm):
def save(self, commit=True):
# Because we define custom field, so we need rewrite :method: `save`
instance = super().save()
password = self.cleaned_data.get('password')
private_key, public_key = super().gen_keys()
instance.set_auth(password=password, private_key=private_key)
return instance
class Meta:
model = Gateway
fields = [
'name', 'ip', 'port', 'username', 'protocol', 'domain', 'password',
'private_key_file', 'is_active', 'comment',
]
widgets = {
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
}
help_texts = {
'name': '* required',
'username': '* required',
}

View File

@@ -8,7 +8,7 @@ from common.utils import validate_ssh_private_key, ssh_pubkey_gen, get_logger
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = [
'FileForm', 'SystemUserForm', 'AdminUserForm', 'FileForm', 'SystemUserForm', 'AdminUserForm', 'PasswordAndKeyAuthForm',
] ]
@@ -114,22 +114,15 @@ class SystemUserForm(PasswordAndKeyAuthForm):
fields = [ fields = [
'name', 'username', 'protocol', 'auto_generate_key', 'name', 'username', 'protocol', 'auto_generate_key',
'password', 'private_key_file', 'auto_push', 'sudo', 'password', 'private_key_file', 'auto_push', 'sudo',
'comment', 'shell', 'nodes', 'priority', 'comment', 'shell', 'priority',
] ]
widgets = { widgets = {
'name': forms.TextInput(attrs={'placeholder': _('Name')}), 'name': forms.TextInput(attrs={'placeholder': _('Name')}),
'username': forms.TextInput(attrs={'placeholder': _('Username')}), 'username': forms.TextInput(attrs={'placeholder': _('Username')}),
'nodes': forms.SelectMultiple(
attrs={
'class': 'select2',
'data-placeholder': _('Nodes')
}
),
} }
help_texts = { help_texts = {
'name': '* required', 'name': '* required',
'username': '* required', 'username': '* required',
'nodes': _('If auto push checked, system user will be create at node assets'),
'auto_push': _('Auto push system user to asset'), 'auto_push': _('Auto push system user to asset'),
'priority': _('High level will be using login asset as default, if user was granted more than 2 system user'), 'priority': _('High level will be using login asset as default, if user was granted more than 2 system user'),
} }

View File

@@ -5,6 +5,7 @@ from .user import AdminUser, SystemUser
from .label import Label from .label import Label
from .cluster import * from .cluster import *
from .group import * from .group import *
from .domain import *
from .node import * from .node import *
from .asset import * from .asset import *
from .utils import * from .utils import *

View File

@@ -10,8 +10,6 @@ from django.utils.translation import ugettext_lazy as _
from django.core.cache import cache from django.core.cache import cache
from ..const import ASSET_ADMIN_CONN_CACHE_KEY from ..const import ASSET_ADMIN_CONN_CACHE_KEY
from .cluster import Cluster
from .group import AssetGroup
from .user import AdminUser, SystemUser from .user import AdminUser, SystemUser
__all__ = ['Asset'] __all__ = ['Asset']
@@ -36,6 +34,19 @@ def default_node():
return None return None
class AssetQuerySet(models.QuerySet):
def active(self):
return self.filter(is_active=True)
def valid(self):
return self.active()
class AssetManager(models.Manager):
def get_queryset(self):
return AssetQuerySet(self.model, using=self._db)
class Asset(models.Model): class Asset(models.Model):
# Important # Important
PLATFORM_CHOICES = ( PLATFORM_CHOICES = (
@@ -50,6 +61,8 @@ class Asset(models.Model):
ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True) ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True)
hostname = models.CharField(max_length=128, unique=True, verbose_name=_('Hostname')) hostname = models.CharField(max_length=128, unique=True, verbose_name=_('Hostname'))
port = models.IntegerField(default=22, verbose_name=_('Port')) 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")) nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes"))
is_active = models.BooleanField(default=True, verbose_name=_('Is active')) is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
@@ -72,7 +85,6 @@ class Asset(models.Model):
disk_total = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk total')) 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')) disk_info = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk info'))
platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform'))
os = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('OS')) 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_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')) os_arch = models.CharField(max_length=16, blank=True, null=True, verbose_name=_('OS arch'))
@@ -83,8 +95,10 @@ class Asset(models.Model):
date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) 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')) comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment'))
objects = AssetManager()
def __str__(self): def __str__(self):
return self.hostname return '{0.hostname}({0.ip})'.format(self)
@property @property
def is_valid(self): def is_valid(self):
@@ -96,11 +110,15 @@ class Asset(models.Model):
return False, warning return False, warning
def is_unixlike(self): def is_unixlike(self):
if self.platform not in ("Windows", "Other"): if self.platform not in ("Windows",):
return True return True
else: else:
return False return False
def get_nodes(self):
from .node import Node
return self.nodes.all() or [Node.root()]
@property @property
def hardware_info(self): def hardware_info(self):
if self.cpu_count: if self.cpu_count:
@@ -122,12 +140,24 @@ class Asset(models.Model):
return False return False
def to_json(self): def to_json(self):
return { info = {
'id': self.id, 'id': self.id,
'hostname': self.hostname, 'hostname': self.hostname,
'ip': self.ip, 'ip': self.ip,
'port': self.port, '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 get_auth_info(self):
if self.admin_user:
return {
'username': self.admin_user.username,
'password': self.admin_user.password,
'private_key': self.admin_user.private_key_file,
'become': self.admin_user.become_info,
}
def _to_secret_json(self): def _to_secret_json(self):
""" """
@@ -168,9 +198,7 @@ class Asset(models.Model):
try: try:
asset.save() asset.save()
asset.system_users = [choice(SystemUser.objects.all()) for i in range(3)] asset.system_users = [choice(SystemUser.objects.all()) for i in range(3)]
asset.groups = [choice(AssetGroup.objects.all()) for i in range(3)]
logger.debug('Generate fake asset : %s' % asset.ip) logger.debug('Generate fake asset : %s' % asset.ip)
except IntegrityError: except IntegrityError:
print('Error continue') print('Error continue')
continue continue

127
apps/assets/models/base.py Normal file
View File

@@ -0,0 +1,127 @@
# -*- coding: utf-8 -*-
#
import os
import uuid
from hashlib import md5
import sshpubkeys
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.validators import alphanumeric
from .utils import private_key_validator
signer = get_signer()
class AssetUser(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
username = models.CharField(max_length=32, verbose_name=_('Username'), validators=[alphanumeric])
_password = models.CharField(max_length=256, blank=True, null=True, verbose_name=_('Password'))
_private_key = models.TextField(max_length=4096, blank=True, null=True, verbose_name=_('SSH private key'), validators=[private_key_validator, ])
_public_key = models.TextField(max_length=4096, blank=True, 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'))
@property
def password(self):
if self._password:
return signer.unsign(self._password)
else:
return None
@password.setter
def password(self, password_raw):
raise AttributeError("Using set_auth do that")
# self._password = signer.sign(password_raw)
@property
def private_key(self):
if self._private_key:
return signer.unsign(self._private_key)
@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)
@property
def private_key_obj(self):
if self._private_key:
key_str = signer.unsign(self._private_key)
return ssh_key_string_to_obj(key_str, password=self.password)
else:
return None
@property
def private_key_file(self):
if not self.private_key_obj:
return None
project_dir = settings.PROJECT_DIR
tmp_dir = os.path.join(project_dir, 'tmp')
key_str = signer.unsign(self._private_key)
key_name = '.' + md5(key_str.encode('utf-8')).hexdigest()
key_path = os.path.join(tmp_dir, key_name)
if not os.path.exists(key_path):
self.private_key_obj.write_private_key_file(key_path)
os.chmod(key_path, 0o400)
return key_path
@property
def public_key(self):
key = signer.unsign(self._public_key)
if key:
return key
else:
return None
@property
def public_key_obj(self):
if self.public_key:
try:
return sshpubkeys.SSHKey(self.public_key)
except TabError:
pass
return None
def set_auth(self, password=None, private_key=None, public_key=None):
update_fields = []
if password:
self._password = signer.sign(password)
update_fields.append('_password')
if private_key:
self._private_key = signer.sign(private_key)
update_fields.append('_private_key')
if public_key:
self._public_key = signer.sign(public_key)
update_fields.append('_public_key')
if update_fields:
self.save(update_fields=update_fields)
def auto_gen_auth(self):
password = str(uuid.uuid4())
private_key, public_key = ssh_key_gen(
username=self.username, password=password
)
self.set_auth(password=password,
private_key=private_key,
public_key=public_key)
def _to_secret_json(self):
"""Push system user use it"""
return {
'name': self.name,
'username': self.username,
'password': self.password,
'public_key': self.public_key,
'private_key': self.private_key_file,
}
class Meta:
abstract = True

View File

@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
#
import uuid
import random
from django.db import models
from django.utils.translation import ugettext_lazy as _
from .base import AssetUser
__all__ = ['Domain', 'Gateway']
class Domain(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
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'))
def __str__(self):
return self.name
def has_gateway(self):
return self.gateway_set.filter(is_active=True).exists()
@property
def gateways(self):
return self.gateway_set.filter(is_active=True)
def random_gateway(self):
return random.choice(self.gateways)
class Gateway(AssetUser):
SSH_PROTOCOL = 'ssh'
RDP_PROTOCOL = 'rdp'
PROTOCOL_CHOICES = (
(SSH_PROTOCOL, 'ssh'),
(RDP_PROTOCOL, '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"))
domain = models.ForeignKey(Domain, 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"))
def __str__(self):
return self.name

View File

@@ -16,8 +16,10 @@ class Node(models.Model):
child_mark = models.IntegerField(default=0) child_mark = models.IntegerField(default=0)
date_create = models.DateTimeField(auto_now_add=True) date_create = models.DateTimeField(auto_now_add=True)
is_node = True
def __str__(self): def __str__(self):
return self.value return self.full_value
@property @property
def name(self): def name(self):
@@ -28,7 +30,7 @@ class Node(models.Model):
if self == self.__class__.root(): if self == self.__class__.root():
return self.value return self.value
else: else:
return '{}/{}'.format(self.value, self.parent.full_value) return '{} / {}'.format(self.parent.full_value, self.value)
@property @property
def level(self): def level(self):
@@ -61,8 +63,8 @@ class Node(models.Model):
assets = Asset.objects.filter(nodes__id=self.id) assets = Asset.objects.filter(nodes__id=self.id)
return assets return assets
def get_active_assets(self): def get_valid_assets(self):
return self.get_assets().filter(is_active=True) return self.get_assets().valid()
def get_all_assets(self): def get_all_assets(self):
from .asset import Asset from .asset import Asset
@@ -70,11 +72,14 @@ class Node(models.Model):
assets = Asset.objects.all() assets = Asset.objects.all()
else: else:
nodes = self.get_family() nodes = self.get_family()
assets = Asset.objects.filter(nodes__in=nodes) assets = Asset.objects.filter(nodes__in=nodes).distinct()
return assets return assets
def get_all_active_assets(self): def has_assets(self):
return self.get_all_assets().filter(is_active=True) return self.get_all_assets()
def get_all_valid_assets(self):
return self.get_all_assets().valid()
def is_root(self): def is_root(self):
return self.key == '0' return self.key == '0'

View File

@@ -2,20 +2,16 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import os
import logging import logging
import uuid import uuid
from hashlib import md5
import sshpubkeys
from django.core.cache import cache from django.core.cache import cache
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ 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
from .utils import private_key_validator
from ..const import SYSTEM_USER_CONN_CACHE_KEY from ..const import SYSTEM_USER_CONN_CACHE_KEY
from .base import AssetUser
__all__ = ['AdminUser', 'SystemUser',] __all__ = ['AdminUser', 'SystemUser',]
@@ -23,117 +19,6 @@ logger = logging.getLogger(__name__)
signer = get_signer() signer = get_signer()
class AssetUser(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
username = models.CharField(max_length=128, verbose_name=_('Username'))
_password = models.CharField(max_length=256, blank=True, null=True, verbose_name=_('Password'))
_private_key = models.TextField(max_length=4096, blank=True, null=True, verbose_name=_('SSH private key'), validators=[private_key_validator, ])
_public_key = models.TextField(max_length=4096, blank=True, 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'))
@property
def password(self):
if self._password:
return signer.unsign(self._password)
else:
return None
@password.setter
def password(self, password_raw):
raise AttributeError("Using set_auth do that")
# self._password = signer.sign(password_raw)
@property
def private_key(self):
if self._private_key:
return signer.unsign(self._private_key)
@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)
@property
def private_key_obj(self):
if self._private_key:
key_str = signer.unsign(self._private_key)
return ssh_key_string_to_obj(key_str, password=self.password)
else:
return None
@property
def private_key_file(self):
if not self.private_key_obj:
return None
project_dir = settings.PROJECT_DIR
tmp_dir = os.path.join(project_dir, 'tmp')
key_str = signer.unsign(self._private_key)
key_name = '.' + md5(key_str.encode('utf-8')).hexdigest()
key_path = os.path.join(tmp_dir, key_name)
if not os.path.exists(key_path):
self.private_key_obj.write_private_key_file(key_path)
os.chmod(key_path, 0o400)
return key_path
@property
def public_key(self):
key = signer.unsign(self._public_key)
if key:
return key
else:
return None
@property
def public_key_obj(self):
if self.public_key:
try:
return sshpubkeys.SSHKey(self.public_key)
except TabError:
pass
return None
def set_auth(self, password=None, private_key=None, public_key=None):
update_fields = []
if password:
self._password = signer.sign(password)
update_fields.append('_password')
if private_key:
self._private_key = signer.sign(private_key)
update_fields.append('_private_key')
if public_key:
self._public_key = signer.sign(public_key)
update_fields.append('_public_key')
if update_fields:
self.save(update_fields=update_fields)
def auto_gen_auth(self):
password = str(uuid.uuid4())
private_key, public_key = ssh_key_gen(
username=self.name, password=password
)
self.set_auth(password=password,
private_key=private_key,
public_key=public_key)
def _to_secret_json(self):
"""Push system user use it"""
return {
'name': self.name,
'username': self.username,
'password': self.password,
'public_key': self.public_key,
'private_key': self.private_key_file,
}
class Meta:
abstract = True
class AdminUser(AssetUser): class AdminUser(AssetUser):
""" """
A privileged user that ansible can use it to push system user and so on A privileged user that ansible can use it to push system user and so on
@@ -216,14 +101,15 @@ class SystemUser(AssetUser):
) )
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes")) nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
priority = models.IntegerField(default=10, verbose_name=_("Priority")) priority = models.IntegerField(default=10, verbose_name=_("Priority"))
protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol')) protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol'))
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push')) auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
sudo = models.TextField(default='/sbin/ifconfig', verbose_name=_('Sudo')) sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell')) shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
def __str__(self): def __str__(self):
return self.name return '{0.name}({0.username})'.format(self)
def to_json(self): def to_json(self):
return { return {
@@ -235,11 +121,8 @@ class SystemUser(AssetUser):
'auto_push': self.auto_push, 'auto_push': self.auto_push,
} }
@property def get_assets(self):
def assets(self): assets = set(self.assets.all())
assets = set()
for node in self.nodes.all():
assets.update(set(node.get_all_assets()))
return assets return assets
@property @property
@@ -284,6 +167,3 @@ class SystemUser(AssetUser):
except IntegrityError: except IntegrityError:
print('Error continue') print('Error continue')
continue continue

View File

@@ -10,15 +10,15 @@ __all__ = ['init_model', 'generate_fake']
def init_model(): def init_model():
from . import Cluster, SystemUser, AdminUser, AssetGroup, Asset from . import SystemUser, AdminUser, Asset
for cls in [Cluster, SystemUser, AdminUser, AssetGroup, Asset]: for cls in [SystemUser, AdminUser, Asset]:
if hasattr(cls, 'initial'): if hasattr(cls, 'initial'):
cls.initial() cls.initial()
def generate_fake(): def generate_fake():
from . import Cluster, SystemUser, AdminUser, AssetGroup, Asset from . import SystemUser, AdminUser, Asset
for cls in [Cluster, SystemUser, AdminUser, AssetGroup, Asset]: for cls in [SystemUser, AdminUser, Asset]:
if hasattr(cls, 'generate_fake'): if hasattr(cls, 'generate_fake'):
cls.generate_fake() cls.generate_fake()

View File

@@ -6,3 +6,4 @@ from .admin_user import *
from .label import * from .label import *
from .system_user import * from .system_user import *
from .node import * from .node import *
from .domain import *

View File

@@ -2,9 +2,12 @@
# #
from django.core.cache import cache from django.core.cache import cache
from rest_framework import serializers from rest_framework import serializers
from ..models import Node, AdminUser from ..models import Node, AdminUser
from ..const import ADMIN_USER_CONN_CACHE_KEY from ..const import ADMIN_USER_CONN_CACHE_KEY
from .base import AuthSerializer
class AdminUserSerializer(serializers.ModelSerializer): class AdminUserSerializer(serializers.ModelSerializer):
""" """
@@ -18,6 +21,10 @@ class AdminUserSerializer(serializers.ModelSerializer):
model = AdminUser model = AdminUser
fields = '__all__' fields = '__all__'
def get_field_names(self, declared_fields, info):
fields = super().get_field_names(declared_fields, info)
return [f for f in fields if not f.startswith('_')]
@staticmethod @staticmethod
def get_unreachable_amount(obj): def get_unreachable_amount(obj):
data = cache.get(ADMIN_USER_CONN_CACHE_KEY.format(obj.name)) data = cache.get(ADMIN_USER_CONN_CACHE_KEY.format(obj.name))
@@ -39,6 +46,13 @@ class AdminUserSerializer(serializers.ModelSerializer):
return obj.assets_amount return obj.assets_amount
class AdminUserAuthSerializer(AuthSerializer):
class Meta:
model = AdminUser
fields = ['password', 'private_key']
class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer): class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer):
""" """
管理用户更新关联到的集群 管理用户更新关联到的集群
@@ -50,3 +64,6 @@ class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = AdminUser model = AdminUser
fields = ['id', 'nodes'] fields = ['id', 'nodes']

View File

@@ -4,9 +4,37 @@ from rest_framework import serializers
from rest_framework_bulk.serializers import BulkListSerializer from rest_framework_bulk.serializers import BulkListSerializer
from common.mixins import BulkSerializerMixin from common.mixins import BulkSerializerMixin
from ..models import Asset from ..models import Asset, Node
from .system_user import AssetSystemUserSerializer from .system_user import AssetSystemUserSerializer
__all__ = [
'AssetSerializer', 'AssetGrantedSerializer', 'MyAssetGrantedSerializer',
]
class NodeTMPSerializer(serializers.ModelSerializer):
parent = serializers.SerializerMethodField()
assets_amount = serializers.SerializerMethodField()
class Meta:
model = Node
fields = ['id', 'key', 'value', 'parent', 'assets_amount', 'is_node']
list_serializer_class = BulkListSerializer
@staticmethod
def get_parent(obj):
return obj.parent.id
@staticmethod
def get_assets_amount(obj):
return obj.get_all_assets().count()
def get_fields(self):
fields = super().get_fields()
field = fields["key"]
field.required = False
return fields
class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer): class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer):
""" """
@@ -33,12 +61,13 @@ class AssetGrantedSerializer(serializers.ModelSerializer):
""" """
system_users_granted = AssetSystemUserSerializer(many=True, read_only=True) system_users_granted = AssetSystemUserSerializer(many=True, read_only=True)
system_users_join = serializers.SerializerMethodField() system_users_join = serializers.SerializerMethodField()
nodes = NodeTMPSerializer(many=True, read_only=True)
class Meta: class Meta:
model = Asset model = Asset
fields = ( fields = (
"id", "hostname", "ip", "port", "system_users_granted", "id", "hostname", "ip", "port", "system_users_granted",
"is_active", "system_users_join", "os", "is_active", "system_users_join", "os", 'domain', "nodes",
"platform", "comment" "platform", "comment"
) )

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from common.utils import ssh_pubkey_gen
class AuthSerializer(serializers.ModelSerializer):
password = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=1024)
private_key = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=4096)
def gen_keys(self, private_key=None, password=None):
if private_key is None:
return None, None
public_key = ssh_pubkey_gen(private_key=private_key, password=password)
return private_key, public_key
def save(self, **kwargs):
password = self.validated_data.pop('password', None) or None
private_key = self.validated_data.pop('private_key', None) or None
self.instance = super().save(**kwargs)
if password or private_key:
private_key, public_key = self.gen_keys(private_key, password)
self.instance.set_auth(password=password, private_key=private_key,
public_key=public_key)
return self.instance

View File

@@ -1,46 +0,0 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from common.mixins import BulkSerializerMixin
from ..models import Asset, Cluster
class ClusterUpdateAssetsSerializer(serializers.ModelSerializer):
"""
集群更新资产数据结构
"""
assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all())
class Meta:
model = Cluster
fields = ['id', 'assets']
class ClusterSerializer(BulkSerializerMixin, serializers.ModelSerializer):
"""
cluster
"""
assets_amount = serializers.SerializerMethodField()
admin_user_name = serializers.SerializerMethodField()
assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all())
system_users = serializers.SerializerMethodField()
class Meta:
model = Cluster
fields = '__all__'
@staticmethod
def get_assets_amount(obj):
return obj.assets.count()
@staticmethod
def get_admin_user_name(obj):
try:
return obj.admin_user.name
except AttributeError:
return ''
@staticmethod
def get_system_users(obj):
return ', '.join(obj.name for obj in obj.systemuser_set.all())

View File

@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from ..models import Domain, Gateway
class DomainSerializer(serializers.ModelSerializer):
asset_count = serializers.SerializerMethodField()
gateway_count = serializers.SerializerMethodField()
class Meta:
model = Domain
fields = '__all__'
@staticmethod
def get_asset_count(obj):
return obj.assets.count()
@staticmethod
def get_gateway_count(obj):
return obj.gateway_set.all().count()
class GatewaySerializer(serializers.ModelSerializer):
class Meta:
model = Gateway
fields = [
'id', 'name', 'ip', 'port', 'protocol', 'username',
'domain', 'is_active', 'date_created', 'date_updated',
'created_by', 'comment',
]
class GatewayWithAuthSerializer(GatewaySerializer):
def get_field_names(self, declared_fields, info):
fields = super().get_field_names(declared_fields, info)
fields.extend(
['password', 'private_key']
)
return fields
class DomainWithGatewaySerializer(serializers.ModelSerializer):
gateways = GatewayWithAuthSerializer(many=True, read_only=True)
class Meta:
model = Domain
fields = '__all__'

View File

@@ -7,6 +7,12 @@ from ..models import Asset, Node
from .asset import AssetGrantedSerializer from .asset import AssetGrantedSerializer
__all__ = [
'NodeSerializer', "NodeGrantedSerializer", "NodeAddChildrenSerializer",
"NodeAssetsSerializer",
]
class NodeGrantedSerializer(BulkSerializerMixin, serializers.ModelSerializer): class NodeGrantedSerializer(BulkSerializerMixin, serializers.ModelSerializer):
""" """
授权资产组 授权资产组
@@ -42,7 +48,7 @@ class NodeSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Node model = Node
fields = ['id', 'key', 'value', 'parent', 'assets_amount'] fields = ['id', 'key', 'value', 'parent', 'assets_amount', 'is_node']
list_serializer_class = BulkListSerializer list_serializer_class = BulkListSerializer
@staticmethod @staticmethod

View File

@@ -1,6 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from ..models import SystemUser from ..models import SystemUser
from .base import AuthSerializer
class SystemUserSerializer(serializers.ModelSerializer): class SystemUserSerializer(serializers.ModelSerializer):
@@ -33,15 +34,13 @@ class SystemUserSerializer(serializers.ModelSerializer):
@staticmethod @staticmethod
def get_assets_amount(obj): def get_assets_amount(obj):
return len(obj.assets) return len(obj.get_assets())
class SystemUserAuthSerializer(serializers.ModelSerializer): class SystemUserAuthSerializer(AuthSerializer):
""" """
系统用户认证信息 系统用户认证信息
""" """
password = serializers.CharField(max_length=1024)
private_key = serializers.CharField(max_length=4096)
class Meta: class Meta:
model = SystemUser model = SystemUser

View File

@@ -1,14 +1,13 @@
# -*- coding: utf-8 -*- # -*- 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
from django.dispatch import receiver from django.dispatch import receiver
from common.utils import get_logger from common.utils import get_logger
from .models import Asset, SystemUser, Node from .models import Asset, SystemUser, Node
from .tasks import update_assets_hardware_info_util, \ from .tasks import update_assets_hardware_info_util, \
test_asset_connectability_util, push_system_user_to_node, \ test_asset_connectability_util, push_system_user_to_assets
push_node_system_users_to_asset
logger = get_logger(__file__) logger = get_logger(__file__)
@@ -31,7 +30,6 @@ def set_asset_root_node(asset):
@receiver(post_save, sender=Asset, dispatch_uid="my_unique_identifier") @receiver(post_save, sender=Asset, dispatch_uid="my_unique_identifier")
def on_asset_created_or_update(sender, instance=None, created=False, **kwargs): def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
set_asset_root_node(instance)
if created: if created:
logger.info("Asset `{}` create signal received".format(instance)) logger.info("Asset `{}` create signal received".format(instance))
update_asset_hardware_info_on_created(instance) update_asset_hardware_info_on_created(instance)
@@ -41,25 +39,39 @@ def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
@receiver(post_save, sender=SystemUser, dispatch_uid="my_unique_identifier") @receiver(post_save, sender=SystemUser, dispatch_uid="my_unique_identifier")
def on_system_user_update(sender, instance=None, created=True, **kwargs): def on_system_user_update(sender, instance=None, created=True, **kwargs):
if instance and not created: if instance and not created:
for node in instance.nodes.all(): logger.info("System user `{}` update signal received".format(instance))
push_system_user_to_node(instance, node) assets = instance.assets.all()
push_system_user_to_assets.delay(instance, assets)
@receiver(m2m_changed, sender=SystemUser.nodes.through) @receiver(m2m_changed, sender=SystemUser.nodes.through)
def on_system_user_node_change(sender, instance=None, **kwargs): def on_system_user_nodes_change(sender, instance=None, **kwargs):
if instance and kwargs["action"] == "post_add": if instance and kwargs["action"] == "post_add":
for pk in kwargs['pk_set']: assets = set()
node = kwargs['model'].objects.get(pk=pk) nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
push_system_user_to_node(instance, node) for node in nodes:
assets.update(set(node.get_all_assets()))
instance.assets.add(*tuple(assets))
@receiver(m2m_changed, sender=SystemUser.assets.through)
def on_system_user_assets_change(sender, instance=None, **kwargs):
if instance and kwargs["action"] == "post_add":
assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
push_system_user_to_assets(instance, assets)
@receiver(m2m_changed, sender=Asset.nodes.through) @receiver(m2m_changed, sender=Asset.nodes.through)
def on_asset_node_changed(sender, instance=None, **kwargs): def on_asset_node_changed(sender, instance=None, **kwargs):
if isinstance(instance, Asset) and kwargs['action'] == 'post_add': if isinstance(instance, Asset) and kwargs['action'] == 'post_add':
logger.debug("Asset node change signal received") logger.debug("Asset node change signal received")
for pk in kwargs['pk_set']: nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
node = kwargs['model'].objects.get(pk=pk) system_users_assets = defaultdict(set)
push_node_system_users_to_asset(node, [instance]) system_users = SystemUser.objects.filter(nodes__in=nodes)
for system_user in system_users:
system_users_assets[system_user].update({instance})
for system_user, assets in system_users_assets.items():
system_user.assets.add(*tuple(assets))
@receiver(m2m_changed, sender=Asset.nodes.through) @receiver(m2m_changed, sender=Asset.nodes.through)
@@ -67,5 +79,6 @@ def on_node_assets_changed(sender, instance=None, **kwargs):
if isinstance(instance, Node) and kwargs['action'] == 'post_add': if isinstance(instance, Node) and kwargs['action'] == 'post_add':
logger.debug("Node assets change signal received") logger.debug("Node assets change signal received")
assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
push_node_system_users_to_asset(instance, assets) system_users = SystemUser.objects.filter(nodes=instance)
for system_user in system_users:
system_user.assets.add(*tuple(assets))

View File

@@ -9,10 +9,11 @@ from django.utils.translation import ugettext as _
from common.utils import get_object_or_none, capacity_convert, \ from common.utils import get_object_or_none, capacity_convert, \
sum_capacity, encrypt_password, get_logger sum_capacity, encrypt_password, get_logger
from common.celery import register_as_period_task, after_app_shutdown_clean, \ from ops.celery.utils import register_as_period_task, after_app_shutdown_clean, \
after_app_ready_start, app as celery_app after_app_ready_start
from ops.celery import app as celery_app
from .models import SystemUser, AdminUser, Asset, Cluster from .models import SystemUser, AdminUser, Asset
from . import const from . import const
@@ -95,6 +96,9 @@ def update_assets_hardware_info_util(assets, task_name=None):
task_name = _("更新资产硬件信息") task_name = _("更新资产硬件信息")
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
hostname_list = [asset.hostname for asset in assets if asset.is_active and asset.is_unixlike()] hostname_list = [asset.hostname for asset in assets if asset.is_active and asset.is_unixlike()]
if not hostname_list:
logger.info("Not hosts get, may be asset is not active or not unixlike platform")
return {}
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name, hosts=hostname_list, tasks=tasks, pattern='all', task_name, hosts=hostname_list, tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System', options=const.TASK_OPTIONS, run_as_admin=True, created_by='System',
@@ -214,7 +218,7 @@ def test_admin_user_connectability_period():
def test_admin_user_connectability_manual(admin_user): def test_admin_user_connectability_manual(admin_user):
# task_name = _("Test admin user connectability: {}").format(admin_user.name) # task_name = _("Test admin user connectability: {}").format(admin_user.name)
task_name = _("测试管理行号可连接性: {}").format(admin_user.name) task_name = _("测试管理行号可连接性: {}").format(admin_user.name)
return test_admin_user_connectability_util.delay(admin_user, task_name) return test_admin_user_connectability_util(admin_user, task_name)
@shared_task @shared_task
@@ -275,7 +279,7 @@ def test_system_user_connectability_util(system_user, task_name):
:return: :return:
""" """
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
assets = system_user.assets assets = system_user.get_assets()
hosts = [asset.hostname for asset in assets if asset.is_active and asset.is_unixlike()] hosts = [asset.hostname for asset in assets if asset.is_active and asset.is_unixlike()]
tasks = const.TEST_SYSTEM_USER_CONN_TASKS tasks = const.TEST_SYSTEM_USER_CONN_TASKS
if not hosts: if not hosts:
@@ -385,50 +389,17 @@ def push_system_user_util(system_users, assets, task_name):
return task.run() return task.run()
def get_node_push_system_user_task_name(system_user, node):
# return _("Push system user to node: {} => {}").format(
return _("推送系统用户到节点资产: {} => {}").format(
system_user.name,
node.value
)
def push_system_user_to_node(system_user, node):
assets = node.get_all_assets()
task_name = get_node_push_system_user_task_name(system_user, node)
push_system_user_util.delay([system_user], assets, task_name)
@shared_task
def push_system_user_related_nodes(system_user):
if not system_user.is_need_push():
msg = "push system user `{}` passed, may be not auto push or ssh " \
"protocol is not ssh".format(system_user.name)
logger.info(msg)
return
nodes = system_user.nodes.all()
for node in nodes:
push_system_user_to_node(system_user, node)
@shared_task @shared_task
def push_system_user_to_assets_manual(system_user): def push_system_user_to_assets_manual(system_user):
push_system_user_related_nodes(system_user) assets = system_user.get_assets()
task_name = "推送系统用户到入资产: {}".format(system_user.name)
return push_system_user_util([system_user], assets, task_name=task_name)
def push_node_system_users_to_asset(node, assets): @shared_task
system_users = [] def push_system_user_to_assets(system_user, assets):
nodes = node.ancestor_with_node task_name = _("推送系统用户到入资产: {}").format(system_user.name)
# 获取该节点所有父节点有的系统用户, 然后推送 return push_system_user_util.delay([system_user], assets, task_name)
for n in nodes:
system_users.extend(list(n.systemuser_set.all()))
if system_users:
# task_name = _("Push system users to node: {}").format(node.value)
task_name = _("推送节点系统用户到新加入资产中: {}").format(node.value)
push_system_user_util.delay(system_users, assets, task_name)
# @shared_task # @shared_task
@@ -438,3 +409,7 @@ def push_node_system_users_to_asset(node, assets):
# def push_system_user_period(): # def push_system_user_period():
# for system_user in SystemUser.objects.all(): # for system_user in SystemUser.objects.all():
# push_system_user_related_nodes(system_user) # push_system_user_related_nodes(system_user)

View File

@@ -31,7 +31,7 @@
<div class="form-group"> <div class="form-group">
<div class="col-sm-9 col-lg-9 col-sm-offset-2"> <div class="col-sm-9 col-lg-9 col-sm-offset-2">
<div class="checkbox checkbox-success"> <div class="checkbox checkbox-success">
<input type="checkbox" name="enable_otp" checked id="id_enable_otp"><label for="id_enable_otp">{% trans 'Enable-OTP' %}</label> <input type="checkbox" name="enable_otp" checked id="id_enable_otp"><label for="id_enable_otp">{% trans 'Enable-MFA' %}</label>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,132 +1,125 @@
{% extends '_modal.html' %} {% extends '_modal.html' %}
{% load i18n %} {% load i18n %}
{% load static %}
{% block modal_class %}modal-lg{% endblock %} {% block modal_class %}modal-lg{% endblock %}
{% block modal_id %}asset_list_modal{% endblock %} {% block modal_id %}asset_list_modal{% endblock %}
{#{% block modal_title%}{% trans "Please select assets" %}{% endblock %}#} {% block modal_title%}{% trans "Asset list" %}{% endblock %}
{% block modal_body %} {% block modal_body %}
{#<div class="btn-group" style="float: right">#} <link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">
{# <button data-toggle="dropdown" class="btn btn-default btn-sm dropdown-toggle">{% trans 'Label' %} <span class="caret"></span></button>#} <script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>
{# <ul class="dropdown-menu labels">#} <script src="{% static 'js/jquery.form.min.js' %}"></script>
{# {% for label in labels %}#} <style>
{# <li><a style="font-weight: bolder">{{ label.name }}:{{ label.value }}</a></li>#} .inmodal .modal-header {
{# {% endfor %}#} padding: 10px 10px;
{# </ul>#} text-align: center;
{#</div>#} }
<table class="table table-striped table-bordered table-hover " id="asset_modal_table" width="100%">
<thead> #assetTree2.ztree * {
<tr> background-color: #f8fafb;
<th class="text-center"><input type="checkbox" class="ipt_check_all"></th> }
<th class="text-center">{% trans 'Hostname' %}</th> #assetTree2.ztree {
<th class="text-center">{% trans 'IP' %}</th> background-color: #f8fafb;
<th class="text-center">{% trans 'Hardware' %}</th> }
<th class="text-center">{% trans 'Active' %}</th> </style>
<th class="text-center">{% trans 'Reachable' %}</th>
<th class="text-center">{% trans 'Action' %}</th> <div class="wrapper wrapper-content">
</tr> <div class="row">
</thead> <div class="col-lg-3" id="split-left" style="padding-left: 3px">
<tbody> <div class="ibox float-e-margins">
</tbody> <div class="ibox-content mailbox-content" style="padding-top: 0;padding-left: 1px">
</table> <div class="file-manager ">
<div id="actions" class="hide"> <div id="assetTree2" class="ztree">
<div class="input-group"> </div>
<select class="form-control m-b" style="width: auto" id="slct_bulk_update"> <div class="clearfix"></div>
<option value="delete">{% trans 'Delete selected' %}</option> </div>
<option value="update">{% trans 'Update selected' %}</option> </div>
<option value="deactive">{% trans 'Deactive selected' %}</option> </div>
<option value="active">{% trans 'Active selected' %}</option> </div>
</select> <div class="col-lg-9 animated fadeInRight" id="split-right">
<div class="input-group-btn pull-left" style="padding-left: 5px;"> <div class="mail-box-header">
<button id='btn_bulk_update' style="height: 32px;" class="btn btn-sm btn-primary"> <table class="table table-striped table-bordered table-hover " id="asset_list_modal_table" style="width: 100%">
{% trans 'Submit' %} <thead>
</button> <tr>
<th class="text-center"><input type="checkbox" class="ipt_check_all"></th>
<th class="text-center">{% trans 'Hostname' %}</th>
<th class="text-center">{% trans 'IP' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div> </div>
</div> </div>
</div> </div>
<script> <script>
var zTree2, asset_table2 = 0;
var modal_table; function initTable2() {
function initModalTable() {
var options = { var options = {
ele: $('#asset_modal_table'), ele: $('#asset_list_modal_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
{% 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));
}},
{targets: 3, createdCell: function (td, cellData, rowData) {
$(td).html(rowData.hardware_info)
}},
{targets: 4, createdCell: function (td, cellData) {
if (!cellData) {
$(td).html('<i class="fa fa-times text-danger"></i>')
} else {
$(td).html('<i class="fa fa-check text-navy"></i>')
}
}},
{targets: 5, createdCell: function (td, cellData) {
if (cellData === 'Unknown'){
$(td).html('<i class="fa fa-circle text-warning"></i>')
} else if (!cellData) {
$(td).html('<i class="fa fa-circle text-danger"></i>')
} else {
$(td).html('<i class="fa fa-circle text-navy"></i>')
}
}},
{targets: 6, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "assets:asset-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_asset_delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
$(td).html(update_btn + del_btn)
}}
],
ajax_url: '{% url "api-assets:asset-list" %}', ajax_url: '{% url "api-assets:asset-list" %}',
columns: [ columns: [
{data: "id"}, {data: "hostname" }, {data: "ip" }, {data: "id"}, {data: "hostname" }, {data: "ip" }
{data: "cpu_cores"}, {data: "is_active", orderable: false },
{data: "is_connective", orderable: false}, {data: "id", orderable: false }
], ],
op_html: $('#actions').html() pageLength: 10
}; };
modal_table = jumpserver.initServerSideDataTable(options); asset_table2 = jumpserver.initServerSideDataTable(options);
return modal_table; return asset_table2
} }
$(document).ready(function(){ function onSelected2(event, treeNode) {
initModalTable(); var url = asset_table2.ajax.url();
}).on('click', '#btn_select_assets', function () { url = setUrlParam(url, "node_id", treeNode.id);
var data_table = $('#asset_modal_table').DataTable(); setCookie('node_selected', treeNode.id);
var id_list = []; asset_table2.ajax.url(url);
data_table.rows({selected: true}).every(function(){ asset_table2.ajax.reload();
id_list.push(this.data().id); }
function initTree2() {
var setting = {
view: {
dblClickExpand: false,
showLine: true
},
data: {
simpleData: {
enable: true
}
},
callback: {
onSelected: onSelected2
}
};
var zNodes = [];
$.get("{% url 'api-assets:node-list' %}", function(data, status){
$.each(data, function (index, value) {
value["pId"] = value["parent"];
value["open"] = true;
value["name"] = value["value"] + ' (' + value['assets_amount'] + ')';
value['value'] = value['value'];
});
zNodes = data;
$.fn.zTree.init($("#assetTree2"), setting, zNodes);
zTree2 = $.fn.zTree.getZTreeObj("assetTree2");
}); });
var current_node; }
var nodes = zTree.getSelectedNodes();
if (nodes && nodes.length === 1) {
current_node = nodes[0]
} else {
return
}
var data = {
'assets': id_list
};
var success = function () { $(document).ready(function(){
modal_table.ajax.reload() initTable2();
}; initTree2();
APIUpdateAttr({
'url': '/api/assets/v1/nodes/' + current_node.id + '/assets/add/',
'method': 'PUT',
'body': JSON.stringify(data),
'success': success
})
}) })
</script> </script>
{% endblock %} {% endblock %}
{% block modal_confirm_id %}btn_select_assets{% endblock %}
{% block modal_button %}
{{ block.super }}
{% endblock %}
{% block modal_confirm_id %}btn_asset_modal_confirm{% endblock %}

View File

@@ -121,14 +121,16 @@ $(document).ready(function () {
}) })
.on('click', '.btn-test-connective', function () { .on('click', '.btn-test-connective', function () {
var the_url = "{% url 'api-assets:admin-user-connective' pk=admin_user.id %}"; var the_url = "{% url 'api-assets:admin-user-connective' pk=admin_user.id %}";
var error = function (data) { var success = function (data) {
alert(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')
}; };
APIUpdateAttr({ APIUpdateAttr({
url: the_url, url: the_url,
error: error,
method: 'GET', method: 'GET',
success_message: "{% trans 'Task has been send, seen left asset status' %}" success: success,
flash_message: false
}); });
}) })
</script> </script>

View File

@@ -15,10 +15,11 @@
{% csrf_token %} {% csrf_token %}
<h3>{% trans 'Basic' %}</h3> <h3>{% trans 'Basic' %}</h3>
{% bootstrap_field form.hostname layout="horizontal" %} {% bootstrap_field form.hostname layout="horizontal" %}
{% bootstrap_field form.platform layout="horizontal" %}
{% bootstrap_field form.ip layout="horizontal" %} {% bootstrap_field form.ip layout="horizontal" %}
{% bootstrap_field form.port layout="horizontal" %} {% bootstrap_field form.port layout="horizontal" %}
{% bootstrap_field form.platform layout="horizontal" %}
{% bootstrap_field form.public_ip layout="horizontal" %} {% bootstrap_field form.public_ip layout="horizontal" %}
{% bootstrap_field form.domain layout="horizontal" %}
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
<h3>{% trans 'Auth' %}</h3> <h3>{% trans 'Auth' %}</h3>
@@ -33,7 +34,7 @@
<div class="form-group {% if form.errors.labels %} has-error {% endif %}"> <div class="form-group {% if form.errors.labels %} has-error {% endif %}">
<label for="{{ form.labels.id_for_label }}" class="col-md-2 control-label">{% trans 'Label' %}</label> <label for="{{ form.labels.id_for_label }}" class="col-md-2 control-label">{% trans 'Label' %}</label>
<div class="col-md-9"> <div class="col-md-9">
<select name="labels" class="select2 labels" data-placeholder="{% trans 'Select labels' %}" style="width: 100%" multiple="" tabindex="4" id="{{ form.labels.id_for_label }}"> <select name="labels" class="select2 labels" data-placeholder="{% trans 'Label' %}" style="width: 100%" multiple="" tabindex="4" id="{{ form.labels.id_for_label }}">
{% for name, labels in form.labels.field.queryset|group_labels %} {% for name, labels in form.labels.field.queryset|group_labels %}
<optgroup label="{{ name }}"> <optgroup label="{{ name }}">
{% for label in labels %} {% for label in labels %}
@@ -84,6 +85,17 @@ $(document).ready(function () {
allowClear: true, allowClear: true,
templateSelection: format templateSelection: format
}); });
$("#id_platform").change(function (){
var platform = $("#id_platform option:selected").text();
var port = 22;
if(platform === 'Windows'){
port = 3389;
}
if(platform === 'Other'){
port = null;
}
$("#id_port").val(port);
});
}) })
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -269,16 +269,15 @@ function updateAssetNodes(nodes) {
function refreshAssetHardware() { function refreshAssetHardware() {
var the_url = "{% url 'api-assets:asset-refresh' pk=asset.id %}"; var the_url = "{% url 'api-assets:asset-refresh' pk=asset.id %}";
var success = function (data) { var success = function(data) {
location.reload(); console.log(data);
}; var task_id = data.task;
var error = function (data) { var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
alert(data) window.open(url, '', 'width=800,height=600')
}; };
APIUpdateAttr({ APIUpdateAttr({
url: the_url, url: the_url,
success: success, success: success,
error: error,
method: 'GET' method: 'GET'
}); });
} }
@@ -306,9 +305,9 @@ $(document).ready(function () {
success_message: success success_message: success
}); });
if (status === "False") { if (status === "False") {
$(".ibox-content > table > tbody > tr:nth-child(13) > td:last >b").html('True'); $(".ibox-content > table > tbody > tr:nth-child(13) > td:last >b").html('True');
}else{ }else{
$(".ibox-content > table > tbody > tr:nth-child(13) > td:last >b").html('False'); $(".ibox-content > table > tbody > tr:nth-child(13) > td:last >b").html('False');
} }
}).on('click', '#btn-update-nodes', function () { }).on('click', '#btn-update-nodes', function () {
if (Object.keys(jumpserver.nodes_selected).length === 0) { if (Object.keys(jumpserver.nodes_selected).length === 0) {
@@ -344,19 +343,20 @@ $(document).ready(function () {
var redirect_url = "{% url 'assets:asset-list' %}"; var redirect_url = "{% url 'assets:asset-list' %}";
objectDelete($this, name, the_url, redirect_url); objectDelete($this, name, the_url, redirect_url);
}).on('click', '#btn_refresh_asset', function () { }).on('click', '#btn_refresh_asset', function () {
alert('关闭alert, 等待完成, 自动刷新页面');
refreshAssetHardware() refreshAssetHardware()
}).on('click', '#btn-test-is-alive', function () { }).on('click', '#btn-test-is-alive', function () {
var the_url = "{% url 'api-assets:asset-alive-test' pk=asset.id %}"; var the_url = "{% url 'api-assets:asset-alive-test' pk=asset.id %}";
var error = function (data) {
alert(data) 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')
}; };
alert('关闭alert, 等待完成');
APIUpdateAttr({ APIUpdateAttr({
url: the_url, url: the_url,
error: error,
method: 'GET', method: 'GET',
success_message: "{% trans "Reachable" %}" success: success
}); });
}) })

View File

@@ -41,9 +41,9 @@
{% block content %} {% block content %}
<div class="wrapper wrapper-content"> <div class="wrapper wrapper-content">
<div class="row"> <div class="row">
<div class="col-lg-3" id="split-left"> <div class="col-lg-3" id="split-left" style="padding-left: 3px">
<div class="ibox float-e-margins"> <div class="ibox float-e-margins">
<div class="ibox-content mailbox-content" style="padding-top: 0"> <div class="ibox-content mailbox-content" style="padding-top: 0;padding-left: 1px">
<div class="file-manager "> <div class="file-manager ">
<div id="assetTree" class="ztree"> <div id="assetTree" class="ztree">
</div> </div>
@@ -59,57 +59,57 @@
<i class="fa fa-angle-left fa-x" id="toggle-icon"></i> <i class="fa fa-angle-left fa-x" id="toggle-icon"></i>
</div> </div>
</div> </div>
<div class="mail-box-header"> <div class="mail-box-header">
<div class="uc pull-left m-r-5"><a class="btn btn-sm btn-primary btn-create-asset"> {% trans "Create asset" %} </a></div> <div class="uc pull-left m-r-5"><a class="btn btn-sm btn-primary btn-create-asset"> {% trans "Create asset" %} </a></div>
<div class="html5buttons"> <div class="html5buttons">
<div class="dt-buttons btn-group"> <div class="dt-buttons btn-group">
<a class="btn btn-default btn_import" data-toggle="modal" data-target="#asset_import_modal" tabindex="0"> <a class="btn btn-default btn_import" data-toggle="modal" data-target="#asset_import_modal" tabindex="0">
<span>{% trans "Import" %}</span> <span>{% trans "Import" %}</span>
</a> </a>
<a class="btn btn-default btn_export" tabindex="0"> <a class="btn btn-default btn_export" tabindex="0">
<span>{% trans "Export" %}</span> <span>{% trans "Export" %}</span>
</a> </a>
</div>
</div>
<div class="btn-group" style="float: right">
<button data-toggle="dropdown" class="btn btn-default btn-sm dropdown-toggle">{% trans 'Label' %} <span class="caret"></span></button>
<ul class="dropdown-menu labels">
{% for label in labels %}
<li><a style="font-weight: bolder">{{ label.name }}:{{ label.value }}</a></li>
{% endfor %}
</ul>
</div> </div>
<table class="table table-striped table-bordered table-hover " id="asset_list_table" style="width: 100%"> </div>
<thead> <div class="btn-group" style="float: right">
<tr> <button data-toggle="dropdown" class="btn btn-default btn-sm dropdown-toggle">{% trans 'Label' %} <span class="caret"></span></button>
<th class="text-center"><input type="checkbox" class="ipt_check_all"></th> <ul class="dropdown-menu labels">
<th class="text-center">{% trans 'Hostname' %}</th> {% for label in labels %}
<th class="text-center">{% trans 'IP' %}</th> <li><a style="font-weight: bolder">{{ label.name }}:{{ label.value }}</a></li>
<th class="text-center">{% trans 'Hardware' %}</th> {% endfor %}
<th class="text-center">{% trans 'Active' %}</th> </ul>
<th class="text-center">{% trans 'Reachable' %}</th> </div>
<th class="text-center">{% trans 'Action' %}</th> <table class="table table-striped table-bordered table-hover " id="asset_list_table" style="width: 100%">
</tr> <thead>
</thead> <tr>
<tbody> <th class="text-center"><input type="checkbox" class="ipt_check_all"></th>
</tbody> <th class="text-center">{% trans 'Hostname' %}</th>
</table> <th class="text-center">{% trans 'IP' %}</th>
<div id="actions" class="hide"> <th class="text-center">{% trans 'Hardware' %}</th>
<div class="input-group"> <th class="text-center">{% trans 'Active' %}</th>
<select class="form-control m-b" style="width: auto" id="slct_bulk_update"> <th class="text-center">{% trans 'Reachable' %}</th>
<option value="delete">{% trans 'Delete selected' %}</option> <th class="text-center">{% trans 'Action' %}</th>
<option value="update">{% trans 'Update selected' %}</option> </tr>
<option value="remove">{% trans 'Remove from this node' %}</option> </thead>
<option value="deactive">{% trans 'Deactive selected' %}</option> <tbody>
<option value="active">{% trans 'Active selected' %}</option> </tbody>
</select> </table>
<div class="input-group-btn pull-left" style="padding-left: 5px;"> <div id="actions" class="hide">
<button id='btn_bulk_update' style="height: 32px;" class="btn btn-sm btn-primary"> <div class="input-group">
{% trans 'Submit' %} <select class="form-control m-b" style="width: auto" id="slct_bulk_update">
</button> <option value="delete">{% trans 'Delete selected' %}</option>
</div> <option value="update">{% trans 'Update selected' %}</option>
<option value="remove">{% trans 'Remove from this node' %}</option>
<option value="deactive">{% trans 'Deactive selected' %}</option>
<option value="active">{% trans 'Active selected' %}</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">
{% trans 'Submit' %}
</button>
</div> </div>
</div> </div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -117,15 +117,16 @@
<div id="rMenu"> <div id="rMenu">
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li id="menu_asset_create" class="btn-create-asset" tabindex="-1"><a>{% trans 'Create asset' %}</a></li>
<li id="menu_asset_add" class="btn-add-asset" data-toggle="modal" data-target="#asset_list_modal" tabindex="0"><a>{% trans 'Add asset' %}</a></li>
<li id="menu_refresh_hardware_info" class="btn-refresh-hardware" tabindex="-1"><a>{% trans 'Refresh node hardware info' %}</a></li>
<li id="menu_test_connective" class="btn-test-connective" tabindex="-1"><a>{% trans 'Test node connective' %}</a></li>
<li class="divider"></li> <li class="divider"></li>
<li id="m_create" tabindex="-1" onclick="addTreeNode();"><a>{% trans 'Add node' %}</a></li> <li id="m_create" tabindex="-1" onclick="addTreeNode();"><a><i class="fa fa-plus-square-o"></i> {% trans 'Add node' %}</a></li>
<li id="m_del" tabindex="-1" onclick="editTreeNode();"><a>{% trans 'Rename node' %}</a></li> <li id="m_del" tabindex="-1" onclick="editTreeNode();"><a><i class="fa fa-pencil-square-o"></i> {% trans 'Rename node' %}</a></li>
<li id="m_del" tabindex="-1" onclick="removeTreeNode();"><a><i class="fa fa-minus-square"></i> {% trans 'Delete node' %}</a></li>
<li class="divider"></li> <li class="divider"></li>
<li id="m_del" tabindex="-1" onclick="removeTreeNode();"><a>{% trans 'Delete node' %}</a></li> <li id="menu_asset_add" class="btn-add-asset" data-toggle="modal" data-target="#asset_list_modal" tabindex="0"><a><i class="fa fa-copy"></i> {% trans 'Add assets to node' %}</a></li>
<li id="menu_asset_move" class="btn-move-asset" data-toggle="modal" data-target="#asset_list_modal" tabindex="0"><a><i class="fa fa-cut"></i> {% trans 'Move assets to node' %}</a></li>
<li class="divider"></li>
<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>
</ul> </ul>
</div> </div>
@@ -136,6 +137,7 @@
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
var zTree, rMenu, asset_table, show = 0; var zTree, rMenu, asset_table, show = 0;
var update_node_action = "";
function initTable() { function initTable() {
var options = { var options = {
ele: $('#asset_list_table'), ele: $('#asset_list_table'),
@@ -182,7 +184,6 @@ function initTable() {
return asset_table return asset_table
} }
function addTreeNode() { function addTreeNode() {
hideRMenu(); hideRMenu();
var parentNode = zTree.getSelectedNodes()[0]; var parentNode = zTree.getSelectedNodes()[0];
@@ -211,10 +212,11 @@ function removeTreeNode() {
if (!current_node){ if (!current_node){
return return
} }
if (current_node.children && current_node.children.length > 0) { if (current_node.children && current_node.children.length > 0) {
alert("{% trans 'Have child node, cancel' %}") toastr.error("{% trans 'Have child node, cancel' %}");
} else { } else if (current_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.id ); var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.id );
$.ajax({ $.ajax({
url: url, url: url,
@@ -238,7 +240,6 @@ function editTreeNode() {
zTree.editName(current_node); zTree.editName(current_node);
} }
function OnRightClick(event, treeId, treeNode) { function OnRightClick(event, treeId, treeNode) {
if (!treeNode && event.target.tagName.toLowerCase() !== "button" && $(event.target).parents("a").length === 0) { if (!treeNode && event.target.tagName.toLowerCase() !== "button" && $(event.target).parents("a").length === 0) {
zTree.cancelSelectedNode(); zTree.cancelSelectedNode();
@@ -251,13 +252,6 @@ function OnRightClick(event, treeId, treeNode) {
function showRMenu(type, x, y) { function showRMenu(type, x, y) {
$("#rMenu ul").show(); $("#rMenu ul").show();
{#if (type === "root") {#}
{# return#}
{# } else {#}
{# $("#m_del").show();#}
{# $("#m_check").show();#}
{# $("#m_unCheck").show();#}
{# }#}
x -= 220; x -= 220;
rMenu.css({"top":y+"px", "left":x+"px", "visibility":"visible"}); rMenu.css({"top":y+"px", "left":x+"px", "visibility":"visible"});
@@ -432,6 +426,11 @@ $(document).ready(function(){
.on('click', '.btn_export', function () { .on('click', '.btn_export', function () {
var $data_table = $('#asset_list_table').DataTable(); var $data_table = $('#asset_list_table').DataTable();
var rows = $data_table.rows('.selected').data(); 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 = []; var assets = [];
$.each(rows, function (index, obj) { $.each(rows, function (index, obj) {
assets.push(obj.id) assets.push(obj.id)
@@ -439,7 +438,7 @@ $(document).ready(function(){
$.ajax({ $.ajax({
url: "{% url "assets:asset-export" %}", url: "{% url "assets:asset-export" %}",
method: 'POST', method: 'POST',
data: JSON.stringify({assets_id: assets}), data: JSON.stringify({assets_id: assets, node_id: current_node.id}),
dataType: "json", dataType: "json",
success: function (data, textStatus) { success: function (data, textStatus) {
window.open(data.redirect) window.open(data.redirect)
@@ -451,6 +450,15 @@ $(document).ready(function(){
}) })
.on('click', '#btn_asset_import', function () { .on('click', '#btn_asset_import', function () {
var $form = $('#fm_asset_import'); 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.id);
{#action += "?node_id=" + current_node.id;#}
$form.attr("action", action)
}
$form.find('.help-block').remove(); $form.find('.help-block').remove();
function success (data) { function success (data) {
if (data.valid === false) { if (data.valid === false) {
@@ -489,14 +497,17 @@ $(document).ready(function(){
} }
var the_url = url.replace("{{ DEFAULT_PK }}", current_node.id); var the_url = url.replace("{{ DEFAULT_PK }}", current_node.id);
function success() { function success(data) {
rMenu.css({"visibility" : "hidden"}); rMenu.css({"visibility" : "hidden"});
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')
} }
APIUpdateAttr({ APIUpdateAttr({
url: the_url, url: the_url,
method: "GET", method: "GET",
success_message: "更新硬件信息任务下发成功", success: success,
success: success flash_message: false
}); });
}) })
@@ -511,14 +522,17 @@ $(document).ready(function(){
} }
var the_url = url.replace("{{ DEFAULT_PK }}", current_node.id); var the_url = url.replace("{{ DEFAULT_PK }}", current_node.id);
function success() { function success(data) {
rMenu.css({"visibility" : "hidden"}); rMenu.css({"visibility" : "hidden"});
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')
} }
APIUpdateAttr({ APIUpdateAttr({
url: the_url, url: the_url,
method: "GET", method: "GET",
success_message: "测试可连接性任务下发成功", success: success,
success: success flash_message: false
}); });
}) })
.on('click', '.btn_asset_delete', function () { .on('click', '.btn_asset_delete', function () {
@@ -655,7 +669,46 @@ $(document).ready(function(){
default: default:
break; break;
} }
}); $(".ipt_check_all").prop("checked", false)
})
.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 {
return
}
var data = {
'assets': assets_selected
};
var success = function () {
asset_table2.selected = [];
asset_table2.ajax.reload()
};
var url = '';
if (update_node_action === "move") {
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.id);
}
APIUpdateAttr({
'url': url,
'method': 'PUT',
'body': JSON.stringify(data),
'success': success
})
}).on('hidden.bs.modal', '#asset_list_modal', function () {
window.location.reload();
}).on('click', '#menu_asset_add', function () {
update_node_action = "add"
}).on('click', '#menu_asset_move', function () {
update_node_action = "move"
})
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -24,6 +24,7 @@
{% bootstrap_field form.port layout="horizontal" %} {% bootstrap_field form.port layout="horizontal" %}
{% bootstrap_field form.platform layout="horizontal" %} {% bootstrap_field form.platform layout="horizontal" %}
{% bootstrap_field form.public_ip layout="horizontal" %} {% bootstrap_field form.public_ip layout="horizontal" %}
{% bootstrap_field form.domain layout="horizontal" %}
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
<h3>{% trans 'Auth' %}</h3> <h3>{% trans 'Auth' %}</h3>
@@ -38,7 +39,7 @@
<div class="form-group"> <div class="form-group">
<label for="{{ form.labels.id_for_label }}" class="col-md-2 control-label">{% trans 'Label' %}</label> <label for="{{ form.labels.id_for_label }}" class="col-md-2 control-label">{% trans 'Label' %}</label>
<div class="col-md-9"> <div class="col-md-9">
<select name="labels" class="select2 labels" data-placeholder="Select labels" style="width: 100%" multiple="" tabindex="4" id="{{ form.labels.id_for_label }}"> <select name="labels" class="select2 labels" data-placeholder="{% trans 'Label' %}" style="width: 100%" multiple="" tabindex="4" id="{{ form.labels.id_for_label }}">
{% for name, labels in form.labels.field.queryset|group_labels %} {% for name, labels in form.labels.field.queryset|group_labels %}
<optgroup label="{{ name }}"> <optgroup label="{{ name }}">
{% for label in labels %} {% for label in labels %}

View File

@@ -0,0 +1,42 @@
{% extends '_base_create_update.html' %}
{% load static %}
{% load bootstrap3 %}
{% load i18n %}
{% block form %}
<form id="groupForm" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.assets layout="horizontal" %}
{% bootstrap_field form.comment layout="horizontal" %}
<div class="hr-line-dashed"></div>
<div class="form-group">
<div class="col-sm-4 col-sm-offset-2">
<button class="btn btn-default" type="reset"> {% trans 'Reset' %}</button>
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
</div>
</div>
</form>
{% include 'assets/_asset_list_modal.html' %}
{% endblock %}
{% 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();
})
.on('click', '#btn_asset_modal_confirm', function () {
var assets = asset_table2.selected;
$.each(assets, function (id, data) {
$('.select2').val(assets).trigger('change');
});
$("#asset_list_modal").modal('hide');
})
</script>
{% endblock %}

View File

@@ -0,0 +1,132 @@
{% extends 'base.html' %}
{% load static %}
{% load i18n %}
{% block custom_head_css_js %}
<link href='{% static "css/plugins/select2/select2.min.css" %}' rel="stylesheet">
<script src='{% static "js/plugins/select2/select2.full.min.js" %}'></script>
{% 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 class="active">
<a href="{% url 'assets:domain-detail' pk=object.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
</li>
<li>
<a href="{% url 'assets:domain-gateway-list' pk=object.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Gateway' %} </a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-default" href="{% url 'assets:domain-update' pk=object.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-danger btn-del">
<i class="fa fa-trash-o"></i>{% trans 'Delete' %}
</a>
</li>
</ul>
</div>
<div class="tab-content">
<div class="col-sm-9" style="padding-left: 0;">
<div class="ibox float-e-margins">
<div class="ibox-title">
<span class="label"><b>{{ object.name }}</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">
<tbody>
<tr class="no-borders-tr">
<td>{% trans 'Name' %}:</td>
<td><b>{{ object.name }}</b></td>
</tr>
<tr>
<td>{% trans 'Asset' %}:</td>
<td><b>{{ object.assets.count }}</b></td>
</tr>
<tr>
<td>{% trans 'Gateway' %}:</td>
<td><b>{{ object.gateway_set.count }}</b></td>
</tr>
<tr>
<td>{% trans 'Date created' %}:</td>
<td><b>{{ object.date_created }}</b></td>
</tr>
<tr>
<td>{% trans 'Created by' %}:</td>
<td><b>{{ object.created_by }}</b></td>
</tr>
<tr>
<td>{% trans 'Comment' %}:</td>
<td><b>{{ object.comment }}</b></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %}
<script>
function initTable() {
var options = {
ele: $('#domain_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
var detail_btn = '<a href="{% url "assets:domain-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},
{targets: 5, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "assets:domain-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);
$(td).html(update_btn + del_btn)
}}
],
ajax_url: '{% url "api-assets:domain-list" %}',
columns: [
{data: "id"}, {data: "name" }, {data: "asset_count" },
{data: "gateway_count" }, {data: "comment" }, {data: "id"}
],
op_html: $('#actions').html()
};
jumpserver.initDataTable(options);
}
$(document).ready(function(){
initTable();
})
.on('click', '.btn-delete', function () {
var $this = $(this);
var $data_table = $('#domain_list_table').DataTable();
var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
var uid = $this.data('uid');
var the_url = '{% url "api-assets:domain-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
objectDelete($this, name, the_url);
setTimeout( function () {
$data_table.ajax.reload();
}, 3000);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,126 @@
{% extends 'base.html' %}
{% load static %}
{% load i18n %}
{% block custom_head_css_js %}
<link href='{% static "css/plugins/select2/select2.min.css" %}' rel="stylesheet">
<script src='{% static "js/plugins/select2/select2.full.min.js" %}'></script>
{% 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: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>
</li>
</ul>
</div>
<div class="tab-content">
<div class="col-sm-12" style="padding-left: 0;">
<div class="" id="content_start">
</div>
<div class="ibox float-e-margins">
<div class="ibox-title">
<span style="float: left"><b>{% trans 'Gateway list' %}</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">
<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>
</div>
<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" >
</th>
<th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% trans 'IP' %}</th>
<th class="text-center">{% trans 'Port' %}</th>
<th class="text-center">{% trans 'Protocol' %}</th>
<th class="text-center">{% trans 'Username' %}</th>
<th class="text-center">{% trans 'Comment' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %}
<script>
function initTable() {
var options = {
ele: $('#domain_list_table'),
columnDefs: [
{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);
$(td).html(update_btn + test_btn + del_btn)
}}
],
ajax_url: '{% url "api-assets:gateway-list" %}?domain={{ object.id }}',
columns: [
{data: "id"}, {data: "name" }, {data: 'ip'}, {data: 'port'},
{data: "protocol"}, {data: "username" }, {data: "comment" }, {data: "id"}
],
op_html: $('#actions').html()
};
jumpserver.initDataTable(options);
}
$(document).ready(function(){
initTable();
})
.on('click', '.btn-delete', function () {
var $this = $(this);
var $data_table = $('#domain_list_table').DataTable();
var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
var uid = $this.data('uid');
var the_url = '{% url "api-assets:gateway-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
objectDelete($this, name, the_url);
setTimeout( function () {
$data_table.ajax.reload();
}, 3000);
}).on('click', '.btn-test', function () {
var $this = $(this);
var uid = $this.data('uid');
var the_url = '{% url "api-assets:test-gateway-connective" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
APIUpdateAttr({
url: the_url,
method: "GET",
success_message: "可连接",
fail_message: "连接失败"
})
})
</script>
{% endblock %}

View File

@@ -0,0 +1,74 @@
{% extends '_base_list.html' %}
{% load i18n static %}
{% block table_search %}{% endblock %}
{% block table_container %}
<div class="uc pull-left m-r-5">
<a href="{% url 'assets:domain-create' %}" class="btn btn-sm btn-primary"> {% trans "Create domain" %} </a>
</div>
<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" >
</th>
<th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% trans 'Asset' %}</th>
<th class="text-center">{% trans 'Gateway' %}</th>
<th class="text-center">{% trans 'Comment' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
{% endblock %}
{% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %}
<script>
function initTable() {
var options = {
ele: $('#domain_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
var detail_btn = '<a href="{% url "assets:domain-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},
{targets: 3, createdCell: function (td, cellData, rowData) {
var gateway_list_btn = '<a href="{% url "assets:domain-gateway-list" pk=DEFAULT_PK %}">' + cellData + '</a>';
gateway_list_btn = gateway_list_btn.replace("{{ DEFAULT_PK }}", rowData.id);
$(td).html(gateway_list_btn);
}},
{targets: 5, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "assets:domain-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);
$(td).html(update_btn + del_btn)
}}
],
ajax_url: '{% url "api-assets:domain-list" %}',
columns: [
{data: "id"}, {data: "name" }, {data: "asset_count" },
{data: "gateway_count" }, {data: "comment" }, {data: "id"}
],
op_html: $('#actions').html()
};
jumpserver.initDataTable(options);
}
$(document).ready(function(){
initTable();
})
.on('click', '.btn-delete', function () {
var $this = $(this);
var $data_table = $('#domain_list_table').DataTable();
var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
var uid = $this.data('uid');
var the_url = '{% url "api-assets:domain-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
objectDelete($this, name, the_url);
setTimeout( function () {
$data_table.ajax.reload();
}, 3000);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,68 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% load bootstrap3 %}
{% block custom_head_css_js %}
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
{% 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="ibox-title">
<h5>{{ action }}</h5>
<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>
<a class="close-link">
<i class="fa fa-times"></i>
</a>
</div>
</div>
<div class="ibox-content">
<form enctype="multipart/form-data" method="post" class="form-horizontal" action="" >
{% csrf_token %}
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}
<h3>{% trans 'Basic' %}</h3>
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.ip layout="horizontal" %}
{% bootstrap_field form.port layout="horizontal" %}
{% bootstrap_field form.protocol layout="horizontal" %}
{% bootstrap_field form.domain layout="horizontal" %}
{% block auth %}
<h3>{% trans 'Auth' %}</h3>
<div class="auth-fields">
{% bootstrap_field form.username layout="horizontal" %}
{% bootstrap_field form.password layout="horizontal" %}
{% bootstrap_field form.private_key_file layout="horizontal" %}
</div>
{% endblock %}
<h3>{% trans 'Other' %}</h3>
{% bootstrap_field form.is_active layout="horizontal" %}
{% bootstrap_field form.comment layout="horizontal" %}
<div class="form-group">
<div class="col-sm-4 col-sm-offset-2">
<button class="btn btn-white" type="reset">{% trans 'Reset' %}</button>
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -3,6 +3,8 @@
{% load bootstrap3 %} {% load bootstrap3 %}
{% load i18n %} {% load i18n %}
{% block form %} {% block form %}
<form id="groupForm" method="post" class="form-horizontal"> <form id="groupForm" method="post" class="form-horizontal">
{% csrf_token %} {% csrf_token %}
@@ -18,14 +20,28 @@
</div> </div>
</div> </div>
</form> </form>
{% include 'assets/_asset_list_modal.html' %}
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function () { $(document).ready(function () {
$('.select2').select2({ $('.select2').select2({
closeOnSelect: false closeOnSelect: false
}); })
}).on('click', '.select2-selection__rendered', function (e) {
e.preventDefault();
$("#asset_list_modal").modal();
})
.on('click', '#btn_asset_modal_confirm', function () {
var assets = asset_table2.selected;
$('.select2 option:selected').each(function (i, data) {
assets.push($(data).attr('value'))
}); });
$.each(assets, function (id, data) {
$('.select2').val(assets).trigger('change');
});
$("#asset_list_modal").modal('hide');
})
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -173,7 +173,7 @@
<td colspan="2" class="no-borders"> <td colspan="2" class="no-borders">
<select data-placeholder="{% trans 'Add to node' %}" id="node_selected" class="select2" style="width: 100%" multiple="" tabindex="4"> <select data-placeholder="{% trans 'Add to node' %}" id="node_selected" class="select2" style="width: 100%" multiple="" tabindex="4">
{% for node in nodes_remain %} {% for node in nodes_remain %}
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node.name }}</option> <option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node }}</option>
{% endfor %} {% endfor %}
</select> </select>
</td> </td>
@@ -187,7 +187,7 @@
{% for node in system_user.nodes.all %} {% for node in system_user.nodes.all %}
<tr> <tr>
<td ><b class="bdg_node" data-gid={{ node.id }}>{{ node.name }}</b></td> <td ><b class="bdg_node" data-gid={{ node.id }}>{{ node }}</b></td>
<td> <td>
<button class="btn btn-danger pull-right btn-xs btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button> <button class="btn btn-danger pull-right btn-xs btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button>
</td> </td>
@@ -206,7 +206,7 @@
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
function updateSystemUserCluster(nodes) { function updateSystemUserNode(nodes) {
var the_url = "{% url 'api-assets:system-user-detail' pk=system_user.id %}"; var the_url = "{% url 'api-assets:system-user-detail' pk=system_user.id %}";
var body = { var body = {
nodes: Object.assign([], nodes) nodes: Object.assign([], nodes)
@@ -267,7 +267,7 @@ $(document).ready(function () {
$.map(jumpserver.nodes_selected, function(value, index) { $.map(jumpserver.nodes_selected, function(value, index) {
nodes.push(index); nodes.push(index);
}); });
updateSystemUserCluster(nodes); updateSystemUserNode(nodes);
}) })
.on('click', '.btn-remove-from-node', function() { .on('click', '.btn-remove-from-node', function() {
var $this = $(this); var $this = $(this);
@@ -282,7 +282,7 @@ $(document).ready(function () {
var nodes = $('.bdg_node').map(function () { var nodes = $('.bdg_node').map(function () {
return $(this).data('gid'); return $(this).data('gid');
}).get(); }).get();
updateSystemUserCluster(nodes); updateSystemUserNode(nodes);
}).on('click', '.btn-del', function () { }).on('click', '.btn-del', function () {
var $this = $(this); var $this = $(this);
var name = "{{ system_user.name}}"; var name = "{{ system_user.name}}";
@@ -293,26 +293,30 @@ $(document).ready(function () {
}) })
.on('click', '.btn-push', function () { .on('click', '.btn-push', function () {
var the_url = "{% url 'api-assets:system-user-push' pk=system_user.id %}"; var the_url = "{% url 'api-assets:system-user-push' pk=system_user.id %}";
var error = function (data) { var success = function (data) {
alert(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')
}; };
APIUpdateAttr({ APIUpdateAttr({
url: the_url, url: the_url,
error: error,
method: 'GET', method: 'GET',
success_message: "{% trans "Task has been send, Go to ops task list seen result" %}" success: success,
flash_message: false
}); });
}) })
.on('click', '.btn-test-connective', function () { .on('click', '.btn-test-connective', function () {
var the_url = "{% url 'api-assets:system-user-connective' pk=system_user.id %}"; var the_url = "{% url 'api-assets:system-user-connective' pk=system_user.id %}";
var error = function (data) { var success = function (data) {
alert(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')
}; };
APIUpdateAttr({ APIUpdateAttr({
url: the_url, url: the_url,
error: error,
method: 'GET', method: 'GET',
success_message: "{% trans "Task has been send, seen left assets status" %}" success: success,
flash_message: false
}); });
}) })
</script> </script>

View File

@@ -25,6 +25,7 @@
</th> </th>
<th class="text-center">{% trans 'Name' %}</th> <th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% trans 'Username' %}</th> <th class="text-center">{% trans 'Username' %}</th>
<th class="text-center">{% trans 'Protocol' %}</th>
<th class="text-center">{% trans 'Asset' %}</th> <th class="text-center">{% trans 'Asset' %}</th>
<th class="text-center">{% trans 'Reachable' %}</th> <th class="text-center">{% trans 'Reachable' %}</th>
<th class="text-center">{% trans 'Unreachable' %}</th> <th class="text-center">{% trans 'Unreachable' %}</th>
@@ -47,7 +48,7 @@ function initTable() {
var detail_btn = '<a href="{% url "assets:system-user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>'; var detail_btn = '<a href="{% url "assets:system-user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id)); $(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}}, }},
{targets: 4, createdCell: function (td, cellData) { {targets: 5, createdCell: function (td, cellData) {
var innerHtml = ""; var innerHtml = "";
if (cellData !== 0) { if (cellData !== 0) {
innerHtml = "<span class='text-navy'>" + cellData + "</span>"; innerHtml = "<span class='text-navy'>" + cellData + "</span>";
@@ -56,7 +57,7 @@ function initTable() {
} }
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData +'">' + innerHtml + '</span>'); $(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData +'">' + innerHtml + '</span>');
}}, }},
{targets: 5, createdCell: function (td, cellData) { {targets: 6, createdCell: function (td, cellData) {
var innerHtml = ""; var innerHtml = "";
if (cellData !== 0) { if (cellData !== 0) {
innerHtml = "<span class='text-danger'>" + cellData + "</span>"; innerHtml = "<span class='text-danger'>" + cellData + "</span>";
@@ -65,7 +66,7 @@ function initTable() {
} }
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>'); $(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
}}, }},
{targets: 6, createdCell: function (td, cellData, rowData) { {targets: 7, createdCell: function (td, cellData, rowData) {
var val = 0; var val = 0;
var innerHtml = ""; var innerHtml = "";
var total = rowData.assets_amount; var total = rowData.assets_amount;
@@ -83,14 +84,14 @@ function initTable() {
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>'); $(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
}}, }},
{targets: 8, createdCell: function (td, cellData, rowData) { {targets: 9, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "assets:system-user-update" pk=DEFAULT_PK %}" class="btn btn-xs m-l-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData); var update_btn = '<a href="{% url "assets:system-user-update" pk=DEFAULT_PK %}" class="btn btn-xs m-l-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_admin_user_delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData); var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_admin_user_delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
$(td).html(update_btn + del_btn) $(td).html(update_btn + del_btn)
}}], }}],
ajax_url: '{% url "api-assets:system-user-list" %}', ajax_url: '{% url "api-assets:system-user-list" %}',
columns: [ columns: [
{data: "id" }, {data: "name" }, {data: "username" }, {data: "assets_amount" }, {data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "assets_amount" },
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" } {data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" }
], ],
op_html: $('#actions').html() op_html: $('#actions').html()

View File

@@ -1,81 +1,171 @@
{% extends '_base_list.html' %} {% extends 'base.html' %}
{% load i18n %}
{% load static %} {% load static %}
{% load i18n %}
{% block custom_head_css_js %} {% block custom_head_css_js %}
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet"> <link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script> <script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>
<script src="{% static 'js/jquery.form.min.js' %}"></script>
{% endblock %}
{% block content_left_head %}{% endblock %}
{% block table_search %}
{% endblock %} {% endblock %}
{% block table_container %} {% block content %}
<table class="table table-striped table-bordered table-hover " id="asset_list_table" > <div class="wrapper wrapper-content">
<thead> <div class="row">
<tr> <div class="col-lg-3" id="split-left" style="padding-left: 3px">
<th class="text-center"><input type="checkbox" class="ipt_check_all"></th> <div class="ibox float-e-margins">
<th class="text-center">{% trans 'Hostname' %}</th> <div class="ibox-content mailbox-content" style="padding-top: 0;padding-left: 1px">
<th class="text-center">{% trans 'IP' %}</th> <div class="file-manager ">
<th class="text-center">{% trans 'Port' %}</th> <div id="assetTree" class="ztree">
<th class="text-center">{% trans 'Hardware' %}</th> </div>
<th class="text-center">{% trans 'Active' %}</th>
<th class="text-center">{% trans 'Connective' %}</th> <div class="clearfix"></div>
</tr> </div>
</thead> </div>
<tbody> </div>
</tbody> </div>
</table> <div class="col-lg-9 animated fadeInRight" id="split-right">
<div class="tree-toggle">
<div class="btn btn-sm btn-primary tree-toggle-btn" onclick="toggle()">
<i class="fa fa-angle-left fa-x" id="toggle-icon"></i>
</div>
</div>
<div class="mail-box-header">
<div class="btn-group" style="float: right">
<button data-toggle="dropdown" class="btn btn-default btn-sm dropdown-toggle">{% trans 'Label' %} <span class="caret"></span></button>
<ul class="dropdown-menu labels">
{% for label in labels %}
<li><a style="font-weight: bolder">{{ label.name }}:{{ label.value }}</a></li>
{% endfor %}
</ul>
</div>
<table class="table table-striped table-bordered table-hover " id="user_assets_table" style="width: 100%">
<thead>
<tr>
<th class="text-center"><input type="checkbox" class="ipt_check_all"></th>
<th class="text-center">{% trans 'Hostname' %}</th>
<th class="text-center">{% trans 'IP' %}</th>
<th class="text-center">{% trans 'Active' %}</th>
<th class="text-center">{% trans 'System users' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script src="{% static 'js/jquery.form.min.js' %}"></script> <script>
<script type="text/javascript"> var zTree, rMenu, asset_table;
var inited = false;
var url;
function initTable() { function initTable() {
if (inited){
return
} else {
inited = true;
}
var options = { var options = {
ele: $('#asset_list_table'), ele: $('#user_assets_table'),
columnDefs: [ columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) { {targets: 1, createdCell: function (td, cellData, rowData) {
{% url 'assets:asset-detail' pk=DEFAULT_PK as the_url %} {% url 'assets:asset-detail' pk=DEFAULT_PK as the_url %}
var detail_btn = '<a href="{{ the_url }}">' + cellData + '</a>'; var detail_btn = '<a href="{{ the_url }}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id)); $(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}}, }},
{targets: 5, createdCell: function (td, cellData) { {targets: 3, createdCell: function (td, cellData) {
if (!cellData) { if (!cellData) {
$(td).html('<i class="fa fa-times text-danger"></i>') $(td).html('<i class="fa fa-times text-danger"></i>')
} else { } else {
$(td).html('<i class="fa fa-check text-navy"></i>') $(td).html('<i class="fa fa-check text-navy"></i>')
} }
}}, }},
{targets: 6, createdCell: function (td, cellData) { {targets: 4, createdCell: function (td, cellData) {
if (cellData == 'Unknown'){ var users = [];
$(td).html('<i class="fa fa-circle text-warning"></i>') $.each(cellData, function (id, data) {
} else if (!cellData) { users.push(data.name);
$(td).html('<i class="fa fa-circle text-danger"></i>') });
} else { $(td).html(users.join(', '))
$(td).html('<i class="fa fa-circle text-navy"></i>') }}
}
}},
{# {targets: 9, createdCell: function (td, cellData, rowData) {#}
{# var conn_btn = '<a href="{% url "terminal:web-terminal" %}?id={{ DEFAULT_PK }}" class="btn btn-xs btn-info">{% trans "Connect" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);#}
{# $(td).html(conn_btn)#}
{# }}#}
], ],
ajax_url: '{% url "api-assets:user-asset-list" %}', ajax_url: url,
columns: [ columns: [
{data: "id"}, {data: "hostname" }, {data: "ip" }, {data: "port" }, {data: "id"}, {data: "hostname" }, {data: "ip" },
{data: "hardware_info"}, {data: "is_active" }, {data: "is_connective"} {data: "is_active", orderable: false },
], {data: "system_users_granted", orderable: false}
op_html: $('#actions').html() ]
}; };
return jumpserver.initDataTable(options); asset_table = jumpserver.initDataTable(options);
return asset_table
} }
$(document).ready(function(){ function onSelected(event, treeNode) {
console.log("select");
url = '{% url "api-perms:my-node-assets" node_id=DEFAULT_PK %}';
url = url.replace("{{ DEFAULT_PK }}", treeNode.id);
initTable(); initTable();
}); setCookie('node_selected', treeNode.id);
asset_table.ajax.url(url);
asset_table.ajax.reload();
}
function selectQueryNode() {
var query_node_id = $.getUrlParam("node");
var cookie_node_id = getCookie('node_selected');
var node;
var node_id;
if (query_node_id !== null) {
node_id = query_node_id
} else if (cookie_node_id !== null) {
node_id = cookie_node_id;
}
node = zTree.getNodesByParam("id", node_id, null);
if (node){
zTree.selectNode(node[0]);
}
}
function initTree() {
var setting = {
view: {
dblClickExpand: false,
showLine: true
},
data: {
simpleData: {
enable: true
}
},
callback: {
onSelected: onSelected
}
};
var zNodes = [];
$.get("{% url 'api-perms:my-nodes' %}", function(data, status){
$.each(data, function (index, value) {
value["pId"] = value["parent"];
if (value["key"] === "0") {
value["open"] = true;
}
value["name"] = value["value"]
});
zNodes = data;
$.fn.zTree.init($("#assetTree"), setting, zNodes);
zTree = $.fn.zTree.getZTreeObj("assetTree");
rMenu = $("#rMenu");
selectQueryNode();
});
}
$(document).ready(function () {
initTree();
});
</script> </script>
{% endblock %}
{% endblock %}

View File

@@ -7,13 +7,13 @@ app_name = 'assets'
router = BulkRouter() router = BulkRouter()
# router.register(r'v1/groups', api.AssetGroupViewSet, 'asset-group')
router.register(r'v1/assets', api.AssetViewSet, 'asset') router.register(r'v1/assets', api.AssetViewSet, 'asset')
# router.register(r'v1/clusters', api.ClusterViewSet, 'cluster')
router.register(r'v1/admin-user', api.AdminUserViewSet, 'admin-user') router.register(r'v1/admin-user', api.AdminUserViewSet, 'admin-user')
router.register(r'v1/system-user', api.SystemUserViewSet, 'system-user') router.register(r'v1/system-user', api.SystemUserViewSet, 'system-user')
router.register(r'v1/labels', api.LabelViewSet, 'label') router.register(r'v1/labels', api.LabelViewSet, 'label')
router.register(r'v1/nodes', api.NodeViewSet, 'node') router.register(r'v1/nodes', api.NodeViewSet, 'node')
router.register(r'v1/domain', api.DomainViewSet, 'domain')
router.register(r'v1/gateway', api.GatewayViewSet, 'gateway')
urlpatterns = [ urlpatterns = [
url(r'^v1/assets-bulk/$', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'), url(r'^v1/assets-bulk/$', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'),
@@ -25,18 +25,10 @@ urlpatterns = [
api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'), api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'),
url(r'^v1/assets/user-assets/$', url(r'^v1/assets/user-assets/$',
api.UserAssetListView.as_view(), name='user-asset-list'), api.UserAssetListView.as_view(), name='user-asset-list'),
# update the asset group, which add or delete the asset to the group
#url(r'^v1/groups/(?P<pk>[0-9a-zA-Z\-]{36})/assets/$',
# api.GroupUpdateAssetsApi.as_view(), name='group-update-assets'),
#url(r'^v1/groups/(?P<pk>[0-9a-zA-Z\-]{36})/assets/add/$',
# api.GroupAddAssetsApi.as_view(), name='group-add-assets'),
# update the Cluster, and add or delete the assets to the Cluster
#url(r'^v1/cluster/(?P<pk>[0-9a-zA-Z\-]{36})/assets/$',
# api.ClusterAddAssetsApi.as_view(), name='cluster-add-assets'),
#url(r'^v1/cluster/(?P<pk>[0-9a-zA-Z\-]{36})/assets/connective/$',
# api.ClusterTestAssetsAliveApi.as_view(), name='cluster-test-connective'),
url(r'^v1/admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/nodes/$', url(r'^v1/admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/nodes/$',
api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'), api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'),
url(r'^v1/admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/auth/$',
api.AdminUserAuthApi.as_view(), name='admin-user-auth'),
url(r'^v1/admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/connective/$', url(r'^v1/admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/connective/$',
api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'), api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'),
url(r'^v1/system-user/(?P<pk>[0-9a-zA-Z\-]{36})/push/$', url(r'^v1/system-user/(?P<pk>[0-9a-zA-Z\-]{36})/push/$',
@@ -44,11 +36,16 @@ urlpatterns = [
url(r'^v1/system-user/(?P<pk>[0-9a-zA-Z\-]{36})/connective/$', url(r'^v1/system-user/(?P<pk>[0-9a-zA-Z\-]{36})/connective/$',
api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'), api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'),
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/children/$', api.NodeChildrenApi.as_view(), name='node-children'), url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/children/$', api.NodeChildrenApi.as_view(), name='node-children'),
url(r'^v1/nodes/children/$', api.NodeChildrenApi.as_view(), name='node-children-2'),
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/children/add/$', api.NodeAddChildrenApi.as_view(), name='node-add-children'), url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/children/add/$', api.NodeAddChildrenApi.as_view(), name='node-add-children'),
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/$', api.NodeAssetsApi.as_view(), name='node-assets'),
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/add/$', api.NodeAddAssetsApi.as_view(), name='node-add-assets'), url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/add/$', api.NodeAddAssetsApi.as_view(), name='node-add-assets'),
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/replace/$', api.NodeReplaceAssetsApi.as_view(), name='node-replace-assets'),
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/remove/$', api.NodeRemoveAssetsApi.as_view(), name='node-remove-assets'), url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/remove/$', api.NodeRemoveAssetsApi.as_view(), name='node-remove-assets'),
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/refresh-hardware-info/$', api.RefreshNodeHardwareInfoApi.as_view(), name='node-refresh-hardware-info'), url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/refresh-hardware-info/$', api.RefreshNodeHardwareInfoApi.as_view(), name='node-refresh-hardware-info'),
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/test-connective/$', api.TestNodeConnectiveApi.as_view(), name='node-test-connective'), url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/test-connective/$', api.TestNodeConnectiveApi.as_view(), name='node-test-connective'),
url(r'^v1/gateway/(?P<pk>[0-9a-zA-Z\-]{36})/test-connective/$', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
] ]
urlpatterns += router.urls urlpatterns += router.urls

View File

@@ -39,5 +39,15 @@ urlpatterns = [
url(r'^label/create/$', views.LabelCreateView.as_view(), name='label-create'), url(r'^label/create/$', views.LabelCreateView.as_view(), name='label-create'),
url(r'^label/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.LabelUpdateView.as_view(), name='label-update'), url(r'^label/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.LabelUpdateView.as_view(), name='label-update'),
url(r'^label/(?P<pk>[0-9a-zA-Z\-]{36})/delete/$', views.LabelDeleteView.as_view(), name='label-delete'), url(r'^label/(?P<pk>[0-9a-zA-Z\-]{36})/delete/$', views.LabelDeleteView.as_view(), name='label-delete'),
url(r'^domain/$', views.DomainListView.as_view(), name='domain-list'),
url(r'^domain/create/$', views.DomainCreateView.as_view(), name='domain-create'),
url(r'^domain/(?P<pk>[0-9a-zA-Z\-]{36})/$', views.DomainDetailView.as_view(), name='domain-detail'),
url(r'^domain/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.DomainUpdateView.as_view(), name='domain-update'),
url(r'^domain/(?P<pk>[0-9a-zA-Z\-]{36})/delete/$', views.DomainDeleteView.as_view(), name='domain-delete'),
url(r'^domain/(?P<pk>[0-9a-zA-Z\-]{36})/gateway/$', views.DomainGatewayListView.as_view(), name='domain-gateway-list'),
url(r'^domain/(?P<pk>[0-9a-zA-Z\-]{36})/gateway/create/$', views.DomainGatewayCreateView.as_view(), name='domain-gateway-create'),
url(r'^domain/gateway/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.DomainGatewayUpdateView.as_view(), name='domain-gateway-update'),
] ]

View File

@@ -1,10 +1,7 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
# #
from collections import defaultdict
from functools import reduce
import operator
from django.db.models import Q import paramiko
from common.utils import get_object_or_none from common.utils import get_object_or_none
from .models import Asset, SystemUser, Label from .models import Asset, SystemUser, Label
@@ -44,5 +41,41 @@ class LabelFilter:
return queryset return queryset
def test_gateway_connectability(gateway):
"""
Test system cant connect his assets or not.
:param gateway:
:return:
"""
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
proxy_command = [
"ssh", "{}@{}".format(gateway.username, gateway.ip),
"-p", str(gateway.port), "-W", "127.0.0.1:{}".format(gateway.port),
]
if gateway.password:
proxy_command.insert(0, "sshpass -p '{}'".format(gateway.password))
if gateway.private_key:
proxy_command.append("-i {}".format(gateway.private_key_file))
try:
sock = paramiko.ProxyCommand(" ".join(proxy_command))
except paramiko.ProxyCommandFailure as e:
return False, str(e)
try:
client.connect("127.0.0.1", port=gateway.port,
username=gateway.username,
password=gateway.password,
key_filename=gateway.private_key_file,
sock=sock,
timeout=5
)
except (paramiko.SSHException, paramiko.ssh_exception.SSHException,
paramiko.AuthenticationException, TimeoutError) as e:
return False, str(e)
finally:
client.close()
return True, None

View File

@@ -3,3 +3,4 @@ from .asset import *
from .system_user import * from .system_user import *
from .admin_user import * from .admin_user import *
from .label import * from .label import *
from .domain import *

View File

@@ -9,6 +9,7 @@ import chardet
from io import StringIO from io import StringIO
from django.conf import settings from django.conf import settings
from django.db import transaction
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView, ListView, View from django.views.generic import TemplateView, ListView, View
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
@@ -27,7 +28,7 @@ from common.mixins import JSONResponseMixin
from common.utils import get_object_or_none, get_logger, is_uuid from common.utils import get_object_or_none, get_logger, is_uuid
from common.const import create_success_msg, update_success_msg from common.const import create_success_msg, update_success_msg
from .. import forms from .. import forms
from ..models import Asset, AssetGroup, AdminUser, Cluster, SystemUser, Label, Node from ..models import Asset, AdminUser, SystemUser, Label, Node, Domain
from ..hands import AdminUserRequiredMixin from ..hands import AdminUserRequiredMixin
@@ -48,6 +49,7 @@ class AssetListView(AdminUserRequiredMixin, TemplateView):
'app': _('Assets'), 'app': _('Assets'),
'action': _('Asset list'), 'action': _('Asset list'),
'labels': Label.objects.all().order_by('name'), 'labels': Label.objects.all().order_by('name'),
'nodes': Node.objects.all().order_by('-key'),
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
@@ -232,8 +234,16 @@ class AssetExportView(View):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
try: try:
assets_id = json.loads(request.body).get('assets_id', []) assets_id = json.loads(request.body).get('assets_id', [])
assets_node_id = json.loads(request.body).get('node_id', None)
except ValueError: except ValueError:
return HttpResponse('Json object not valid', status=400) return HttpResponse('Json object not valid', status=400)
if not assets_id and assets_node_id:
assets_node = get_object_or_none(Node, id=assets_node_id)
assets = assets_node.get_all_assets()
for asset in assets:
assets_id.append(asset.id)
spm = uuid.uuid4().hex spm = uuid.uuid4().hex
cache.set(spm, assets_id, 300) cache.set(spm, assets_id, 300)
url = reverse_lazy('assets:asset-export') + '?spm=%s' % spm url = reverse_lazy('assets:asset-export') + '?spm=%s' % spm
@@ -244,6 +254,8 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView):
form_class = forms.FileForm form_class = forms.FileForm
def form_valid(self, form): def form_valid(self, form):
node_id = self.request.GET.get("node_id")
node = get_object_or_none(Node, id=node_id) if node_id else Node.root()
f = form.cleaned_data['file'] f = form.cleaned_data['file']
det_result = chardet.detect(f.read()) det_result = chardet.detect(f.read())
f.seek(0) # reset file seek index f.seek(0) # reset file seek index
@@ -273,35 +285,44 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView):
if set(row) == {''}: if set(row) == {''}:
continue continue
asset_dict = dict(zip(attr, row)) asset_dict_raw = dict(zip(attr, row))
id_ = asset_dict.pop('id', 0) asset_dict = dict()
for k, v in asset_dict.items(): for k, v in asset_dict_raw.items():
v = v.strip()
if k == 'is_active': if k == 'is_active':
v = True if v in ['TRUE', 1, 'true'] else False v = False if v in ['False', 0, 'false'] else True
elif k == 'admin_user': elif k == 'admin_user':
v = get_object_or_none(AdminUser, name=v) v = get_object_or_none(AdminUser, name=v)
elif k in ['port', 'cpu_count', 'cpu_cores']: elif k in ['port', 'cpu_count', 'cpu_cores']:
try: try:
v = int(v) v = int(v)
except ValueError: except ValueError:
v = 0 v = ''
else: elif k == 'domain':
continue v = get_object_or_none(Domain, name=v)
asset_dict[k] = v
asset = get_object_or_none(Asset, id=id_) if is_uuid(id_) else None if v != '':
asset_dict[k] = v
asset = None
asset_id = asset_dict.pop('id', None)
if asset_id:
asset = get_object_or_none(Asset, id=asset_id)
if not asset: if not asset:
try: try:
if len(Asset.objects.filter(hostname=asset_dict.get('hostname'))): if len(Asset.objects.filter(hostname=asset_dict.get('hostname'))):
raise Exception(_('already exists')) raise Exception(_('already exists'))
asset = Asset.objects.create(**asset_dict) with transaction.atomic():
created.append(asset_dict['hostname']) asset = Asset.objects.create(**asset_dict)
assets.append(asset) if node:
asset.nodes.set([node])
created.append(asset_dict['hostname'])
assets.append(asset)
except Exception as e: except Exception as e:
failed.append('%s: %s' % (asset_dict['hostname'], str(e))) failed.append('%s: %s' % (asset_dict['hostname'], str(e)))
else: else:
for k, v in asset_dict.items(): for k, v in asset_dict.items():
if v: if v != '':
setattr(asset, k, v) setattr(asset, k, v)
try: try:
asset.save() asset.save()

154
apps/assets/views/domain.py Normal file
View File

@@ -0,0 +1,154 @@
# -*- coding: utf-8 -*-
#
from django.views.generic import TemplateView, CreateView, \
UpdateView, DeleteView, DetailView
from django.views.generic.detail import SingleObjectMixin
from django.utils.translation import ugettext_lazy as _
from django.urls import reverse_lazy, reverse
from common.mixins import AdminUserRequiredMixin
from common.const import create_success_msg, update_success_msg
from common.utils import get_object_or_none
from ..models import Domain, Gateway
from ..forms import DomainForm, GatewayForm
__all__ = (
"DomainListView", "DomainCreateView", "DomainUpdateView",
"DomainDetailView", "DomainDeleteView", "DomainGatewayListView",
"DomainGatewayCreateView", 'DomainGatewayUpdateView',
)
class DomainListView(AdminUserRequiredMixin, TemplateView):
template_name = 'assets/domain_list.html'
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('Domain list'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class DomainCreateView(AdminUserRequiredMixin, CreateView):
model = Domain
template_name = 'assets/domain_create_update.html'
form_class = DomainForm
success_url = reverse_lazy('assets:domain-list')
success_message = create_success_msg
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('Create domain'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class DomainUpdateView(AdminUserRequiredMixin, UpdateView):
model = Domain
template_name = 'assets/domain_create_update.html'
form_class = DomainForm
success_url = reverse_lazy('assets:domain-list')
success_message = update_success_msg
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('Update domain'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class DomainDetailView(AdminUserRequiredMixin, DetailView):
model = Domain
template_name = 'assets/domain_detail.html'
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('Domain detail'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class DomainDeleteView(AdminUserRequiredMixin, DeleteView):
model = Domain
template_name = 'delete_confirm.html'
success_url = reverse_lazy('assets:domain-list')
class DomainGatewayListView(AdminUserRequiredMixin, SingleObjectMixin, TemplateView):
template_name = 'assets/domain_gateway_list.html'
model = Domain
object = None
def get(self, request, *args, **kwargs):
self.object = self.get_object(queryset=self.model.objects.all())
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('Domain gateway list'),
'object': self.get_object()
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class DomainGatewayCreateView(AdminUserRequiredMixin, CreateView):
model = Gateway
template_name = 'assets/gateway_create_update.html'
form_class = GatewayForm
success_message = create_success_msg
def get_success_url(self):
domain = self.object.domain
return reverse('assets:domain-gateway-list', kwargs={"pk": domain.id})
def get_form(self, form_class=None):
form = super().get_form(form_class=form_class)
domain_id = self.kwargs.get("pk")
domain = get_object_or_none(Domain, id=domain_id)
if domain:
form['domain'].initial = domain
return form
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('Create gateway'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class DomainGatewayUpdateView(AdminUserRequiredMixin, UpdateView):
model = Gateway
template_name = 'assets/gateway_create_update.html'
form_class = GatewayForm
success_message = update_success_msg
def get_success_url(self):
domain = self.object.domain
return reverse('assets:domain-gateway-list', kwargs={"pk": domain.id})
def form_valid(self, form):
response = super().form_valid(form)
print(form.cleaned_data)
return response
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('Update gateway'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)

0
apps/audits/__init__.py Normal file
View File

3
apps/audits/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

14
apps/audits/api.py Normal file
View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
#
from rest_framework import viewsets
from common.permissions import IsSuperUserOrAppUser
from .models import FTPLog
from .serializers import FTPLogSerializer
class FTPLogViewSet(viewsets.ModelViewSet):
queryset = FTPLog.objects.all()
serializer_class = FTPLogSerializer
permission_classes = (IsSuperUserOrAppUser,)

5
apps/audits/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class AuditsConfig(AppConfig):
name = 'audits'

View File

16
apps/audits/models.py Normal file
View File

@@ -0,0 +1,16 @@
import uuid
from django.db import models
from django.utils.translation import ugettext_lazy as _
class FTPLog(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
user = models.CharField(max_length=128, verbose_name=_('User'))
remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True)
asset = models.CharField(max_length=1024, verbose_name=_("Asset"))
system_user = models.CharField(max_length=128, verbose_name=_("System user"))
operate = models.CharField(max_length=16, verbose_name=_("Operate"))
filename = models.CharField(max_length=1024, verbose_name=_("Filename"))
is_success = models.BooleanField(default=True, verbose_name=_("Success"))
date_start = models.DateTimeField(auto_now_add=True)

View File

@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from .models import FTPLog
class FTPLogSerializer(serializers.ModelSerializer):
class Meta:
model = FTPLog
fields = '__all__'

View File

@@ -0,0 +1,135 @@
{% extends '_base_list.html' %}
{% load i18n %}
{% load static %}
{% load terminal_tags %}
{% load common_tags %}
{% block custom_head_css_js %}
<link href="{% static 'css/plugins/datepicker/datepicker3.css' %}" rel="stylesheet">
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
<style>
#search_btn {
margin-bottom: 0;
}
</style>
{% endblock %}
{% block content_left_head %}
{% endblock %}
{% block table_search %}
<form id="search_form" method="get" action="" class="pull-right form-inline">
<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' }}">
<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' }}">
</div>
</div>
<div class="input-group">
<select class="select2 form-control" name="user">
<option value="">{% trans 'User' %}</option>
{% for u in user_list %}
<option value="{{ u }}" {% if u == user %} selected {% endif %}>{{ u }}</option>
{% endfor %}
</select>
</div>
<div class="input-group">
<select class="select2 form-control" name="asset">
<option value="">{% trans 'Asset' %}</option>
{% for a in asset_list %}
<option value="{{ a }}" {% if a == asset %} selected {% endif %}>{{ a }}</option>
{% endfor %}
</select>
</div>
<div class="input-group">
<select class="select2 form-control" name="system_user">
<option value="">{% trans 'System user' %}</option>
{% for su in system_user_list %}
<option value="{{ su }}" {% if su == system_user %} selected {% endif %}>{{ su }}</option>
{% endfor %}
</select>
</div>
<div class="input-group">
<input type="text" class="form-control input-sm" name="filename" placeholder="{% trans 'Filename' %}" value="{{ filename }}">
</div>
<div class="input-group">
<div class="input-group-btn">
<button id='search_btn' type="submit" class="btn btn-sm btn-primary">
搜索
</button>
</div>
</div>
</form>
{% endblock %}
{% block table_head %}
<th class="text-center"></th>
{# <th class="text-center">{% trans 'ID' %}</th>#}
<th class="text-center">{% trans 'User' %}</th>
<th class="text-center">{% trans 'Asset' %}</th>
<th class="text-center">{% trans 'System user' %}</th>
<th class="text-center">{% trans 'Remote addr' %}</th>
<th class="text-center">{% trans 'Operate' %}</th>
<th class="text-center">{% trans 'Filename' %}</th>
<th class="text-center">{% trans 'Success' %}</th>
<th class="text-center">{% trans 'Date start' %}</th>
{# <th class="text-center">{% trans 'Action' %}</th>#}
{% endblock %}
{% block table_body %}
{% for object in object_list %}
<tr class="gradeX">
<td class="text-center"><input type="checkbox" value="{{ object.id }}"></td>
{# <td class="text-center">#}
{# <a href="{% url 'terminal:object-detail' pk=object.id %}">{{ forloop.counter }}</a>#}
{# </td>#}
<td class="text-center">{{ object.user }}</td>
<td class="text-center">{{ object.asset }}</td>
<td class="text-center">{{ object.system_user }}</td>
<td class="text-center">{{ object.remote_addr|default:"" }}</td>
<td class="text-center">{{ object.operate }}</td>
<td class="text-center">{{ object.filename }}</td>
<td class="text-center">
{% if object.is_success %}
<i class="fa fa-check text-navy"></i>
{% else %}
<i class="fa fa-times text-danger"></i>
{% endif %}
</td>
<td class="text-center">{{ object.date_start }}</td>
</tr>
{% endfor %}
{% endblock %}
{% block content_bottom_left %}
{% endblock %}
{% block custom_foot_js %}
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
<script>
$(document).ready(function() {
$('table').DataTable({
"searching": false,
"paging": false,
"bInfo" : false,
"order": []
});
$('.select2').select2({
dropdownAutoWidth: true,
width: "auto"
});
$('.input-daterange.input-group').datepicker({
format: "yyyy-mm-dd",
todayBtn: "linked",
keyboardNavigation: false,
forceParse: false,
calendarWeeks: true,
autoclose: true
});
})
</script>
{% endblock %}

3
apps/audits/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

View File

@@ -0,0 +1,18 @@
# ~*~ coding: utf-8 ~*~
from __future__ import unicode_literals
from django.conf.urls import url
from rest_framework.routers import DefaultRouter
from .. import api
app_name = "audits"
router = DefaultRouter()
router.register(r'v1/ftp-log', api.FTPLogViewSet, 'ftp-log')
urlpatterns = [
# url(r'^v1/celery/task/(?P<pk>[0-9a-zA-Z\-]{36})/log/$', api.CeleryTaskLogApi.as_view(), name='celery-task-log'),
]
urlpatterns += router.urls

View File

@@ -0,0 +1,14 @@
# ~*~ coding: utf-8 ~*~
from __future__ import unicode_literals
from django.conf.urls import url
from .. import views
__all__ = ["urlpatterns"]
app_name = "audits"
urlpatterns = [
url(r'^ftp-log/$', views.FTPLogListView.as_view(), name='ftp-log-list'),
]

54
apps/audits/views.py Normal file
View File

@@ -0,0 +1,54 @@
from django.conf import settings
from django.views.generic import ListView
from django.utils.translation import ugettext as _
from common.mixins import AdminUserRequiredMixin, DatetimeSearchMixin
from .models import FTPLog
class FTPLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
model = FTPLog
template_name = 'audits/ftp_log_list.html'
paginate_by = settings.DISPLAY_PER_PAGE
user = asset = system_user = filename = ''
date_from = date_to = None
def get_queryset(self):
self.queryset = super().get_queryset()
self.user = self.request.GET.get('user')
self.asset = self.request.GET.get('asset')
self.system_user = self.request.GET.get('system_user')
self.filename = self.request.GET.get('filename', '')
filter_kwargs = dict()
filter_kwargs['date_start__gt'] = self.date_from
filter_kwargs['date_start__lt'] = self.date_to
if self.user:
filter_kwargs['user'] = self.user
if self.asset:
filter_kwargs['asset'] = self.asset
if self.system_user:
filter_kwargs['system_user'] = self.system_user
if self.filename:
filter_kwargs['filename__contains'] = self.filename
if filter_kwargs:
self.queryset = self.queryset.filter(**filter_kwargs).order_by('-date_start')
return self.queryset
def get_context_data(self, **kwargs):
context = {
'user_list': FTPLog.objects.values_list('user', flat=True).distinct(),
'asset_list': FTPLog.objects.values_list('asset', flat=True).distinct(),
'system_user_list': FTPLog.objects.values_list('system_user', flat=True).distinct(),
'date_from': self.date_from,
'date_to': self.date_to,
'user': self.user,
'asset': self.asset,
'system_user': self.system_user,
'filename': self.filename,
"app": _("Audits"),
"action": _("FTP log"),
}
kwargs.update(context)
return super().get_context_data(**kwargs)

View File

@@ -2,4 +2,4 @@ from __future__ import absolute_import
# This will make sure the app is always imported when # This will make sure the app is always imported when
# Django starts so that shared_task will use this app. # Django starts so that shared_task will use this app.
from .celery import app as celery_app

View File

@@ -2,14 +2,13 @@
# #
import json import json
from rest_framework.views import APIView from rest_framework.views import Response, APIView
from rest_framework.views import Response
from ldap3 import Server, Connection from ldap3 import Server, Connection
from django.core.mail import get_connection, send_mail from django.core.mail import get_connection, send_mail
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.conf import settings from django.conf import settings
from .permissions import IsSuperUser, IsAppUser from .permissions import IsSuperUser
from .serializers import MailTestSerializer, LDAPTestSerializer from .serializers import MailTestSerializer, LDAPTestSerializer
@@ -105,3 +104,6 @@ class DjangoSettingsAPI(APIView):
if i.isupper(): if i.isupper():
configs[i] = str(getattr(settings, i)) configs[i] = str(getattr(settings, i))
return Response(configs) return Response(configs)

View File

@@ -1,7 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy as _
create_success_msg = _("<b>%(name)s</b> was created successfully") create_success_msg = _("<b>%(name)s</b> was created successfully")
update_success_msg = _("<b>%(name)s</b> was updated successfully") update_success_msg = _("<b>%(name)s</b> was updated successfully")
FILE_END_GUARD = ">>> Content End <<<"
celery_task_pre_key = "CELERY_"

View File

@@ -2,11 +2,15 @@
# #
import json import json
from django.db import models
from django import forms from django import forms
from django.utils import six from django.utils import six
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from rest_framework import serializers from rest_framework import serializers
from .utils import get_signer
signer = get_signer()
class DictField(forms.Field): class DictField(forms.Field):
@@ -43,3 +47,30 @@ class StringIDField(serializers.Field):
def to_representation(self, value): def to_representation(self, value):
return {"pk": value.pk, "name": value.__str__()} return {"pk": value.pk, "name": value.__str__()}
class StringManyToManyField(serializers.RelatedField):
def to_representation(self, value):
return value.__str__()
class EncryptMixin:
def from_db_value(self, value, expression, connection, context):
if value is not None:
return signer.unsign(value)
return super().from_db_value(self, value, expression, connection, context)
def get_prep_value(self, value):
if value is None:
return value
return signer.sign(value).decode('utf-8')
class EncryptTextField(EncryptMixin, models.TextField):
description = _("Encrypt field using Secret Key")
class EncryptCharField(EncryptMixin, models.CharField):
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 2048
super().__init__(*args, **kwargs)

View File

@@ -79,3 +79,4 @@ class Setting(models.Model):
class Meta: class Meta:
db_table = "settings" db_table = "settings"

View File

@@ -1,13 +1,13 @@
from django.core.mail import send_mail from django.core.mail import send_mail
from django.conf import settings from django.conf import settings
from .celery import app from celery import shared_task
from .utils import get_logger from .utils import get_logger
logger = get_logger(__file__) logger = get_logger(__file__)
@app.task @shared_task
def send_mail_async(*args, **kwargs): def send_mail_async(*args, **kwargs):
""" Using celery to send email async """ Using celery to send email async

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import re import re
import sys
from collections import OrderedDict from collections import OrderedDict
from six import string_types from six import string_types
import base64 import base64
@@ -71,6 +72,8 @@ class Signer(metaclass=Singleton):
return s.dumps(value) return s.dumps(value)
def unsign(self, value): def unsign(self, value):
if value is None:
return value
s = JSONWebSignatureSerializer(self.secret_key) s = JSONWebSignatureSerializer(self.secret_key)
try: try:
return s.loads(value) return s.loads(value)
@@ -231,6 +234,14 @@ def setattr_bulk(seq, key, value):
return map(set_attr, seq) return map(set_attr, seq)
def set_or_append_attr_bulk(seq, key, value):
for obj in seq:
ori = getattr(obj, key, None)
if ori:
value += " " + ori
setattr(obj, key, value)
def content_md5(data): def content_md5(data):
"""计算data的MD5值经过Base64编码并返回str类型。 """计算data的MD5值经过Base64编码并返回str类型。
@@ -349,13 +360,38 @@ def get_short_uuid_str():
return str(uuid.uuid4()).split('-')[-1] return str(uuid.uuid4()).split('-')[-1]
def is_uuid(s): def is_uuid(seq):
if UUID_PATTERN.match(s): if isinstance(seq, str):
return True if UUID_PATTERN.match(seq):
return True
else:
return False
else: else:
return False for s in seq:
if not is_uuid(s):
return False
return True
def get_signer(): def get_signer():
signer = Signer(settings.SECRET_KEY) signer = Signer(settings.SECRET_KEY)
return signer return signer
class TeeObj:
origin_stdout = sys.stdout
def __init__(self, file_obj):
self.file_obj = file_obj
def write(self, msg):
self.origin_stdout.write(msg)
self.file_obj.write(msg.replace('*', ''))
def flush(self):
self.origin_stdout.flush()
self.file_obj.flush()
def close(self):
self.file_obj.close()

View File

@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
#
from django.core.validators import RegexValidator
from django.utils.translation import ugettext_lazy as _
alphanumeric = RegexValidator(r'^[0-9a-zA-Z_-]*$', _('Special char not allowed'))

View File

@@ -1,12 +1,13 @@
from django.views.generic import TemplateView
from django.shortcuts import render, redirect from django.core.cache import cache
from django.views.generic import TemplateView, View, DetailView
from django.shortcuts import render, redirect, Http404, reverse
from django.contrib import messages from django.contrib import messages
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.conf import settings from django.conf import settings
from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \ from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \
TerminalSettingForm TerminalSettingForm
from .models import Setting
from .mixins import AdminUserRequiredMixin from .mixins import AdminUserRequiredMixin
from .signals import ldap_auth_enable from .signals import ldap_auth_enable
@@ -120,3 +121,4 @@ class TerminalSettingView(AdminUserRequiredMixin, TemplateView):
context.update({"form": form}) context.update({"form": form})
return render(request, self.template_name, context) return render(request, self.template_name, context)

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -62,6 +62,7 @@ INSTALLED_APPS = [
'ops.apps.OpsConfig', 'ops.apps.OpsConfig',
'common.apps.CommonConfig', 'common.apps.CommonConfig',
'terminal.apps.TerminalConfig', 'terminal.apps.TerminalConfig',
'audits.apps.AuditsConfig',
'rest_framework', 'rest_framework',
'rest_framework_swagger', 'rest_framework_swagger',
'django_filters', 'django_filters',
@@ -234,7 +235,7 @@ LOGGING = {
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/ # https://docs.djangoproject.com/en/1.10/topics/i18n/
LANGUAGE_CODE = 'zh-cn' LANGUAGE_CODE = 'en'
TIME_ZONE = 'Asia/Shanghai' TIME_ZONE = 'Asia/Shanghai'

View File

@@ -19,6 +19,7 @@ urlpatterns = [
url(r'^perms/', include('perms.urls.views_urls', namespace='perms')), url(r'^perms/', include('perms.urls.views_urls', namespace='perms')),
url(r'^terminal/', include('terminal.urls.views_urls', namespace='terminal')), url(r'^terminal/', include('terminal.urls.views_urls', namespace='terminal')),
url(r'^ops/', include('ops.urls.view_urls', namespace='ops')), url(r'^ops/', include('ops.urls.view_urls', namespace='ops')),
url(r'^audits/', include('audits.urls.view_urls', namespace='audits')),
url(r'^settings/', include('common.urls.view_urls', namespace='settings')), url(r'^settings/', include('common.urls.view_urls', namespace='settings')),
url(r'^common/', include('common.urls.view_urls', namespace='common')), url(r'^common/', include('common.urls.view_urls', namespace='common')),
@@ -28,15 +29,17 @@ urlpatterns = [
url(r'^api/perms/', include('perms.urls.api_urls', namespace='api-perms')), url(r'^api/perms/', include('perms.urls.api_urls', namespace='api-perms')),
url(r'^api/terminal/', include('terminal.urls.api_urls', namespace='api-terminal')), url(r'^api/terminal/', include('terminal.urls.api_urls', namespace='api-terminal')),
url(r'^api/ops/', include('ops.urls.api_urls', namespace='api-ops')), url(r'^api/ops/', include('ops.urls.api_urls', namespace='api-ops')),
url(r'^api/audits/', include('audits.urls.api_urls', namespace='api-audits')),
url(r'^api/common/', include('common.urls.api_urls', namespace='api-common')), url(r'^api/common/', include('common.urls.api_urls', namespace='api-common')),
# External apps url # External apps url
url(r'^captcha/', include('captcha.urls')), url(r'^captcha/', include('captcha.urls')),
] ]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \
+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
if settings.DEBUG: if settings.DEBUG:
urlpatterns += [ urlpatterns += [
url(r'^docs/', schema_view, name="docs"), url(r'^docs/', schema_view, name="docs"),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) \ ]
+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -1 +1 @@
from .celery import app as celery_app

View File

@@ -1,14 +1,18 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
import sys
from ansible.plugins.callback import CallbackBase from ansible.plugins.callback import CallbackBase
from ansible.plugins.callback.default import CallbackModule from ansible.plugins.callback.default import CallbackModule
from .display import TeeObj
class AdHocResultCallback(CallbackModule): class AdHocResultCallback(CallbackModule):
""" """
Task result Callback Task result Callback
""" """
def __init__(self, display=None, options=None): def __init__(self, display=None, options=None, file_obj=None):
# result_raw example: { # result_raw example: {
# "ok": {"hostname": {"task_name": {}...},..}, # "ok": {"hostname": {"task_name": {}...},..},
# "failed": {"hostname": {"task_name": {}..}, ..}, # "failed": {"hostname": {"task_name": {}..}, ..},
@@ -22,6 +26,8 @@ class AdHocResultCallback(CallbackModule):
self.results_raw = dict(ok={}, failed={}, unreachable={}, skipped={}) self.results_raw = dict(ok={}, failed={}, unreachable={}, skipped={})
self.results_summary = dict(contacted=[], dark={}) self.results_summary = dict(contacted=[], dark={})
super().__init__() super().__init__()
if file_obj is not None:
sys.stdout = TeeObj(file_obj)
def gather_result(self, t, res): def gather_result(self, t, res):
self._clean_results(res._result, res._task.action) self._clean_results(res._result, res._task.action)

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
#
import sys
class TeeObj:
origin_stdout = sys.stdout
def __init__(self, file_obj):
self.file_obj = file_obj
def write(self, msg):
self.origin_stdout.write(msg)
self.file_obj.write(msg.replace('*', ''))
def flush(self):
self.origin_stdout.flush()
self.file_obj.flush()

View File

@@ -78,7 +78,7 @@ class BaseInventory(InventoryManager):
variable_manager_class = VariableManager variable_manager_class = VariableManager
host_manager_class = BaseHost host_manager_class = BaseHost
def __init__(self, host_list=None): def __init__(self, host_list=None, group_list=None):
""" """
用于生成动态构建Ansible Inventory. super().__init__ 会自动调用 用于生成动态构建Ansible Inventory. super().__init__ 会自动调用
host_list: [{ host_list: [{
@@ -97,11 +97,14 @@ class BaseInventory(InventoryManager):
"vars": {}, "vars": {},
}, },
] ]
group_list: [
{"name: "", children: [""]},
]
:param host_list: :param host_list:
:param group_list
""" """
if host_list is None: self.host_list = host_list or []
host_list = [] self.group_list = group_list or []
self.host_list = host_list
assert isinstance(host_list, list) assert isinstance(host_list, list)
self.loader = self.loader_class() self.loader = self.loader_class()
self.variable_manager = self.variable_manager_class() self.variable_manager = self.variable_manager_class()
@@ -113,25 +116,40 @@ class BaseInventory(InventoryManager):
def get_group(self, name): def get_group(self, name):
return self._inventory.groups.get(name, None) return self._inventory.groups.get(name, None)
def parse_sources(self, cache=False): def get_or_create_group(self, name):
group_all = self.get_group('all') group = self.get_group(name)
ungrouped = self.get_group('ungrouped') if not group:
self.add_group(name)
return self.get_or_create_group(name)
else:
return group
def parse_groups(self):
for g in self.group_list:
parent = self.get_or_create_group(g.get("name"))
children = [self.get_or_create_group(n) for n in g.get('children', [])]
for child in children:
parent.add_child_group(child)
def parse_hosts(self):
group_all = self.get_or_create_group('all')
ungrouped = self.get_or_create_group('ungrouped')
for host_data in self.host_list: for host_data in self.host_list:
host = self.host_manager_class(host_data=host_data) host = self.host_manager_class(host_data=host_data)
self.hosts[host_data['hostname']] = host self.hosts[host_data['hostname']] = host
groups_data = host_data.get('groups') groups_data = host_data.get('groups')
if groups_data: if groups_data:
for group_name in groups_data: for group_name in groups_data:
group = self.get_group(group_name) group = self.get_or_create_group(group_name)
if group is None:
self.add_group(group_name)
group = self.get_group(group_name)
group.add_host(host) group.add_host(host)
else: else:
ungrouped.add_host(host) ungrouped.add_host(host)
group_all.add_host(host) group_all.add_host(host)
def parse_sources(self, cache=False):
self.parse_groups()
self.parse_hosts()
def get_matched_hosts(self, pattern): def get_matched_hosts(self, pattern):
return self.get_hosts(pattern) return self.get_hosts(pattern)

View File

@@ -9,6 +9,7 @@ from ansible.parsing.dataloader import DataLoader
from ansible.executor.playbook_executor import PlaybookExecutor from ansible.executor.playbook_executor import PlaybookExecutor
from ansible.playbook.play import Play from ansible.playbook.play import Play
import ansible.constants as C import ansible.constants as C
from ansible.utils.display import Display
from .callback import AdHocResultCallback, PlaybookResultCallBack, \ from .callback import AdHocResultCallback, PlaybookResultCallBack, \
CommandResultCallback CommandResultCallback
@@ -21,6 +22,13 @@ C.HOST_KEY_CHECKING = False
logger = get_logger(__name__) logger = get_logger(__name__)
class CustomDisplay(Display):
def display(self, msg, color=None, stderr=False, screen_only=False, log_only=False):
pass
display = CustomDisplay()
Options = namedtuple('Options', [ Options = namedtuple('Options', [
'listtags', 'listtasks', 'listhosts', 'syntax', 'connection', 'listtags', 'listtasks', 'listhosts', 'syntax', 'connection',
'module_path', 'forks', 'remote_user', 'private_key_file', 'timeout', 'module_path', 'forks', 'remote_user', 'private_key_file', 'timeout',
@@ -123,20 +131,22 @@ class AdHocRunner:
ADHoc Runner接口 ADHoc Runner接口
""" """
results_callback_class = AdHocResultCallback results_callback_class = AdHocResultCallback
results_callback = None
loader_class = DataLoader loader_class = DataLoader
variable_manager_class = VariableManager variable_manager_class = VariableManager
options = get_default_options()
default_options = get_default_options() default_options = get_default_options()
def __init__(self, inventory, options=None): def __init__(self, inventory, options=None):
if options: self.options = self.update_options(options)
self.options = options
self.inventory = inventory self.inventory = inventory
self.loader = DataLoader() self.loader = DataLoader()
self.variable_manager = VariableManager( self.variable_manager = VariableManager(
loader=self.loader, inventory=self.inventory loader=self.loader, inventory=self.inventory
) )
def get_result_callback(self, file_obj=None):
return self.__class__.results_callback_class(file_obj=file_obj)
@staticmethod @staticmethod
def check_module_args(module_name, module_args=''): def check_module_args(module_name, module_args=''):
if module_name in C.MODULE_REQUIRE_ARGS and not module_args: if module_name in C.MODULE_REQUIRE_ARGS and not module_args:
@@ -160,19 +170,24 @@ class AdHocRunner:
cleaned_tasks.append(task) cleaned_tasks.append(task)
return cleaned_tasks return cleaned_tasks
def set_option(self, k, v): def update_options(self, options):
kwargs = {k: v} if options and isinstance(options, dict):
self.options = self.options._replace(**kwargs) options = self.__class__.default_options._replace(**options)
else:
options = self.__class__.default_options
return options
def run(self, tasks, pattern, play_name='Ansible Ad-hoc', gather_facts='no'): def run(self, tasks, pattern, play_name='Ansible Ad-hoc', gather_facts='no', file_obj=None):
""" """
:param tasks: [{'action': {'module': 'shell', 'args': 'ls'}, ...}, ] :param tasks: [{'action': {'module': 'shell', 'args': 'ls'}, ...}, ]
:param pattern: all, *, or others :param pattern: all, *, or others
:param play_name: The play name :param play_name: The play name
:param gather_facts:
:param file_obj: logging to file_obj
:return: :return:
""" """
self.check_pattern(pattern) self.check_pattern(pattern)
results_callback = self.results_callback_class() self.results_callback = self.get_result_callback(file_obj)
cleaned_tasks = self.clean_tasks(tasks) cleaned_tasks = self.clean_tasks(tasks)
play_source = dict( play_source = dict(
@@ -193,16 +208,16 @@ class AdHocRunner:
variable_manager=self.variable_manager, variable_manager=self.variable_manager,
loader=self.loader, loader=self.loader,
options=self.options, options=self.options,
stdout_callback=results_callback, stdout_callback=self.results_callback,
passwords=self.options.passwords, passwords=self.options.passwords,
) )
logger.debug("Get inventory matched hosts: {}".format( print("Get matched hosts: {}".format(
self.inventory.get_matched_hosts(pattern) self.inventory.get_matched_hosts(pattern)
)) ))
try: try:
tqm.run(play) tqm.run(play)
return results_callback return self.results_callback
except Exception as e: except Exception as e:
raise AnsibleError(e) raise AnsibleError(e)
finally: finally:

View File

@@ -1,13 +1,17 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
import uuid
import os
from django.core.cache import cache
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _
from rest_framework import viewsets, generics from rest_framework import viewsets, generics
from rest_framework.views import Response from rest_framework.views import Response
from .hands import IsSuperUser from .hands import IsSuperUser
from .models import Task, AdHoc, AdHocRunHistory from .models import Task, AdHoc, AdHocRunHistory, CeleryTask
from .serializers import TaskSerializer, AdHocSerializer, AdHocRunHistorySerializer from .serializers import TaskSerializer, AdHocSerializer, \
AdHocRunHistorySerializer
from .tasks import run_ansible_task from .tasks import run_ansible_task
@@ -24,8 +28,8 @@ class TaskRun(generics.RetrieveAPIView):
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
task = self.get_object() task = self.get_object()
run_ansible_task.delay(str(task.id)) t = run_ansible_task.delay(str(task.id))
return Response({"msg": "start"}) return Response({"task": t.id})
class AdHocViewSet(viewsets.ModelViewSet): class AdHocViewSet(viewsets.ModelViewSet):
@@ -58,3 +62,30 @@ class AdHocRunHistorySet(viewsets.ModelViewSet):
adhoc = get_object_or_404(AdHoc, id=adhoc_id) adhoc = get_object_or_404(AdHoc, id=adhoc_id)
self.queryset = self.queryset.filter(adhoc=adhoc) self.queryset = self.queryset.filter(adhoc=adhoc)
return self.queryset return self.queryset
class CeleryTaskLogApi(generics.RetrieveAPIView):
permission_classes = (IsSuperUser,)
buff_size = 1024 * 10
end = False
queryset = CeleryTask.objects.all()
def get(self, request, *args, **kwargs):
mark = request.query_params.get("mark") or str(uuid.uuid4())
task = super().get_object()
log_path = task.full_log_path
if not log_path or not os.path.isfile(log_path):
return Response({"data": _("Waiting ...")}, status=203)
with open(log_path, 'r') as f:
offset = cache.get(mark, 0)
f.seek(offset)
data = f.read(self.buff_size).replace('\n', '\r\n')
mark = str(uuid.uuid4())
cache.set(mark, f.tell(), 5)
if data == '' and task.is_finished():
self.end = True
return Response({"data": data, 'end': self.end, 'mark': mark})

View File

@@ -5,3 +5,7 @@ from django.apps import AppConfig
class OpsConfig(AppConfig): class OpsConfig(AppConfig):
name = 'ops' name = 'ops'
def ready(self):
super().ready()
from .celery import signal_handler

View File

@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
import os
from celery import Celery
# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'jumpserver.settings')
from django.conf import settings
app = Celery('jumpserver')
# Using a string here means the worker will not have to
# pickle the object when using Windows.
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks(lambda: [app_config.split('.')[0] for app_config in settings.INSTALLED_APPS])

3
apps/ops/celery/const.py Normal file
View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
#

View File

@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
#
import os
import datetime
import sys
import time
from django.conf import settings
from django.utils import timezone
from django.core.cache import cache
from django.db import transaction
from celery import subtask
from celery.signals import worker_ready, worker_shutdown, task_prerun, \
task_postrun, after_task_publish
from django_celery_beat.models import PeriodicTask
from common.utils import get_logger, TeeObj, get_object_or_none
from common.const import celery_task_pre_key
from .utils import get_after_app_ready_tasks, get_after_app_shutdown_clean_tasks
from ..models import CeleryTask
logger = get_logger(__file__)
@worker_ready.connect
def on_app_ready(sender=None, headers=None, body=None, **kwargs):
if cache.get("CELERY_APP_READY", 0) == 1:
return
cache.set("CELERY_APP_READY", 1, 10)
logger.debug("App ready signal recv")
tasks = get_after_app_ready_tasks()
logger.debug("Start need start task: [{}]".format(
", ".join(tasks))
)
for task in tasks:
subtask(task).delay()
@worker_shutdown.connect
def after_app_shutdown(sender=None, headers=None, body=None, **kwargs):
if cache.get("CELERY_APP_SHUTDOWN", 0) == 1:
return
cache.set("CELERY_APP_SHUTDOWN", 1, 10)
tasks = get_after_app_shutdown_clean_tasks()
logger.debug("App shutdown signal recv")
logger.debug("Clean need cleaned period tasks: [{}]".format(
', '.join(tasks))
)
PeriodicTask.objects.filter(name__in=tasks).delete()
@after_task_publish.connect
def after_task_publish_signal_handler(sender, headers=None, **kwargs):
CeleryTask.objects.create(
id=headers["id"], status=CeleryTask.WAITING, name=headers["task"]
)
cache.set(headers["id"], True, 3600)
@task_prerun.connect
def pre_run_task_signal_handler(sender, task_id=None, task=None, **kwargs):
time.sleep(0.1)
for i in range(5):
if cache.get(task_id, False):
break
else:
time.sleep(0.1)
continue
t = get_object_or_none(CeleryTask, id=task_id)
if t is None:
logger.warn("Not get the task: {}".format(task_id))
return
now = datetime.datetime.now().strftime("%Y-%m-%d")
log_path = os.path.join(now, task_id + '.log')
full_path = os.path.join(CeleryTask.LOG_DIR, log_path)
if not os.path.exists(os.path.dirname(full_path)):
os.makedirs(os.path.dirname(full_path))
with transaction.atomic():
t.date_start = timezone.now()
t.status = CeleryTask.RUNNING
t.log_path = log_path
t.save()
f = open(full_path, 'w')
tee = TeeObj(f)
sys.stdout = tee
task.log_f = tee
@task_postrun.connect
def post_run_task_signal_handler(sender, task_id=None, task=None, **kwargs):
t = get_object_or_none(CeleryTask, id=task_id)
if t is None:
logger.warn("Not get the task: {}".format(task_id))
return
with transaction.atomic():
t.status = CeleryTask.FINISHED
t.date_finished = timezone.now()
t.save()
task.log_f.flush()
sys.stdout = task.log_f.origin_stdout
task.log_f.close()

View File

@@ -1,33 +1,50 @@
# ~*~ coding: utf-8 ~*~ # -*- coding: utf-8 -*-
#
import os
import json import json
from functools import wraps from functools import wraps
from celery import Celery, subtask
from celery.signals import worker_ready, worker_shutdown
from django.db.utils import ProgrammingError, OperationalError from django.db.utils import ProgrammingError, OperationalError
from .utils import get_logger
logger = get_logger(__file__)
# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'jumpserver.settings')
from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django_celery_beat.models import PeriodicTask, IntervalSchedule, CrontabSchedule
app = Celery('jumpserver')
# Using a string here means the worker will not have to def add_register_period_task(name):
# pickle the object when using Windows. key = "__REGISTER_PERIODIC_TASKS"
app.config_from_object('django.conf:settings', namespace='CELERY') value = cache.get(key, [])
app.autodiscover_tasks(lambda: [app_config.split('.')[0] for app_config in settings.INSTALLED_APPS]) value.append(name)
cache.set(key, value)
def get_register_period_tasks():
key = "__REGISTER_PERIODIC_TASKS"
return cache.get(key, [])
def add_after_app_shutdown_clean_task(name):
key = "__AFTER_APP_SHUTDOWN_CLEAN_TASKS"
value = cache.get(key, [])
value.append(name)
cache.set(key, value)
def get_after_app_shutdown_clean_tasks():
key = "__AFTER_APP_SHUTDOWN_CLEAN_TASKS"
return cache.get(key, [])
def add_after_app_ready_task(name):
key = "__AFTER_APP_READY_RUN_TASKS"
value = cache.get(key, [])
value.append(name)
cache.set(key, value)
def get_after_app_ready_tasks():
key = "__AFTER_APP_READY_RUN_TASKS"
return cache.get(key, [])
def create_or_update_celery_periodic_tasks(tasks): def create_or_update_celery_periodic_tasks(tasks):
from django_celery_beat.models import PeriodicTask, IntervalSchedule, CrontabSchedule
""" """
:param tasks: { :param tasks: {
'add-every-monday-morning': { 'add-every-monday-morning': {
@@ -106,11 +123,6 @@ def delete_celery_periodic_task(task_name):
PeriodicTask.objects.filter(name=task_name).delete() PeriodicTask.objects.filter(name=task_name).delete()
__REGISTER_PERIODIC_TASKS = []
__AFTER_APP_SHUTDOWN_CLEAN_TASKS = []
__AFTER_APP_READY_RUN_TASKS = []
def register_as_period_task(crontab=None, interval=None): def register_as_period_task(crontab=None, interval=None):
""" """
Warning: Task must be have not any args and kwargs Warning: Task must be have not any args and kwargs
@@ -128,7 +140,7 @@ def register_as_period_task(crontab=None, interval=None):
# Because when this decorator run, the task was not created, # Because when this decorator run, the task was not created,
# So we can't use func.name # So we can't use func.name
name = '{func.__module__}.{func.__name__}'.format(func=func) name = '{func.__module__}.{func.__name__}'.format(func=func)
if name not in __REGISTER_PERIODIC_TASKS: if name not in get_register_period_tasks():
create_or_update_celery_periodic_tasks({ create_or_update_celery_periodic_tasks({
name: { name: {
'task': name, 'task': name,
@@ -138,7 +150,7 @@ def register_as_period_task(crontab=None, interval=None):
'enabled': True, 'enabled': True,
} }
}) })
__REGISTER_PERIODIC_TASKS.append(name) add_register_period_task(name)
@wraps(func) @wraps(func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
@@ -151,13 +163,12 @@ def after_app_ready_start(func):
# Because when this decorator run, the task was not created, # Because when this decorator run, the task was not created,
# So we can't use func.name # So we can't use func.name
name = '{func.__module__}.{func.__name__}'.format(func=func) name = '{func.__module__}.{func.__name__}'.format(func=func)
if name not in __AFTER_APP_READY_RUN_TASKS: if name not in get_after_app_ready_tasks():
__AFTER_APP_READY_RUN_TASKS.append(name) add_after_app_ready_task(name)
@wraps(func) @wraps(func)
def decorate(*args, **kwargs): def decorate(*args, **kwargs):
return func(*args, **kwargs) return func(*args, **kwargs)
return decorate return decorate
@@ -165,37 +176,10 @@ def after_app_shutdown_clean(func):
# Because when this decorator run, the task was not created, # Because when this decorator run, the task was not created,
# So we can't use func.name # So we can't use func.name
name = '{func.__module__}.{func.__name__}'.format(func=func) name = '{func.__module__}.{func.__name__}'.format(func=func)
if name not in __AFTER_APP_READY_RUN_TASKS: if name not in get_after_app_shutdown_clean_tasks():
__AFTER_APP_SHUTDOWN_CLEAN_TASKS.append(name) add_after_app_shutdown_clean_task(name)
@wraps(func) @wraps(func)
def decorate(*args, **kwargs): def decorate(*args, **kwargs):
return func(*args, **kwargs) return func(*args, **kwargs)
return decorate return decorate
@worker_ready.connect
def on_app_ready(sender=None, headers=None, body=None, **kwargs):
if cache.get("CELERY_APP_READY", 0) == 1:
return
cache.set("CELERY_APP_READY", 1, 10)
logger.debug("App ready signal recv")
logger.debug("Start need start task: [{}]".format(
", ".join(__AFTER_APP_READY_RUN_TASKS))
)
for task in __AFTER_APP_READY_RUN_TASKS:
subtask(task).delay()
@worker_shutdown.connect
def after_app_shutdown(sender=None, headers=None, body=None, **kwargs):
if cache.get("CELERY_APP_SHUTDOWN", 0) == 1:
return
cache.set("CELERY_APP_SHUTDOWN", 1, 10)
from django_celery_beat.models import PeriodicTask
logger.debug("App shutdown signal recv")
logger.debug("Clean need cleaned period tasks: [{}]".format(
', '.join(__AFTER_APP_SHUTDOWN_CLEAN_TASKS))
)
PeriodicTask.objects.filter(name__in=__AFTER_APP_SHUTDOWN_CLEAN_TASKS).delete()

View File

@@ -15,32 +15,89 @@ class JMSInventory(BaseInventory):
write you own manager, construct you inventory write you own manager, construct you inventory
""" """
def __init__(self, hostname_list, run_as_admin=False, run_as=None, become_info=None): def __init__(self, hostname_list, run_as_admin=False, run_as=None, become_info=None):
"""
:param hostname_list: ["test1", ]
:param run_as_admin: True 是否使用管理用户去执行, 每台服务器的管理用户可能不同
:param run_as: 是否统一使用某个系统用户去执行
:param become_info: 是否become成某个用户去执行
"""
self.hostname_list = hostname_list self.hostname_list = hostname_list
self.using_admin = run_as_admin self.using_admin = run_as_admin
self.run_as = run_as self.run_as = run_as
self.become_info = become_info self.become_info = become_info
assets = self.get_jms_assets() assets = self.get_jms_assets()
if run_as_admin: host_list = []
host_list = [asset._to_secret_json() for asset in assets]
else: for asset in assets:
host_list = [asset.to_json() for asset in assets] info = self.convert_to_ansible(asset, run_as_admin=run_as_admin)
if run_as: host_list.append(info)
run_user_info = self.get_run_user_info()
for host in host_list: if run_as:
host.update(run_user_info) run_user_info = self.get_run_user_info()
if become_info: for host in host_list:
for host in host_list: host.update(run_user_info)
host.update(become_info)
if become_info:
for host in host_list:
host.update(become_info)
super().__init__(host_list=host_list) super().__init__(host_list=host_list)
def get_jms_assets(self): def get_jms_assets(self):
assets = get_assets_by_hostname_list(self.hostname_list) assets = get_assets_by_hostname_list(self.hostname_list)
return assets return assets
def convert_to_ansible(self, asset, run_as_admin=False):
info = {
'id': asset.id,
'hostname': asset.hostname,
'ip': asset.ip,
'port': asset.port,
'vars': dict(),
'groups': [],
}
if asset.domain and asset.domain.has_gateway():
info["vars"].update(self.make_proxy_command(asset))
if run_as_admin:
info.update(asset.get_auth_info())
for node in asset.nodes.all():
info["groups"].append(node.value)
for label in asset.labels.all():
info["vars"].update({
label.name: label.value
})
info["groups"].append("{}:{}".format(label.name, label.value))
if asset.domain:
info["vars"].update({
"domain": asset.domain.name,
})
info["groups"].append("domain_"+asset.domain.name)
return info
def get_run_user_info(self): def get_run_user_info(self):
system_user = get_system_user_by_name(self.run_as) system_user = get_system_user_by_name(self.run_as)
if not system_user: if not system_user:
return {} return {}
else: else:
return system_user._to_secret_json() return system_user._to_secret_json()
@staticmethod
def make_proxy_command(asset):
gateway = asset.domain.random_gateway()
proxy_command_list = [
"ssh", "-p", str(gateway.port),
"{}@{}".format(gateway.username, gateway.ip),
"-W", "%h:%p", "-q",
]
if gateway.password:
proxy_command_list.insert(
0, "sshpass -p {}".format(gateway.password)
)
if gateway.private_key:
proxy_command_list.append("-i {}".format(gateway.private_key_file))
proxy_command = "'-o ProxyCommand={}'".format(
" ".join(proxy_command_list)
)
return {"ansible_ssh_common_args": proxy_command}

View File

@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
#
from .adhoc import *
from .celery import *

View File

@@ -2,18 +2,23 @@
import json import json
import uuid import uuid
import os
import time import time
import datetime
from celery import current_task
from django.db import models from django.db import models
from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django_celery_beat.models import CrontabSchedule, IntervalSchedule, PeriodicTask from django_celery_beat.models import PeriodicTask
from common.utils import get_signer, get_logger from common.utils import get_signer, get_logger
from common.celery import delete_celery_periodic_task, create_or_update_celery_periodic_tasks, \ from ..celery.utils import delete_celery_periodic_task, \
disable_celery_periodic_task create_or_update_celery_periodic_tasks, \
from .ansible import AdHocRunner, AnsibleError disable_celery_periodic_task
from .inventory import JMSInventory from ..ansible import AdHocRunner, AnsibleError
from ..inventory import JMSInventory
__all__ = ["Task", "AdHoc", "AdHocRunHistory"] __all__ = ["Task", "AdHoc", "AdHocRunHistory"]
@@ -85,7 +90,7 @@ class Task(models.Model):
def save(self, force_insert=False, force_update=False, using=None, def save(self, force_insert=False, force_update=False, using=None,
update_fields=None): update_fields=None):
from .tasks import run_ansible_task from ..tasks import run_ansible_task
super().save( super().save(
force_insert=force_insert, force_update=force_update, force_insert=force_insert, force_update=force_update,
using=using, update_fields=update_fields, using=using, update_fields=update_fields,
@@ -206,10 +211,18 @@ class AdHoc(models.Model):
return self._run_only() return self._run_only()
def _run_and_record(self): def _run_and_record(self):
history = AdHocRunHistory(adhoc=self, task=self.task) try:
hid = current_task.request.id
except AttributeError:
hid = str(uuid.uuid4())
history = AdHocRunHistory(id=hid, adhoc=self, task=self.task)
time_start = time.time() time_start = time.time()
try: try:
date_start = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print("{} Start task: {}\r\n".format(date_start, self.task.name))
raw, summary = self._run_only() raw, summary = self._run_only()
date_end = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print("\r\n{} Task finished".format(date_end))
history.is_finished = True history.is_finished = True
if summary.get('dark'): if summary.get('dark'):
history.is_success = False history.is_success = False
@@ -221,17 +234,20 @@ class AdHoc(models.Model):
except Exception as e: except Exception as e:
return {}, {"dark": {"all": str(e)}, "contacted": []} return {}, {"dark": {"all": str(e)}, "contacted": []}
finally: finally:
# f.close()
history.date_finished = timezone.now() history.date_finished = timezone.now()
history.timedelta = time.time() - time_start history.timedelta = time.time() - time_start
history.save() history.save()
def _run_only(self): def _run_only(self, file_obj=None):
runner = AdHocRunner(self.inventory) runner = AdHocRunner(self.inventory, options=self.options)
for k, v in self.options.items():
runner.set_option(k, v)
try: try:
result = runner.run(self.tasks, self.pattern, self.task.name) result = runner.run(
self.tasks,
self.pattern,
self.task.name,
file_obj=file_obj,
)
return result.results_raw, result.results_summary return result.results_raw, result.results_summary
except AnsibleError as e: except AnsibleError as e:
logger.warn("Failed run adhoc {}, {}".format(self.task.name, e)) logger.warn("Failed run adhoc {}, {}".format(self.task.name, e))
@@ -316,6 +332,14 @@ class AdHocRunHistory(models.Model):
def short_id(self): def short_id(self):
return str(self.id).split('-')[-1] return str(self.id).split('-')[-1]
@property
def log_path(self):
dt = datetime.datetime.now().strftime('%Y-%m-%d')
log_dir = os.path.join(settings.PROJECT_DIR, 'data', 'ansible', dt)
if not os.path.exists(log_dir):
os.makedirs(log_dir)
return os.path.join(log_dir, str(self.id) + '.log')
@property @property
def result(self): def result(self):
if self._result: if self._result:

38
apps/ops/models/celery.py Normal file
View File

@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
#
import uuid
import os
from django.conf import settings
from django.db import models
class CeleryTask(models.Model):
WAITING = "waiting"
RUNNING = "running"
FINISHED = "finished"
LOG_DIR = os.path.join(settings.PROJECT_DIR, 'data', 'celery')
STATUS_CHOICES = (
(WAITING, WAITING),
(RUNNING, RUNNING),
(FINISHED, FINISHED),
)
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
name = models.CharField(max_length=1024)
status = models.CharField(max_length=128, choices=STATUS_CHOICES)
log_path = models.CharField(max_length=256, blank=True, null=True)
date_published = models.DateTimeField(auto_now_add=True)
date_start = models.DateTimeField(null=True)
date_finished = models.DateTimeField(null=True)
def __str__(self):
return "{}: {}".format(self.name, self.id)
def is_finished(self):
return self.status == self.FINISHED
@property
def full_log_path(self):
if not self.log_path:
return None
return os.path.join(self.LOG_DIR, self.log_path)

View File

@@ -12,14 +12,13 @@ def rerun_task():
@shared_task @shared_task
def run_ansible_task(task_id, callback=None, **kwargs): def run_ansible_task(tid, callback=None, **kwargs):
""" """
:param task_id: is the tasks serialized data :param tid: is the tasks serialized data
:param callback: callback function name :param callback: callback function name
:return: :return:
""" """
task = get_object_or_none(Task, id=tid)
task = get_object_or_none(Task, id=task_id)
if task: if task:
result = task.run() result = task.run()
if callback is not None: if callback is not None:

View File

@@ -82,7 +82,8 @@ function initTable() {
select: [], select: [],
columnDefs: [ columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) { {targets: 1, createdCell: function (td, cellData, rowData) {
$(td).html(cellData); var d = new Date(cellData);
$(td).html(d);
}}, }},
{targets: 2, createdCell: function (td, cellData) { {targets: 2, createdCell: function (td, cellData) {
var total = "<span>" + cellData.total + "</span>"; var total = "<span>" + cellData.total + "</span>";

View File

@@ -18,6 +18,9 @@
<li class="active"> <li class="active">
<a href="{% url 'ops:adhoc-history-detail' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history detail' %} </a> <a href="{% url 'ops:adhoc-history-detail' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history detail' %} </a>
</li> </li>
<li>
<a class="text-center celery-task-log" onclick="window.open('{% url 'ops:celery-task-log' pk=object.pk %}','', 'width=800,height=600')"><i class="fa fa-laptop"></i> {% trans 'Output' %} </a>
</li>
</ul> </ul>
</div> </div>
<div class="tab-content"> <div class="tab-content">

View File

@@ -0,0 +1,96 @@
{% load static %}
<head>
<title>term.js</title>
<script src="{% static 'js/jquery-2.1.1.js' %}"></script>
<style>
html {
background: #000;
}
h1 {
margin-bottom: 20px;
font: 20px/1.5 sans-serif;
}
.terminal {
float: left;
font-family: 'Monaco', 'Consolas', "DejaVu Sans Mono", "Liberation Mono", monospace;
font-size: 12px;
color: #f0f0f0;
background-color: #555;
padding: 20px 20px 20px;
}
.terminal-cursor {
color: #000;
background: #f0f0f0;
}
</style>
</head>
<div class="container">
<div id="term">
</div>
</div>
<script src="{% static 'js/term.js' %}"></script>
<script>
var rowHeight = 1;
var colWidth = 1;
var mark = '';
var url = "{% url 'api-ops:celery-task-log' pk=object.id %}";
var term;
var end = false;
var error = false;
var interval = 200;
function calWinSize() {
var t = $('.terminal');
rowHeight = 1.00 * t.height() / 24;
colWidth = 1.00 * t.width() / 80;
}
function resize() {
var rows = Math.floor(window.innerHeight / rowHeight) - 2;
var cols = Math.floor(window.innerWidth / colWidth) - 10;
term.resize(cols, rows);
}
function requestAndWrite() {
if (!end) {
$.ajax({
url: url + '?mark=' + mark,
method: "GET",
contentType: "application/json; charset=utf-8"
}).done(function(data, textStatue, jqXHR) {
if (jqXHR.status === 203) {
error = true;
term.write('.');
interval = 500;
}
if (jqXHR.status === 200){
term.write(data.data);
mark = data.mark;
if (data.end){
end = true
}
}
})
}
}
$(document).ready(function () {
term = new Terminal({
cols: 80,
rows: 24,
useStyle: true,
screenKeys: false,
convertEol: false,
cursorBlink: false
});
term.open();
term.on('data', function (data) {
term.write(data.replace('\r', '\r\n'))
});
calWinSize();
resize();
$('.terminal').detach().appendTo('#term');
setInterval(function () {
requestAndWrite()
}, interval)
});
</script>

View File

@@ -24,6 +24,9 @@
<li> <li>
<a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a> <a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a>
</li> </li>
<li>
<a class="text-center celery-task-log" onclick="window.open('{% url 'ops:celery-task-log' pk=object.latest_history.pk %}','', 'width=800,height=600')"><i class="fa fa-laptop"></i> {% trans 'Last run output' %} </a>
</li>
</ul> </ul>
</div> </div>
<div class="tab-content"> <div class="tab-content">
@@ -105,6 +108,10 @@
$(td).html(cellData.user) $(td).html(cellData.user)
} }
}}, }},
{targets: 6, createdCell: function (td, cellData) {
var d = new Date(cellData);
$(td).html(d.toLocaleString())
}},
{targets: 7, createdCell: function (td, cellData, rowData) { {targets: 7, createdCell: function (td, cellData, rowData) {
var detail_btn = '<a class="btn btn-xs btn-primary m-l-xs btn-run" href="{% url 'ops:adhoc-detail' pk=DEFAULT_PK %}">{% trans "Detail" %}</a>'.replace('{{ DEFAULT_PK }}', cellData); var detail_btn = '<a class="btn btn-xs btn-primary m-l-xs btn-run" href="{% url 'ops:adhoc-detail' pk=DEFAULT_PK %}">{% trans "Detail" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
if (cellData) { if (cellData) {

View File

@@ -24,6 +24,9 @@
<li> <li>
<a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a> <a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a>
</li> </li>
<li>
<a class="text-center celery-task-log" onclick="window.open('{% url 'ops:celery-task-log' pk=object.latest_history.pk %}','', 'width=800,height=600')"><i class="fa fa-laptop"></i> {% trans 'Last run output' %} </a>
</li>
</ul> </ul>
</div> </div>
<div class="tab-content"> <div class="tab-content">
@@ -160,6 +163,5 @@
</div> </div>
</div> </div>
</div> </div>
{% include 'users/_user_update_pk_modal.html' %}
{% endblock %} {% endblock %}

View File

@@ -24,13 +24,16 @@
<li class="active"> <li class="active">
<a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a> <a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a>
</li> </li>
<li>
<a class="text-center celery-task-log" onclick="window.open('{% url 'ops:celery-task-log' pk=object.latest_history.pk %}','', 'width=800,height=600')"><i class="fa fa-laptop"></i> {% trans 'Last run output' %} </a>
</li>
</ul> </ul>
</div> </div>
<div class="tab-content"> <div class="tab-content">
<div class="col-sm-12" style="padding-left: 0"> <div class="col-sm-12" style="padding-left: 0">
<div class="ibox float-e-margins"> <div class="ibox float-e-margins">
<div class="ibox-title"> <div class="ibox-title">
<span style="float: left">{% trans 'History of ' %} <b>{{ object.task.name }}:{{ object.short_id }}</b></span> <span style="float: left">{% trans 'History of ' %} <b>{{ object.name }}:{{ object.short_id }}</b></span>
<div class="ibox-tools"> <div class="ibox-tools">
<a class="collapse-link"> <a class="collapse-link">
<i class="fa fa-chevron-up"></i> <i class="fa fa-chevron-up"></i>
@@ -85,7 +88,8 @@ function initTable() {
select: [], select: [],
columnDefs: [ columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) { {targets: 1, createdCell: function (td, cellData, rowData) {
$(td).html(cellData); var d = new Date(cellData);
$(td).html(d.toLocaleString());
}}, }},
{targets: 2, createdCell: function (td, cellData) { {targets: 2, createdCell: function (td, cellData) {
var total = "<span>" + cellData.total + "</span>"; var total = "<span>" + cellData.total + "</span>";

View File

@@ -1,8 +1,9 @@
{% extends '_base_list.html' %} {% extends '_base_list.html' %}
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% block content_left_head %} {% block content_left_head %}
<link href="{% static 'css/plugins/datepicker/datepicker3.css' %}" rel="stylesheet"> {# <div class="uc pull-left m-r-5"><a class="btn btn-sm btn-primary btn-create-asset"> {% trans "Create task" %} </a></div>#}
{% endblock %} {% endblock %}
@@ -111,9 +112,10 @@ $(document).ready(function() {
var error = function (data) { var error = function (data) {
alert(data) alert(data)
}; };
var success = function () { var success = function(data) {
alert("任务开始执行,重定向到任务详情页面,多刷新几次查看结果") var task_id = data.task;
window.location = "{% url 'ops:task-detail' pk=DEFAULT_PK %}".replace('{{ DEFAULT_PK }}', uid); var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
window.open(url, '', 'width=800,height=600')
}; };
APIUpdateAttr({ APIUpdateAttr({
url: the_url, url: the_url,

View File

@@ -15,6 +15,7 @@ router.register(r'v1/history', api.AdHocRunHistorySet, 'history')
urlpatterns = [ urlpatterns = [
url(r'^v1/tasks/(?P<pk>[0-9a-zA-Z\-]{36})/run/$', api.TaskRun.as_view(), name='task-run'), url(r'^v1/tasks/(?P<pk>[0-9a-zA-Z\-]{36})/run/$', api.TaskRun.as_view(), name='task-run'),
url(r'^v1/celery/task/(?P<pk>[0-9a-zA-Z\-]{36})/log/$', api.CeleryTaskLogApi.as_view(), name='celery-task-log'),
] ]
urlpatterns += router.urls urlpatterns += router.urls

Some files were not shown because too many files have changed in this diff Show More