From c2abd58dcb3379b543eea886a8fbbe1a197f398a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=B9=BF?= Date: Wed, 7 Mar 2018 21:21:56 +0800 Subject: [PATCH] Merge to dev (#1051) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 修改 success message, 添加资产组时可以添加资产 * [Update] system user form add label * [Update] set default cluster * [Update] 修改一些翻译 * [Bugfix] 修复重置密码bug * [Bugfix] 默认default cluster * [Bugfix] 用户添加报错 * 修改tab样式 * [Bugfix] 修复了一些显示上的bug * 修复全选按钮在搜索后仍然选择全部的问题 * [Bugfix] 修复以下bug 1. 查看执行历史异常 2. 用户授权资产页显示message * [Update] api 返回platform, 并增加web terminal nav * [Feature] 添加setting页面 * [Feature] 添加basic settings * [Update] 修改翻译 * [Update] 修改config * [Update] 启动加载common setting * [Bugfix] 修复cluster创建的bug * [Bugfix] 修复title显示Jumpserver * [Bugfix] setting tables not found * [Bugfix] settings add option * [Feature] 添加后端paging * [Bugfix] 资产列表选择别的页会报错 * [Update] check all 只选择当前页面 * [Bugfix] user login ip * [Bugfix] for login ip * [Bugfix] 修复资产列表显示bug * [Remove] labels * [Bugfix] task运行失败,因为tasks没有设置 * [Feature] 增加标签 * [Bugfix] 读取不到prefix * For storage * [Change] 修改部分翻译 * [Update] 启用ldap移动位置 * [Update] 修改翻译 * [Feature] 支持es存储命令 * Update README.md * [Feature] 添加es支持 * [update] 修改用户创建时 姓名和用户名的位置 * [Update] 修改install.md * [Update] remote default PAGE_SIZE stting * [Feature] terminal config load * [Feature] es support * [Update] 修改requirement * [Update] 修改requirements * [Update] 修改dictfiled * [Fix] 修改Logger * [Bugfix] 倒序显示 * [Update] 修改默认头像和logo * [Update] 修改django-celery-beat的版本 * [Feature] 添加修改用户密码api * add logo test * [Bugfix] 修复一些bug * [Update] 修改copyrite * [Update] 修改copyright * Update ISSUE_TEMPLATE.md * [Update] 修改禁止排序的颜色 * [Feature] 标签管理功能 * [Bugfix] git status * [Model] 修改create_by字段 * [Update] 修改位置 * [Update] 修改签名md5算法 * [Feature] 资产列表标签搜索 * [Feature] 添加资产详情标签 * [Bugfix] 修复资产搜索bug * [Update] ansible disk bug * [Update] ansible disk bug * [Bugfix] 修复获取kvmcpu的bug * [Bugfix] 修复bsd获取cpu数量bug * [Bugfix] 修改翻译 * [Bugfix] 资产model 太长 * [Bugfix] 修改项目结构描述 修正"项目多语言目录" * Update project_structure.md * [Update] add debug log * refactor: rename folder i18n * [Feature] 添加链接token * [Feature] Label 删除修改 * [Update] 修改部分翻译 * [Update] 修改小bug * [Update] 修复获取资产信息异常bug * [Bugfix] 修复系统用户上传秘钥的bug * [Update] 修改获取资产信息产生的异常 * [Update] 删除部分资产属性 * [Bugfix] 资产批量便捷 * [Update] 修改认证 * [Feature] 支持popover * [Feature] tree * [Feature] 添加资产树 * [Feature] 使用ztree * [Feature] tree增删功能 * [Bugfix] 修复组详情bug * [Bugfix] 修复组详情bug * [Bugfix] 修改创建label时报错的bug * [Bugfix] 修改label api bug * [Update] 去掉资产组添加 * [Update] 修改ztrr * Update README.md * [Update] 修改资产创建 * [Bugfix] 修复ldap认证bug * [Update] 修改一处翻译 * [Update] 更改授权规则前commit * [Abandon] ... * Update README.md * Update README.md * Update README.md * [Feature] 完成资产授权和资产添加 * [Update] 修改授权 * [Bugfix] 修改创建系统用户的bug * feat: rdp support * [Update] 拆分asset api module * [Update] 资产列表选中和移除资产 * [Feature] 更改perms api * [Update] 使用资产树,去掉集群和资产组 * [Update] 修改系统用户推送,拆分assets的部分模块 * [Update] 完成树形改造 * [Update] 完成资产书 * [Update] 修改资产model * ubuntu16.04 deb_requirements.txt update (#1007) * Update run server.py (#915) Fix for not callable error when config.py not exists * [Update]一些修改 * [Update] 修改初始 * feat: replay setting page and api * 增加隐藏树功能 * [Update] 修改翻译 * 对齐菜单文字。修改英文 * feat: update app setting * fix: app get replay storage * [Update] 修改文案 * [Docs] 初始化doc * [Bugfix] 用户csv导入编码问题 * [Update] 修改设置的一些require * [Bugfix] 修复管理用户无法查看的bug * [Update] 修改授权api, windows资产只有rdp协议,linux只有ssh协议 * [Update] terminal可以更改名称 * [Update] 统一copyright * [Update] 修改文档 * [Bugfix] 修复资产禁用还可以登录 * [Update] 修改文案 * [Update] 支持拖拽更新 * [Bugfix] 修复bug,修改celery beat版本依赖 * [Update] 修改一些小问题 * 添加普通用户使用内容 * [Update] 修改一些文案 * Update README.md * Update README.md * Update README.md * 用户列表 * [Update] 修改一些bug和文案 * [Delete] 删除build 页面 * [Update] 修改conf * [Update] bugfix * [Update] 更新文档地址 * [Update] 修改部分翻译和文档 * [Update] 修改一些bug * [Update] 修改链接 * [Update] 增加批量终端session api * [Update] 修改Node value唯一 * [Bugfix] 修复首页无法显示数据的bug * feat: s3 replay file get * feat: update * [Update] 修改bug --- .github/ISSUE_TEMPLATE.md | 2 +- .gitignore | 3 +- README.md | 30 +- apps/assets/api.py | 297 --- apps/assets/api/__init__.py | 5 + apps/assets/api/admin_user.py | 76 + apps/assets/api/asset.py | 112 + apps/assets/api/label.py | 38 + apps/assets/api/node.py | 119 + apps/assets/api/system_user.py | 85 + apps/assets/forms.py | 379 ---- apps/assets/forms/__init__.py | 5 + apps/assets/forms/asset.py | 133 ++ apps/assets/forms/label.py | 33 + apps/assets/forms/user.py | 135 ++ apps/assets/hands.py | 9 +- apps/assets/models/__init__.py | 2 + apps/assets/models/asset.py | 81 +- apps/assets/models/group.py | 2 +- apps/assets/models/label.py | 37 + apps/assets/models/node.py | 119 + apps/assets/models/user.py | 45 +- apps/assets/serializers.py | 286 --- apps/assets/serializers/__init__.py | 8 + apps/assets/serializers/admin_user.py | 52 + apps/assets/serializers/asset.py | 62 + apps/assets/serializers/cluster.py | 46 + apps/assets/serializers/label.py | 37 + apps/assets/serializers/node.py | 72 + apps/assets/serializers/system_user.py | 54 + apps/assets/signals.py | 1 - apps/assets/signals_handler.py | 142 +- apps/assets/tasks.py | 172 +- .../_asset_group_bulk_update_modal.html | 1 - .../templates/assets/_asset_list_modal.html | 132 ++ .../assets/templates/assets/_system_user.html | 27 +- .../templates/assets/admin_user_assets.html | 24 +- .../assets/admin_user_create_update.html | 2 +- .../templates/assets/admin_user_detail.html | 62 +- .../templates/assets/admin_user_list.html | 5 +- .../assets/templates/assets/asset_create.html | 60 +- .../assets/templates/assets/asset_detail.html | 103 +- .../templates/assets/asset_group_detail.html | 244 --- .../templates/assets/asset_group_list.html | 164 -- apps/assets/templates/assets/asset_list.html | 491 ++++- .../templates/assets/asset_modal_list.html | 124 -- .../assets/templates/assets/asset_update.html | 60 +- .../templates/assets/cluster_assets.html | 215 -- .../assets/cluster_create_update.html | 76 - .../templates/assets/cluster_detail.html | 164 -- .../assets/templates/assets/cluster_list.html | 119 - ...p_create.html => label_create_update.html} | 2 +- apps/assets/templates/assets/label_list.html | 70 + .../templates/assets/system_user_asset.html | 3 - .../templates/assets/system_user_detail.html | 115 +- .../templates/assets/system_user_list.html | 5 +- .../templates/assets/system_user_update.html | 2 +- .../templates/assets/user_asset_list.html | 9 +- apps/assets/templatetags/__init__.py | 2 + apps/assets/templatetags/asset_tags.py | 10 +- apps/assets/urls/api_urls.py | 30 +- apps/assets/urls/views_urls.py | 22 +- apps/assets/utils.py | 37 +- apps/assets/views/__init__.py | 4 +- apps/assets/views/admin_user.py | 12 +- apps/assets/views/asset.py | 100 +- apps/assets/views/cluster.py | 116 - apps/assets/views/group.py | 97 - apps/assets/views/label.py | 72 + apps/assets/views/system_user.py | 9 +- apps/common/api.py | 13 +- apps/common/fields.py | 18 +- apps/common/forms.py | 65 +- apps/common/mixins.py | 9 +- apps/common/models.py | 25 +- .../templates/common/basic_setting.html | 3 + .../templates/common/email_setting.html | 3 + .../common/templates/common/ldap_setting.html | 3 + .../templates/common/terminal_setting.html | 150 ++ apps/common/templatetags/common_tags.py | 28 +- apps/common/urls/view_urls.py | 1 + apps/common/utils.py | 3 +- apps/common/views.py | 38 +- apps/i18n/zh/LC_MESSAGES/django.mo | Bin 0 -> 29810 bytes .../{locale => i18n}/zh/LC_MESSAGES/django.po | 1932 ++++++++--------- apps/jumpserver/settings.py | 39 +- apps/jumpserver/urls.py | 4 +- apps/jumpserver/views.py | 43 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 31661 -> 0 bytes apps/ops/utils.py | 2 +- apps/perms/api.py | 357 +-- apps/perms/apps.py | 4 + apps/perms/forms.py | 97 +- apps/perms/hands.py | 4 +- apps/perms/models.py | 19 + apps/perms/serializers.py | 45 +- apps/perms/signals_handler.py | 18 + apps/perms/tasks.py | 3 - .../perms/asset_permission_asset.html | 10 +- .../perms/asset_permission_create_update.html | 30 +- .../perms/asset_permission_list.html | 240 +- .../perms/asset_permission_user.html | 10 +- apps/perms/urls/api_urls.py | 22 +- apps/perms/urls/views_urls.py | 6 +- apps/perms/utils.py | 274 +-- apps/perms/views.py | 117 +- apps/static/css/jumpserver.css | 324 ++- .../plugins/datatables/datatables.min.css.bak | 39 + apps/static/css/plugins/jstree/32px.png | Bin 0 -> 3121 bytes apps/static/css/plugins/jstree/39px.png | Bin 0 -> 5593 bytes apps/static/css/plugins/jstree/40px.png | Bin 0 -> 1880 bytes apps/static/css/plugins/jstree/style.css | 1050 +++++++++ apps/static/css/plugins/jstree/style.min.css | 1 + apps/static/css/plugins/jstree/throbber.gif | Bin 0 -> 1720 bytes .../plugins/ztree/awesomeStyle/awesome.css | 388 ++++ .../plugins/ztree/awesomeStyle/awesome.less | 150 ++ .../css/plugins/ztree/awesomeStyle/fa.css | 0 .../css/plugins/ztree/awesomeStyle/fa.less | 480 ++++ .../ztree/awesomeStyle/img/loading.gif | Bin 0 -> 381 bytes apps/static/css/plugins/ztree/demo.css | 33 + .../ztree/metroStyle/img/line_conn.png | Bin 0 -> 933 bytes .../plugins/ztree/metroStyle/img/loading.gif | Bin 0 -> 381 bytes .../plugins/ztree/metroStyle/img/metro.gif | Bin 0 -> 4679 bytes .../plugins/ztree/metroStyle/img/metro.png | Bin 0 -> 5283 bytes .../plugins/ztree/metroStyle/metroStyle.css | 96 + .../ztree/ztreestyle/img/diy/1_close.png | Bin 0 -> 601 bytes .../ztree/ztreestyle/img/diy/1_open.png | Bin 0 -> 580 bytes .../plugins/ztree/ztreestyle/img/diy/2.png | Bin 0 -> 570 bytes .../plugins/ztree/ztreestyle/img/diy/3.png | Bin 0 -> 762 bytes .../plugins/ztree/ztreestyle/img/diy/4.png | Bin 0 -> 399 bytes .../plugins/ztree/ztreestyle/img/diy/5.png | Bin 0 -> 710 bytes .../plugins/ztree/ztreestyle/img/diy/6.png | Bin 0 -> 432 bytes .../plugins/ztree/ztreestyle/img/diy/7.png | Bin 0 -> 534 bytes .../plugins/ztree/ztreestyle/img/diy/8.png | Bin 0 -> 529 bytes .../plugins/ztree/ztreestyle/img/diy/9.png | Bin 0 -> 467 bytes .../ztree/ztreestyle/img/line_conn.gif | Bin 0 -> 45 bytes .../plugins/ztree/ztreestyle/img/loading.gif | Bin 0 -> 381 bytes .../ztree/ztreestyle/img/zTreeStandard.gif | Bin 0 -> 5564 bytes .../ztree/ztreestyle/img/zTreeStandard.png | Bin 0 -> 11173 bytes .../plugins/ztree/ztreestyle/ztreestyle.css | 97 + apps/static/css/style.css | 4 +- apps/static/img/avatar/admin.png | Bin 108909 -> 11750 bytes apps/static/img/avatar/user.png | Bin 62768 -> 11750 bytes apps/static/img/logo-text.png | Bin 0 -> 18125 bytes apps/static/js/jumpserver.js | 98 +- apps/static/js/plugins/echarts/chart/tree.js | 2 +- .../js/plugins/echarts/chart/treemap.js | 2 +- apps/static/js/plugins/echarts/echarts-all.js | 4 +- .../plugins/highcharts/modules/heatmap.src.js | 2 +- apps/static/js/plugins/jstree/jstree.min.js | 5 + .../js/plugins/ztree/jquery-1.4.4.min.js | 167 ++ .../js/plugins/ztree/jquery.ztree.all.min.js | 167 ++ apps/templates/_copyright.html | 1 + apps/templates/_footer.html | 2 +- apps/templates/_header_bar.html | 29 +- apps/templates/_message.html | 13 +- apps/templates/_nav.html | 41 +- apps/templates/_nav_user.html | 6 +- apps/templates/_user_profile.html | 7 +- apps/templates/base.html | 2 +- apps/templates/flash_message_standalone.html | 4 +- apps/templates/index.html | 10 +- apps/terminal/api.py | 74 +- apps/terminal/backends/__init__.py | 36 +- apps/terminal/backends/command/base.py | 6 + apps/terminal/backends/command/db.py | 36 +- apps/terminal/backends/command/es.py | 38 + apps/terminal/backends/command/models.py | 21 + apps/terminal/backends/command/multi.py | 27 + apps/terminal/forms.py | 34 +- apps/terminal/models.py | 33 +- apps/terminal/serializers.py | 14 +- apps/terminal/tasks.py | 30 +- .../templates/terminal/session_list.html | 50 +- .../templates/terminal/terminal_list.html | 2 +- .../terminal/terminal_modal_accept.html | 2 + .../templates/terminal/terminal_update.html | 2 + apps/terminal/templatetags/terminal_tags.py | 7 +- apps/terminal/urls/api_urls.py | 6 +- apps/terminal/views/command.py | 10 +- apps/terminal/views/session.py | 7 +- apps/users/api.py | 60 +- apps/users/apps.py | 3 + apps/users/forms.py | 1 - apps/users/hands.py | 2 +- apps/users/migrations/0001_initial.py | 3 + apps/users/models/authentication.py | 3 +- apps/users/models/group.py | 3 +- apps/users/models/user.py | 8 + apps/users/serializers.py | 12 +- apps/users/signals.py | 19 +- apps/users/signals_handler.py | 20 + apps/users/templates/users/_user.html | 8 +- .../templates/users/forgot_password.html | 7 +- apps/users/templates/users/login.html | 30 +- .../users/templates/users/reset_password.html | 7 +- apps/users/templates/users/user_detail.html | 27 +- .../templates/users/user_granted_asset.html | 242 ++- .../templates/users/user_group_detail.html | 6 +- .../users/user_group_granted_asset.html | 260 ++- .../templates/users/user_group_list.html | 8 +- apps/users/templates/users/user_list.html | 2 +- apps/users/templates/users/user_profile.html | 4 - apps/users/urls/api_urls.py | 3 + apps/users/utils.py | 5 +- apps/users/views/group.py | 9 +- apps/users/views/login.py | 26 +- apps/users/views/user.py | 32 +- docs/Makefile | 20 + docs/_static/img/logo-text.png | Bin 0 -> 12674 bytes docs/_static/img/structure.png | Bin 0 -> 29785 bytes docs/admin_asset.rst | 2 + docs/admin_guide.rst | 10 + docs/admin_user.rst | 51 + docs/api_style_guide.rst | 166 ++ docs/conf.py | 168 ++ docs/contact.rst | 33 + docs/contributor.rst | 13 + docs/development.rst | 12 + docs/faq.rst | 2 + docs/index.rst | 29 + docs/install.md | 129 -- docs/installation.rst | 9 + docs/intro.rst | 51 + docs/{ => old}/README.md | 0 docs/{ => old}/api_style_guide.md | 0 .../django_class_base_view_inheritance.py | 0 docs/old/install.md | 1 + docs/{ => old}/project_structure.md | 2 +- docs/{ => old}/python_style_guide.md | 0 docs/{ => old}/table_design.xml | 0 docs/project_structure.rst | 51 + docs/python_style_guide.rst | 216 ++ docs/quickstart.rst | 48 + docs/step_by_step.rst | 294 +++ docs/upgrade.rst | 18 + docs/user_asset.rst | 27 + docs/user_guide.rst | 10 + docs/user_info.rst | 34 + requirements/deb_requirements.txt | 2 +- requirements/requirements.txt | 4 +- run_server.py | 2 +- 242 files changed, 9461 insertions(+), 5516 deletions(-) delete mode 100644 apps/assets/api.py create mode 100644 apps/assets/api/__init__.py create mode 100644 apps/assets/api/admin_user.py create mode 100644 apps/assets/api/asset.py create mode 100644 apps/assets/api/label.py create mode 100644 apps/assets/api/node.py create mode 100644 apps/assets/api/system_user.py delete mode 100644 apps/assets/forms.py create mode 100644 apps/assets/forms/__init__.py create mode 100644 apps/assets/forms/asset.py create mode 100644 apps/assets/forms/label.py create mode 100644 apps/assets/forms/user.py create mode 100644 apps/assets/models/label.py create mode 100644 apps/assets/models/node.py delete mode 100644 apps/assets/serializers.py create mode 100644 apps/assets/serializers/__init__.py create mode 100644 apps/assets/serializers/admin_user.py create mode 100644 apps/assets/serializers/asset.py create mode 100644 apps/assets/serializers/cluster.py create mode 100644 apps/assets/serializers/label.py create mode 100644 apps/assets/serializers/node.py create mode 100644 apps/assets/serializers/system_user.py create mode 100644 apps/assets/templates/assets/_asset_list_modal.html delete mode 100644 apps/assets/templates/assets/asset_group_detail.html delete mode 100644 apps/assets/templates/assets/asset_group_list.html delete mode 100644 apps/assets/templates/assets/asset_modal_list.html delete mode 100644 apps/assets/templates/assets/cluster_assets.html delete mode 100644 apps/assets/templates/assets/cluster_create_update.html delete mode 100644 apps/assets/templates/assets/cluster_detail.html delete mode 100644 apps/assets/templates/assets/cluster_list.html rename apps/assets/templates/assets/{asset_group_create.html => label_create_update.html} (92%) create mode 100644 apps/assets/templates/assets/label_list.html create mode 100644 apps/assets/templatetags/__init__.py delete mode 100644 apps/assets/views/cluster.py delete mode 100644 apps/assets/views/group.py create mode 100644 apps/assets/views/label.py create mode 100644 apps/common/templates/common/terminal_setting.html create mode 100644 apps/i18n/zh/LC_MESSAGES/django.mo rename apps/{locale => i18n}/zh/LC_MESSAGES/django.po (63%) delete mode 100644 apps/locale/zh/LC_MESSAGES/django.mo create mode 100644 apps/perms/signals_handler.py create mode 100644 apps/static/css/plugins/datatables/datatables.min.css.bak create mode 100755 apps/static/css/plugins/jstree/32px.png create mode 100755 apps/static/css/plugins/jstree/39px.png create mode 100755 apps/static/css/plugins/jstree/40px.png create mode 100755 apps/static/css/plugins/jstree/style.css create mode 100755 apps/static/css/plugins/jstree/style.min.css create mode 100755 apps/static/css/plugins/jstree/throbber.gif create mode 100644 apps/static/css/plugins/ztree/awesomeStyle/awesome.css create mode 100644 apps/static/css/plugins/ztree/awesomeStyle/awesome.less create mode 100644 apps/static/css/plugins/ztree/awesomeStyle/fa.css create mode 100644 apps/static/css/plugins/ztree/awesomeStyle/fa.less create mode 100644 apps/static/css/plugins/ztree/awesomeStyle/img/loading.gif create mode 100644 apps/static/css/plugins/ztree/demo.css create mode 100644 apps/static/css/plugins/ztree/metroStyle/img/line_conn.png create mode 100644 apps/static/css/plugins/ztree/metroStyle/img/loading.gif create mode 100644 apps/static/css/plugins/ztree/metroStyle/img/metro.gif create mode 100644 apps/static/css/plugins/ztree/metroStyle/img/metro.png create mode 100644 apps/static/css/plugins/ztree/metroStyle/metroStyle.css create mode 100644 apps/static/css/plugins/ztree/ztreestyle/img/diy/1_close.png create mode 100644 apps/static/css/plugins/ztree/ztreestyle/img/diy/1_open.png create mode 100644 apps/static/css/plugins/ztree/ztreestyle/img/diy/2.png create mode 100644 apps/static/css/plugins/ztree/ztreestyle/img/diy/3.png create mode 100644 apps/static/css/plugins/ztree/ztreestyle/img/diy/4.png create mode 100644 apps/static/css/plugins/ztree/ztreestyle/img/diy/5.png create mode 100644 apps/static/css/plugins/ztree/ztreestyle/img/diy/6.png create mode 100644 apps/static/css/plugins/ztree/ztreestyle/img/diy/7.png create mode 100644 apps/static/css/plugins/ztree/ztreestyle/img/diy/8.png create mode 100644 apps/static/css/plugins/ztree/ztreestyle/img/diy/9.png create mode 100644 apps/static/css/plugins/ztree/ztreestyle/img/line_conn.gif create mode 100644 apps/static/css/plugins/ztree/ztreestyle/img/loading.gif create mode 100644 apps/static/css/plugins/ztree/ztreestyle/img/zTreeStandard.gif create mode 100644 apps/static/css/plugins/ztree/ztreestyle/img/zTreeStandard.png create mode 100644 apps/static/css/plugins/ztree/ztreestyle/ztreestyle.css create mode 100644 apps/static/img/logo-text.png create mode 100755 apps/static/js/plugins/jstree/jstree.min.js create mode 100644 apps/static/js/plugins/ztree/jquery-1.4.4.min.js create mode 100644 apps/static/js/plugins/ztree/jquery.ztree.all.min.js create mode 100644 apps/templates/_copyright.html create mode 100644 apps/terminal/backends/command/es.py create mode 100644 apps/terminal/backends/command/multi.py create mode 100644 apps/users/signals_handler.py create mode 100644 docs/Makefile create mode 100644 docs/_static/img/logo-text.png create mode 100644 docs/_static/img/structure.png create mode 100644 docs/admin_asset.rst create mode 100644 docs/admin_guide.rst create mode 100644 docs/admin_user.rst create mode 100644 docs/api_style_guide.rst create mode 100644 docs/conf.py create mode 100644 docs/contact.rst create mode 100644 docs/contributor.rst create mode 100644 docs/development.rst create mode 100644 docs/faq.rst create mode 100644 docs/index.rst delete mode 100644 docs/install.md create mode 100644 docs/installation.rst create mode 100644 docs/intro.rst rename docs/{ => old}/README.md (100%) rename docs/{ => old}/api_style_guide.md (100%) rename docs/{ => old}/django_class_base_view_inheritance.py (100%) create mode 100644 docs/old/install.md rename docs/{ => old}/project_structure.md (97%) rename docs/{ => old}/python_style_guide.md (100%) rename docs/{ => old}/table_design.xml (100%) create mode 100644 docs/project_structure.rst create mode 100644 docs/python_style_guide.rst create mode 100644 docs/quickstart.rst create mode 100644 docs/step_by_step.rst create mode 100644 docs/upgrade.rst create mode 100644 docs/user_asset.rst create mode 100644 docs/user_guide.rst create mode 100644 docs/user_info.rst diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 7690516f9..baec216a2 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,7 +1,7 @@ [简述你的问题] ##### 使用版本 -[请提供你使用的Jumpserver版本 0.3.2 或 0.4.0] +[请提供你使用的Jumpserver版本 0.3.2 或 0.5.0] ##### 问题复现步骤 1. [步骤1] diff --git a/.gitignore b/.gitignore index 38830de50..abf165e2e 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ media celerybeat.pid django.db celerybeat-schedule.db -static +data/static +_build/ diff --git a/README.md b/README.md index 0177c1abe..818294247 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,18 @@ [![Ansible](https://img.shields.io/badge/ansible-2.2.2.0-blue.svg?style=plastic)](https://www.ansible.com/) [![Paramiko](https://img.shields.io/badge/paramiko-2.1.2-green.svg?style=plastic)](http://www.paramiko.org/) -Jumpserver is a open source proxy server, developed by `Python` and `Django`, aim to help -companies to efficiently user, assets, authority and audit management -Jumpserver是一款使用Python, Django开发的开源跳板机系统, 助力互联网企业高效 用户、资产、权限、审计 管理 +---- + +Jumpserver是全球首款完全开源的堡垒机,使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计系统。 + +Jumpserver使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。 + +Jumpserver采纳分布式架构,支持多机房跨区域部署,中心节点提供 API,各机房部署登录节点,可横向扩展、无并发访问限制。 + +改变世界,从一点点开始。 + +---- ### Feature 功能 - Auth 统一认证 @@ -24,9 +32,16 @@ Jumpserver是一款使用Python, Django开发的开源跳板机系统, 助力互 * Python 3.6 * Django 1.11 -### Install 安装 +### 快速启动 -    [详细安装](https://github.com/jumpserver/jumpserver/wiki/v0.5.0-%E5%9F%BA%E4%BA%8E-CentOS7) +``` +$ docker run -p 8080:80 -p 2222:2222 jumpserver/jumpserver:0.5.0-beta2 +``` +更多见 [Dockerfile](https://github.com/jumpserver/Dockerfile.git) + +### 详细安装步骤 + +    [文档](https://github.com/jumpserver/jumpserver/wiki/v0.5.0-%E5%9F%BA%E4%BA%8E-CentOS7) ### Usage 使用 @@ -70,6 +85,11 @@ demo使用了开发者模式,并发只能为1 参见 https://github.com/jumpserver/jumpserver/milestone/2 +### SDK + +- python: https://github.com/jumpserver/jumpserver-python-sdk +- java: https://github.com/KaiJunYan/jumpserver-java-sdk.git + ### Docs 开发者文档 diff --git a/apps/assets/api.py b/apps/assets/api.py deleted file mode 100644 index 5f1c73bf0..000000000 --- a/apps/assets/api.py +++ /dev/null @@ -1,297 +0,0 @@ -# ~*~ coding: utf-8 ~*~ -# Copyright (C) 2014-2017 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved. -# -# Licensed under the GNU General Public License v2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.gnu.org/licenses/gpl-2.0.html -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from rest_framework import generics -from rest_framework.response import Response -from rest_framework_bulk import BulkModelViewSet -from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView -from django.shortcuts import get_object_or_404 -from django.db.models import Q, Count -from rest_framework.pagination import LimitOffsetPagination - -from common.mixins import CustomFilterMixin -from common.utils import get_logger -from .hands import IsSuperUser, IsValidUser, IsSuperUserOrAppUser, \ - get_user_granted_assets -from .models import AssetGroup, Asset, Cluster, SystemUser, AdminUser -from . import serializers -from .tasks import update_asset_hardware_info_manual, test_admin_user_connectability_manual, \ - test_asset_connectability_manual, push_system_user_to_cluster_assets_manual, \ - test_system_user_connectability_manual - - -logger = get_logger(__file__) - - -class AssetViewSet(CustomFilterMixin, BulkModelViewSet): - """ - API endpoint that allows Asset to be viewed or edited. - """ - filter_fields = ("hostname", "ip") - search_fields = filter_fields - ordering_fields = ("hostname", "ip", "port", "cluster", "type", "env", "cpu_cores") - queryset = Asset.objects.all() - serializer_class = serializers.AssetSerializer - pagination_class = LimitOffsetPagination - permission_classes = (IsSuperUserOrAppUser,) - - def get_queryset(self): - queryset = super().get_queryset() - cluster_id = self.request.query_params.get('cluster_id') - asset_group_id = self.request.query_params.get('asset_group_id') - admin_user_id = self.request.query_params.get('admin_user_id') - system_user_id = self.request.query_params.get('system_user_id') - - if cluster_id: - queryset = queryset.filter(cluster__id=cluster_id) - if asset_group_id: - queryset = queryset.filter(groups__id=asset_group_id) - if admin_user_id: - admin_user = get_object_or_404(AdminUser, id=admin_user_id) - assets_direct = [asset.id for asset in admin_user.asset_set.all()] - clusters = [cluster.id for cluster in admin_user.cluster_set.all()] - queryset = queryset.filter(Q(cluster__id__in=clusters)|Q(id__in=assets_direct)) - if system_user_id: - system_user = get_object_or_404(SystemUser, id=system_user_id) - clusters = system_user.get_clusters() - queryset = queryset.filter(cluster__in=clusters) - return queryset - - -class UserAssetListView(generics.ListAPIView): - queryset = Asset.objects.all() - serializer_class = serializers.AssetSerializer - permission_classes = (IsValidUser,) - - def get_queryset(self): - assets_granted = get_user_granted_assets(self.request.user) - queryset = self.queryset.filter( - id__in=[asset.id for asset in assets_granted] - ) - return queryset - - -class AssetGroupViewSet(CustomFilterMixin, BulkModelViewSet): - """ - Asset group api set, for add,delete,update,list,retrieve resource - """ - queryset = AssetGroup.objects.all().annotate(asset_count=Count("assets")) - serializer_class = serializers.AssetGroupSerializer - permission_classes = (IsSuperUser,) - - -class GroupUpdateAssetsApi(generics.RetrieveUpdateAPIView): - """ - Asset group, update it's asset member - """ - queryset = AssetGroup.objects.all() - serializer_class = serializers.GroupUpdateAssetsSerializer - permission_classes = (IsSuperUser,) - - -class GroupAddAssetsApi(generics.UpdateAPIView): - queryset = AssetGroup.objects.all() - serializer_class = serializers.GroupUpdateAssetsSerializer - permission_classes = (IsSuperUser,) - - def update(self, request, *args, **kwargs): - group = self.get_object() - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - assets = serializer.validated_data['assets'] - group.assets.add(*tuple(assets)) - return Response({"msg": "ok"}) - else: - return Response({'error': serializer.errors}, status=400) - - -class ClusterViewSet(CustomFilterMixin, BulkModelViewSet): - """ - Cluster api set, for add,delete,update,list,retrieve resource - """ - queryset = Cluster.objects.all() - serializer_class = serializers.ClusterSerializer - permission_classes = (IsSuperUser,) - - -class ClusterTestAssetsAliveApi(generics.RetrieveAPIView): - """ - Test cluster asset can connect using admin user or not - """ - queryset = Cluster.objects.all() - permission_classes = (IsSuperUser,) - - def retrieve(self, request, *args, **kwargs): - cluster = self.get_object() - admin_user = cluster.admin_user - test_admin_user_connectability_manual.delay(admin_user) - return Response("Task has been send, seen left assets status") - - -class ClusterAddAssetsApi(generics.UpdateAPIView): - queryset = Cluster.objects.all() - serializer_class = serializers.ClusterUpdateAssetsSerializer - permission_classes = (IsSuperUser,) - - def update(self, request, *args, **kwargs): - cluster = self.get_object() - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - assets = serializer.validated_data['assets'] - for asset in assets: - asset.cluster = cluster - asset.save() - return Response({"msg": "ok"}) - else: - return Response({'error': serializer.errors}, status=400) - - -class AdminUserViewSet(CustomFilterMixin, BulkModelViewSet): - """ - Admin user api set, for add,delete,update,list,retrieve resource - """ - queryset = AdminUser.objects.all() - serializer_class = serializers.AdminUserSerializer - permission_classes = (IsSuperUser,) - - -class AdminUserAddClustersApi(generics.UpdateAPIView): - queryset = AdminUser.objects.all() - serializer_class = serializers.AdminUserUpdateClusterSerializer - permission_classes = (IsSuperUser,) - - def update(self, request, *args, **kwargs): - admin_user = self.get_object() - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - clusters = serializer.validated_data['clusters'] - for cluster in clusters: - cluster.admin_user = admin_user - cluster.save() - return Response({"msg": "ok"}) - else: - return Response({'error': serializer.errors}, status=400) - - -class SystemUserViewSet(BulkModelViewSet): - """ - System user api set, for add,delete,update,list,retrieve resource - """ - queryset = SystemUser.objects.all() - serializer_class = serializers.SystemUserSerializer - permission_classes = (IsSuperUserOrAppUser,) - - -class AssetListUpdateApi(CustomFilterMixin, ListBulkCreateUpdateDestroyAPIView): - """ - Asset bulk update api - """ - queryset = Asset.objects.all() - serializer_class = serializers.AssetSerializer - permission_classes = (IsSuperUser,) - - -class SystemUserAuthInfoApi(generics.RetrieveAPIView): - """ - Get system user auth info - """ - queryset = SystemUser.objects.all() - permission_classes = (IsSuperUserOrAppUser,) - - def retrieve(self, request, *args, **kwargs): - system_user = self.get_object() - data = { - 'id': system_user.id, - 'name': system_user.name, - 'username': system_user.username, - 'password': system_user.password, - 'private_key': system_user.private_key, - } - return Response(data) - - -class AssetRefreshHardwareApi(generics.RetrieveAPIView): - """ - Refresh asset hardware info - """ - queryset = Asset.objects.all() - serializer_class = serializers.AssetSerializer - permission_classes = (IsSuperUser,) - - def retrieve(self, request, *args, **kwargs): - asset_id = kwargs.get('pk') - asset = get_object_or_404(Asset, pk=asset_id) - summary = update_asset_hardware_info_manual(asset)[1] - logger.debug("Refresh summary: {}".format(summary)) - if summary.get('dark'): - return Response(summary['dark'].values(), status=501) - else: - return Response({"msg": "ok"}) - - -class AssetAdminUserTestApi(generics.RetrieveAPIView): - """ - Test asset admin user connectivity - """ - queryset = Asset.objects.all() - permission_classes = (IsSuperUser,) - - def retrieve(self, request, *args, **kwargs): - asset_id = kwargs.get('pk') - asset = get_object_or_404(Asset, pk=asset_id) - ok, msg = test_asset_connectability_manual(asset) - if ok: - return Response({"msg": "pong"}) - else: - return Response({"error": msg}, status=502) - - -class AdminUserTestConnectiveApi(generics.RetrieveAPIView): - """ - Test asset admin user connectivity - """ - queryset = AdminUser.objects.all() - permission_classes = (IsSuperUser,) - - def retrieve(self, request, *args, **kwargs): - admin_user = self.get_object() - test_admin_user_connectability_manual.delay(admin_user) - return Response({"msg": "Task created"}) - - -class SystemUserPushApi(generics.RetrieveAPIView): - """ - Push system user to cluster assets api - """ - queryset = SystemUser.objects.all() - permission_classes = (IsSuperUser,) - - def retrieve(self, request, *args, **kwargs): - system_user = self.get_object() - push_system_user_to_cluster_assets_manual.delay(system_user) - return Response({"msg": "Task created"}) - - -class SystemUserTestConnectiveApi(generics.RetrieveAPIView): - """ - Push system user to cluster assets api - """ - queryset = SystemUser.objects.all() - permission_classes = (IsSuperUser,) - - def retrieve(self, request, *args, **kwargs): - system_user = self.get_object() - test_system_user_connectability_manual.delay(system_user) - return Response({"msg": "Task created"}) diff --git a/apps/assets/api/__init__.py b/apps/assets/api/__init__.py new file mode 100644 index 000000000..4a41c0706 --- /dev/null +++ b/apps/assets/api/__init__.py @@ -0,0 +1,5 @@ +from .admin_user import * +from .asset import * +from .label import * +from .system_user import * +from .node import * diff --git a/apps/assets/api/admin_user.py b/apps/assets/api/admin_user.py new file mode 100644 index 000000000..a69d771b3 --- /dev/null +++ b/apps/assets/api/admin_user.py @@ -0,0 +1,76 @@ +# ~*~ coding: utf-8 ~*~ +# Copyright (C) 2014-2018 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved. +# +# Licensed under the GNU General Public License v2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.gnu.org/licenses/gpl-2.0.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from django.db import transaction +from rest_framework import generics +from rest_framework.response import Response +from rest_framework_bulk import BulkModelViewSet + +from common.mixins import IDInFilterMixin +from common.utils import get_logger +from ..hands import IsSuperUser +from ..models import AdminUser, Asset +from .. import serializers +from ..tasks import test_admin_user_connectability_manual + + +logger = get_logger(__file__) +__all__ = [ + 'AdminUserViewSet', 'ReplaceNodesAdminUserApi', 'AdminUserTestConnectiveApi' +] + + +class AdminUserViewSet(IDInFilterMixin, BulkModelViewSet): + """ + Admin user api set, for add,delete,update,list,retrieve resource + """ + queryset = AdminUser.objects.all() + serializer_class = serializers.AdminUserSerializer + permission_classes = (IsSuperUser,) + + +class ReplaceNodesAdminUserApi(generics.UpdateAPIView): + queryset = AdminUser.objects.all() + serializer_class = serializers.ReplaceNodeAdminUserSerializer + permission_classes = (IsSuperUser,) + + def update(self, request, *args, **kwargs): + admin_user = self.get_object() + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + nodes = serializer.validated_data['nodes'] + assets = [] + for node in nodes: + assets.extend([asset.id for asset in node.get_all_assets()]) + + with transaction.atomic(): + Asset.objects.filter(id__in=assets).update(admin_user=admin_user) + + return Response({"msg": "ok"}) + else: + return Response({'error': serializer.errors}, status=400) + + +class AdminUserTestConnectiveApi(generics.RetrieveAPIView): + """ + Test asset admin user connectivity + """ + queryset = AdminUser.objects.all() + permission_classes = (IsSuperUser,) + + def retrieve(self, request, *args, **kwargs): + admin_user = self.get_object() + test_admin_user_connectability_manual.delay(admin_user) + return Response({"msg": "Task created"}) \ No newline at end of file diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py new file mode 100644 index 000000000..405beaf53 --- /dev/null +++ b/apps/assets/api/asset.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +# + +from rest_framework import generics +from rest_framework.response import Response +from rest_framework_bulk import BulkModelViewSet +from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView +from rest_framework.pagination import LimitOffsetPagination +from django.shortcuts import get_object_or_404 +from django.db.models import Q + +from common.mixins import IDInFilterMixin +from common.utils import get_logger +from ..hands import IsSuperUser, IsValidUser, IsSuperUserOrAppUser, \ + NodePermissionUtil +from ..models import Asset, SystemUser, AdminUser, Node +from .. import serializers +from ..tasks import update_asset_hardware_info_manual, \ + test_asset_connectability_manual +from ..utils import LabelFilter + + +logger = get_logger(__file__) +__all__ = [ + 'AssetViewSet', 'UserAssetListView', 'AssetListUpdateApi', + 'AssetRefreshHardwareApi', 'AssetAdminUserTestApi' +] + + +class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet): + """ + API endpoint that allows Asset to be viewed or edited. + """ + filter_fields = ("hostname", "ip") + search_fields = filter_fields + ordering_fields = ("hostname", "ip", "port", "cpu_cores") + queryset = Asset.objects.all() + serializer_class = serializers.AssetSerializer + pagination_class = LimitOffsetPagination + permission_classes = (IsSuperUserOrAppUser,) + + def get_queryset(self): + queryset = super().get_queryset() + admin_user_id = self.request.query_params.get('admin_user_id') + node_id = self.request.query_params.get("node_id") + + if admin_user_id: + admin_user = get_object_or_404(AdminUser, id=admin_user_id) + queryset = queryset.filter(admin_user=admin_user) + if node_id: + node = get_object_or_404(Node, id=node_id) + if not node.is_root(): + queryset = queryset.filter(nodes__key__startswith=node.key).distinct() + return queryset + + +class UserAssetListView(generics.ListAPIView): + queryset = Asset.objects.all() + serializer_class = serializers.AssetSerializer + permission_classes = (IsValidUser,) + + def get_queryset(self): + assets_granted = NodePermissionUtil.get_user_assets(self.request.user).keys() + queryset = self.queryset.filter( + id__in=[asset.id for asset in assets_granted] + ) + return queryset + + +class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView): + """ + Asset bulk update api + """ + queryset = Asset.objects.all() + serializer_class = serializers.AssetSerializer + permission_classes = (IsSuperUser,) + + +class AssetRefreshHardwareApi(generics.RetrieveAPIView): + """ + Refresh asset hardware info + """ + queryset = Asset.objects.all() + serializer_class = serializers.AssetSerializer + permission_classes = (IsSuperUser,) + + def retrieve(self, request, *args, **kwargs): + asset_id = kwargs.get('pk') + asset = get_object_or_404(Asset, pk=asset_id) + summary = update_asset_hardware_info_manual(asset)[1] + logger.debug("Refresh summary: {}".format(summary)) + if summary.get('dark'): + return Response(summary['dark'].values(), status=501) + else: + return Response({"msg": "ok"}) + + +class AssetAdminUserTestApi(generics.RetrieveAPIView): + """ + Test asset admin user connectivity + """ + queryset = Asset.objects.all() + permission_classes = (IsSuperUser,) + + def retrieve(self, request, *args, **kwargs): + asset_id = kwargs.get('pk') + asset = get_object_or_404(Asset, pk=asset_id) + ok, msg = test_asset_connectability_manual(asset) + if ok: + return Response({"msg": "pong"}) + else: + return Response({"error": msg}, status=502) \ No newline at end of file diff --git a/apps/assets/api/label.py b/apps/assets/api/label.py new file mode 100644 index 000000000..858834d0a --- /dev/null +++ b/apps/assets/api/label.py @@ -0,0 +1,38 @@ +# ~*~ coding: utf-8 ~*~ +# Copyright (C) 2014-2018 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved. +# +# Licensed under the GNU General Public License v2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.gnu.org/licenses/gpl-2.0.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rest_framework_bulk import BulkModelViewSet +from django.db.models import Count + +from common.utils import get_logger +from ..hands import IsSuperUser +from ..models import Label +from .. import serializers + + +logger = get_logger(__file__) +__all__ = ['LabelViewSet'] + + +class LabelViewSet(BulkModelViewSet): + queryset = Label.objects.annotate(asset_count=Count("assets")) + permission_classes = (IsSuperUser,) + serializer_class = serializers.LabelSerializer + + def list(self, request, *args, **kwargs): + if request.query_params.get("distinct"): + self.serializer_class = serializers.LabelDistinctSerializer + self.queryset = self.queryset.values("name").distinct() + return super().list(request, *args, **kwargs) diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py new file mode 100644 index 000000000..78cd7a27a --- /dev/null +++ b/apps/assets/api/node.py @@ -0,0 +1,119 @@ +# ~*~ coding: utf-8 ~*~ +# Copyright (C) 2014-2018 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved. +# +# Licensed under the GNU General Public License v2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.gnu.org/licenses/gpl-2.0.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rest_framework import generics, mixins +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework_bulk import BulkModelViewSet +from django.utils.translation import ugettext_lazy as _ + +from common.utils import get_logger, get_object_or_none +from ..hands import IsSuperUser +from ..models import Node +from .. import serializers + + +logger = get_logger(__file__) +__all__ = [ + 'NodeViewSet', 'NodeChildrenApi', + 'NodeAddAssetsApi', 'NodeRemoveAssetsApi', + 'NodeAddChildrenApi', +] + + +class NodeViewSet(BulkModelViewSet): + queryset = Node.objects.all() + permission_classes = (IsSuperUser,) + serializer_class = serializers.NodeSerializer + + def perform_create(self, serializer): + child_key = Node.root().get_next_child_key() + serializer.validated_data["key"] = child_key + serializer.save() + + +class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView): + queryset = Node.objects.all() + permission_classes = (IsSuperUser,) + serializer_class = serializers.NodeSerializer + instance = None + + def post(self, request, *args, **kwargs): + if not request.data.get("value"): + request.data["value"] = _("New node {}").format( + Node.root().get_next_child_key().split(":")[-1] + ) + return super().post(request, *args, **kwargs) + + def create(self, request, *args, **kwargs): + instance = self.get_object() + value = request.data.get("value") + node = instance.create_child(value=value) + return Response( + {"id": node.id, "key": node.key, "value": node.value}, + status=201, + ) + + def get(self, request, *args, **kwargs): + instance = self.get_object() + if self.request.query_params.get("all"): + children = instance.get_all_children() + else: + children = instance.get_children() + response = [{"id": node.id, "key": node.key, "value": node.value} for node in children] + return Response(response, status=200) + + +class NodeAddChildrenApi(generics.UpdateAPIView): + queryset = Node.objects.all() + permission_classes = (IsSuperUser,) + serializer_class = serializers.NodeAddChildrenSerializer + instance = None + + def put(self, request, *args, **kwargs): + instance = self.get_object() + nodes_id = request.data.get("nodes") + children = [get_object_or_none(Node, id=pk) for pk in nodes_id] + for node in children: + if not node: + continue + node.parent = instance + node.save() + return Response("OK") + + +class NodeAddAssetsApi(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() + instance.assets.add(*tuple(assets)) + + +class NodeRemoveAssetsApi(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() + if instance != Node.root(): + instance.assets.remove(*tuple(assets)) diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py new file mode 100644 index 000000000..c34690076 --- /dev/null +++ b/apps/assets/api/system_user.py @@ -0,0 +1,85 @@ +# ~*~ coding: utf-8 ~*~ +# Copyright (C) 2014-2018 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved. +# +# Licensed under the GNU General Public License v2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.gnu.org/licenses/gpl-2.0.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rest_framework import generics +from rest_framework.response import Response +from rest_framework_bulk import BulkModelViewSet +from common.utils import get_logger +from ..hands import IsSuperUser, IsSuperUserOrAppUser +from ..models import SystemUser +from .. import serializers +from ..tasks import push_system_user_to_assets_manual, \ + test_system_user_connectability_manual + + +logger = get_logger(__file__) +__all__ = [ + 'SystemUserViewSet', 'SystemUserAuthInfoApi', + 'SystemUserPushApi', 'SystemUserTestConnectiveApi' +] + + +class SystemUserViewSet(BulkModelViewSet): + """ + System user api set, for add,delete,update,list,retrieve resource + """ + queryset = SystemUser.objects.all() + serializer_class = serializers.SystemUserSerializer + permission_classes = (IsSuperUserOrAppUser,) + + +class SystemUserAuthInfoApi(generics.RetrieveAPIView): + """ + Get system user auth info + """ + queryset = SystemUser.objects.all() + permission_classes = (IsSuperUserOrAppUser,) + + def retrieve(self, request, *args, **kwargs): + system_user = self.get_object() + data = { + 'id': system_user.id, + 'name': system_user.name, + 'username': system_user.username, + 'password': system_user.password, + 'private_key': system_user.private_key, + } + return Response(data) + + +class SystemUserPushApi(generics.RetrieveAPIView): + """ + Push system user to cluster assets api + """ + queryset = SystemUser.objects.all() + permission_classes = (IsSuperUser,) + + def retrieve(self, request, *args, **kwargs): + system_user = self.get_object() + push_system_user_to_assets_manual.delay(system_user) + return Response({"msg": "Task created"}) + + +class SystemUserTestConnectiveApi(generics.RetrieveAPIView): + """ + Push system user to cluster assets api + """ + queryset = SystemUser.objects.all() + permission_classes = (IsSuperUser,) + + def retrieve(self, request, *args, **kwargs): + system_user = self.get_object() + test_system_user_connectability_manual.delay(system_user) + return Response({"msg": "Task created"}) \ No newline at end of file diff --git a/apps/assets/forms.py b/apps/assets/forms.py deleted file mode 100644 index 824ef3fcd..000000000 --- a/apps/assets/forms.py +++ /dev/null @@ -1,379 +0,0 @@ -# coding:utf-8 -from django import forms -from django.utils.translation import gettext_lazy as _ - -from .models import Cluster, Asset, AssetGroup, AdminUser, SystemUser -from common.utils import validate_ssh_private_key, ssh_pubkey_gen, ssh_key_gen, get_logger - - -logger = get_logger(__file__) - - -class AssetCreateForm(forms.ModelForm): - - class Meta: - model = Asset - fields = [ - 'hostname', 'ip', 'public_ip', 'port', 'type', 'comment', - 'cluster', 'groups', 'status', 'env', 'is_active', - 'admin_user' - - ] - widgets = { - 'groups': forms.SelectMultiple(attrs={'class': 'select2', 'data-placeholder': _('Select asset groups')}), - 'cluster': forms.Select(attrs={'class': 'select2', 'data-placeholder': _('Select cluster')}), - 'admin_user': forms.Select(attrs={'class': 'select2', 'data-placeholder': _('Select admin user')}), - 'port': forms.TextInput() - } - help_texts = { - 'hostname': '* required', - 'ip': '* required', - 'port': '* required', - 'cluster': '* required', - 'admin_user': _('Host level admin user, If not set using cluster admin user default') - } - - def clean_admin_user(self): - cluster = self.cleaned_data.get('cluster') - admin_user = self.cleaned_data.get('admin_user') - if not admin_user and (cluster and not cluster.admin_user): - raise forms.ValidationError(_("You need set a admin user if cluster not have")) - return self.cleaned_data['admin_user'] - - -class AssetUpdateForm(forms.ModelForm): - class Meta: - model = Asset - fields = [ - 'hostname', 'ip', 'port', 'groups', "cluster", 'is_active', - 'type', 'env', 'status', 'public_ip', 'remote_card_ip', 'cabinet_no', - 'cabinet_pos', 'number', 'comment', 'admin_user', - ] - widgets = { - 'groups': forms.SelectMultiple(attrs={'class': 'select2', 'data-placeholder': _('Select asset groups')}), - 'admin_user': forms.Select(attrs={'class': 'select2', 'data-placeholder': _("Default using cluster admin user")}) - } - help_texts = { - 'hostname': '* required', - 'ip': '* required', - 'port': '* required', - 'cluster': '* required', - 'admin_user': _('Host level admin user, If not set using cluster admin user default') - } - - def clean_admin_user(self): - cluster = self.cleaned_data.get('cluster') - admin_user = self.cleaned_data.get('admin_user') - if not admin_user and (cluster and not cluster.admin_user): - raise forms.ValidationError(_("You need set a admin user if cluster not have")) - return self.cleaned_data['admin_user'] - - -class AssetBulkUpdateForm(forms.ModelForm): - assets = forms.ModelMultipleChoiceField( - required=True, - help_text='* required', - label=_('Select assets'), - queryset=Asset.objects.all(), - widget=forms.SelectMultiple( - attrs={ - 'class': 'select2', - 'data-placeholder': _('Select assets') - } - ) - ) - port = forms.IntegerField( - label=_('Port'), - required=False, - min_value=1, - max_value=65535, - ) - - class Meta: - model = Asset - fields = [ - 'assets', 'port', 'groups', "cluster", - 'type', 'env', - ] - widgets = { - 'groups': forms.SelectMultiple(attrs={'class': 'select2', 'data-placeholder': _('Select asset groups')}), - } - - def save(self, commit=True): - changed_fields = [] - for field in self._meta.fields: - if self.data.get(field) is not None: - changed_fields.append(field) - - cleaned_data = {k: v for k, v in self.cleaned_data.items() - if k in changed_fields} - assets = cleaned_data.pop('assets') - groups = cleaned_data.pop('groups', []) - assets = Asset.objects.filter(id__in=[asset.id for asset in assets]) - assets.update(**cleaned_data) - if groups: - for asset in assets: - asset.groups.set(groups) - return assets - - -class AssetGroupForm(forms.ModelForm): - # See AdminUserForm comment same it - assets = forms.ModelMultipleChoiceField( - queryset=Asset.objects.all(), - label=_('Asset'), - required=False, - widget=forms.SelectMultiple( - attrs={'class': 'select2', 'data-placeholder': _('Select assets')} - ) - ) - - def __init__(self, **kwargs): - instance = kwargs.get('instance') - if instance: - initial = kwargs.get('initial', {}) - initial.update({ - 'assets': instance.assets.all(), - }) - kwargs['initial'] = initial - super().__init__(**kwargs) - - def save(self, commit=True): - group = super().save(commit=commit) - assets= self.cleaned_data['assets'] - group.assets.set(assets) - return group - - class Meta: - model = AssetGroup - fields = [ - "name", "comment", - ] - help_texts = { - 'name': '* required', - } - - -class ClusterForm(forms.ModelForm): - system_users = forms.ModelMultipleChoiceField( - queryset=SystemUser.objects.all(), - widget=forms.SelectMultiple( - attrs={'class': 'select2', 'data-placeholder': _('Select system users')} - ), - label=_('System users'), - required=False, - help_text=_("Selected system users will be create at cluster assets"), - ) - - class Meta: - model = Cluster - fields = ['name', "bandwidth", "operator", 'contact', 'admin_user', 'system_users', - 'phone', 'address', 'intranet', 'extranet', 'comment'] - widgets = { - 'name': forms.TextInput(attrs={'placeholder': _('Name')}), - 'intranet': forms.Textarea(attrs={'placeholder': 'IP段之间用逗号隔开,如:192.168.1.0/24,192.168.1.0/24'}), - 'extranet': forms.Textarea(attrs={'placeholder': 'IP段之间用逗号隔开,如:201.1.32.1/24,202.2.32.1/24'}) - } - help_texts = { - 'name': '* required', - 'admin_user': _("Cluster level admin user"), - } - - def __init__(self, *args, **kwargs): - if kwargs.get('instance', None): - initial = kwargs.get('initial', {}) - initial['system_users'] = kwargs['instance'].systemuser_set.all() - super().__init__(*args, **kwargs) - - def save(self, commit=True): - instance = super().save(commit=commit) - system_users = self.cleaned_data['system_users'] - instance.systemuser_set.set(system_users) - return instance - - -class AdminUserForm(forms.ModelForm): - # Form field name can not start with `_`, so redefine it, - password = forms.CharField( - widget=forms.PasswordInput, max_length=128, - strip=True, required=False, - help_text=_('Password or private key password'), - label=_("Password"), - ) - # Need use upload private key file except paste private key content - private_key_file = forms.FileField(required=False, label=_("Private key")) - - def save(self, commit=True): - # Because we define custom field, so we need rewrite :method: `save` - admin_user = super().save(commit=commit) - password = self.cleaned_data['password'] - private_key = self.cleaned_data['private_key_file'] - public_key = None - - if not password: - password = None - - if private_key: - public_key = ssh_pubkey_gen(private_key, password=password) - - admin_user.set_auth(password=password, public_key=public_key, private_key=private_key) - return admin_user - - def clean_private_key_file(self): - private_key_file = self.cleaned_data['private_key_file'] - password = self.cleaned_data['password'] - - if private_key_file: - private_key = private_key_file.read() - if not validate_ssh_private_key(private_key, password): - raise forms.ValidationError(_('Invalid private key')) - return private_key - return private_key_file - - def clean(self): - super().clean() - password = self.cleaned_data['password'] - private_key_file = self.cleaned_data.get('private_key_file', '') - - if not password and not private_key_file: - raise forms.ValidationError(_( - 'Password and private key file must be input one' - )) - - class Meta: - model = AdminUser - fields = ['name', 'username', 'password', - 'private_key_file', 'comment'] - widgets = { - 'name': forms.TextInput(attrs={'placeholder': _('Name')}), - 'username': forms.TextInput(attrs={'placeholder': _('Username')}), - } - help_texts = { - 'name': '* required', - 'username': '* required', - } - - -class SystemUserForm(forms.ModelForm): - # Admin user assets define, let user select, save it in form not in view - auto_generate_key = forms.BooleanField(initial=True, required=False) - # Form field name can not start with `_`, so redefine it, - password = forms.CharField(widget=forms.PasswordInput, required=False, - max_length=128, strip=True, label=_("Password")) - # Need use upload private key file except paste private key content - private_key_file = forms.FileField(required=False, label=_("Private key")) - - def save(self, commit=True): - # Because we define custom field, so we need rewrite :method: `save` - system_user = super().save() - password = self.cleaned_data.get('password', None) - private_key_file = self.cleaned_data.get('private_key_file') - auto_generate_key = self.cleaned_data.get('auto_generate_key') - private_key = None - public_key = None - - if auto_generate_key: - logger.info('Auto set system user auth') - system_user.auto_gen_auth() - else: - if private_key_file: - private_key = private_key_file.read().strip().decode('utf-8') - public_key = ssh_pubkey_gen(private_key=private_key) - system_user.set_auth(password=password, private_key=private_key, public_key=public_key) - return system_user - - def clean_private_key_file(self): - if self.cleaned_data.get('private_key_file'): - key_string = self.cleaned_data['private_key_file'].read() - self.cleaned_data['private_key_file'].seek(0) - if not validate_ssh_private_key(key_string): - raise forms.ValidationError(_('Invalid private key')) - return self.cleaned_data['private_key_file'] - - def clean_password(self): - if not self.cleaned_data.get('password') and \ - not self.cleaned_data.get('private_key_file') and \ - not self.cleaned_data.get('auto_generate_key'): - raise forms.ValidationError(_('Auth info required, private_key or password')) - return self.cleaned_data['password'] - - class Meta: - model = SystemUser - fields = [ - 'name', 'username', 'protocol', 'auto_generate_key', - 'password', 'private_key_file', 'auto_push', 'sudo', - 'comment', 'shell', 'cluster', 'priority', - ] - widgets = { - 'name': forms.TextInput(attrs={'placeholder': _('Name')}), - 'username': forms.TextInput(attrs={'placeholder': _('Username')}), - 'cluster': forms.SelectMultiple( - attrs={ - 'class': 'select2', - 'data-placeholder': _(' Select clusters') - } - ), - } - help_texts = { - 'name': '* required', - 'username': '* required', - 'cluster': _('If auto push checked, system user will be create at cluster assets'), - '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'), - } - - -class SystemUserUpdateForm(SystemUserForm): - def save(self, commit=True): - # Because we define custom field, so we need rewrite :method: `save` - password = self.cleaned_data.get('password', None) - private_key_file = self.cleaned_data.get('private_key_file') - system_user = super(forms.ModelForm, self).save() - - if private_key_file: - private_key = private_key_file.read().strip().decode('utf-8') - public_key = ssh_pubkey_gen(private_key=private_key) - else: - private_key = public_key = None - system_user.set_auth(password=password, private_key=private_key, public_key=public_key) - return system_user - - def clean_password(self): - return self.cleaned_data['password'] - - -class SystemUserAuthForm(forms.Form): - password = forms.CharField(widget=forms.PasswordInput, required=False, max_length=128, strip=True) - private_key_file = forms.FileField(required=False) - - def clean_private_key_file(self): - if self.cleaned_data.get('private_key_file'): - key_string = self.cleaned_data['private_key_file'].read() - self.cleaned_data['private_key_file'].seek(0) - if not validate_ssh_private_key(key_string): - raise forms.ValidationError(_('Invalid private key')) - return self.cleaned_data['private_key_file'] - - def clean_password(self): - if not self.cleaned_data.get('password') and \ - not self.cleaned_data.get('private_key_file'): - msg = _('Auth info required, private_key or password') - raise forms.ValidationError(msg) - return self.cleaned_data['password'] - - def update(self, system_user): - password = self.cleaned_data.get('password') - private_key_file = self.cleaned_data.get('private_key_file') - - if private_key_file: - private_key = private_key_file.read().strip() - public_key = ssh_pubkey_gen(private_key=private_key) - else: - private_key = None - public_key = None - system_user.set_auth(password=password, private_key=private_key, public_key=public_key) - return system_user - - -class FileForm(forms.Form): - file = forms.FileField() diff --git a/apps/assets/forms/__init__.py b/apps/assets/forms/__init__.py new file mode 100644 index 000000000..9175d7c43 --- /dev/null +++ b/apps/assets/forms/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# +from .asset import * +from .label import * +from .user import * diff --git a/apps/assets/forms/asset.py b/apps/assets/forms/asset.py new file mode 100644 index 000000000..34865c544 --- /dev/null +++ b/apps/assets/forms/asset.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +# +from django import forms +from django.utils.translation import gettext_lazy as _ + +from ..models import Asset, AdminUser +from common.utils import get_logger + +logger = get_logger(__file__) +__all__ = ['AssetCreateForm', 'AssetUpdateForm', 'AssetBulkUpdateForm'] + + +class AssetCreateForm(forms.ModelForm): + class Meta: + model = Asset + fields = [ + 'hostname', 'ip', 'public_ip', 'port', 'comment', + 'nodes', 'is_active', 'admin_user', 'labels', 'platform', + + ] + widgets = { + 'nodes': forms.SelectMultiple(attrs={ + 'class': 'select2', 'data-placeholder': _('Nodes') + }), + 'admin_user': forms.Select(attrs={ + 'class': 'select2', 'data-placeholder': _('Admin user') + }), + 'labels': forms.SelectMultiple(attrs={ + 'class': 'select2', 'data-placeholder': _('Labels') + }), + 'port': forms.TextInput(), + } + help_texts = { + 'hostname': '* required', + 'ip': '* required', + 'port': '* required', + 'admin_user': _('Admin user is a privilege user exist on this asset,' + 'Example: root or other NOPASSWD sudo privilege user' + ) + } + + +class AssetUpdateForm(forms.ModelForm): + class Meta: + model = Asset + fields = [ + 'hostname', 'ip', 'port', 'nodes', 'is_active', 'platform', + 'public_ip', 'number', 'comment', 'admin_user', 'labels', + ] + widgets = { + 'nodes': forms.SelectMultiple(attrs={ + 'class': 'select2', 'data-placeholder': _('Nodes') + }), + 'admin_user': forms.Select(attrs={ + 'class': 'select2', 'data-placeholder': _('Admin user') + }), + 'labels': forms.SelectMultiple(attrs={ + 'class': 'select2', 'data-placeholder': _('Labels') + }), + 'port': forms.TextInput(), + } + help_texts = { + 'hostname': '* required', + 'ip': '* required', + 'port': '* required', + 'cluster': '* required', + 'admin_user': _( + 'Admin user is a privilege user exist on this asset,' + 'Example: root or other NOPASSWD sudo privilege user' + ) + } + + +class AssetBulkUpdateForm(forms.ModelForm): + assets = forms.ModelMultipleChoiceField( + required=True, help_text='* required', + label=_('Select assets'), queryset=Asset.objects.all(), + widget=forms.SelectMultiple( + attrs={ + 'class': 'select2', + 'data-placeholder': _('Select assets') + } + ) + ) + port = forms.IntegerField( + label=_('Port'), required=False, min_value=1, max_value=65535, + ) + admin_user = forms.ModelChoiceField( + required=False, queryset=AdminUser.objects.all(), + label=_("Admin user"), + widget=forms.Select( + attrs={ + 'class': 'select2', + 'data-placeholder': _('Admin user') + } + ) + ) + + class Meta: + model = Asset + fields = [ + 'assets', 'port', 'admin_user', 'labels', 'nodes', + ] + widgets = { + 'labels': forms.SelectMultiple( + attrs={'class': 'select2', 'data-placeholder': _('Select labels')} + ), + 'nodes': forms.SelectMultiple( + attrs={'class': 'select2', 'data-placeholder': _('Select nodes')} + ), + } + + def save(self, commit=True): + changed_fields = [] + for field in self._meta.fields: + if self.data.get(field) not in [None, '']: + changed_fields.append(field) + + cleaned_data = {k: v for k, v in self.cleaned_data.items() + if k in changed_fields} + assets = cleaned_data.pop('assets') + labels = cleaned_data.pop('labels', []) + nodes = cleaned_data.pop('nodes') + assets = Asset.objects.filter(id__in=[asset.id for asset in assets]) + assets.update(**cleaned_data) + + if labels: + for label in labels: + label.assets.add(*tuple(assets)) + if nodes: + for node in nodes: + node.assets.add(*tuple(assets)) + return assets diff --git a/apps/assets/forms/label.py b/apps/assets/forms/label.py new file mode 100644 index 000000000..ebdc9384e --- /dev/null +++ b/apps/assets/forms/label.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# +from django import forms +from django.utils.translation import gettext_lazy as _ + +from ..models import Label, Asset + +__all__ = ['LabelForm'] + + +class LabelForm(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 = Label + fields = ['name', 'value', '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): + label = super().save(commit=commit) + assets = self.cleaned_data['assets'] + label.assets.set(assets) + return label diff --git a/apps/assets/forms/user.py b/apps/assets/forms/user.py new file mode 100644 index 000000000..24807ca08 --- /dev/null +++ b/apps/assets/forms/user.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +# +from django import forms +from django.utils.translation import gettext_lazy as _ + +from ..models import AdminUser, SystemUser +from common.utils import validate_ssh_private_key, ssh_pubkey_gen, get_logger + +logger = get_logger(__file__) +__all__ = [ + 'FileForm', 'SystemUserForm', 'AdminUserForm', +] + + +class FileForm(forms.Form): + file = forms.FileField() + + +class PasswordAndKeyAuthForm(forms.ModelForm): + # Form field name can not start with `_`, so redefine it, + password = forms.CharField( + widget=forms.PasswordInput, max_length=128, + strip=True, required=False, + help_text=_('Password or private key passphrase'), + label=_("Password"), + ) + # Need use upload private key file except paste private key content + private_key_file = forms.FileField(required=False, label=_("Private key")) + + def clean_private_key_file(self): + private_key_file = self.cleaned_data['private_key_file'] + password = self.cleaned_data['password'] + + if private_key_file: + key_string = private_key_file.read() + private_key_file.seek(0) + if not validate_ssh_private_key(key_string, password): + raise forms.ValidationError(_('Invalid private key')) + return private_key_file + + def validate_password_key(self): + password = self.cleaned_data['password'] + private_key_file = self.cleaned_data.get('private_key_file', '') + + if not password and not private_key_file: + raise forms.ValidationError(_( + 'Password and private key file must be input one' + )) + + def gen_keys(self): + password = self.cleaned_data.get('password', '') or None + private_key_file = self.cleaned_data['private_key_file'] + public_key = private_key = None + + if private_key_file: + private_key = private_key_file.read().strip().decode('utf-8') + public_key = ssh_pubkey_gen(private_key=private_key, password=password) + return private_key, public_key + + +class AdminUserForm(PasswordAndKeyAuthForm): + def save(self, commit=True): + # Because we define custom field, so we need rewrite :method: `save` + admin_user = super().save(commit=commit) + password = self.cleaned_data.get('password', '') or None + private_key, public_key = super().gen_keys() + admin_user.set_auth(password=password, public_key=public_key, private_key=private_key) + return admin_user + + def clean(self): + super().clean() + if not self.instance: + super().validate_password_key() + + class Meta: + model = AdminUser + fields = ['name', 'username', 'password', 'private_key_file', 'comment'] + widgets = { + 'name': forms.TextInput(attrs={'placeholder': _('Name')}), + 'username': forms.TextInput(attrs={'placeholder': _('Username')}), + } + help_texts = { + 'name': '* required', + 'username': '* required', + } + + +class SystemUserForm(PasswordAndKeyAuthForm): + # Admin user assets define, let user select, save it in form not in view + auto_generate_key = forms.BooleanField(initial=True, required=False) + + def save(self, commit=True): + # Because we define custom field, so we need rewrite :method: `save` + system_user = super().save() + password = self.cleaned_data.get('password', '') or None + auto_generate_key = self.cleaned_data.get('auto_generate_key', False) + private_key, public_key = super().gen_keys() + + if auto_generate_key: + logger.info('Auto generate key and set system user auth') + system_user.auto_gen_auth() + else: + system_user.set_auth(password=password, private_key=private_key, public_key=public_key) + return system_user + + def clean(self): + super().clean() + auto_generate = self.cleaned_data.get('auto_generate_key') + if not self.instance and not auto_generate: + super().validate_password_key() + + class Meta: + model = SystemUser + fields = [ + 'name', 'username', 'protocol', 'auto_generate_key', + 'password', 'private_key_file', 'auto_push', 'sudo', + 'comment', 'shell', 'nodes', 'priority', + ] + widgets = { + 'name': forms.TextInput(attrs={'placeholder': _('Name')}), + 'username': forms.TextInput(attrs={'placeholder': _('Username')}), + 'nodes': forms.SelectMultiple( + attrs={ + 'class': 'select2', + 'data-placeholder': _('Nodes') + } + ), + } + help_texts = { + 'name': '* required', + 'username': '* required', + 'nodes': _('If auto push checked, system user will be create at node assets'), + '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'), + } \ No newline at end of file diff --git a/apps/assets/hands.py b/apps/assets/hands.py index 1545bce62..ad44052d3 100644 --- a/apps/assets/hands.py +++ b/apps/assets/hands.py @@ -6,13 +6,12 @@ Other module of this app shouldn't connect with other app. - :copyright: (c) 2014-2017 by Jumpserver Team. + :copyright: (c) 2014-2018 by Jumpserver Team. :license: GPL v2, see LICENSE for more details. """ -from users.utils import AdminUserRequiredMixin -from users.permissions import IsAppUser, IsSuperUser, IsValidUser, IsSuperUserOrAppUser +from common.mixins import AdminUserRequiredMixin +from common.permissions import IsAppUser, IsSuperUser, IsValidUser, IsSuperUserOrAppUser from users.models import User, UserGroup -from perms.utils import get_user_granted_assets -from perms.tasks import push_users \ No newline at end of file +from perms.utils import NodePermissionUtil diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index 38ed90721..dbc703706 100644 --- a/apps/assets/models/__init__.py +++ b/apps/assets/models/__init__.py @@ -2,7 +2,9 @@ # -*- coding: utf-8 -*- # from .user import AdminUser, SystemUser +from .label import Label from .cluster import * from .group import * +from .node import * from .asset import * from .utils import * diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index c83443077..3f7dec4ec 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -28,46 +28,36 @@ def default_cluster(): return cluster.id -class Asset(models.Model): - # Todo: Move them to settings - STATUS_CHOICES = ( - ('In use', _('In use')), - ('Out of use', _('Out of use')), - ) - TYPE_CHOICES = ( - ('Server', _('Server')), - ('VM', _('VM')), - ('Switch', _('Switch')), - ('Router', _('Router')), - ('Firewall', _('Firewall')), - ('Storage', _("Storage")), - ) - ENV_CHOICES = ( - ('Prod', _('Production')), - ('Dev', _('Development')), - ('Test', _('Testing')), - ) +def default_node(): + try: + from .node import Node + return Node.root() + except: + return None + +class Asset(models.Model): # Important + PLATFORM_CHOICES = ( + ('Linux', 'Linux'), + ('Unix', 'Unix'), + ('MacOS', 'MacOS'), + ('BSD', 'BSD'), + ('Windows', 'Windows'), + ('Other', 'Other'), + ) id = models.UUIDField(default=uuid.uuid4, primary_key=True) ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True) hostname = models.CharField(max_length=128, unique=True, verbose_name=_('Hostname')) port = models.IntegerField(default=22, verbose_name=_('Port')) - groups = models.ManyToManyField(AssetGroup, blank=True, related_name='assets', verbose_name=_('Asset groups')) - cluster = models.ForeignKey(Cluster, related_name='assets', default=default_cluster, on_delete=models.SET_DEFAULT, verbose_name=_('Cluster')) + nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes")) is_active = models.BooleanField(default=True, verbose_name=_('Is active')) - type = models.CharField(choices=TYPE_CHOICES, max_length=16, blank=True, null=True, default='Server', verbose_name=_('Asset type'),) - env = models.CharField(choices=ENV_CHOICES, max_length=8, blank=True, null=True, default='Prod', verbose_name=_('Asset environment'),) - status = models.CharField(choices=STATUS_CHOICES, max_length=12, null=True, blank=True, default='In use', verbose_name=_('Asset status')) # Auth - admin_user = models.ForeignKey('assets.AdminUser', null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_("Admin user")) + admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, null=True, verbose_name=_("Admin user")) # Some information public_ip = models.GenericIPAddressField(max_length=32, blank=True, null=True, verbose_name=_('Public IP')) - remote_card_ip = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('Remote control card IP')) - cabinet_no = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Cabinet number')) - cabinet_pos = models.IntegerField(null=True, blank=True, verbose_name=_('Cabinet position')) number = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Asset number')) # Collect @@ -82,12 +72,13 @@ class Asset(models.Model): 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')) - platform = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Platform')) + 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_version = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('OS version')) os_arch = models.CharField(max_length=16, blank=True, null=True, verbose_name=_('OS arch')) hostname_raw = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Hostname raw')) + labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels")) created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by')) date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) @@ -104,6 +95,12 @@ class Asset(models.Model): return True, '' return False, warning + def is_unixlike(self): + if self.platform not in ("Windows", "Other"): + return True + else: + return False + @property def hardware_info(self): if self.cpu_count: @@ -116,36 +113,20 @@ class Asset(models.Model): @property def is_connective(self): + if not self.is_unixlike(): + return True val = cache.get(ASSET_ADMIN_CONN_CACHE_KEY.format(self.hostname)) if val == 1: return True else: return False - @property - def admin_user_avail(self): - if self.admin_user: - admin_user = self.admin_user - elif self.cluster and self.cluster.admin_user: - admin_user = self.cluster.admin_user - else: - return None - return admin_user - - @property - def is_has_private_admin_user(self): - if self.admin_user: - return True - else: - return False - def to_json(self): return { 'id': self.id, 'hostname': self.hostname, 'ip': self.ip, 'port': self.port, - 'groups': [group.name for group in self.groups.all()], } def _to_secret_json(self): @@ -156,13 +137,14 @@ class Asset(models.Model): Todo: May be move to ops implements it """ data = self.to_json() - if self.admin_user_avail: - admin_user = self.admin_user_avail + if self.admin_user: + admin_user = self.admin_user data.update({ 'username': admin_user.username, 'password': admin_user.password, 'private_key': admin_user.private_key_file, 'become': admin_user.become_info, + 'groups': [node.value for node in self.nodes.all()], }) return data @@ -181,7 +163,6 @@ class Asset(models.Model): asset = cls(ip='%s.%s.%s.%s' % (i, i, i, i), hostname=forgery_py.internet.user_name(True), admin_user=choice(AdminUser.objects.all()), - cluster=choice(Cluster.objects.all()), port=22, created_by='Fake') try: diff --git a/apps/assets/models/group.py b/apps/assets/models/group.py index c01a2a529..b9bf16f18 100644 --- a/apps/assets/models/group.py +++ b/apps/assets/models/group.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) class AssetGroup(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=64, unique=True, verbose_name=_('Name')) - created_by = models.CharField(max_length=32, blank=True, verbose_name=_('Created by')) + created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by')) date_created = models.DateTimeField(auto_now_add=True, null=True, verbose_name=_('Date created')) comment = models.TextField(blank=True, verbose_name=_('Comment')) diff --git a/apps/assets/models/label.py b/apps/assets/models/label.py new file mode 100644 index 000000000..990a71ca8 --- /dev/null +++ b/apps/assets/models/label.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# + +import uuid +from django.db import models +from django.utils.translation import ugettext_lazy as _ + + +class Label(models.Model): + SYSTEM_CATEGORY = "S" + USER_CATEGORY = "U" + CATEGORY_CHOICES = ( + ("S", _("System")), + ("U", _("User")) + ) + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + name = models.CharField(max_length=128, verbose_name=_("Name")) + value = models.CharField(max_length=128, verbose_name=_("Value")) + category = models.CharField(max_length=128, choices=CATEGORY_CHOICES, default=USER_CATEGORY, verbose_name=_("Category")) + is_active = models.BooleanField(default=True, verbose_name=_("Is active")) + comment = models.TextField(blank=True, null=True, verbose_name=_("Comment")) + date_created = models.DateTimeField( + auto_now_add=True, null=True, blank=True, verbose_name=_('Date created') + ) + + @classmethod + def get_queryset_group_by_name(cls): + names = cls.objects.values_list('name', flat=True) + for name in names: + yield name, cls.objects.filter(name=name) + + def __str__(self): + return "{}:{}".format(self.name, self.value) + + class Meta: + db_table = "assets_label" + unique_together = ('name', 'value') diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py new file mode 100644 index 000000000..5ce195783 --- /dev/null +++ b/apps/assets/models/node.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# +import uuid + +from django.db import models +from django.utils.translation import ugettext_lazy as _ + + +__all__ = ['Node'] + + +class Node(models.Model): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + key = models.CharField(unique=True, max_length=64, verbose_name=_("Key")) # '1:1:1:1' + value = models.CharField(max_length=128, unique=True, verbose_name=_("Value")) + child_mark = models.IntegerField(default=0) + date_create = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.value + + @property + def name(self): + return self.value + + @property + def full_value(self): + if self == self.__class__.root(): + return self.value + else: + return '{}/{}'.format(self.value, self.parent.full_value) + + @property + def level(self): + return len(self.key.split(':')) + + def get_next_child_key(self): + mark = self.child_mark + self.child_mark += 1 + self.save() + return "{}:{}".format(self.key, mark) + + def create_child(self, value): + child_key = self.get_next_child_key() + child = self.__class__.objects.create(key=child_key, value=value) + return child + + def get_children(self): + return self.__class__.objects.filter(key__regex=r'{}:[0-9]+$'.format(self.key)) + + def get_all_children(self): + return self.__class__.objects.filter(key__startswith='{}:'.format(self.key)) + + def get_family(self): + children = list(self.get_all_children()) + children.append(self) + return children + + def get_assets(self): + from .asset import Asset + assets = Asset.objects.filter(nodes__id=self.id) + return assets + + def get_active_assets(self): + return self.get_assets().filter(is_active=True) + + def get_all_assets(self): + from .asset import Asset + if self.is_root(): + assets = Asset.objects.all() + else: + nodes = self.get_family() + assets = Asset.objects.filter(nodes__in=nodes) + return assets + + def get_all_active_assets(self): + return self.get_all_assets().filter(is_active=True) + + def is_root(self): + return self.key == '0' + + @property + def parent(self): + if self.key == "0": + return self.__class__.root() + elif not self.key.startswith("0"): + return self.__class__.root() + + parent_key = ":".join(self.key.split(":")[:-1]) + try: + parent = self.__class__.objects.get(key=parent_key) + except Node.DoesNotExist: + return self.__class__.root() + else: + return parent + + @parent.setter + def parent(self, parent): + self.key = parent.get_next_child_key() + + @property + def ancestor(self): + if self.parent == self.__class__.root(): + return [self.__class__.root()] + else: + return [self.parent, *tuple(self.parent.ancestor)] + + @property + def ancestor_with_node(self): + ancestor = self.ancestor + ancestor.insert(0, self) + return ancestor + + @classmethod + def root(cls): + obj, created = cls.objects.get_or_create( + key='0', defaults={"key": '0', 'value': "ROOT"} + ) + return obj diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index aa44a539c..7eaff63e6 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -26,14 +26,14 @@ 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=16, verbose_name=_('Username')) + 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=32, null=True, verbose_name=_('Created by')) + created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by')) @property def password(self): @@ -175,15 +175,12 @@ class AdminUser(AssetUser): return info def get_related_assets(self): - assets = [] - for cluster in self.cluster_set.all(): - assets.extend(cluster.assets.all()) - assets.extend(self.asset_set.all()) - return list(set(assets)) + assets = self.asset_set.all() + return assets @property def assets_amount(self): - return len(self.get_related_assets()) + return self.get_related_assets().count() class Meta: ordering = ['name'] @@ -212,11 +209,13 @@ class AdminUser(AssetUser): class SystemUser(AssetUser): SSH_PROTOCOL = 'ssh' + RDP_PROTOCOL = 'rdp' PROTOCOL_CHOICES = ( (SSH_PROTOCOL, 'ssh'), + (RDP_PROTOCOL, 'rdp'), ) - cluster = models.ManyToManyField('assets.Cluster', blank=True, verbose_name=_("Cluster")) + nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes")) priority = models.IntegerField(default=10, verbose_name=_("Priority")) protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol')) auto_push = models.BooleanField(default=True, verbose_name=_('Auto push')) @@ -226,21 +225,6 @@ class SystemUser(AssetUser): def __str__(self): return self.name - def get_clusters_assets(self): - from .asset import Asset - clusters = self.get_clusters() - return Asset.objects.filter(cluster__in=clusters) - - def get_clusters(self): - return self.cluster.all() - - def get_clusters_joined(self): - return ', '.join([cluster.name for cluster in self.get_clusters()]) - - @property - def assets_amount(self): - return len(self.get_clusters_assets()) - def to_json(self): return { 'id': self.id, @@ -251,6 +235,13 @@ class SystemUser(AssetUser): 'auto_push': self.auto_push, } + @property + def assets(self): + assets = set() + for node in self.nodes.all(): + assets.update(set(node.get_all_assets())) + return assets + @property def assets_connective(self): _result = cache.get(SYSTEM_USER_CONN_CACHE_KEY.format(self.name), {}) @@ -264,6 +255,12 @@ class SystemUser(AssetUser): def reachable_assets(self): return self.assets_connective.get('contacted', []) + def is_need_push(self): + if self.auto_push and self.protocol == self.__class__.SSH_PROTOCOL: + return True + else: + return False + class Meta: ordering = ['name'] verbose_name = _("System user") diff --git a/apps/assets/serializers.py b/apps/assets/serializers.py deleted file mode 100644 index 024d88e22..000000000 --- a/apps/assets/serializers.py +++ /dev/null @@ -1,286 +0,0 @@ -# -*- coding: utf-8 -*- -from django.core.cache import cache -from rest_framework import serializers -from rest_framework_bulk.serializers import BulkListSerializer - -from common.mixins import BulkSerializerMixin -from .models import AssetGroup, Asset, Cluster, AdminUser, SystemUser -from .const import ADMIN_USER_CONN_CACHE_KEY, SYSTEM_USER_CONN_CACHE_KEY - - -class AssetGroupSerializer(BulkSerializerMixin, serializers.ModelSerializer): - """ - 资产组序列化数据模型 - """ - assets_amount = serializers.SerializerMethodField() - assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all()) - - class Meta: - model = AssetGroup - list_serializer_class = BulkListSerializer - fields = ['id', 'name', 'comment', 'assets_amount', 'assets'] - - @staticmethod - def get_assets_amount(obj): - return obj.asset_count - - -class AssetUpdateSystemUserSerializer(serializers.ModelSerializer): - """ - 资产更新其系统用户请求的数据结构定义 - """ - system_users = serializers.PrimaryKeyRelatedField(many=True, queryset=SystemUser.objects.all()) - - class Meta: - model = Asset - fields = ['id', 'system_users'] - - -class GroupUpdateAssetsSerializer(serializers.ModelSerializer): - """ - 资产组更新需要的数据结构 - """ - assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all()) - - class Meta: - model = AssetGroup - fields = ['id', 'assets'] - - -class ClusterUpdateAssetsSerializer(serializers.ModelSerializer): - """ - 集群更新资产数据结构 - """ - assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all()) - - class Meta: - model = Cluster - fields = ['id', 'assets'] - - -class AdminUserSerializer(serializers.ModelSerializer): - """ - 管理用户 - """ - assets_amount = serializers.SerializerMethodField() - unreachable_amount = serializers.SerializerMethodField() - reachable_amount = serializers.SerializerMethodField() - - class Meta: - model = AdminUser - fields = '__all__' - - @staticmethod - def get_unreachable_amount(obj): - data = cache.get(ADMIN_USER_CONN_CACHE_KEY.format(obj.name)) - if data: - return len(data.get('dark')) - else: - return 0 - - @staticmethod - def get_reachable_amount(obj): - data = cache.get(ADMIN_USER_CONN_CACHE_KEY.format(obj.name)) - if data: - return len(data.get('contacted')) - else: - return 0 - - @staticmethod - def get_assets_amount(obj): - return obj.assets_amount - - -class SystemUserSerializer(serializers.ModelSerializer): - """ - 系统用户 - """ - unreachable_amount = serializers.SerializerMethodField() - reachable_amount = serializers.SerializerMethodField() - unreachable_assets = serializers.SerializerMethodField() - reachable_assets = serializers.SerializerMethodField() - assets_amount = serializers.SerializerMethodField() - - class Meta: - model = SystemUser - exclude = ('_password', '_private_key', '_public_key') - - @staticmethod - def get_unreachable_assets(obj): - return obj.unreachable_assets - - @staticmethod - def get_reachable_assets(obj): - return obj.reachable_assets - - def get_unreachable_amount(self, obj): - return len(self.get_unreachable_assets(obj)) - - def get_reachable_amount(self, obj): - return len(self.get_reachable_assets(obj)) - - @staticmethod - def get_assets_amount(obj): - amount = 0 - for cluster in obj.cluster.all(): - amount += cluster.assets.all().count() - return amount - - -class AdminUserUpdateClusterSerializer(serializers.ModelSerializer): - """ - 管理用户更新关联到的集群 - """ - clusters = serializers.PrimaryKeyRelatedField(many=True, queryset=Cluster.objects.all()) - - class Meta: - model = AdminUser - fields = ['id', 'clusters'] - - -class AssetSystemUserSerializer(serializers.ModelSerializer): - """ - 查看授权的资产系统用户的数据结构,这个和AssetSerializer不同,字段少 - """ - class Meta: - model = SystemUser - fields = ('id', 'name', 'username', 'priority', 'protocol', 'comment',) - - -class SystemUserSimpleSerializer(serializers.ModelSerializer): - """ - 系统用户最基本信息的数据结构 - """ - class Meta: - model = SystemUser - fields = ('id', 'name', 'username') - - -class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer): - """ - 资产的数据结构 - """ - cluster_name = serializers.SerializerMethodField() - - class Meta(object): - model = Asset - list_serializer_class = BulkListSerializer - fields = '__all__' - validators = [] # If not set to [], partial bulk update will be error - - def get_field_names(self, declared_fields, info): - fields = super().get_field_names(declared_fields, info) - fields.extend([ - 'get_type_display', 'get_env_display', - 'hardware_info', 'is_connective', - ]) - return fields - - @staticmethod - def get_cluster_name(obj): - return obj.cluster.name - - -class AssetGrantedSerializer(serializers.ModelSerializer): - """ - 被授权资产的数据结构 - """ - system_users_granted = AssetSystemUserSerializer(many=True, read_only=True) - is_inherited = serializers.SerializerMethodField() - system_users_join = serializers.SerializerMethodField() - - class Meta(object): - model = Asset - fields = ( - "id", "hostname", "ip", "port", "system_users_granted", - "is_inherited", "is_active", "system_users_join", "os", - "platform", "comment" - ) - - @staticmethod - def get_is_inherited(obj): - if getattr(obj, 'inherited', ''): - return True - else: - return False - - @staticmethod - def get_system_users_join(obj): - return ', '.join([system_user.username for system_user in obj.system_users_granted]) - - -class MyAssetGrantedSerializer(AssetGrantedSerializer): - """ - 普通用户获取授权的资产定义的数据结构 - """ - - class Meta(object): - model = Asset - fields = ( - "id", "hostname", "system_users_granted", - "is_inherited", "is_active", "system_users_join", - "os", "platform", "comment", - ) - - -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()) - - -class AssetGroupGrantedSerializer(BulkSerializerMixin, serializers.ModelSerializer): - """ - 授权资产组 - """ - assets_granted = AssetGrantedSerializer(many=True, read_only=True) - assets_amount = serializers.SerializerMethodField() - - class Meta: - model = AssetGroup - list_serializer_class = BulkListSerializer - fields = '__all__' - - @staticmethod - def get_assets_amount(obj): - return len(obj.assets_granted) - - -class MyAssetGroupGrantedSerializer(serializers.ModelSerializer): - """ - 普通用户授权资产组结构 - """ - assets_granted = MyAssetGrantedSerializer(many=True, read_only=True) - assets_amount = serializers.SerializerMethodField() - - class Meta: - model = AssetGroup - list_serializer_class = BulkListSerializer - fields = '__all__' - - @staticmethod - def get_assets_amount(obj): - return len(obj.assets_granted) diff --git a/apps/assets/serializers/__init__.py b/apps/assets/serializers/__init__.py new file mode 100644 index 000000000..c39d34767 --- /dev/null +++ b/apps/assets/serializers/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# + +from .asset import * +from .admin_user import * +from .label import * +from .system_user import * +from .node import * diff --git a/apps/assets/serializers/admin_user.py b/apps/assets/serializers/admin_user.py new file mode 100644 index 000000000..d59ca64ae --- /dev/null +++ b/apps/assets/serializers/admin_user.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# +from django.core.cache import cache +from rest_framework import serializers +from ..models import Node, AdminUser +from ..const import ADMIN_USER_CONN_CACHE_KEY + + +class AdminUserSerializer(serializers.ModelSerializer): + """ + 管理用户 + """ + assets_amount = serializers.SerializerMethodField() + unreachable_amount = serializers.SerializerMethodField() + reachable_amount = serializers.SerializerMethodField() + + class Meta: + model = AdminUser + fields = '__all__' + + @staticmethod + def get_unreachable_amount(obj): + data = cache.get(ADMIN_USER_CONN_CACHE_KEY.format(obj.name)) + if data: + return len(data.get('dark')) + else: + return 0 + + @staticmethod + def get_reachable_amount(obj): + data = cache.get(ADMIN_USER_CONN_CACHE_KEY.format(obj.name)) + if data: + return len(data.get('contacted')) + else: + return 0 + + @staticmethod + def get_assets_amount(obj): + return obj.assets_amount + + +class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer): + """ + 管理用户更新关联到的集群 + """ + nodes = serializers.PrimaryKeyRelatedField( + many=True, queryset=Node.objects.all() + ) + + class Meta: + model = AdminUser + fields = ['id', 'nodes'] diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py new file mode 100644 index 000000000..e479a7149 --- /dev/null +++ b/apps/assets/serializers/asset.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import serializers +from rest_framework_bulk.serializers import BulkListSerializer + +from common.mixins import BulkSerializerMixin +from ..models import Asset +from .system_user import AssetSystemUserSerializer + + +class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer): + """ + 资产的数据结构 + """ + + class Meta: + model = Asset + list_serializer_class = BulkListSerializer + fields = '__all__' + validators = [] # If not set to [], partial bulk update will be error + + def get_field_names(self, declared_fields, info): + fields = super().get_field_names(declared_fields, info) + fields.extend([ + 'hardware_info', 'is_connective', + ]) + return fields + + +class AssetGrantedSerializer(serializers.ModelSerializer): + """ + 被授权资产的数据结构 + """ + system_users_granted = AssetSystemUserSerializer(many=True, read_only=True) + system_users_join = serializers.SerializerMethodField() + + class Meta: + model = Asset + fields = ( + "id", "hostname", "ip", "port", "system_users_granted", + "is_active", "system_users_join", "os", + "platform", "comment" + ) + + @staticmethod + def get_system_users_join(obj): + system_users = [s.username for s in obj.system_users_granted] + return ', '.join(system_users) + + +class MyAssetGrantedSerializer(AssetGrantedSerializer): + """ + 普通用户获取授权的资产定义的数据结构 + """ + + class Meta: + model = Asset + fields = ( + "id", "hostname", "system_users_granted", + "is_active", "system_users_join", + "os", "platform", "comment", + ) diff --git a/apps/assets/serializers/cluster.py b/apps/assets/serializers/cluster.py new file mode 100644 index 000000000..43724a4a2 --- /dev/null +++ b/apps/assets/serializers/cluster.py @@ -0,0 +1,46 @@ +# -*- 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()) diff --git a/apps/assets/serializers/label.py b/apps/assets/serializers/label.py new file mode 100644 index 000000000..b45e1e508 --- /dev/null +++ b/apps/assets/serializers/label.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import serializers +from rest_framework_bulk.serializers import BulkListSerializer + +from ..models import Label + + +class LabelSerializer(serializers.ModelSerializer): + asset_count = serializers.SerializerMethodField() + + class Meta: + model = Label + fields = '__all__' + list_serializer_class = BulkListSerializer + + @staticmethod + def get_asset_count(obj): + return obj.assets.count() + + def get_field_names(self, declared_fields, info): + fields = super().get_field_names(declared_fields, info) + fields.extend(['get_category_display']) + return fields + + +class LabelDistinctSerializer(serializers.ModelSerializer): + value = serializers.SerializerMethodField() + + class Meta: + model = Label + fields = ("name", "value") + + @staticmethod + def get_value(obj): + labels = Label.objects.filter(name=obj["name"]) + return ', '.join([label.value for label in labels]) diff --git a/apps/assets/serializers/node.py b/apps/assets/serializers/node.py new file mode 100644 index 000000000..f6654aef9 --- /dev/null +++ b/apps/assets/serializers/node.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +from rest_framework import serializers +from rest_framework_bulk.serializers import BulkListSerializer + +from common.mixins import BulkSerializerMixin +from ..models import Asset, Node +from .asset import AssetGrantedSerializer + + +class NodeGrantedSerializer(BulkSerializerMixin, serializers.ModelSerializer): + """ + 授权资产组 + """ + assets_granted = AssetGrantedSerializer(many=True, read_only=True) + assets_amount = serializers.SerializerMethodField() + parent = serializers.SerializerMethodField() + name = serializers.SerializerMethodField() + + class Meta: + model = Node + fields = [ + 'id', 'key', 'name', 'value', 'parent', + 'assets_granted', 'assets_amount', + ] + + @staticmethod + def get_assets_amount(obj): + return len(obj.assets_granted) + + @staticmethod + def get_name(obj): + return obj.name + + @staticmethod + def get_parent(obj): + return obj.parent.id + + +class NodeSerializer(serializers.ModelSerializer): + parent = serializers.SerializerMethodField() + assets_amount = serializers.SerializerMethodField() + + class Meta: + model = Node + fields = ['id', 'key', 'value', 'parent', 'assets_amount'] + 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 NodeAssetsSerializer(serializers.ModelSerializer): + assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all()) + + class Meta: + model = Node + fields = ['assets'] + + +class NodeAddChildrenSerializer(serializers.Serializer): + nodes = serializers.ListField() diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py new file mode 100644 index 000000000..794c841f2 --- /dev/null +++ b/apps/assets/serializers/system_user.py @@ -0,0 +1,54 @@ +from rest_framework import serializers + +from ..models import SystemUser + + +class SystemUserSerializer(serializers.ModelSerializer): + """ + 系统用户 + """ + unreachable_amount = serializers.SerializerMethodField() + reachable_amount = serializers.SerializerMethodField() + unreachable_assets = serializers.SerializerMethodField() + reachable_assets = serializers.SerializerMethodField() + assets_amount = serializers.SerializerMethodField() + + class Meta: + model = SystemUser + exclude = ('_password', '_private_key', '_public_key') + + @staticmethod + def get_unreachable_assets(obj): + return obj.unreachable_assets + + @staticmethod + def get_reachable_assets(obj): + return obj.reachable_assets + + def get_unreachable_amount(self, obj): + return len(self.get_unreachable_assets(obj)) + + def get_reachable_amount(self, obj): + return len(self.get_reachable_assets(obj)) + + @staticmethod + def get_assets_amount(obj): + return len(obj.assets) + + +class AssetSystemUserSerializer(serializers.ModelSerializer): + """ + 查看授权的资产系统用户的数据结构,这个和AssetSerializer不同,字段少 + """ + class Meta: + model = SystemUser + fields = ('id', 'name', 'username', 'priority', 'protocol', 'comment',) + + +class SystemUserSimpleSerializer(serializers.ModelSerializer): + """ + 系统用户最基本信息的数据结构 + """ + class Meta: + model = SystemUser + fields = ('id', 'name', 'username') \ No newline at end of file diff --git a/apps/assets/signals.py b/apps/assets/signals.py index a02a16d20..20da657b1 100644 --- a/apps/assets/signals.py +++ b/apps/assets/signals.py @@ -3,4 +3,3 @@ from django.dispatch import Signal on_app_ready = Signal() -on_system_user_auth_changed = Signal(providing_args=['system_user']) diff --git a/apps/assets/signals_handler.py b/apps/assets/signals_handler.py index 171cfa6d9..2577c7a6c 100644 --- a/apps/assets/signals_handler.py +++ b/apps/assets/signals_handler.py @@ -1,15 +1,14 @@ # -*- coding: utf-8 -*- # -from django.db.models.signals import post_save, post_init +from django.db.models.signals import post_save, m2m_changed from django.dispatch import receiver -from django.utils.translation import gettext as _ from common.utils import get_logger -from .models import Asset, SystemUser, Cluster +from .models import Asset, SystemUser, Node from .tasks import update_assets_hardware_info_util, \ - test_asset_connectability_util, \ - push_system_user_util + test_asset_connectability_util, push_system_user_to_node, \ + push_node_system_users_to_asset logger = get_logger(__file__) @@ -25,105 +24,48 @@ def test_asset_conn_on_created(asset): test_asset_connectability_util.delay(asset) -def push_cluster_system_users_to_asset(asset): - if asset.cluster: - logger.info("Push cluster system user to asset: {}".format(asset)) - task_name = _("Push cluster system users to asset") - system_users = asset.cluster.systemuser_set.all() - push_system_user_util.delay(system_users, [asset], task_name) +def set_asset_root_node(asset): + logger.debug("Set asset default node: {}".format(Node.root())) + asset.nodes.add(Node.root()) @receiver(post_save, sender=Asset, dispatch_uid="my_unique_identifier") -def on_asset_created(sender, instance=None, created=False, **kwargs): - if instance and created: +def on_asset_created_or_update(sender, instance=None, created=False, **kwargs): + set_asset_root_node(instance) + if created: logger.info("Asset `{}` create signal received".format(instance)) update_asset_hardware_info_on_created(instance) test_asset_conn_on_created(instance) - push_cluster_system_users_to_asset(instance) - - -@receiver(post_init, sender=Asset) -def on_asset_init(sender, instance, created=False, **kwargs): - if instance and created is False: - instance.__original_cluster = instance.cluster - - -@receiver(post_save, sender=Asset) -def on_asset_cluster_changed(sender, instance=None, created=False, **kwargs): - if instance and created is False and instance.cluster != instance.__original_cluster: - logger.info("Asset cluster changed signal received") - push_cluster_system_users_to_asset(instance) - - -def push_to_cluster_assets_on_system_user_created_or_update(system_user): - if not system_user.auto_push: - return - logger.debug("Push system user `{}` to cluster assets".format(system_user.name)) - for cluster in system_user.cluster.all(): - task_name = _("Push system user to cluster assets: {}->{}").format( - cluster.name, system_user.name - ) - assets = cluster.assets.all() - push_system_user_util.delay([system_user], assets, task_name) - - -@receiver(post_save, sender=SystemUser) -def on_system_user_created_or_updated(sender, instance=None, **kwargs): - if instance and instance.auto_push: - logger.info("System user `{}` create or update signal received".format(instance)) - push_to_cluster_assets_on_system_user_created_or_update(instance) - - -@receiver(post_init, sender=Cluster, dispatch_uid="my_unique_identifier") -def on_cluster_init(sender, instance, **kwargs): - instance.__original_assets = tuple(instance.assets.values_list('pk', flat=True)) - instance.__origin_system_users = tuple(instance.systemuser_set.values_list('pk', flat=True)) - - -@receiver(post_save, sender=Cluster, dispatch_uid="my_unique_identifier") -def on_cluster_assets_changed(sender, instance, **kwargs): - assets_origin = instance.__original_assets - assets_new = instance.assets.values_list('pk', flat=True) - assets_added = set(assets_new) - set(assets_origin) - if assets_added: - logger.debug("Receive cluster change assets signal") - logger.debug("Push cluster `{}` system users to: {}".format( - instance, ', '.join([str(asset) for asset in assets_added]) - )) - assets = [] - for asset_id in assets_added: - try: - asset = Asset.objects.get(pk=asset_id) - except Asset.DoesNotExist: - continue - else: - assets.append(asset) - system_users = [s for s in instance.systemuser_set.all() if s.auto_push] - task_name = _("Push system user to assets") - push_system_user_util.delay(system_users, assets, task_name) - - -@receiver(post_save, sender=Cluster, dispatch_uid="my_unique_identifier2") -def on_cluster_system_user_changed(sender, instance, **kwargs): - system_users_origin = instance.__origin_system_users - system_user_new = instance.systemuser_set.values_list('pk', flat=True) - system_users_added = set(system_user_new) - set(system_users_origin) - if system_users_added: - logger.debug("Receive cluster change system users signal") - system_users = [] - for system_user_id in system_users_added: - try: - system_user = SystemUser.objects.get(pk=system_user_id) - except SystemUser.DoesNotExist: - continue - else: - system_users.append(system_user) - logger.debug("Push new system users `{}` to cluster `{}` assets".format( - ','.join([s.name for s in system_users]), instance - )) - task_name = _( - "Push system user to cluster assets: {}->{}").format( - instance.name, ', '.join(s.name for s in system_users) - ) - push_system_user_util.delay(system_users, instance.assets.all(), task_name) + + +@receiver(post_save, sender=SystemUser, dispatch_uid="my_unique_identifier") +def on_system_user_update(sender, instance=None, created=True, **kwargs): + if instance and not created: + for node in instance.nodes.all(): + push_system_user_to_node(instance, node) + + +@receiver(m2m_changed, sender=SystemUser.nodes.through) +def on_system_user_node_change(sender, instance=None, **kwargs): + if instance and kwargs["action"] == "post_add": + for pk in kwargs['pk_set']: + node = kwargs['model'].objects.get(pk=pk) + push_system_user_to_node(instance, node) + + +@receiver(m2m_changed, sender=Asset.nodes.through) +def on_asset_node_changed(sender, instance=None, **kwargs): + if isinstance(instance, Asset) and kwargs['action'] == 'post_add': + logger.debug("Asset node change signal received") + for pk in kwargs['pk_set']: + node = kwargs['model'].objects.get(pk=pk) + push_node_system_users_to_asset(node, [instance]) + + +@receiver(m2m_changed, sender=Asset.nodes.through) +def on_node_assets_changed(sender, instance=None, **kwargs): + if isinstance(instance, Node) and kwargs['action'] == 'post_add': + logger.debug("Node assets change signal received") + assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) + push_node_system_users_to_asset(instance, assets) diff --git a/apps/assets/tasks.py b/apps/assets/tasks.py index f06f720d9..a20ab3a74 100644 --- a/apps/assets/tasks.py +++ b/apps/assets/tasks.py @@ -19,7 +19,7 @@ FORKS = 10 TIMEOUT = 60 logger = get_logger(__file__) CACHE_MAX_TIME = 60*60*60 -disk_pattern = re.compile(r'^hd|sd|xvd') +disk_pattern = re.compile(r'^hd|sd|xvd|vd') @shared_task @@ -36,39 +36,40 @@ def set_assets_hardware_info(result, **kwargs): result_raw = result[0] assets_updated = [] for hostname, info in result_raw.get('ok', {}).items(): - if info: - info = info['setup']['ansible_facts'] - else: + info = info.get('setup', {}).get('ansible_facts', {}) + if not info: + logger.error("Get asset info failed: {}".format(hostname)) continue asset = get_object_or_none(Asset, hostname=hostname) if not asset: continue - ___vendor = info['ansible_system_vendor'] - ___model = info['ansible_product_version'] - ___sn = info['ansible_product_serial'] + ___vendor = info.get('ansible_system_vendor', 'Unknown') + ___model = info.get('ansible_product_name', 'Unknown') + ___sn = info.get('ansible_product_serial', 'Unknown') - for ___cpu_model in info['ansible_processor']: - if ___cpu_model.endswith('GHz'): + for ___cpu_model in info.get('ansible_processor', []): + if ___cpu_model.endswith('GHz') or ___cpu_model.startswith("Intel"): break else: ___cpu_model = 'Unknown' - ___cpu_count = info['ansible_processor_count'] - ___cpu_cores = info['ansible_processor_cores'] - ___memory = '%s %s' % capacity_convert('{} MB'.format(info['ansible_memtotal_mb'])) + ___cpu_model = ___cpu_model[:64] + ___cpu_count = info.get('ansible_processor_count', 0) + ___cpu_cores = info.get('ansible_processor_cores', None) or len(info.get('ansible_processor', [])) + ___memory = '%s %s' % capacity_convert('{} MB'.format(info.get('ansible_memtotal_mb'))) disk_info = {} - for dev, dev_info in info['ansible_devices'].items(): + for dev, dev_info in info.get('ansible_devices', {}).items(): if disk_pattern.match(dev) and dev_info['removable'] == '0': disk_info[dev] = dev_info['size'] ___disk_total = '%s %s' % sum_capacity(disk_info.values()) ___disk_info = json.dumps(disk_info) - ___platform = info['ansible_system'] - ___os = info['ansible_distribution'] - ___os_version = info['ansible_distribution_version'] - ___os_arch = info['ansible_architecture'] - ___hostname_raw = info['ansible_hostname'] + ___platform = info.get('ansible_system', 'Unknown') + ___os = info.get('ansible_distribution', 'Unknown') + ___os_version = info.get('ansible_distribution_version', 'Unknown') + ___os_arch = info.get('ansible_architecture', 'Unknown') + ___hostname_raw = info.get('ansible_hostname', 'Unknown') for k, v in locals().items(): if k.startswith('___'): @@ -90,7 +91,7 @@ def update_assets_hardware_info_util(assets, task_name=None): if task_name is None: task_name = _("Update some assets hardware info") tasks = const.UPDATE_ASSETS_HARDWARE_TASKS - hostname_list = [asset.hostname for asset in assets] + hostname_list = [asset.hostname for asset in assets if asset.is_active and asset.is_unixlike()] task, created = update_or_create_ansible_task( task_name, hosts=hostname_list, tasks=tasks, pattern='all', options=const.TASK_OPTIONS, run_as_admin=True, created_by='System', @@ -98,7 +99,7 @@ def update_assets_hardware_info_util(assets, task_name=None): result = task.run() # Todo: may be somewhere using # Manual run callback function - assets_updated = set_assets_hardware_info(result) + set_assets_hardware_info(result) return result @@ -119,7 +120,10 @@ def update_assets_hardware_info_period(): """ from ops.utils import update_or_create_ansible_task task_name = _("Update assets hardware info period") - hostname_list = [asset.hostname for asset in Asset.objects.all()] + hostname_list = [ + asset.hostname for asset in Asset.objects.all() + if asset.is_active and asset.is_unixlike() + ] tasks = const.UPDATE_ASSETS_HARDWARE_TASKS # Only create, schedule by celery beat @@ -164,7 +168,10 @@ def test_admin_user_connectability_util(admin_user, task_name): from ops.utils import update_or_create_ansible_task assets = admin_user.get_related_assets() - hosts = [asset.hostname for asset in assets] + hosts = [asset.hostname for asset in assets + if asset.is_active and asset.is_unixlike()] + if not hosts: + return tasks = const.TEST_ADMIN_USER_CONN_TASKS task, created = update_or_create_ansible_task( task_name=task_name, hosts=hosts, tasks=tasks, pattern='all', @@ -183,19 +190,10 @@ def test_admin_user_connectability_period(): """ A period task that update the ansible task period """ - from ops.utils import update_or_create_ansible_task admin_users = AdminUser.objects.all() for admin_user in admin_users: - task_name = _("Test admin user connectability period: {}").format(admin_user) - assets = admin_user.get_related_assets() - hosts = [asset.hostname for asset in assets] - tasks = const.TEST_ADMIN_USER_CONN_TASKS - update_or_create_ansible_task( - task_name=task_name, hosts=hosts, tasks=tasks, pattern='all', - options=const.TASK_OPTIONS, run_as_admin=True, created_by='System', - interval=3600, is_periodic=True, - callback=set_admin_user_connectability_info.name, - ) + task_name = _("Test admin user connectability period: {}".format(admin_user.name)) + test_admin_user_connectability_util(admin_user, task_name) @shared_task @@ -262,8 +260,8 @@ def test_system_user_connectability_util(system_user, task_name): :return: """ from ops.utils import update_or_create_ansible_task - assets = system_user.get_clusters_assets() - hosts = [asset.hostname for asset in assets] + assets = system_user.assets + hosts = [asset.hostname for asset in assets if asset.is_active and asset.is_unixlike()] tasks = const.TEST_SYSTEM_USER_CONN_TASKS if not hosts: logger.info("No hosts, passed") @@ -289,21 +287,10 @@ def test_system_user_connectability_manual(system_user): @after_app_ready_start @after_app_shutdown_clean def test_system_user_connectability_period(): - from ops.utils import update_or_create_ansible_task system_users = SystemUser.objects.all() for system_user in system_users: - task_name = _("Test system user connectability period: {}").format( - system_user.name - ) - assets = system_user.get_clusters_assets() - hosts = [asset.hostname for asset in assets] - tasks = const.TEST_SYSTEM_USER_CONN_TASKS - update_or_create_ansible_task( - task_name=task_name, hosts=hosts, tasks=tasks, pattern='all', - options=const.TASK_OPTIONS, run_as_admin=False, run_as=system_user.name, - created_by='System', interval=3600, is_periodic=True, - callback=set_admin_user_connectability_info.name, - ) + task_name = _("test system user connectability period: {}".format(system_user)) + test_system_user_connectability_util(system_user, task_name) #### Push system user tasks #### @@ -313,8 +300,9 @@ def get_push_system_user_tasks(system_user): if system_user.username == "root": return [] - tasks = [ - { + tasks = [] + if system_user.password: + tasks.append({ 'name': 'Add user {}'.format(system_user.username), 'action': { 'module': 'user', @@ -323,8 +311,9 @@ def get_push_system_user_tasks(system_user): encrypt_password(system_user.password, salt="K3mIlKK"), ), } - }, - { + }) + if system_user.public_key: + tasks.append({ 'name': 'Set {} authorized key'.format(system_user.username), 'action': { 'module': 'authorized_key', @@ -332,8 +321,9 @@ def get_push_system_user_tasks(system_user): system_user.username, system_user.public_key ) } - }, - { + }) + if system_user.sudo: + tasks.append({ 'name': 'Set {} sudo setting'.format(system_user.username), 'action': { 'module': 'lineinfile', @@ -344,8 +334,7 @@ def get_push_system_user_tasks(system_user): system_user.sudo, ) } - } - ] + }) return tasks @@ -354,13 +343,14 @@ def push_system_user_util(system_users, assets, task_name): from ops.utils import update_or_create_ansible_task tasks = [] for system_user in system_users: - tasks.extend(get_push_system_user_tasks(system_user)) + if system_user.is_need_push(): + tasks.extend(get_push_system_user_tasks(system_user)) if not tasks: logger.info("Not tasks, passed") return {} - hosts = [asset.hostname for asset in assets] + hosts = [asset.hostname for asset in assets if asset.is_active and asset.is_unixlike()] if not hosts: logger.info("Not hosts, passed") return {} @@ -371,35 +361,47 @@ def push_system_user_util(system_users, assets, task_name): return task.run() +def get_node_push_system_user_task_name(system_user, node): + return _("Push system user to node: {} => {}").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_to_cluster_assets_manual(system_user): - task_name = _("Push system user to cluster assets: {}").format(system_user.name) - assets = system_user.get_clusters_assets() - return push_system_user_util([system_user], assets, task_name) +def push_system_user_related_nodes(system_user): + nodes = system_user.nodes.all() + for node in nodes: + push_system_user_to_node(system_user, node) @shared_task -@register_as_period_task(interval=3600) -@after_app_ready_start -@after_app_shutdown_clean -def push_system_user_period(): - from ops.utils import update_or_create_ansible_task - clusters = Cluster.objects.all() - - for cluster in clusters: - tasks = [] - system_users = [system_user for system_user in cluster.systemuser_set.all() if system_user.auto_push] - if not system_users: - return - for system_user in system_users: - tasks.extend(get_push_system_user_tasks(system_user)) +def push_system_user_to_assets_manual(system_user): + push_system_user_related_nodes(system_user) + + +def push_node_system_users_to_asset(node, assets): + system_users = [] + nodes = node.ancestor_with_node + # 获取该节点所有父节点有的系统用户, 然后推送 + 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) + push_system_user_util.delay(system_users, assets, task_name) + - task_name = _("Push cluster system users to assets period: {}").format( - cluster.name - ) - hosts = [asset.hostname for asset in cluster.assets.all()] - update_or_create_ansible_task( - task_name=task_name, hosts=hosts, tasks=tasks, pattern='all', - options=const.TASK_OPTIONS, run_as_admin=True, created_by='System', - interval=60*60*24, is_periodic=True, - ) +# @shared_task +# @register_as_period_task(interval=3600) +# @after_app_ready_start +# # @after_app_shutdown_clean +# def push_system_user_period(): +# for system_user in SystemUser.objects.all(): +# push_system_user_related_nodes(system_user) diff --git a/apps/assets/templates/assets/_asset_group_bulk_update_modal.html b/apps/assets/templates/assets/_asset_group_bulk_update_modal.html index c253c4d65..0e76ff923 100644 --- a/apps/assets/templates/assets/_asset_group_bulk_update_modal.html +++ b/apps/assets/templates/assets/_asset_group_bulk_update_modal.html @@ -7,7 +7,6 @@ {% load bootstrap3 %}

{% trans "Hint: only change the field you want to update." %}

-
diff --git a/apps/assets/templates/assets/_asset_list_modal.html b/apps/assets/templates/assets/_asset_list_modal.html new file mode 100644 index 000000000..b68ab0378 --- /dev/null +++ b/apps/assets/templates/assets/_asset_list_modal.html @@ -0,0 +1,132 @@ +{% extends '_modal.html' %} +{% load i18n %} + +{% block modal_class %}modal-lg{% endblock %} +{% block modal_id %}asset_list_modal{% endblock %} +{#{% block modal_title%}{% trans "Please select assets" %}{% endblock %}#} +{% block modal_body %} +{#
#} +{# #} +{# #} +{#
#} + + + + + + + + + + + + + + +
{% trans 'Hostname' %}{% trans 'IP' %}{% trans 'Hardware' %}{% trans 'Active' %}{% trans 'Reachable' %}{% trans 'Action' %}
+
+
+ +
+ +
+
+
+ + +{% endblock %} +{% block modal_confirm_id %}btn_select_assets{% endblock %} + + diff --git a/apps/assets/templates/assets/_system_user.html b/apps/assets/templates/assets/_system_user.html index 34501ef49..528e271e6 100644 --- a/apps/assets/templates/assets/_system_user.html +++ b/apps/assets/templates/assets/_system_user.html @@ -13,7 +13,7 @@
- {% bootstrap_field form.private_key_file layout="horizontal" %} {% bootstrap_field form.password layout="horizontal" %} + {% bootstrap_field form.private_key_file layout="horizontal" %}
@@ -82,6 +81,14 @@ {% block custom_foot_js %} {% endblock %} diff --git a/apps/assets/templates/assets/admin_user_list.html b/apps/assets/templates/assets/admin_user_list.html index f7c13a32f..7cccc6d03 100644 --- a/apps/assets/templates/assets/admin_user_list.html +++ b/apps/assets/templates/assets/admin_user_list.html @@ -5,7 +5,7 @@ {% block help_message %}
- 管理用户是 服务器上已存在的特权用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。可以设置主机级别管理用户,也设置集群级别管理用户,这样资产可以不用再单独设置 + 管理用户是 服务器上已存在的特权用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。
{% endblock %} @@ -74,7 +74,8 @@ $(document).ready(function(){ if (val === 100) { innerHtml = "" + val + "% "; } else { - innerHtml = "" + val + "% "; + var num = new Number(val); + innerHtml = "" + num.toFixed(1) + "% "; } $(td).html('' + innerHtml + ''); diff --git a/apps/assets/templates/assets/asset_create.html b/apps/assets/templates/assets/asset_create.html index f96f1b0fb..1eae7ebd4 100644 --- a/apps/assets/templates/assets/asset_create.html +++ b/apps/assets/templates/assets/asset_create.html @@ -2,6 +2,8 @@ {% load static %} {% load bootstrap3 %} {% load i18n %} +{% load asset_tags %} +{% load common_tags %} {% block form %} @@ -15,25 +17,48 @@ {% bootstrap_field form.hostname layout="horizontal" %} {% bootstrap_field form.ip layout="horizontal" %} {% bootstrap_field form.port layout="horizontal" %} - {% bootstrap_field form.cluster layout="horizontal" %} + {% bootstrap_field form.platform layout="horizontal" %} {% bootstrap_field form.public_ip layout="horizontal" %} - {% bootstrap_field form.type layout="horizontal" %} - {% bootstrap_field form.env layout="horizontal" %}

{% trans 'Auth' %}

{% bootstrap_field form.admin_user layout="horizontal" %}
-

{% trans 'Group' %}

- {% bootstrap_field form.groups layout="horizontal" %} +

{% trans 'Node' %}

+ {% bootstrap_field form.nodes layout="horizontal" %} + +
+

{% trans 'Labels' %}

+
+ +
+ + {% if form.errors.labels %} + {% for e in form.errors.labels %} +
{{ e }}
+ {% endfor %} + {% endif %} +
+

{% trans 'Other' %}

{% bootstrap_field form.comment layout="horizontal" %} {% bootstrap_field form.is_active layout="horizontal" %} -
@@ -45,11 +70,20 @@ {% endblock %} {% block custom_foot_js %} - + {% endblock %} \ No newline at end of file diff --git a/apps/assets/templates/assets/asset_detail.html b/apps/assets/templates/assets/asset_detail.html index fc2320420..80916340f 100644 --- a/apps/assets/templates/assets/asset_detail.html +++ b/apps/assets/templates/assets/asset_detail.html @@ -71,27 +71,7 @@ {% trans 'Admin user' %}: - {% if asset.admin_user_avail %} - {{ asset.admin_user_avail.name }} - {% else %} - - {% endif %} - - - {% trans 'Remote card IP' %}: - {{ asset.remote_card_ip|default:"" }} - - - {% trans 'Cluster' %}: - {{ asset.cluster.name }} - - - {% trans 'Cabinet number' %}: - {{ asset.cabinet_no|default:"" }} - - - {% trans 'Cabinet position' %}: - {{ asset.cabinet_pos|default:"" }} + {{ asset.admin_user }} {% trans 'Vendor' %}: @@ -121,22 +101,10 @@ {% trans 'OS' %}: {{ asset.os|default:"" }} {{ asset.os_version|default:"" }} {{ asset.os_arch|default:"" }} - - {% trans 'Asset status' %}: - {{ asset.status }} - {% trans 'Is active' %}: {{ asset.is_active|yesno:"Yes,No" }} - - {% trans 'Asset type' %}: - {{ asset.type }} - - - {% trans 'Asset environment' %}: - {{ asset.env }} - {% trans 'Serial number' %}: {{ asset.sn|default:"" }} @@ -210,7 +178,7 @@
- {% trans 'Asset groups' %} + {% trans 'Nodes' %}
@@ -218,25 +186,25 @@ - {% for asset_group in asset_groups %} + {% for node in asset.nodes.all %} - + {% endfor %} @@ -244,6 +212,19 @@
- + {% for node in nodes_remain %} + {% endfor %}
- +
{{ asset_group.name }}{{ node.name }} - +
+ +
{% endif %}
@@ -254,28 +235,28 @@ {% endblock %} {% block custom_foot_js %} -{% endblock %} -{% block content %} -
-
-
-
- -
-
-
-
- {% trans 'Asset list of ' %} {{ asset_group.name }} {{ asset_group.assets.all.count }} -
- - - - - - - - - - -
-
-
- - - - - - - - - - - - - -
{% trans 'Hostname' %}{% trans 'IP' %}{% trans 'Port' %}{% trans 'Type' %}{% trans 'Alive' %}{% trans 'Action' %}
-
-
-
-
-
-
- {% trans 'Add assets to this group' %} -
-
- - - - - - - - - - - -
- -
- -
-
-
-
-
-
-
-
-
-{% endblock %} -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/assets/templates/assets/asset_group_list.html b/apps/assets/templates/assets/asset_group_list.html deleted file mode 100644 index 58fbbc51c..000000000 --- a/apps/assets/templates/assets/asset_group_list.html +++ /dev/null @@ -1,164 +0,0 @@ -{% extends '_base_list.html' %} -{% load i18n static %} -{% block table_search %} -{% endblock %} -{% block table_container %} - - - - - - - - - - - - - -
- - {% trans 'Name' %}{% trans 'Asset' %}{% trans 'Comment' %}{% trans 'Action' %}
-{% include 'assets/_asset_group_bulk_update_modal.html' %} -{% endblock %} -{% block content_bottom_left %}{% endblock %} -{% block custom_foot_js %} - -{% endblock %} - diff --git a/apps/assets/templates/assets/asset_list.html b/apps/assets/templates/assets/asset_list.html index 625fc030d..628d0f905 100644 --- a/apps/assets/templates/assets/asset_list.html +++ b/apps/assets/templates/assets/asset_list.html @@ -1,68 +1,139 @@ -{% extends '_base_list.html' %} -{% load i18n %} +{% extends 'base.html' %} {% load static %} -{% block custom_head_css_js %} - - +{% load i18n %} -{% endblock %} -{% block content_left_head %}{% endblock %} - -{% block table_search %} -
- +{% block help_message %} +
+ 左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产
{% endblock %} -{% block table_container %} - - - - - - - - - - - - - - - - - -
{% trans 'Hostname' %}{% trans 'IP' %}{% trans 'Port' %}{% trans 'Cluster' %}{% trans 'Hardware' %}{% trans 'Active' %}{% trans 'Reachable' %}{% trans 'Action' %}
-
-
- -
- -
-
+{% block custom_head_css_js %} + + + + + +{% endblock %} + +{% block content %} +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+ + +
+ + +
+ + + + + + + + + + + + + + +
{% trans 'Hostname' %}{% trans 'IP' %}{% trans 'Hardware' %}{% trans 'Active' %}{% trans 'Reachable' %}{% trans 'Action' %}
+
+
+ +
+ +
+
+
+
+
+
+ + + {% include 'assets/_asset_import_modal.html' %} -{#{% include 'assets/_asset_bulk_update_modal.html' %}#} +{% include 'assets/_asset_list_modal.html' %} {% endblock %} {% block custom_foot_js %} - - -{% endblock %} + +{% endblock %} \ No newline at end of file diff --git a/apps/assets/templates/assets/asset_modal_list.html b/apps/assets/templates/assets/asset_modal_list.html deleted file mode 100644 index ee82d530e..000000000 --- a/apps/assets/templates/assets/asset_modal_list.html +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - diff --git a/apps/assets/templates/assets/asset_update.html b/apps/assets/templates/assets/asset_update.html index b0cbc484b..5d8006451 100644 --- a/apps/assets/templates/assets/asset_update.html +++ b/apps/assets/templates/assets/asset_update.html @@ -2,6 +2,8 @@ {% load static %} {% load bootstrap3 %} {% load i18n %} +{% load asset_tags %} +{% load common_tags %} {% block custom_head_css_js_create %} @@ -20,32 +22,44 @@ {% bootstrap_field form.hostname layout="horizontal" %} {% bootstrap_field form.ip layout="horizontal" %} {% bootstrap_field form.port layout="horizontal" %} - {% bootstrap_field form.cluster layout="horizontal" %} + {% bootstrap_field form.platform layout="horizontal" %} {% bootstrap_field form.public_ip layout="horizontal" %} - {% bootstrap_field form.type layout="horizontal" %} - {% bootstrap_field form.env layout="horizontal" %}

{% trans 'Auth' %}

{% bootstrap_field form.admin_user layout="horizontal" %}
-

{% trans 'Group' %}

- {% bootstrap_field form.groups layout="horizontal" %} +

{% trans 'Node' %}

+ {% bootstrap_field form.nodes layout="horizontal" %}
-

{% trans 'Configuration' %}

- {% bootstrap_field form.number layout="horizontal" %} - {% bootstrap_field form.remote_card_ip layout="horizontal" %} +

{% trans 'Labels' %}

+
+ +
+ +
+
-

{% trans 'Location' %}

- {% bootstrap_field form.cabinet_no layout="horizontal" %} - {% bootstrap_field form.cabinet_pos layout="horizontal" %} +

{% trans 'Configuration' %}

+ {% bootstrap_field form.number layout="horizontal" %}

{% trans 'Other' %}

- {% bootstrap_field form.status layout="horizontal" %} {% bootstrap_field form.comment layout="horizontal" %} {% bootstrap_field form.is_active layout="horizontal" %} @@ -62,14 +76,18 @@ {% block custom_foot_js %} {% endblock %} \ No newline at end of file diff --git a/apps/assets/templates/assets/cluster_assets.html b/apps/assets/templates/assets/cluster_assets.html deleted file mode 100644 index b2e33e578..000000000 --- a/apps/assets/templates/assets/cluster_assets.html +++ /dev/null @@ -1,215 +0,0 @@ -{% extends 'base.html' %} -{% load static %} -{% load i18n %} - -{% block custom_head_css_js %} - - - -{% endblock %} -{% block content %} -
-
-
-
- -
-
-
-
- {% trans 'Cluster assets' %} {{ cluster.name }} -
- - - - - - - - - - -
-
- -
- - - - - - - - - - - - - -
{% trans 'Hostname' %}{% trans 'IP' %}{% trans 'Port' %}{% trans 'Type' %}{% trans 'Alive' %}{% trans 'Action' %}
-
-
-
-
-
-
- {% trans 'Quick update' %} -
-
- - - - - - - -
{% trans 'Test assets connective' %}: - - - -
-
-
-
-
- {% trans 'Add assets to' %} {{ cluster.name }} -
-
- - - - - - - - - - - -
- -
- -
-
-
-
-
-
-
-
-
-{% endblock %} -{% block custom_foot_js %} - - -{% endblock %} diff --git a/apps/assets/templates/assets/cluster_create_update.html b/apps/assets/templates/assets/cluster_create_update.html deleted file mode 100644 index 66c36bece..000000000 --- a/apps/assets/templates/assets/cluster_create_update.html +++ /dev/null @@ -1,76 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} -{% load static %} -{% load bootstrap3 %} -{% block custom_head_css_js %} - - -{% endblock %} -{% block content %} -
-
-
-
-
-
{{ action }}
- -
- -
-
-
-
-
- {% csrf_token %} -

{% trans 'Basic' %}

- {% bootstrap_field form.name layout="horizontal" %} - {% bootstrap_field form.address layout="horizontal" %} - {% bootstrap_field form.contact layout="horizontal" %} - {% bootstrap_field form.phone layout="horizontal" %} - -

{% trans 'Setting' %}

- {% bootstrap_field form.admin_user layout="horizontal" %} - {% bootstrap_field form.system_users layout="horizontal" %} - -
-

{% trans 'Other' %}

- {% bootstrap_field form.operator layout="horizontal" %} - {% bootstrap_field form.intranet layout="horizontal" %} - {% bootstrap_field form.extranet layout="horizontal" %} - -
-
-
- - -
-
-
-
-
-
-
-
-
-
-
-
-{% endblock %} - -{% block custom_foot_js %} - -{% endblock %} \ No newline at end of file diff --git a/apps/assets/templates/assets/cluster_detail.html b/apps/assets/templates/assets/cluster_detail.html deleted file mode 100644 index 837e36e68..000000000 --- a/apps/assets/templates/assets/cluster_detail.html +++ /dev/null @@ -1,164 +0,0 @@ -{% extends 'base.html' %} -{% load static %} -{% load i18n %} - -{% block custom_head_css_js %} - - -{% endblock %} -{% block content %} -
-
-
-
- -
-
-
-
- {{ cluster.name }} -
- - - - - - - - - - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{% trans 'Name' %}:{{ cluster.name }}
{% trans 'Bandwidth' %}:{{ cluster.bandwidth }}
{% trans 'Contact' %}:{{ cluster.contact }}
{% trans 'Phone' %}:{{ cluster.phone }}
{% trans 'Address' %}:{{ cluster.address }}
{% trans 'Intranet' %}:{{ cluster.Intranet }}
{% trans 'Extranet' %}:{{ cluster.extranet }}
{% trans 'Operator' %}:{{ cluster.operator }}
{% trans 'Date created' %}:{{ system_user.date_created }}
{% trans 'Created by' %}:{{ asset_group.created_by }}
{% trans 'Comment' %}:{{ system_user.comment }}
-
-
-
-{#
#} -{#
#} -{#
#} -{# {% trans 'Update admin user' %}#} -{#
#} -{#
#} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{##} -{# {% for cluster in system_user.cluster.all %}#} -{# #} -{# #} -{# #} -{# #} -{# {% endfor %}#} -{# #} -{#
#} -{# #} -{#
#} -{# #} -{#
{{ cluster.name }}#} -{# #} -{#
#} -{#
#} -{#
#} -{#
#} -
-
-
-
-
- -{% endblock %} -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/assets/templates/assets/cluster_list.html b/apps/assets/templates/assets/cluster_list.html deleted file mode 100644 index e4743b346..000000000 --- a/apps/assets/templates/assets/cluster_list.html +++ /dev/null @@ -1,119 +0,0 @@ -{% extends '_base_list.html' %} -{% load i18n static %} -{% block custom_head_css_js %} - - -{% endblock %} - -{% block table_search %}{% endblock %} -{% block table_container %} - - - - - - - - - - - - - - -
- - {% trans 'Name' %}{% trans 'Admin user' %}{% trans 'Asset num' %}{% trans 'System users' %}{% trans 'Action' %}
-{% endblock %} -{% block content_bottom_left %}{% endblock %} -{% block custom_foot_js %} - -{% endblock %} - - - diff --git a/apps/assets/templates/assets/asset_group_create.html b/apps/assets/templates/assets/label_create_update.html similarity index 92% rename from apps/assets/templates/assets/asset_group_create.html rename to apps/assets/templates/assets/label_create_update.html index 36ca40fc1..358faaab8 100644 --- a/apps/assets/templates/assets/asset_group_create.html +++ b/apps/assets/templates/assets/label_create_update.html @@ -7,8 +7,8 @@
{% csrf_token %} {% bootstrap_field form.name layout="horizontal" %} + {% bootstrap_field form.value layout="horizontal" %} {% bootstrap_field form.assets layout="horizontal" %} - {% bootstrap_field form.comment layout="horizontal" %}
diff --git a/apps/assets/templates/assets/label_list.html b/apps/assets/templates/assets/label_list.html new file mode 100644 index 000000000..1c1b380d5 --- /dev/null +++ b/apps/assets/templates/assets/label_list.html @@ -0,0 +1,70 @@ +{% extends '_base_list.html' %} +{% load i18n static %} +{% block table_search %}{% endblock %} +{% block table_container %} + + + + + + + + + + + + + +
+ + {% trans 'Name' %}{% trans 'Value' %}{% trans 'Asset' %}{% trans 'Action' %}
+{% endblock %} +{% block content_bottom_left %}{% endblock %} +{% block custom_foot_js %} + +{% endblock %} + + + diff --git a/apps/assets/templates/assets/system_user_asset.html b/apps/assets/templates/assets/system_user_asset.html index 91c2ffbe0..f82f97ab8 100644 --- a/apps/assets/templates/assets/system_user_asset.html +++ b/apps/assets/templates/assets/system_user_asset.html @@ -21,9 +21,6 @@ {% trans 'Assets' %} -
  • - {% trans 'Update' %} -
  • diff --git a/apps/assets/templates/assets/system_user_detail.html b/apps/assets/templates/assets/system_user_detail.html index 0bf591bb2..c655436b9 100644 --- a/apps/assets/templates/assets/system_user_detail.html +++ b/apps/assets/templates/assets/system_user_detail.html @@ -17,11 +17,11 @@
  • {% trans 'Detail' %}
  • -
  • - - {% trans 'Attached assets' %} - -
  • +{#
  • #} +{# #} +{# {% trans 'Attached assets' %}#} +{# #} +{#
  • #}
  • {% trans 'Update' %}
  • @@ -130,6 +130,23 @@ + + {% trans 'Push system user manually' %}: + + + + + + + + {% trans 'Test assets connective' %}: + + + + + + + {# #} {# {% trans 'Change auth period' %}:#} {# #} @@ -144,33 +161,33 @@
    - {% trans 'Clusters' %} + {% trans 'Nodes' %}
    - +
    - {% for cluster in system_user.cluster.all %} + {% for node in system_user.nodes.all %} - + {% endfor %} @@ -187,27 +204,27 @@ {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/apps/assets/templates/assets/system_user_list.html b/apps/assets/templates/assets/system_user_list.html index 5ad20a57f..b1dd401bc 100644 --- a/apps/assets/templates/assets/system_user_list.html +++ b/apps/assets/templates/assets/system_user_list.html @@ -4,7 +4,7 @@ {% block help_message %}
    系统用户是 用户登录资产(服务器)时使用的用户,如 web, sa, dba等具有特殊功能的用户。系统用户创建时,如果选择了自动推送 - Jumpserver会使用ansible自动推送到系统用户所在集群的资产中,如果资产(交换机)不支持ansible, 请手动填写账号密码。 + Jumpserver会使用ansible自动推送系统用户到资产中,如果资产(交换机、windows)不支持ansible, 请手动填写账号密码。
    {% endblock %} @@ -75,7 +75,8 @@ function initTable() { if (val === 100) { innerHtml = "" + val + "% "; } else { - innerHtml = "" + val + "% "; + var num = new Number(val); + innerHtml = "" + num.toFixed(1) + "% "; } $(td).html('' + innerHtml + ''); diff --git a/apps/assets/templates/assets/system_user_update.html b/apps/assets/templates/assets/system_user_update.html index 0d2029ee8..46ef8d6a3 100644 --- a/apps/assets/templates/assets/system_user_update.html +++ b/apps/assets/templates/assets/system_user_update.html @@ -5,8 +5,8 @@ {% block auth %}

    {% trans 'Auth' %}

    - {% bootstrap_field form.private_key_file layout="horizontal" %} {% bootstrap_field form.password layout="horizontal" %} + {% bootstrap_field form.private_key_file layout="horizontal" %}
    diff --git a/apps/assets/templates/assets/user_asset_list.html b/apps/assets/templates/assets/user_asset_list.html index 4a91d4f65..5c9fa2633 100644 --- a/apps/assets/templates/assets/user_asset_list.html +++ b/apps/assets/templates/assets/user_asset_list.html @@ -19,8 +19,6 @@
    - - @@ -44,14 +42,14 @@ function initTable() { var detail_btn = '' + cellData + ''; $(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id)); }}, - {targets: 7, createdCell: function (td, cellData) { + {targets: 5, createdCell: function (td, cellData) { if (!cellData) { $(td).html('') } else { $(td).html('') } }}, - {targets: 8, createdCell: function (td, cellData) { + {targets: 6, createdCell: function (td, cellData) { if (cellData == 'Unknown'){ $(td).html('') } else if (!cellData) { @@ -68,8 +66,7 @@ function initTable() { ajax_url: '{% url "api-assets:user-asset-list" %}', columns: [ {data: "id"}, {data: "hostname" }, {data: "ip" }, {data: "port" }, - {data: "get_type_display" }, {data: "get_env_display"}, {data: "hardware_info"}, - {data: "is_active" }, {data: "is_connective"} + {data: "hardware_info"}, {data: "is_active" }, {data: "is_connective"} ], op_html: $('#actions').html() }; diff --git a/apps/assets/templatetags/__init__.py b/apps/assets/templatetags/__init__.py new file mode 100644 index 000000000..ec51c5a2b --- /dev/null +++ b/apps/assets/templatetags/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# diff --git a/apps/assets/templatetags/asset_tags.py b/apps/assets/templatetags/asset_tags.py index 829ab2a80..15605f835 100644 --- a/apps/assets/templatetags/asset_tags.py +++ b/apps/assets/templatetags/asset_tags.py @@ -1,6 +1,12 @@ +from collections import defaultdict from django import template -from django.utils import timezone -from django.conf import settings register = template.Library() + +@register.filter +def group_labels(queryset): + grouped = defaultdict(list) + for label in queryset: + grouped[label.name].append(label) + return [(name, labels) for name, labels in grouped.items()] diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index 503464b31..38494539c 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -7,11 +7,13 @@ app_name = 'assets' router = BulkRouter() -router.register(r'v1/groups', api.AssetGroupViewSet, 'asset-group') +# router.register(r'v1/groups', api.AssetGroupViewSet, 'asset-group') router.register(r'v1/assets', api.AssetViewSet, 'asset') -router.register(r'v1/clusters', api.ClusterViewSet, 'cluster') +# router.register(r'v1/clusters', api.ClusterViewSet, 'cluster') router.register(r'v1/admin-user', api.AdminUserViewSet, 'admin-user') router.register(r'v1/system-user', api.SystemUserViewSet, 'system-user') +router.register(r'v1/labels', api.LabelViewSet, 'label') +router.register(r'v1/nodes', api.NodeViewSet, 'node') urlpatterns = [ url(r'^v1/assets-bulk/$', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'), @@ -24,23 +26,27 @@ urlpatterns = [ url(r'^v1/assets/user-assets/$', 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[0-9a-zA-Z\-]{36})/assets/$', - api.GroupUpdateAssetsApi.as_view(), name='group-update-assets'), - url(r'^v1/groups/(?P[0-9a-zA-Z\-]{36})/assets/add/$', - api.GroupAddAssetsApi.as_view(), name='group-add-assets'), + #url(r'^v1/groups/(?P[0-9a-zA-Z\-]{36})/assets/$', + # api.GroupUpdateAssetsApi.as_view(), name='group-update-assets'), + #url(r'^v1/groups/(?P[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[0-9a-zA-Z\-]{36})/assets/$', - api.ClusterAddAssetsApi.as_view(), name='cluster-add-assets'), - url(r'^v1/cluster/(?P[0-9a-zA-Z\-]{36})/assets/connective/$', - api.ClusterTestAssetsAliveApi.as_view(), name='cluster-test-connective'), - url(r'^v1/admin-user/(?P[0-9a-zA-Z\-]{36})/clusters/$', - api.AdminUserAddClustersApi.as_view(), name='admin-user-add-clusters'), + #url(r'^v1/cluster/(?P[0-9a-zA-Z\-]{36})/assets/$', + # api.ClusterAddAssetsApi.as_view(), name='cluster-add-assets'), + #url(r'^v1/cluster/(?P[0-9a-zA-Z\-]{36})/assets/connective/$', + # api.ClusterTestAssetsAliveApi.as_view(), name='cluster-test-connective'), + url(r'^v1/admin-user/(?P[0-9a-zA-Z\-]{36})/nodes/$', + api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'), url(r'^v1/admin-user/(?P[0-9a-zA-Z\-]{36})/connective/$', api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'), url(r'^v1/system-user/(?P[0-9a-zA-Z\-]{36})/push/$', api.SystemUserPushApi.as_view(), name='system-user-push'), url(r'^v1/system-user/(?P[0-9a-zA-Z\-]{36})/connective/$', api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'), + url(r'^v1/nodes/(?P[0-9a-zA-Z\-]{36})/children/$', api.NodeChildrenApi.as_view(), name='node-children'), + url(r'^v1/nodes/(?P[0-9a-zA-Z\-]{36})/children/add/$', api.NodeAddChildrenApi.as_view(), name='node-add-children'), + url(r'^v1/nodes/(?P[0-9a-zA-Z\-]{36})/assets/add/$', api.NodeAddAssetsApi.as_view(), name='node-add-assets'), + url(r'^v1/nodes/(?P[0-9a-zA-Z\-]{36})/assets/remove/$', api.NodeRemoveAssetsApi.as_view(), name='node-remove-assets'), ] urlpatterns += router.urls diff --git a/apps/assets/urls/views_urls.py b/apps/assets/urls/views_urls.py index e24721c09..545e7b062 100644 --- a/apps/assets/urls/views_urls.py +++ b/apps/assets/urls/views_urls.py @@ -14,27 +14,11 @@ urlpatterns = [ url(r'^asset/(?P[0-9a-zA-Z\-]{36})/$', views.AssetDetailView.as_view(), name='asset-detail'), url(r'^asset/(?P[0-9a-zA-Z\-]{36})/update/$', views.AssetUpdateView.as_view(), name='asset-update'), url(r'^asset/(?P[0-9a-zA-Z\-]{36})/delete/$', views.AssetDeleteView.as_view(), name='asset-delete'), - url(r'^asset-modal$', views.AssetModalListView.as_view(), name='asset-modal-list'), url(r'^asset/update/$', views.AssetBulkUpdateView.as_view(), name='asset-bulk-update'), # User asset view url(r'^user-asset/$', views.UserAssetListView.as_view(), name='user-asset-list'), - # Resource asset group url - url(r'^asset-group/$', views.AssetGroupListView.as_view(), name='asset-group-list'), - url(r'^asset-group/create/$', views.AssetGroupCreateView.as_view(), name='asset-group-create'), - url(r'^asset-group/(?P[0-9a-zA-Z\-]{36})/$', views.AssetGroupDetailView.as_view(), name='asset-group-detail'), - url(r'^asset-group/(?P[0-9a-zA-Z\-]{36})/update/$', views.AssetGroupUpdateView.as_view(), name='asset-group-update'), - url(r'^asset-group/(?P[0-9a-zA-Z\-]{36})/delete/$', views.AssetGroupDeleteView.as_view(), name='asset-group-delete'), - - # Resource cluster url - url(r'^cluster/$', views.ClusterListView.as_view(), name='cluster-list'), - url(r'^cluster/create/$', views.ClusterCreateView.as_view(), name='cluster-create'), - url(r'^cluster/(?P[0-9a-zA-Z\-]{36})/$', views.ClusterDetailView.as_view(), name='cluster-detail'), - url(r'^cluster/(?P[0-9a-zA-Z\-]{36})/update/', views.ClusterUpdateView.as_view(), name='cluster-update'), - url(r'^cluster/(?P[0-9a-zA-Z\-]{36})/delete/$', views.ClusterDeleteView.as_view(), name='cluster-delete'), - url(r'^cluster/(?P[0-9a-zA-Z\-]{36})/assets/$', views.ClusterAssetsView.as_view(), name='cluster-assets'), - # Resource admin user url url(r'^admin-user/$', views.AdminUserListView.as_view(), name='admin-user-list'), url(r'^admin-user/create/$', views.AdminUserCreateView.as_view(), name='admin-user-create'), @@ -50,8 +34,10 @@ urlpatterns = [ url(r'^system-user/(?P[0-9a-zA-Z\-]{36})/update/$', views.SystemUserUpdateView.as_view(), name='system-user-update'), url(r'^system-user/(?P[0-9a-zA-Z\-]{36})/delete/$', views.SystemUserDeleteView.as_view(), name='system-user-delete'), url(r'^system-user/(?P[0-9a-zA-Z\-]{36})/asset/$', views.SystemUserAssetView.as_view(), name='system-user-asset'), - # url(r'^system-user/(?P[0-9a-zA-Z\-]{36})/asset-group$', views.SystemUserAssetGroupView.as_view(), - # name='system-user-asset-group'), + url(r'^label/$', views.LabelListView.as_view(), name='label-list'), + url(r'^label/create/$', views.LabelCreateView.as_view(), name='label-create'), + url(r'^label/(?P[0-9a-zA-Z\-]{36})/update/$', views.LabelUpdateView.as_view(), name='label-update'), + url(r'^label/(?P[0-9a-zA-Z\-]{36})/delete/$', views.LabelDeleteView.as_view(), name='label-delete'), ] diff --git a/apps/assets/utils.py b/apps/assets/utils.py index ab63bbafc..e3899bd8a 100644 --- a/apps/assets/utils.py +++ b/apps/assets/utils.py @@ -1,8 +1,13 @@ # ~*~ coding: utf-8 ~*~ # from collections import defaultdict +from functools import reduce +import operator + +from django.db.models import Q + from common.utils import get_object_or_none -from .models import Asset, SystemUser +from .models import Asset, SystemUser, Label def get_assets_by_id_list(id_list): @@ -18,12 +23,26 @@ def get_system_user_by_name(name): return system_user -def check_assets_have_system_user(assets, system_users): - errors = defaultdict(list) +class LabelFilter: + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + query_keys = self.request.query_params.keys() + all_label_keys = Label.objects.values_list('name', flat=True) + valid_keys = set(all_label_keys) & set(query_keys) + labels_query = {} + for key in valid_keys: + labels_query[key] = self.request.query_params.get(key) + + conditions = [] + for k, v in labels_query.items(): + query = {'labels__name': k, 'labels__value': v} + conditions.append(query) + + if conditions: + for kwargs in conditions: + queryset = queryset.filter(**kwargs) + return queryset + + + - for system_user in system_users: - clusters = system_user.cluster.all() - for asset in assets: - if asset.cluster not in clusters: - errors[asset].append(system_user) - return errors diff --git a/apps/assets/views/__init__.py b/apps/assets/views/__init__.py index 883120b36..097dec3ae 100644 --- a/apps/assets/views/__init__.py +++ b/apps/assets/views/__init__.py @@ -1,7 +1,5 @@ # coding:utf-8 from .asset import * -from .group import * -from .cluster import * from .system_user import * from .admin_user import * - +from .label import * diff --git a/apps/assets/views/admin_user.py b/apps/assets/views/admin_user.py index ce7b8bfb4..7d7878e88 100644 --- a/apps/assets/views/admin_user.py +++ b/apps/assets/views/admin_user.py @@ -10,7 +10,7 @@ from django.views.generic.detail import DetailView, SingleObjectMixin from common.const import create_success_msg, update_success_msg from .. import forms -from ..models import AdminUser, Cluster +from ..models import AdminUser, Node from ..hands import AdminUserRequiredMixin __all__ = [ @@ -74,11 +74,10 @@ class AdminUserDetailView(AdminUserRequiredMixin, DetailView): object = None def get_context_data(self, **kwargs): - cluster_remain = Cluster.objects.exclude(admin_user=self.object) context = { 'app': _('Assets'), 'action': _('Admin user detail'), - 'cluster_remain': cluster_remain, + 'nodes': Node.objects.all() } kwargs.update(context) return super().get_context_data(**kwargs) @@ -95,11 +94,8 @@ class AdminUserAssetsView(AdminUserRequiredMixin, SingleObjectMixin, ListView): return super().get(request, *args, **kwargs) def get_queryset(self): - queryset = [] - for cluster in self.object.cluster_set.all(): - queryset.extend([asset for asset in cluster.assets.all() if not asset.admin_user]) - self.queryset = queryset - return queryset + self.queryset = self.object.asset_set.all() + return self.queryset def get_context_data(self, **kwargs): context = { diff --git a/apps/assets/views/asset.py b/apps/assets/views/asset.py index b2f57e323..466bf7f4c 100644 --- a/apps/assets/views/asset.py +++ b/apps/assets/views/asset.py @@ -27,15 +27,14 @@ from common.mixins import JSONResponseMixin from common.utils import get_object_or_none, get_logger, is_uuid from common.const import create_success_msg, update_success_msg from .. import forms -from ..models import Asset, AssetGroup, AdminUser, Cluster, SystemUser +from ..models import Asset, AssetGroup, AdminUser, Cluster, SystemUser, Label, Node from ..hands import AdminUserRequiredMixin __all__ = [ 'AssetListView', 'AssetCreateView', 'AssetUpdateView', 'UserAssetListView', 'AssetBulkUpdateView', 'AssetDetailView', - 'AssetModalListView', 'AssetDeleteView', 'AssetExportView', - 'BulkImportAssetView', + 'AssetDeleteView', 'AssetExportView', 'BulkImportAssetView', ] logger = get_logger(__file__) @@ -44,10 +43,11 @@ class AssetListView(AdminUserRequiredMixin, TemplateView): template_name = 'assets/asset_list.html' def get_context_data(self, **kwargs): + Node.root() context = { 'app': _('Assets'), 'action': _('Asset list'), - 'system_users': SystemUser.objects.all(), + 'labels': Label.objects.all().order_by('name'), } kwargs.update(context) return super().get_context_data(**kwargs) @@ -58,8 +58,7 @@ class UserAssetListView(LoginRequiredMixin, TemplateView): def get_context_data(self, **kwargs): context = { - 'app': _('Assets'), - 'action': _('Asset list'), + 'action': _('My assets'), 'system_users': SystemUser.objects.all(), } kwargs.update(context) @@ -72,12 +71,23 @@ class AssetCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): template_name = 'assets/asset_create.html' success_url = reverse_lazy('assets:asset-list') - def form_valid(self, form): - asset = form.save() - asset.created_by = self.request.user.username or 'Admin' - asset.date_created = timezone.now() - asset.save() - return super().form_valid(form) + # def form_valid(self, form): + # print("form valid") + # asset = form.save() + # asset.created_by = self.request.user.username or 'Admin' + # asset.date_created = timezone.now() + # asset.save() + # return super().form_valid(form) + + def get_form(self, form_class=None): + form = super().get_form(form_class=form_class) + node_id = self.request.GET.get("node_id") + if node_id: + node = get_object_or_none(Node, id=node_id) + else: + node = Node.root() + form["nodes"].initial = node + return form def get_context_data(self, **kwargs): context = { @@ -91,22 +101,22 @@ class AssetCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): return create_success_msg % ({"name": cleaned_data["hostname"]}) -class AssetModalListView(AdminUserRequiredMixin, ListView): - paginate_by = settings.DISPLAY_PER_PAGE - model = Asset - context_object_name = 'asset_modal_list' - template_name = 'assets/asset_modal_list.html' - - def get_context_data(self, **kwargs): - assets = Asset.objects.all() - assets_id = self.request.GET.get('assets_id', '') - assets_id_list = [i for i in assets_id.split(',') if i.isdigit()] - context = { - 'all_assets': assets_id_list, - 'assets': assets - } - kwargs.update(context) - return super().get_context_data(**kwargs) +# class AssetModalListView(AdminUserRequiredMixin, ListView): +# paginate_by = settings.DISPLAY_PER_PAGE +# model = Asset +# context_object_name = 'asset_modal_list' +# template_name = 'assets/_asset_list_modal.html' +# +# def get_context_data(self, **kwargs): +# assets = Asset.objects.all() +# assets_id = self.request.GET.get('assets_id', '') +# assets_id_list = [i for i in assets_id.split(',') if i.isdigit()] +# context = { +# 'all_assets': assets_id_list, +# 'assets': assets +# } +# kwargs.update(context) +# return super().get_context_data(**kwargs) class AssetBulkUpdateView(AdminUserRequiredMixin, ListView): @@ -180,14 +190,11 @@ class AssetDetailView(DetailView): template_name = 'assets/asset_detail.html' def get_context_data(self, **kwargs): - asset_groups = self.object.groups.all() + nodes_remain = Node.objects.exclude(assets=self.object) context = { 'app': _('Assets'), 'action': _('Asset detail'), - 'asset_groups_remain': [asset_group for asset_group in AssetGroup.objects.all() - if asset_group not in asset_groups], - 'asset_groups': asset_groups, - 'system_users_all': SystemUser.objects.all(), + 'nodes_remain': nodes_remain, } kwargs.update(context) return super().get_context_data(**kwargs) @@ -206,22 +213,19 @@ class AssetExportView(View): ] ] filename = 'assets-{}.csv'.format( - timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S')) + timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S') + ) response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename="%s"' % filename response.write(codecs.BOM_UTF8) assets = Asset.objects.filter(id__in=assets_id) - writer = csv.writer(response, dialect='excel', - quoting=csv.QUOTE_MINIMAL) + writer = csv.writer(response, dialect='excel', quoting=csv.QUOTE_MINIMAL) header = [field.verbose_name for field in fields] - header.append(_('Asset groups')) writer.writerow(header) for asset in assets: - groups = ','.join([group.name for group in asset.groups.all()]) data = [getattr(asset, field.name) for field in fields] - data.append(groups) writer.writerow(data) return response @@ -243,6 +247,7 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView): f = form.cleaned_data['file'] det_result = chardet.detect(f.read()) f.seek(0) # reset file seek index + file_data = f.read().decode(det_result['encoding']).strip(codecs.BOM_UTF8.decode()) csv_file = StringIO(file_data) reader = csv.reader(csv_file) @@ -255,7 +260,6 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView): ] header_ = csv_data[0] mapping_reverse = {field.verbose_name: field.name for field in fields} - mapping_reverse[_('Asset groups')] = 'groups' attr = [mapping_reverse.get(n, None) for n in header_] if None in attr: data = {'valid': False, @@ -272,20 +276,15 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView): asset_dict = dict(zip(attr, row)) id_ = asset_dict.pop('id', 0) for k, v in asset_dict.items(): - if k == 'cluster': - v = get_object_or_none(Cluster, name=v) - elif k == 'is_active': - v = bool(v) + if k == 'is_active': + v = True if v in ['TRUE', 1, 'true'] else False elif k == 'admin_user': v = get_object_or_none(AdminUser, name=v) - elif k in ['port', 'cabinet_pos', 'cpu_count', 'cpu_cores']: + elif k in ['port', 'cpu_count', 'cpu_cores']: try: v = int(v) except ValueError: v = 0 - elif k == 'groups': - groups_name = v.split(',') - v = AssetGroup.objects.filter(name__in=groups_name) else: continue asset_dict[k] = v @@ -293,20 +292,15 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView): asset = get_object_or_none(Asset, id=id_) if is_uuid(id_) else None if not asset: try: - groups = asset_dict.pop('groups') if len(Asset.objects.filter(hostname=asset_dict.get('hostname'))): raise Exception(_('already exists')) asset = Asset.objects.create(**asset_dict) - asset.groups.set(groups) created.append(asset_dict['hostname']) assets.append(asset) except Exception as e: failed.append('%s: %s' % (asset_dict['hostname'], str(e))) else: for k, v in asset_dict.items(): - if k == 'groups': - asset.groups.set(v) - continue if v: setattr(asset, k, v) try: diff --git a/apps/assets/views/cluster.py b/apps/assets/views/cluster.py deleted file mode 100644 index 835229fc1..000000000 --- a/apps/assets/views/cluster.py +++ /dev/null @@ -1,116 +0,0 @@ -# coding:utf-8 -from django.utils.translation import ugettext as _ -from django.views.generic import TemplateView, ListView, View -from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView -from django.urls import reverse_lazy -from django.views.generic.detail import DetailView, SingleObjectMixin -from django.contrib.messages.views import SuccessMessageMixin - -from common.const import create_success_msg, update_success_msg -from .. import forms -from ..models import Asset, AssetGroup, AdminUser, Cluster, SystemUser -from ..hands import AdminUserRequiredMixin - - -__all__ = [ - 'ClusterListView', 'ClusterCreateView', 'ClusterUpdateView', - 'ClusterDetailView', 'ClusterDeleteView', 'ClusterAssetsView', -] - - -class ClusterListView(AdminUserRequiredMixin, TemplateView): - template_name = 'assets/cluster_list.html' - - def get_context_data(self, **kwargs): - context = { - 'app': _('Assets'), - 'action': _('Cluster list'), - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - -class ClusterCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): - model = Cluster - form_class = forms.ClusterForm - template_name = 'assets/cluster_create_update.html' - success_url = reverse_lazy('assets:cluster-list') - success_message = create_success_msg - - def get_context_data(self, **kwargs): - context = { - 'app': _('assets'), - 'action': _('Create cluster'), - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - def form_valid(self, form): - cluster = form.save() - cluster.created_by = self.request.user.username - cluster.save() - return super().form_valid(form) - - -class ClusterUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): - model = Cluster - form_class = forms.ClusterForm - template_name = 'assets/cluster_create_update.html' - context_object_name = 'cluster' - success_url = reverse_lazy('assets:cluster-list') - success_message = update_success_msg - - def form_valid(self, form): - cluster = form.save(commit=False) - cluster.save() - return super().form_valid(form) - - def get_context_data(self, **kwargs): - context = { - 'app': _('assets'), - 'action': _('Update Cluster'), - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - -class ClusterDetailView(AdminUserRequiredMixin, DetailView): - model = Cluster - template_name = 'assets/cluster_detail.html' - context_object_name = 'cluster' - - def get_context_data(self, **kwargs): - admin_user_list = AdminUser.objects.exclude() - context = { - 'app': _('Assets'), - 'action': _('Cluster detail'), - 'admin_user_list': admin_user_list, - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - -class ClusterAssetsView(AdminUserRequiredMixin, DetailView): - model = Cluster - template_name = 'assets/cluster_assets.html' - context_object_name = 'cluster' - - def get_context_data(self, **kwargs): - assets_remain = Asset.objects.exclude(id__in=self.object.assets.all()) - - context = { - 'app': _('Assets'), - 'action': _('Asset detail'), - 'groups': AssetGroup.objects.all(), - 'system_users': SystemUser.objects.all(), - 'assets_remain': assets_remain, - 'assets': [asset for asset in Asset.objects.all() if asset not in assets_remain], - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - -class ClusterDeleteView(AdminUserRequiredMixin, DeleteView): - model = Cluster - template_name = 'delete_confirm.html' - success_url = reverse_lazy('assets:cluster-list') diff --git a/apps/assets/views/group.py b/apps/assets/views/group.py deleted file mode 100644 index 0ae65eaa0..000000000 --- a/apps/assets/views/group.py +++ /dev/null @@ -1,97 +0,0 @@ -# coding:utf-8 -from __future__ import absolute_import, unicode_literals - -from django.utils.translation import ugettext as _ -from django.views.generic import TemplateView, ListView, View -from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView -from django.urls import reverse_lazy -from django.views.generic.detail import DetailView, SingleObjectMixin -from django.shortcuts import get_object_or_404, reverse, redirect -from django.contrib.messages.views import SuccessMessageMixin - -from common.const import create_success_msg, update_success_msg -from .. import forms -from ..models import Asset, AssetGroup, AdminUser, Cluster, SystemUser -from ..hands import AdminUserRequiredMixin - - -__all__ = [ - 'AssetGroupCreateView', 'AssetGroupDetailView', - 'AssetGroupUpdateView', 'AssetGroupListView', - 'AssetGroupDeleteView', -] - - -class AssetGroupCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): - model = AssetGroup - form_class = forms.AssetGroupForm - template_name = 'assets/asset_group_create.html' - success_url = reverse_lazy('assets:asset-group-list') - success_message = create_success_msg - - def get_context_data(self, **kwargs): - context = { - 'app': _('Assets'), - 'action': _('Create asset group'), - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - def form_valid(self, form): - group = form.save() - group.created_by = self.request.user.username - group.save() - return super().form_valid(form) - - -class AssetGroupListView(AdminUserRequiredMixin, TemplateView): - template_name = 'assets/asset_group_list.html' - - def get_context_data(self, **kwargs): - context = { - 'app': _('Assets'), - 'action': _('Asset group list'), - 'assets': Asset.objects.all(), - 'system_users': SystemUser.objects.all(), - } - kwargs.update(context) - return super(AssetGroupListView, self).get_context_data(**kwargs) - - -class AssetGroupDetailView(AdminUserRequiredMixin, DetailView): - model = AssetGroup - template_name = 'assets/asset_group_detail.html' - context_object_name = 'asset_group' - - def get_context_data(self, **kwargs): - assets_remain = Asset.objects.exclude(groups__in=[self.object]) - context = { - 'app': _('Assets'), - 'action': _('Asset group detail'), - 'assets_remain': assets_remain, - 'assets': self.object.assets.all(), - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - -class AssetGroupUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): - model = AssetGroup - form_class = forms.AssetGroupForm - template_name = 'assets/asset_group_create.html' - success_url = reverse_lazy('assets:asset-group-list') - success_message = update_success_msg - - def get_context_data(self, **kwargs): - context = { - 'app': _('Assets'), - 'action': _('Create asset group'), - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - -class AssetGroupDeleteView(AdminUserRequiredMixin, DeleteView): - template_name = 'delete_confirm.html' - model = AssetGroup - success_url = reverse_lazy('assets:asset-group-list') diff --git a/apps/assets/views/label.py b/apps/assets/views/label.py new file mode 100644 index 000000000..0b2d0d6ad --- /dev/null +++ b/apps/assets/views/label.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# + +from django.views.generic import TemplateView, CreateView, \ + UpdateView, DeleteView, DetailView +from django.utils.translation import ugettext_lazy as _ +from django.urls import reverse_lazy + +from common.mixins import AdminUserRequiredMixin +from common.const import create_success_msg, update_success_msg +from ..models import Label +from ..forms import LabelForm + + +__all__ = ( + "LabelListView", "LabelCreateView", "LabelUpdateView", + "LabelDetailView", "LabelDeleteView", +) + + +class LabelListView(AdminUserRequiredMixin, TemplateView): + template_name = 'assets/label_list.html' + + def get_context_data(self, **kwargs): + context = { + 'app': _('Assets'), + 'action': _('Label list'), + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class LabelCreateView(AdminUserRequiredMixin, CreateView): + model = Label + template_name = 'assets/label_create_update.html' + form_class = LabelForm + success_url = reverse_lazy('assets:label-list') + success_message = create_success_msg + + def get_context_data(self, **kwargs): + context = { + 'app': _('Assets'), + 'action': _('Create label'), + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class LabelUpdateView(AdminUserRequiredMixin, UpdateView): + model = Label + template_name = 'assets/label_create_update.html' + form_class = LabelForm + success_url = reverse_lazy('assets:label-list') + success_message = update_success_msg + + def get_context_data(self, **kwargs): + context = { + 'app': _('Assets'), + 'action': _('Update label'), + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class LabelDetailView(AdminUserRequiredMixin, DetailView): + pass + + +class LabelDeleteView(AdminUserRequiredMixin, DeleteView): + model = Label + template_name = 'delete_confirm.html' + success_url = reverse_lazy('assets:label-list') diff --git a/apps/assets/views/system_user.py b/apps/assets/views/system_user.py index f7e7eaa35..2e4eacf56 100644 --- a/apps/assets/views/system_user.py +++ b/apps/assets/views/system_user.py @@ -8,8 +8,8 @@ from django.contrib.messages.views import SuccessMessageMixin from django.views.generic.detail import DetailView from common.const import create_success_msg, update_success_msg -from ..forms import SystemUserForm, SystemUserUpdateForm -from ..models import SystemUser, Cluster +from ..forms import SystemUserForm +from ..models import SystemUser, Node from ..hands import AdminUserRequiredMixin @@ -50,7 +50,7 @@ class SystemUserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateVi class SystemUserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): model = SystemUser - form_class = SystemUserUpdateForm + form_class = SystemUserForm template_name = 'assets/system_user_update.html' success_url = reverse_lazy('assets:system-user-list') success_message = update_success_msg @@ -70,11 +70,10 @@ class SystemUserDetailView(AdminUserRequiredMixin, DetailView): model = SystemUser def get_context_data(self, **kwargs): - cluster_remain = Cluster.objects.exclude(systemuser=self.object) context = { 'app': _('Assets'), 'action': _('System user detail'), - 'cluster_remain': cluster_remain, + 'nodes_remain': Node.objects.exclude(systemuser=self.object) } kwargs.update(context) return super().get_context_data(**kwargs) diff --git a/apps/common/api.py b/apps/common/api.py index e8680e85e..58a5822c1 100644 --- a/apps/common/api.py +++ b/apps/common/api.py @@ -9,7 +9,7 @@ from django.core.mail import get_connection, send_mail from django.utils.translation import ugettext_lazy as _ from django.conf import settings -from .permissions import IsSuperUser +from .permissions import IsSuperUser, IsAppUser from .serializers import MailTestSerializer, LDAPTestSerializer @@ -63,8 +63,6 @@ class LDAPTestingAPI(APIView): search_filter = serializer.validated_data["AUTH_LDAP_SEARCH_FILTER"] attr_map = serializer.validated_data["AUTH_LDAP_USER_ATTR_MAP"] - print(serializer.validated_data) - try: attr_map = json.loads(attr_map) except json.JSONDecodeError: @@ -77,9 +75,6 @@ class LDAPTestingAPI(APIView): except Exception as e: return Response({"error": str(e)}, status=401) - print(search_ou) - print(search_filter % ({"user": "*"})) - print(attr_map.values()) ok = conn.search(search_ou, search_filter % ({"user": "*"}), attributes=list(attr_map.values())) if not ok: @@ -93,7 +88,7 @@ class LDAPTestingAPI(APIView): user[attr] = getattr(entry, mapping) users.append(user) if len(users) > 0: - return Response({"msg": "Match {} s users".format(len(users))}) + return Response({"msg": _("Match {} s users").format(len(users))}) else: return Response({"error": "Have user but attr mapping error"}, status=401) else: @@ -102,9 +97,11 @@ class LDAPTestingAPI(APIView): class DjangoSettingsAPI(APIView): def get(self, request): + if not settings.DEBUG: + return Response('Only debug mode support') + configs = {} for i in dir(settings): if i.isupper(): configs[i] = str(getattr(settings, i)) return Response(configs) - diff --git a/apps/common/fields.py b/apps/common/fields.py index 36a8bdf9a..e4645b683 100644 --- a/apps/common/fields.py +++ b/apps/common/fields.py @@ -5,6 +5,8 @@ import json from django import forms from django.utils import six from django.core.exceptions import ValidationError +from django.utils.translation import ugettext as _ +from rest_framework import serializers class DictField(forms.Field): @@ -18,16 +20,16 @@ class DictField(forms.Field): # we don't need to handle that explicitly. if isinstance(value, six.string_types): try: - print(value) value = json.loads(value) return value except json.JSONDecodeError: - pass - value = {} - return value + return ValidationError(_("Not a valid json")) + else: + return ValidationError(_("Not a string type")) def validate(self, value): - print(value) + if isinstance(value, ValidationError): + raise value if not value and self.required: raise ValidationError(self.error_messages['required'], code='required') @@ -35,3 +37,9 @@ class DictField(forms.Field): # Sometimes data or initial may be a string equivalent of a boolean # so we should run it through to_python first to get a boolean value return self.to_python(initial) != self.to_python(data) + + +class StringIDField(serializers.Field): + def to_representation(self, value): + return {"pk": value.pk, "name": value.__str__()} + diff --git a/apps/common/forms.py b/apps/common/forms.py index 6b83c54cc..8a6c87fe4 100644 --- a/apps/common/forms.py +++ b/apps/common/forms.py @@ -4,7 +4,9 @@ import json from django import forms from django.utils.translation import ugettext_lazy as _ +from django.utils.html import escape from django.db import transaction +from django.conf import settings from .models import Setting from .fields import DictField @@ -24,34 +26,38 @@ def to_form_value(value): data = value return data except json.JSONDecodeError: - return '' + return "" class BaseForm(forms.Form): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - settings = Setting.objects.all() + db_settings = Setting.objects.all() for name, field in self.fields.items(): - db_value = getattr(settings, name).value - if db_value: + db_value = getattr(db_settings, name).value + django_value = getattr(settings, name) if hasattr(settings, name) else None + if db_value is False or db_value: field.initial = to_form_value(db_value) + elif django_value is False or django_value: + field.initial = to_form_value(to_model_value(django_value)) - def save(self): + def save(self, category="default"): if not self.is_bound: raise ValueError("Form is not bound") - settings = Setting.objects.all() + db_settings = Setting.objects.all() if self.is_valid(): with transaction.atomic(): for name, value in self.cleaned_data.items(): field = self.fields[name] if isinstance(field.widget, forms.PasswordInput) and not value: continue - if value == to_form_value(getattr(settings, name).value): + if value == to_form_value(getattr(db_settings, name).value): continue defaults = { 'name': name, + 'category': category, 'value': to_model_value(value) } Setting.objects.update_or_create(defaults=defaults, name=name) @@ -62,10 +68,10 @@ class BaseForm(forms.Form): class BasicSettingForm(BaseForm): SITE_URL = forms.URLField( label=_("Current SITE URL"), - help_text="http://jumpserver.abc.com:8080" + help_text="eg: http://jumpserver.abc.com:8080" ) USER_GUIDE_URL = forms.URLField( - label=_("User Guide URL"), + label=_("User Guide URL"), required=False, help_text=_("User first login update profile done redirect to it") ) EMAIL_SUBJECT_PREFIX = forms.CharField( @@ -111,7 +117,8 @@ class LDAPSettingForm(BaseForm): label=_("User OU"), initial='ou=tech,dc=jumpserver,dc=org' ) AUTH_LDAP_SEARCH_FILTER = forms.CharField( - label=_("User search filter"), initial='(cn=%(user)s)' + label=_("User search filter"), initial='(cn=%(user)s)', + help_text=_("User search filter must contain ([cn,uid,sAMAccountName,...]=%(user)s)") ) AUTH_LDAP_USER_ATTR_MAP = DictField( label=_("User attr map"), @@ -119,13 +126,45 @@ class LDAPSettingForm(BaseForm): "username": "cn", "name": "sn", "email": "mail" - }) + }), + help_text=_( + "User attr map present how to map LDAP user attr to jumpserver, username,name,email is jumpserver attr") ) # AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU # AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER AUTH_LDAP_START_TLS = forms.BooleanField( label=_("Use SSL"), initial=False, required=False ) - AUTH_LDAP = forms.BooleanField( - label=_("Enable LDAP Auth"), initial=False, required=False + AUTH_LDAP = forms.BooleanField(label=_("Enable LDAP auth"), initial=False, required=False) + + +class TerminalSettingForm(BaseForm): + SORT_BY_CHOICES = ( + ('hostname', _('Hostname')), + ('ip', _('IP')), + ) + TERMINAL_ASSET_LIST_SORT_BY = forms.ChoiceField( + choices=SORT_BY_CHOICES, initial='hostname', label=_("List sort by") + ) + TERMINAL_HEARTBEAT_INTERVAL = forms.IntegerField( + initial=5, label=_("Heartbeat interval"), help_text=_("Units: seconds") + ) + TERMINAL_PASSWORD_AUTH = forms.BooleanField( + initial=True, required=False, label=_("Password auth") ) + TERMINAL_PUBLIC_KEY_AUTH = forms.BooleanField( + initial=True, required=False, label=_("Public key auth") + ) + TERMINAL_COMMAND_STORAGE = DictField( + label=_("Command storage"), help_text=_( + "Set terminal storage setting, `default` is the using as default," + "You can set other storage and some terminal using" + ) + ) + TERMINAL_REPLAY_STORAGE = DictField( + label=_("Replay storage"), help_text=_( + "Set replay storage setting, `default` is the using as default," + "You can set other storage and some terminal using" + ) + ) + diff --git a/apps/common/mixins.py b/apps/common/mixins.py index d1afb081b..243ee93c6 100644 --- a/apps/common/mixins.py +++ b/apps/common/mixins.py @@ -47,9 +47,9 @@ class JSONResponseMixin(object): return JsonResponse(context) -class CustomFilterMixin(object): +class IDInFilterMixin(object): def filter_queryset(self, queryset): - queryset = super(CustomFilterMixin, self).filter_queryset(queryset) + queryset = super(IDInFilterMixin, self).filter_queryset(queryset) id_list = self.request.query_params.get('id__in') if id_list: import json @@ -99,9 +99,8 @@ class DatetimeSearchMixin: if date_from_s: date_from = timezone.datetime.strptime(date_from_s, self.date_format) - self.date_from = date_from.replace( - tzinfo=timezone.get_current_timezone() - ) + tz = timezone.get_current_timezone() + self.date_from = tz.localize(date_from) else: self.date_from = timezone.now() - timezone.timedelta(7) diff --git a/apps/common/models.py b/apps/common/models.py index 091b8b83a..1f634bce2 100644 --- a/apps/common/models.py +++ b/apps/common/models.py @@ -2,6 +2,7 @@ import json import ldap from django.db import models +from django.db.utils import ProgrammingError, OperationalError from django.utils.translation import ugettext_lazy as _ from django.conf import settings from django_auth_ldap.config import LDAPSearch @@ -24,6 +25,7 @@ class SettingManager(models.Manager): class Setting(models.Model): name = models.CharField(max_length=128, unique=True, verbose_name=_("Name")) value = models.TextField(verbose_name=_("Value")) + category = models.CharField(max_length=128, default="default") enabled = models.BooleanField(verbose_name=_("Enabled"), default=True) comment = models.TextField(verbose_name=_("Comment")) @@ -33,17 +35,28 @@ class Setting(models.Model): return self.name @property - def value_(self): + def cleaned_value(self): try: return json.loads(self.value) except json.JSONDecodeError: return None + @cleaned_value.setter + def cleaned_value(self, item): + try: + v = json.dumps(item) + self.value = v + except json.JSONDecodeError as e: + raise ValueError("Json dump error: {}".format(str(e))) + @classmethod def refresh_all_settings(cls): - settings_list = cls.objects.all() - for setting in settings_list: - setting.refresh_setting() + try: + settings_list = cls.objects.all() + for setting in settings_list: + setting.refresh_setting() + except (ProgrammingError, OperationalError): + pass def refresh_setting(self): try: @@ -53,9 +66,9 @@ class Setting(models.Model): setattr(settings, self.name, value) if self.name == "AUTH_LDAP": - if self.value_ and settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS: + if self.cleaned_value and settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS: settings.AUTHENTICATION_BACKENDS.insert(0, settings.AUTH_LDAP_BACKEND) - elif not self.value_ and settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS: + elif not self.cleaned_value and settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS: settings.AUTHENTICATION_BACKENDS.remove(settings.AUTH_LDAP_BACKEND) if self.name == "AUTH_LDAP_SEARCH_FILTER": diff --git a/apps/common/templates/common/basic_setting.html b/apps/common/templates/common/basic_setting.html index fb5039795..496eca977 100644 --- a/apps/common/templates/common/basic_setting.html +++ b/apps/common/templates/common/basic_setting.html @@ -20,6 +20,9 @@
  • {% trans 'LDAP setting' %}
  • +
  • + {% trans 'Terminal setting' %} +
  • diff --git a/apps/common/templates/common/email_setting.html b/apps/common/templates/common/email_setting.html index 7561849bd..1fd772db1 100644 --- a/apps/common/templates/common/email_setting.html +++ b/apps/common/templates/common/email_setting.html @@ -20,6 +20,9 @@
  • {% trans 'LDAP setting' %}
  • +
  • + {% trans 'Terminal setting' %} +
  • diff --git a/apps/common/templates/common/ldap_setting.html b/apps/common/templates/common/ldap_setting.html index 26f021569..f0569f873 100644 --- a/apps/common/templates/common/ldap_setting.html +++ b/apps/common/templates/common/ldap_setting.html @@ -20,6 +20,9 @@
  • {% trans 'LDAP setting' %}
  • +
  • + {% trans 'Terminal setting' %} +
  • diff --git a/apps/common/templates/common/terminal_setting.html b/apps/common/templates/common/terminal_setting.html new file mode 100644 index 000000000..16927c05a --- /dev/null +++ b/apps/common/templates/common/terminal_setting.html @@ -0,0 +1,150 @@ +{% extends 'base.html' %} +{% load static %} +{% load bootstrap3 %} +{% load i18n %} +{% load common_tags %} + +{% block content %} +
    +
    +
    +
    + +
    +
    +
    +
    + {% if form.non_field_errors %} +
    + {{ form.non_field_errors }} +
    + {% endif %} + {% csrf_token %} +

    {% trans "Basic setting" %}

    + {% for field in form %} + {% if not field.field|is_bool_field %} + {% bootstrap_field field layout="horizontal" %} + {% else %} +
    + +
    +
    + {{ field }} +
    +
    + {{ field.help_text }} +
    +
    +
    + {% endif %} + {% endfor %} + +
    +

    {% trans "Command storage" %}

    +
    - + {% for node in nodes_remain %} + {% endfor %}
    - +
    {{ cluster.name }}{{ node.name }} - +
    {% trans 'Hostname' %} {% trans 'IP' %} {% trans 'Port' %}{% trans 'Type' %}{% trans 'Env' %} {% trans 'Hardware' %} {% trans 'Active' %} {% trans 'Connective' %}
    + + + + + + + + {% for name, setting in command_storage.items %} + + + + + {% endfor %} + +
    {% trans 'Name' %}{% trans 'Type' %}
    {{ name }}{{ setting.TYPE }}
    +
    +

    {% trans "Replay storage" %}

    + + + + + + + + + {% for name, setting in replay_storage.items %} + + + + + {% endfor %} + +
    {% trans 'Name' %}{% trans 'Type' %}
    {{ name }}{{ setting.TYPE }}
    +
    +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/common/templatetags/common_tags.py b/apps/common/templatetags/common_tags.py index c10c228c8..9123ecef8 100644 --- a/apps/common/templatetags/common_tags.py +++ b/apps/common/templatetags/common_tags.py @@ -73,17 +73,20 @@ def to_html(s): @register.filter def time_util_with_seconds(date_from, date_to): - if date_from and date_to: - delta = date_to - date_from - seconds = delta.seconds - if seconds < 60: - return '{} s'.format(seconds) - elif seconds < 60*60: - return '{} m'.format(seconds//60) - else: - return '{} h'.format(seconds//3600) - else: + if not date_from: + return '' + if not date_to: return '' + date_to = timezone.now() + + delta = date_to - date_from + seconds = delta.seconds + if seconds < 60: + return '{} s'.format(seconds) + elif seconds < 60*60: + return '{} m'.format(seconds//60) + else: + return '{} h'.format(seconds//3600) @register.filter @@ -92,3 +95,8 @@ def is_bool_field(field): return True else: return False + + +@register.filter +def to_dict(data): + return dict(data) diff --git a/apps/common/urls/view_urls.py b/apps/common/urls/view_urls.py index ff8086bde..466f7c49c 100644 --- a/apps/common/urls/view_urls.py +++ b/apps/common/urls/view_urls.py @@ -10,4 +10,5 @@ urlpatterns = [ url(r'^$', views.BasicSettingView.as_view(), name='basic-setting'), url(r'^email/$', views.EmailSettingView.as_view(), name='email-setting'), url(r'^ldap/$', views.LDAPSettingView.as_view(), name='ldap-setting'), + url(r'^terminal/$', views.TerminalSettingView.as_view(), name='terminal-setting'), ] diff --git a/apps/common/utils.py b/apps/common/utils.py index f366e6786..0915564ae 100644 --- a/apps/common/utils.py +++ b/apps/common/utils.py @@ -238,9 +238,10 @@ def content_md5(data): """ if isinstance(data, str): data = hashlib.md5(data.encode('utf-8')) - value = base64.b64encode(data.digest()) + value = base64.b64encode(data.hexdigest().encode('utf-8')) return value.decode('utf-8') + _STRPTIME_LOCK = threading.Lock() _GMT_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" diff --git a/apps/common/views.py b/apps/common/views.py index 43b249cee..99b924423 100644 --- a/apps/common/views.py +++ b/apps/common/views.py @@ -1,9 +1,12 @@ -from django.views.generic import View, TemplateView +from django.views.generic import TemplateView from django.shortcuts import render, redirect from django.contrib import messages from django.utils.translation import ugettext as _ +from django.conf import settings -from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm +from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \ + TerminalSettingForm +from .models import Setting from .mixins import AdminUserRequiredMixin from .signals import ldap_auth_enable @@ -86,3 +89,34 @@ class LDAPSettingView(AdminUserRequiredMixin, TemplateView): context = self.get_context_data() context.update({"form": form}) return render(request, self.template_name, context) + + +class TerminalSettingView(AdminUserRequiredMixin, TemplateView): + form_class = TerminalSettingForm + template_name = "common/terminal_setting.html" + + def get_context_data(self, **kwargs): + command_storage = settings.TERMINAL_COMMAND_STORAGE + replay_storage = settings.TERMINAL_REPLAY_STORAGE + context = { + 'app': _('Settings'), + 'action': _('Terminal setting'), + 'form': self.form_class(), + 'replay_storage': replay_storage, + 'command_storage': command_storage, + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + def post(self, request): + form = self.form_class(request.POST) + if form.is_valid(): + form.save() + msg = _("Update setting successfully, please restart program") + messages.success(request, msg) + return redirect('settings:terminal-setting') + else: + context = self.get_context_data() + context.update({"form": form}) + return render(request, self.template_name, context) + diff --git a/apps/i18n/zh/LC_MESSAGES/django.mo b/apps/i18n/zh/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..2861b01286de4833fc9ab469d9fbb629eaa7b721 GIT binary patch literal 29810 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 F`=3<3MZ^FA literal 0 HcmV?d00001 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/i18n/zh/LC_MESSAGES/django.po similarity index 63% rename from apps/locale/zh/LC_MESSAGES/django.po rename to apps/i18n/zh/LC_MESSAGES/django.po index ebcc5ac66..2a98c8c0c 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/i18n/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Jumpserver 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-01-17 17:26+0800\n" +"POT-Creation-Date: 2018-03-07 11:54+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: Jumpserver team\n" @@ -17,120 +17,77 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: assets/forms.py:23 assets/forms.py:53 assets/forms.py:99 perms/forms.py:37 -#: perms/templates/perms/asset_permission_asset.html:116 users/forms.py:246 -msgid "Select asset groups" -msgstr "选择资产组" - -#: assets/forms.py:24 assets/templates/assets/admin_user_detail.html:92 -msgid "Select cluster" -msgstr "选择集群" +#: assets/api/node.py:55 +msgid "New node {}" +msgstr "新节点 {}" -#: assets/forms.py:25 -msgid "Select admin user" -msgstr "选择管理用户" +#: assets/forms/asset.py:23 assets/forms/asset.py:52 assets/forms/user.py:125 +#: assets/models/asset.py:53 assets/models/user.py:218 +#: assets/templates/assets/asset_detail.html:181 +#: assets/templates/assets/asset_detail.html:189 +#: assets/templates/assets/system_user_detail.html:164 +msgid "Nodes" +msgstr "节点管理" -#: assets/forms.py:33 assets/forms.py:61 -msgid "Host level admin user, If not set using cluster admin user default" -msgstr "主机级别管理用户,如果没有设置则默认使用集群级别管理用户" +#: assets/forms/asset.py:26 assets/forms/asset.py:55 assets/forms/asset.py:90 +#: assets/forms/asset.py:94 assets/models/asset.py:57 +#: assets/models/cluster.py:19 assets/models/user.py:187 +#: assets/templates/assets/asset_detail.html:73 templates/_nav.html:24 +msgid "Admin user" +msgstr "管理用户" -#: assets/forms.py:40 assets/forms.py:68 -msgid "You need set a admin user if cluster not have" -msgstr "集群没有管理用户,你需要为集群设置管理用户或设置一个主机级别的管理用户" +#: assets/forms/asset.py:29 assets/forms/asset.py:58 assets/models/asset.py:81 +#: assets/templates/assets/asset_create.html:32 +#: assets/templates/assets/asset_detail.html:218 +#: assets/templates/assets/asset_update.html:37 templates/_nav.html:26 +msgid "Labels" +msgstr "标签管理" -#: assets/forms.py:54 -msgid "Default using cluster admin user" -msgstr "默认使用管理用户" +#: assets/forms/asset.py:37 assets/forms/asset.py:68 +msgid "" +"Admin user is a privilege user exist on this asset,Example: root or other " +"NOPASSWD sudo privilege user" +msgstr "管理用户是资产上已经存在的特权用户,如 root或者其它有NOPASSWD的用户" -#: assets/forms.py:76 assets/forms.py:81 assets/forms.py:127 -#: assets/templates/assets/asset_group_detail.html:75 perms/forms.py:34 -#: perms/templates/perms/asset_permission_asset.html:88 users/forms.py:243 +#: assets/forms/asset.py:77 assets/forms/asset.py:81 assets/forms/label.py:15 +#: perms/templates/perms/asset_permission_asset.html:88 users/forms.py:242 msgid "Select assets" msgstr "选择资产" -#: assets/forms.py:86 assets/models/asset.py:55 -#: assets/templates/assets/admin_user_assets.html:61 +#: assets/forms/asset.py:86 assets/models/asset.py:52 +#: assets/templates/assets/admin_user_assets.html:53 #: assets/templates/assets/asset_detail.html:69 -#: assets/templates/assets/asset_group_detail.html:52 -#: assets/templates/assets/asset_list.html:32 -#: assets/templates/assets/cluster_assets.html:53 -#: assets/templates/assets/system_user_asset.html:54 +#: assets/templates/assets/system_user_asset.html:51 #: assets/templates/assets/user_asset_list.html:21 -#: users/templates/users/user_group_granted_asset.html:51 msgid "Port" msgstr "端口" -#: assets/forms.py:124 assets/models/asset.py:171 +#: assets/forms/asset.py:106 assets/templates/assets/asset_create.html:36 +msgid "Select labels" +msgstr "选择标签" + +#: assets/forms/asset.py:109 assets/templates/assets/admin_user_detail.html:91 +msgid "Select nodes" +msgstr "选择节点" + +#: assets/forms/label.py:13 assets/models/asset.py:153 #: assets/templates/assets/admin_user_list.html:24 -#: assets/templates/assets/asset_group_list.html:16 +#: assets/templates/assets/label_list.html:16 #: assets/templates/assets/system_user_list.html:26 perms/models.py:17 -#: perms/templates/perms/asset_permission_create_update.html:40 -#: perms/templates/perms/asset_permission_list.html:28 templates/_nav.html:22 -#: terminal/backends/command/models.py:11 terminal/models.py:93 +#: terminal/backends/command/models.py:11 terminal/models.py:123 #: terminal/templates/terminal/command_list.html:40 #: terminal/templates/terminal/command_list.html:73 #: terminal/templates/terminal/session_list.html:41 #: terminal/templates/terminal/session_list.html:72 -#: users/templates/users/user_granted_asset.html:82 -#: users/templates/users/user_group_granted_asset.html:86 msgid "Asset" msgstr "资产" -#: assets/forms.py:161 perms/forms.py:40 -#: perms/templates/perms/asset_permission_detail.html:144 users/forms.py:249 -msgid "Select system users" -msgstr "选择系统用户" - -#: assets/forms.py:163 -#: assets/templates/assets/_asset_group_bulk_update_modal.html:22 -#: assets/templates/assets/cluster_list.html:22 -msgid "System users" -msgstr "系统用户" - -#: assets/forms.py:165 -msgid "Selected system users will be create at cluster assets" -msgstr "选择的系统用户将会在该集群资产上创建" +#: assets/forms/user.py:24 +msgid "Password or private key passphrase" +msgstr "密码或密钥密码" -#: assets/forms.py:173 assets/forms.py:248 assets/forms.py:308 -#: assets/models/cluster.py:18 assets/models/group.py:20 -#: assets/models/user.py:28 assets/templates/assets/admin_user_detail.html:56 -#: assets/templates/assets/admin_user_list.html:22 -#: assets/templates/assets/asset_group_list.html:15 -#: assets/templates/assets/cluster_detail.html:57 -#: assets/templates/assets/cluster_list.html:19 -#: assets/templates/assets/system_user_detail.html:58 -#: assets/templates/assets/system_user_list.html:24 common/models.py:25 -#: ops/models.py:31 ops/templates/ops/task_detail.html:56 -#: ops/templates/ops/task_list.html:34 perms/models.py:14 -#: perms/templates/perms/asset_permission_create_update.html:33 -#: perms/templates/perms/asset_permission_detail.html:62 -#: perms/templates/perms/asset_permission_list.html:25 -#: perms/templates/perms/asset_permission_user.html:54 terminal/models.py:14 -#: terminal/models.py:118 terminal/templates/terminal/terminal_detail.html:43 -#: terminal/templates/terminal/terminal_list.html:29 users/models/group.py:14 -#: users/models/user.py:35 users/templates/users/_select_user_modal.html:13 -#: users/templates/users/user_detail.html:62 -#: users/templates/users/user_granted_asset.html:81 -#: users/templates/users/user_group_detail.html:55 -#: users/templates/users/user_group_granted_asset.html:85 -#: users/templates/users/user_group_list.html:12 -#: users/templates/users/user_list.html:23 -#: users/templates/users/user_profile.html:51 -#: users/templates/users/user_pubkey_update.html:53 -msgid "Name" -msgstr "名称" - -#: assets/forms.py:179 -msgid "Cluster level admin user" -msgstr "集群级别管理用户" - -#: assets/forms.py:200 -msgid "Password or private key password" -msgstr "密码或秘钥不合法" - -#: assets/forms.py:201 assets/forms.py:262 assets/models/user.py:30 -#: common/forms.py:107 users/forms.py:16 users/forms.py:24 -#: users/templates/users/login.html:56 +#: assets/forms/user.py:25 assets/models/user.py:30 common/forms.py:113 +#: users/forms.py:16 users/forms.py:24 users/templates/users/login.html:59 #: users/templates/users/reset_password.html:52 #: users/templates/users/user_create.html:11 #: users/templates/users/user_password_update.html:40 @@ -139,304 +96,233 @@ msgstr "密码或秘钥不合法" msgid "Password" msgstr "密码" -#: assets/forms.py:204 assets/forms.py:264 users/models/user.py:45 +#: assets/forms/user.py:28 users/models/user.py:45 msgid "Private key" msgstr "ssh私钥" -#: assets/forms.py:229 assets/forms.py:290 assets/forms.py:354 +#: assets/forms/user.py:38 msgid "Invalid private key" msgstr "ssh密钥不合法" -#: assets/forms.py:240 +#: assets/forms/user.py:47 msgid "Password and private key file must be input one" msgstr "密码和私钥, 必须输入一个" -#: assets/forms.py:249 assets/forms.py:309 assets/models/user.py:29 +#: assets/forms/user.py:79 assets/forms/user.py:120 assets/models/cluster.py:18 +#: assets/models/group.py:20 assets/models/label.py:17 assets/models/user.py:28 +#: assets/templates/assets/admin_user_detail.html:56 +#: assets/templates/assets/admin_user_list.html:22 +#: assets/templates/assets/label_list.html:14 +#: assets/templates/assets/system_user_detail.html:58 +#: assets/templates/assets/system_user_list.html:24 common/models.py:26 +#: common/templates/common/terminal_setting.html:67 +#: common/templates/common/terminal_setting.html:85 ops/models.py:31 +#: ops/templates/ops/task_detail.html:56 ops/templates/ops/task_list.html:34 +#: perms/models.py:14 perms/templates/perms/asset_permission_detail.html:62 +#: perms/templates/perms/asset_permission_user.html:54 terminal/models.py:16 +#: terminal/models.py:149 terminal/templates/terminal/terminal_detail.html:43 +#: terminal/templates/terminal/terminal_list.html:29 users/models/group.py:14 +#: users/models/user.py:35 users/templates/users/_select_user_modal.html:13 +#: users/templates/users/user_detail.html:63 +#: users/templates/users/user_group_detail.html:55 +#: users/templates/users/user_group_list.html:12 +#: users/templates/users/user_list.html:23 +#: users/templates/users/user_profile.html:51 +#: users/templates/users/user_pubkey_update.html:53 +msgid "Name" +msgstr "名称" + +#: assets/forms/user.py:80 assets/forms/user.py:121 assets/models/user.py:29 #: assets/templates/assets/admin_user_detail.html:60 #: assets/templates/assets/admin_user_list.html:23 #: assets/templates/assets/system_user_detail.html:62 #: assets/templates/assets/system_user_list.html:25 #: perms/templates/perms/asset_permission_user.html:55 users/forms.py:14 -#: users/models/authentication.py:44 users/models/user.py:34 +#: users/models/authentication.py:45 users/models/user.py:34 #: users/templates/users/_select_user_modal.html:14 -#: users/templates/users/login.html:53 +#: users/templates/users/login.html:56 #: users/templates/users/login_log_list.html:49 -#: users/templates/users/user_detail.html:66 +#: users/templates/users/user_detail.html:67 #: users/templates/users/user_list.html:24 #: users/templates/users/user_profile.html:47 msgid "Username" msgstr "用户名" -#: assets/forms.py:297 assets/forms.py:360 -msgid "Auth info required, private_key or password" -msgstr "密钥和密码必须填写一个" +#: assets/forms/user.py:132 +msgid "If auto push checked, system user will be create at node assets" +msgstr "如果选择了自动推送,系统用户将会创建在节点资产上" -#: assets/forms.py:313 -msgid " Select clusters" -msgstr "选择集群" - -#: assets/forms.py:320 -msgid "If auto push checked, system user will be create at cluster assets" -msgstr "如果选择了自动推送,系统用户将会创建在集群资产上" - -#: assets/forms.py:321 +#: assets/forms/user.py:133 msgid "Auto push system user to asset" msgstr "自动推送系统用户到资产" -#: assets/forms.py:322 +#: assets/forms/user.py:134 msgid "" "High level will be using login asset as default, if user was granted more " "than 2 system user" msgstr "高优先级的系统用户将会作为默认登录用户" -#: assets/models/asset.py:34 -msgid "In use" -msgstr "使用中" - -#: assets/models/asset.py:35 -msgid "Out of use" -msgstr "未使用" - -#: assets/models/asset.py:38 -msgid "Server" -msgstr "物理机" - -#: assets/models/asset.py:39 -msgid "VM" -msgstr "虚拟机" - -#: assets/models/asset.py:40 -msgid "Switch" -msgstr "交换机" - -#: assets/models/asset.py:41 -msgid "Router" -msgstr "路由器" - -#: assets/models/asset.py:42 -msgid "Firewall" -msgstr "防火墙" - -#: assets/models/asset.py:43 -msgid "Storage" -msgstr "存储" - -#: assets/models/asset.py:46 -msgid "Production" -msgstr "生产环境" - -#: assets/models/asset.py:47 -msgid "Development" -msgstr "开发环境" - -#: assets/models/asset.py:48 -msgid "Testing" -msgstr "测试环境" - -#: assets/models/asset.py:53 assets/templates/assets/admin_user_assets.html:60 +#: assets/models/asset.py:50 assets/templates/assets/_asset_list_modal.html:21 +#: assets/templates/assets/admin_user_assets.html:52 #: assets/templates/assets/asset_detail.html:61 -#: assets/templates/assets/asset_group_detail.html:51 -#: assets/templates/assets/asset_list.html:31 -#: assets/templates/assets/cluster_assets.html:52 -#: assets/templates/assets/system_user_asset.html:53 -#: assets/templates/assets/user_asset_list.html:20 +#: assets/templates/assets/asset_list.html:87 +#: assets/templates/assets/system_user_asset.html:50 +#: assets/templates/assets/user_asset_list.html:20 common/forms.py:144 #: perms/templates/perms/asset_permission_asset.html:55 #: users/templates/users/login_log_list.html:52 -#: users/templates/users/user_granted_asset.html:49 -#: users/templates/users/user_group_granted_asset.html:50 +#: users/templates/users/user_granted_asset.html:45 +#: users/templates/users/user_group_granted_asset.html:45 msgid "IP" msgstr "IP" -#: assets/models/asset.py:54 assets/templates/assets/admin_user_assets.html:59 +#: assets/models/asset.py:51 assets/templates/assets/_asset_list_modal.html:20 +#: assets/templates/assets/admin_user_assets.html:51 #: assets/templates/assets/asset_detail.html:57 -#: assets/templates/assets/asset_group_detail.html:50 -#: assets/templates/assets/asset_list.html:30 -#: assets/templates/assets/cluster_assets.html:51 -#: assets/templates/assets/system_user_asset.html:52 -#: assets/templates/assets/user_asset_list.html:19 +#: assets/templates/assets/asset_list.html:86 +#: assets/templates/assets/system_user_asset.html:49 +#: assets/templates/assets/user_asset_list.html:19 common/forms.py:143 #: perms/templates/perms/asset_permission_asset.html:54 -#: users/templates/users/user_granted_asset.html:48 -#: users/templates/users/user_group_granted_asset.html:49 +#: users/templates/users/user_granted_asset.html:44 +#: users/templates/users/user_group_granted_asset.html:44 msgid "Hostname" msgstr "主机名" -#: assets/models/asset.py:56 assets/templates/assets/asset_detail.html:213 -#: assets/views/asset.py:218 assets/views/asset.py:258 -msgid "Asset groups" -msgstr "资产组" - -#: assets/models/asset.py:57 assets/models/cluster.py:40 -#: assets/models/user.py:219 assets/templates/assets/asset_detail.html:85 -#: assets/templates/assets/asset_list.html:33 templates/_nav.html:24 -msgid "Cluster" -msgstr "集群" - -#: assets/models/asset.py:58 assets/templates/assets/asset_detail.html:129 +#: assets/models/asset.py:54 assets/models/label.py:20 +#: assets/templates/assets/asset_detail.html:105 +#: perms/templates/perms/asset_permission_list.html:70 msgid "Is active" msgstr "激活" -#: assets/models/asset.py:59 assets/templates/assets/asset_detail.html:133 -msgid "Asset type" -msgstr "系统类型" - -#: assets/models/asset.py:60 assets/templates/assets/asset_detail.html:137 -msgid "Asset environment" -msgstr "资产环境" - -#: assets/models/asset.py:61 assets/templates/assets/asset_detail.html:125 -msgid "Asset status" -msgstr "资产状态" - -#: assets/models/asset.py:64 assets/models/cluster.py:19 -#: assets/models/user.py:190 assets/templates/assets/asset_detail.html:73 -#: assets/templates/assets/cluster_list.html:20 templates/_nav.html:25 -msgid "Admin user" -msgstr "管理用户" - -#: assets/models/asset.py:67 assets/templates/assets/asset_detail.html:65 +#: assets/models/asset.py:60 assets/templates/assets/asset_detail.html:65 msgid "Public IP" msgstr "公网IP" -#: assets/models/asset.py:68 -msgid "Remote control card IP" -msgstr "远控卡IP" - -#: assets/models/asset.py:69 assets/templates/assets/asset_detail.html:89 -msgid "Cabinet number" -msgstr "机柜编号" - -#: assets/models/asset.py:70 assets/templates/assets/asset_detail.html:93 -msgid "Cabinet position" -msgstr "机柜层号" - -#: assets/models/asset.py:71 assets/templates/assets/asset_detail.html:145 +#: assets/models/asset.py:61 assets/templates/assets/asset_detail.html:113 msgid "Asset number" msgstr "资产编号" -#: assets/models/asset.py:74 assets/templates/assets/asset_detail.html:97 +#: assets/models/asset.py:64 assets/templates/assets/asset_detail.html:77 msgid "Vendor" msgstr "制造商" -#: assets/models/asset.py:75 assets/templates/assets/asset_detail.html:101 +#: assets/models/asset.py:65 assets/templates/assets/asset_detail.html:81 msgid "Model" msgstr "型号" -#: assets/models/asset.py:76 assets/templates/assets/asset_detail.html:141 +#: assets/models/asset.py:66 assets/templates/assets/asset_detail.html:109 msgid "Serial number" msgstr "序列号" -#: assets/models/asset.py:78 +#: assets/models/asset.py:68 msgid "CPU model" msgstr "CPU型号" -#: assets/models/asset.py:79 +#: assets/models/asset.py:69 msgid "CPU count" msgstr "CPU数量" -#: assets/models/asset.py:80 +#: assets/models/asset.py:70 msgid "CPU cores" msgstr "CPU核数" -#: assets/models/asset.py:81 assets/templates/assets/asset_detail.html:109 +#: assets/models/asset.py:71 assets/templates/assets/asset_detail.html:89 msgid "Memory" msgstr "内存" -#: assets/models/asset.py:82 +#: assets/models/asset.py:72 msgid "Disk total" msgstr "硬盘大小" -#: assets/models/asset.py:83 +#: assets/models/asset.py:73 msgid "Disk info" msgstr "硬盘信息" -#: assets/models/asset.py:85 assets/templates/assets/asset_detail.html:117 +#: assets/models/asset.py:75 assets/templates/assets/asset_detail.html:97 msgid "Platform" msgstr "系统平台" -#: assets/models/asset.py:86 assets/templates/assets/asset_detail.html:121 +#: assets/models/asset.py:76 assets/templates/assets/asset_detail.html:101 msgid "OS" msgstr "操作系统" -#: assets/models/asset.py:87 +#: assets/models/asset.py:77 msgid "OS version" msgstr "系统版本" -#: assets/models/asset.py:88 +#: assets/models/asset.py:78 msgid "OS arch" msgstr "系统架构" -#: assets/models/asset.py:89 +#: assets/models/asset.py:79 msgid "Hostname raw" msgstr "主机名原始" -#: assets/models/asset.py:91 assets/models/cluster.py:28 +#: assets/models/asset.py:82 assets/models/cluster.py:28 #: assets/models/group.py:21 assets/models/user.py:36 #: assets/templates/assets/admin_user_detail.html:68 -#: assets/templates/assets/asset_detail.html:149 -#: assets/templates/assets/cluster_detail.html:93 +#: assets/templates/assets/asset_detail.html:117 #: assets/templates/assets/system_user_detail.html:96 -#: ops/templates/ops/adhoc_detail.html:86 perms/models.py:22 +#: ops/templates/ops/adhoc_detail.html:86 perms/models.py:22 perms/models.py:79 #: perms/templates/perms/asset_permission_detail.html:94 -#: users/models/user.py:50 users/templates/users/user_detail.html:98 +#: users/models/user.py:50 users/templates/users/user_detail.html:99 msgid "Created by" msgstr "创建者" -#: assets/models/asset.py:92 assets/models/cluster.py:26 -#: assets/models/group.py:22 assets/templates/assets/admin_user_detail.html:64 -#: assets/templates/assets/cluster_detail.html:89 +#: assets/models/asset.py:83 assets/models/cluster.py:26 +#: assets/models/group.py:22 assets/models/label.py:23 +#: assets/templates/assets/admin_user_detail.html:64 #: assets/templates/assets/system_user_detail.html:92 #: ops/templates/ops/adhoc_detail.html:90 ops/templates/ops/task_detail.html:60 -#: perms/models.py:23 perms/templates/perms/asset_permission_detail.html:90 +#: perms/models.py:23 perms/models.py:80 +#: perms/templates/perms/asset_permission_detail.html:90 #: terminal/templates/terminal/terminal_detail.html:59 users/models/group.py:17 #: users/templates/users/user_group_detail.html:63 msgid "Date created" msgstr "创建日期" -#: assets/models/asset.py:93 assets/models/cluster.py:29 -#: assets/models/group.py:23 assets/models/user.py:33 +#: assets/models/asset.py:84 assets/models/cluster.py:29 +#: assets/models/group.py:23 assets/models/label.py:21 assets/models/user.py:33 #: assets/templates/assets/admin_user_detail.html:72 #: assets/templates/assets/admin_user_list.html:28 -#: assets/templates/assets/asset_detail.html:157 -#: assets/templates/assets/asset_group_list.html:17 -#: assets/templates/assets/cluster_detail.html:97 +#: assets/templates/assets/asset_detail.html:125 #: assets/templates/assets/system_user_detail.html:100 -#: assets/templates/assets/system_user_list.html:30 common/models.py:28 -#: ops/models.py:37 perms/models.py:24 -#: perms/templates/perms/asset_permission_detail.html:98 terminal/models.py:22 +#: assets/templates/assets/system_user_list.html:30 common/models.py:30 +#: ops/models.py:37 perms/models.py:24 perms/models.py:81 +#: perms/templates/perms/asset_permission_detail.html:98 terminal/models.py:26 #: terminal/templates/terminal/terminal_detail.html:63 users/models/group.py:15 -#: users/models/user.py:47 users/templates/users/user_detail.html:110 +#: users/models/user.py:47 users/templates/users/user_detail.html:111 #: users/templates/users/user_group_detail.html:67 #: users/templates/users/user_group_list.html:14 -#: users/templates/users/user_profile.html:118 +#: users/templates/users/user_profile.html:114 msgid "Comment" msgstr "备注" -#: assets/models/cluster.py:20 assets/templates/assets/cluster_detail.html:61 +#: assets/models/cluster.py:20 msgid "Bandwidth" msgstr "带宽" -#: assets/models/cluster.py:21 assets/templates/assets/cluster_detail.html:65 +#: assets/models/cluster.py:21 msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 assets/templates/assets/cluster_detail.html:69 -#: users/models/user.py:41 users/templates/users/user_detail.html:75 +#: assets/models/cluster.py:22 users/models/user.py:41 +#: users/templates/users/user_detail.html:76 msgid "Phone" msgstr "手机" -#: assets/models/cluster.py:23 assets/templates/assets/cluster_detail.html:73 +#: assets/models/cluster.py:23 msgid "Address" msgstr "地址" -#: assets/models/cluster.py:24 assets/templates/assets/cluster_detail.html:77 +#: assets/models/cluster.py:24 msgid "Intranet" msgstr "内网" -#: assets/models/cluster.py:25 assets/templates/assets/cluster_detail.html:81 +#: assets/models/cluster.py:25 msgid "Extranet" msgstr "外网" -#: assets/models/cluster.py:27 assets/templates/assets/cluster_detail.html:85 +#: assets/models/cluster.py:27 msgid "Operator" msgstr "运营商" @@ -444,7 +330,8 @@ msgstr "运营商" msgid "Default" msgstr "默认" -#: assets/models/cluster.py:36 users/models/user.py:258 +#: assets/models/cluster.py:36 assets/models/label.py:13 +#: users/models/user.py:266 msgid "System" msgstr "系统" @@ -452,8 +339,11 @@ msgstr "系统" msgid "Default Cluster" msgstr "默认Cluster" +#: assets/models/cluster.py:40 +msgid "Cluster" +msgstr "集群" + #: assets/models/group.py:30 perms/models.py:18 -#: perms/templates/perms/asset_permission_list.html:29 templates/_nav.html:23 msgid "Asset group" msgstr "资产组" @@ -461,6 +351,31 @@ msgstr "资产组" msgid "Default asset group" msgstr "默认资产组" +#: assets/models/label.py:14 perms/models.py:15 +#: terminal/backends/command/models.py:10 terminal/models.py:122 +#: terminal/templates/terminal/command_list.html:32 +#: terminal/templates/terminal/command_list.html:72 +#: terminal/templates/terminal/session_list.html:33 +#: terminal/templates/terminal/session_list.html:71 users/forms.py:190 +#: users/models/user.py:30 users/models/user.py:254 +#: users/templates/users/user_group_detail.html:78 +#: users/templates/users/user_group_list.html:13 users/views/user.py:333 +msgid "User" +msgstr "用户" + +#: assets/models/label.py:18 assets/models/node.py:15 +#: assets/templates/assets/label_list.html:15 common/models.py:27 +msgid "Value" +msgstr "值" + +#: assets/models/label.py:19 +msgid "Category" +msgstr "分类" + +#: assets/models/node.py:14 +msgid "Key" +msgstr "" + #: assets/models/user.py:31 msgid "SSH private key" msgstr "ssh密钥" @@ -469,38 +384,36 @@ msgstr "ssh密钥" msgid "SSH public key" msgstr "ssh公钥" -#: assets/models/user.py:220 +#: assets/models/user.py:219 msgid "Priority" msgstr "优先级" -#: assets/models/user.py:221 assets/templates/assets/system_user_detail.html:66 +#: assets/models/user.py:220 assets/templates/assets/system_user_detail.html:66 msgid "Protocol" msgstr "协议" -#: assets/models/user.py:222 assets/templates/assets/_system_user.html:59 +#: assets/models/user.py:221 assets/templates/assets/_system_user.html:58 #: assets/templates/assets/system_user_detail.html:118 #: assets/templates/assets/system_user_update.html:11 msgid "Auto push" msgstr "自动推送" -#: assets/models/user.py:223 assets/templates/assets/system_user_detail.html:70 +#: assets/models/user.py:222 assets/templates/assets/system_user_detail.html:70 msgid "Sudo" msgstr "Sudo" -#: assets/models/user.py:224 assets/templates/assets/system_user_detail.html:75 +#: assets/models/user.py:223 assets/templates/assets/system_user_detail.html:75 msgid "Shell" msgstr "Shell" -#: assets/models/user.py:269 perms/models.py:19 -#: perms/templates/perms/asset_permission_detail.html:136 -#: perms/templates/perms/asset_permission_list.html:30 templates/_nav.html:26 -#: terminal/backends/command/models.py:12 terminal/models.py:94 +#: assets/models/user.py:266 perms/forms.py:25 perms/models.py:19 +#: perms/models.py:76 perms/templates/perms/asset_permission_detail.html:136 +#: perms/templates/perms/asset_permission_list.html:69 templates/_nav.html:25 +#: terminal/backends/command/models.py:12 terminal/models.py:124 #: terminal/templates/terminal/command_list.html:48 #: terminal/templates/terminal/command_list.html:74 #: terminal/templates/terminal/session_list.html:49 #: terminal/templates/terminal/session_list.html:73 -#: users/templates/users/user_granted_asset.html:50 -#: users/templates/users/user_group_granted_asset.html:52 msgid "System user" msgstr "系统用户" @@ -509,91 +422,80 @@ msgstr "系统用户" msgid "%(value)s is not an even number" msgstr "%(value)s is not an even number" -#: assets/signals_handler.py:31 -msgid "Push cluster system users to asset" -msgstr "推送集群系统用户到资产" - -#: assets/signals_handler.py:63 assets/signals_handler.py:125 -msgid "Push system user to cluster assets: {}->{}" -msgstr "推送系统用户到: {}->{}" - -#: assets/signals_handler.py:102 -msgid "Push system user to assets" -msgstr "推送系统用户到资产" - -#: assets/tasks.py:91 +#: assets/tasks.py:92 msgid "Update some assets hardware info" msgstr "更新一些资产硬件信息" -#: assets/tasks.py:107 +#: assets/tasks.py:108 msgid "Update asset hardware info" msgstr "更新资产硬件信息" -#: assets/tasks.py:121 +#: assets/tasks.py:122 msgid "Update assets hardware info period" msgstr "定期更新资产硬件信息" -#: assets/tasks.py:189 +#: assets/tasks.py:195 msgid "Test admin user connectability period: {}" msgstr "定期测试管理用户可连接性: {}" -#: assets/tasks.py:203 +#: assets/tasks.py:201 msgid "Test admin user connectability: {}" msgstr "测试管理用户可连接性: {}" -#: assets/tasks.py:212 +#: assets/tasks.py:210 msgid "Test asset connectability" msgstr "测试资产可连接性" -#: assets/tasks.py:283 +#: assets/tasks.py:281 msgid "Test system user connectability: {}" msgstr "测试系统用户可连接性: {}" -#: assets/tasks.py:295 -msgid "Test system user connectability period: {}" -msgstr "定期测试系统用户可连接性: {}" +#: assets/tasks.py:292 +msgid "test system user connectability period: {}" +msgstr "测试系统用户可连接性: {}" -#: assets/tasks.py:376 -msgid "Push system user to cluster assets: {}" -msgstr "推送系统用户到资产: {}" +#: assets/tasks.py:365 +msgid "Push system user to node: {} => {}" +msgstr "推送系统用户到节点: {}->{}" #: assets/tasks.py:397 -msgid "Push cluster system users to assets period: {}" -msgstr "定期推送集群系统用户到资产: {}" +msgid "Push system users to node: {}" +msgstr "推送系统用户到节点: {}" #: assets/templates/assets/_asset_group_bulk_update_modal.html:5 msgid "Update asset group" -msgstr "编辑用户组" +msgstr "更新用户组" #: assets/templates/assets/_asset_group_bulk_update_modal.html:8 msgid "Hint: only change the field you want to update." msgstr "仅修改你需要更新的字段" -#: assets/templates/assets/_asset_group_bulk_update_modal.html:12 +#: assets/templates/assets/_asset_group_bulk_update_modal.html:11 #: assets/templates/assets/system_user_asset.html:21 #: assets/views/admin_user.py:29 assets/views/admin_user.py:47 -#: assets/views/admin_user.py:63 assets/views/admin_user.py:79 -#: assets/views/admin_user.py:106 assets/views/asset.py:48 -#: assets/views/asset.py:61 assets/views/asset.py:84 assets/views/asset.py:144 -#: assets/views/asset.py:161 assets/views/asset.py:185 -#: assets/views/cluster.py:26 assets/views/cluster.py:85 -#: assets/views/cluster.py:102 assets/views/group.py:34 -#: assets/views/group.py:52 assets/views/group.py:69 assets/views/group.py:87 -#: assets/views/system_user.py:28 assets/views/system_user.py:44 -#: assets/views/system_user.py:60 assets/views/system_user.py:75 -#: templates/_nav.html:19 +#: assets/views/admin_user.py:63 assets/views/admin_user.py:78 +#: assets/views/admin_user.py:102 assets/views/asset.py:48 +#: assets/views/asset.py:94 assets/views/asset.py:154 assets/views/asset.py:171 +#: assets/views/asset.py:195 assets/views/label.py:26 assets/views/label.py:42 +#: assets/views/label.py:58 assets/views/system_user.py:28 +#: assets/views/system_user.py:44 assets/views/system_user.py:60 +#: assets/views/system_user.py:74 templates/_nav.html:20 msgid "Assets" msgstr "资产管理" -#: assets/templates/assets/_asset_group_bulk_update_modal.html:14 +#: assets/templates/assets/_asset_group_bulk_update_modal.html:13 msgid "Select Asset" msgstr "选择资产" -#: assets/templates/assets/_asset_group_bulk_update_modal.html:24 +#: assets/templates/assets/_asset_group_bulk_update_modal.html:21 +msgid "System users" +msgstr "系统用户" + +#: assets/templates/assets/_asset_group_bulk_update_modal.html:23 msgid "Select System Users" msgstr "选择系统用户" -#: assets/templates/assets/_asset_group_bulk_update_modal.html:35 +#: assets/templates/assets/_asset_group_bulk_update_modal.html:34 msgid "Enable-OTP" msgstr "二次验证" @@ -619,20 +521,153 @@ msgstr "资产csv文件" msgid "If set id, will use this id update asset existed" msgstr "如果设置了id,则会使用该行信息更新该id的资产" -#: assets/templates/assets/_system_user.html:16 -#: assets/templates/assets/system_user_list.html:16 -#: assets/views/system_user.py:45 -msgid "Create system user" -msgstr "创建系统用户" +#: assets/templates/assets/_asset_list_modal.html:22 +#: assets/templates/assets/asset_list.html:88 +#: assets/templates/assets/user_asset_list.html:22 +msgid "Hardware" +msgstr "硬件" + +#: assets/templates/assets/_asset_list_modal.html:23 +#: assets/templates/assets/asset_detail.html:143 +#: assets/templates/assets/asset_list.html:89 +#: assets/templates/assets/user_asset_list.html:23 perms/models.py:20 +#: perms/models.py:77 +#: perms/templates/perms/asset_permission_create_update.html:51 +#: perms/templates/perms/asset_permission_detail.html:116 +#: terminal/templates/terminal/terminal_list.html:34 +#: users/templates/users/_select_user_modal.html:18 +#: users/templates/users/user_detail.html:128 +#: users/templates/users/user_granted_asset.html:46 +#: users/templates/users/user_group_granted_asset.html:46 +#: users/templates/users/user_list.html:27 +#: users/templates/users/user_profile.html:63 +msgid "Active" +msgstr "激活中" + +#: assets/templates/assets/_asset_list_modal.html:24 +#: assets/templates/assets/admin_user_assets.html:54 +#: assets/templates/assets/admin_user_list.html:25 +#: assets/templates/assets/asset_detail.html:357 +#: assets/templates/assets/asset_list.html:90 +#: assets/templates/assets/system_user_asset.html:52 +#: assets/templates/assets/system_user_list.html:27 +#: users/templates/users/user_granted_asset.html:47 +#: users/templates/users/user_group_granted_asset.html:47 +msgid "Reachable" +msgstr "可连接" + +#: assets/templates/assets/_asset_list_modal.html:25 +#: assets/templates/assets/admin_user_list.html:29 +#: assets/templates/assets/asset_list.html:91 +#: assets/templates/assets/label_list.html:17 +#: assets/templates/assets/system_user_list.html:31 +#: ops/templates/ops/adhoc_history.html:59 ops/templates/ops/task_adhoc.html:61 +#: ops/templates/ops/task_history.html:62 ops/templates/ops/task_list.html:41 +#: perms/templates/perms/asset_permission_list.html:72 +#: terminal/templates/terminal/session_list.html:80 +#: terminal/templates/terminal/terminal_list.html:36 +#: users/templates/users/user_group_list.html:15 +#: users/templates/users/user_list.html:28 +msgid "Action" +msgstr "动作" + +#: assets/templates/assets/_asset_list_modal.html:34 +#: assets/templates/assets/asset_list.html:100 +#: users/templates/users/user_list.html:37 +msgid "Delete selected" +msgstr "批量删除" + +#: assets/templates/assets/_asset_list_modal.html:35 +#: assets/templates/assets/asset_list.html:101 +#: users/templates/users/user_list.html:38 +msgid "Update selected" +msgstr "批量更新" + +#: assets/templates/assets/_asset_list_modal.html:36 +#: assets/templates/assets/asset_list.html:103 +#: users/templates/users/user_list.html:39 +msgid "Deactive selected" +msgstr "禁用所选" + +#: assets/templates/assets/_asset_list_modal.html:37 +#: assets/templates/assets/asset_list.html:104 +#: users/templates/users/user_list.html:40 +msgid "Active selected" +msgstr "激活所选" + +#: assets/templates/assets/_asset_list_modal.html:41 +#: assets/templates/assets/_system_user.html:71 +#: assets/templates/assets/admin_user_create_update.html:46 +#: assets/templates/assets/asset_bulk_update.html:24 +#: assets/templates/assets/asset_create.html:66 +#: assets/templates/assets/asset_list.html:108 +#: assets/templates/assets/asset_update.html:70 +#: assets/templates/assets/label_create_update.html:17 +#: common/templates/common/basic_setting.html:59 +#: common/templates/common/email_setting.html:60 +#: common/templates/common/ldap_setting.html:60 +#: common/templates/common/terminal_setting.html:103 +#: perms/templates/perms/asset_permission_create_update.html:72 +#: terminal/templates/terminal/session_list.html:120 +#: terminal/templates/terminal/terminal_update.html:48 +#: users/templates/users/_user.html:44 +#: users/templates/users/first_login.html:62 +#: users/templates/users/forgot_password.html:44 +#: users/templates/users/user_bulk_update.html:24 +#: users/templates/users/user_list.html:44 +#: users/templates/users/user_password_update.html:59 +#: users/templates/users/user_profile_update.html:64 +#: users/templates/users/user_pubkey_update.html:71 +msgid "Submit" +msgstr "提交" + +#: assets/templates/assets/_asset_list_modal.html:79 +#: assets/templates/assets/admin_user_detail.html:24 +#: assets/templates/assets/admin_user_list.html:84 +#: assets/templates/assets/asset_detail.html:24 +#: assets/templates/assets/asset_list.html:166 +#: assets/templates/assets/label_list.html:38 +#: assets/templates/assets/system_user_detail.html:26 +#: assets/templates/assets/system_user_list.html:85 +#: perms/templates/perms/asset_permission_detail.html:30 +#: perms/templates/perms/asset_permission_list.html:121 +#: terminal/templates/terminal/terminal_detail.html:16 +#: terminal/templates/terminal/terminal_list.html:71 +#: users/templates/users/user_detail.html:25 +#: users/templates/users/user_group_detail.html:28 +#: users/templates/users/user_group_list.html:43 +#: users/templates/users/user_list.html:76 +msgid "Update" +msgstr "更新" + +#: assets/templates/assets/_asset_list_modal.html:80 +#: assets/templates/assets/admin_user_detail.html:28 +#: assets/templates/assets/admin_user_list.html:85 +#: assets/templates/assets/asset_detail.html:28 +#: assets/templates/assets/asset_list.html:167 +#: assets/templates/assets/label_list.html:39 +#: assets/templates/assets/system_user_detail.html:30 +#: assets/templates/assets/system_user_list.html:86 +#: ops/templates/ops/task_list.html:71 +#: perms/templates/perms/asset_permission_detail.html:34 +#: perms/templates/perms/asset_permission_list.html:122 +#: terminal/templates/terminal/terminal_list.html:73 +#: users/templates/users/user_detail.html:30 +#: users/templates/users/user_group_detail.html:32 +#: users/templates/users/user_group_list.html:45 +#: users/templates/users/user_list.html:80 +#: users/templates/users/user_list.html:84 +msgid "Delete" +msgstr "删除" #: assets/templates/assets/_system_user.html:37 -#: assets/templates/assets/asset_create.html:14 -#: assets/templates/assets/asset_update.html:19 -#: assets/templates/assets/cluster_create_update.html:35 +#: assets/templates/assets/asset_create.html:16 +#: assets/templates/assets/asset_update.html:21 +#: perms/templates/perms/asset_permission_create_update.html:38 msgid "Basic" msgstr "基本" -#: assets/templates/assets/_system_user.html:45 +#: assets/templates/assets/_system_user.html:44 #: assets/templates/assets/asset_create.html:24 #: assets/templates/assets/asset_update.html:29 #: assets/templates/assets/system_user_update.html:7 @@ -641,69 +676,42 @@ msgstr "基本" msgid "Auth" msgstr "认证" -#: assets/templates/assets/_system_user.html:48 +#: assets/templates/assets/_system_user.html:47 msgid "Auto generate key" -msgstr "自动生成秘钥" +msgstr "自动生成密钥" -#: assets/templates/assets/_system_user.html:65 -#: assets/templates/assets/asset_create.html:32 -#: assets/templates/assets/asset_update.html:47 -#: assets/templates/assets/cluster_create_update.html:46 -#: perms/templates/perms/asset_permission_create_update.html:45 -#: terminal/templates/terminal/terminal_update.html:40 +#: assets/templates/assets/_system_user.html:64 +#: assets/templates/assets/asset_create.html:58 +#: assets/templates/assets/asset_update.html:62 +#: perms/templates/perms/asset_permission_create_update.html:49 +#: terminal/templates/terminal/terminal_update.html:42 msgid "Other" msgstr "其它" -#: assets/templates/assets/_system_user.html:71 +#: assets/templates/assets/_system_user.html:70 #: assets/templates/assets/admin_user_create_update.html:45 #: assets/templates/assets/asset_bulk_update.html:23 -#: assets/templates/assets/asset_create.html:40 -#: assets/templates/assets/asset_group_create.html:16 -#: assets/templates/assets/asset_update.html:55 -#: assets/templates/assets/cluster_create_update.html:54 -#: common/templates/common/basic_setting.html:55 -#: common/templates/common/email_setting.html:56 -#: common/templates/common/ldap_setting.html:56 -#: perms/templates/perms/asset_permission_create_update.html:67 -#: terminal/templates/terminal/terminal_update.html:45 -#: users/templates/users/_user.html:49 +#: assets/templates/assets/asset_create.html:65 +#: assets/templates/assets/asset_update.html:69 +#: assets/templates/assets/label_create_update.html:16 +#: common/templates/common/basic_setting.html:58 +#: common/templates/common/email_setting.html:59 +#: common/templates/common/ldap_setting.html:59 +#: common/templates/common/terminal_setting.html:101 +#: perms/templates/perms/asset_permission_create_update.html:71 +#: terminal/templates/terminal/terminal_update.html:47 +#: users/templates/users/_user.html:43 #: users/templates/users/user_bulk_update.html:23 #: users/templates/users/user_password_update.html:58 -#: users/templates/users/user_profile.html:139 -#: users/templates/users/user_profile.html:147 +#: users/templates/users/user_profile.html:135 +#: users/templates/users/user_profile.html:143 #: users/templates/users/user_profile_update.html:63 #: users/templates/users/user_pubkey_update.html:70 msgid "Reset" msgstr "重置" -#: assets/templates/assets/_system_user.html:72 -#: assets/templates/assets/admin_user_create_update.html:46 -#: assets/templates/assets/asset_bulk_update.html:24 -#: assets/templates/assets/asset_create.html:41 -#: assets/templates/assets/asset_group_create.html:17 -#: assets/templates/assets/asset_list.html:53 -#: assets/templates/assets/asset_update.html:56 -#: assets/templates/assets/cluster_create_update.html:55 -#: common/templates/common/basic_setting.html:56 -#: common/templates/common/email_setting.html:57 -#: common/templates/common/ldap_setting.html:57 -#: perms/templates/perms/asset_permission_create_update.html:68 -#: terminal/templates/terminal/terminal_update.html:46 -#: users/templates/users/_user.html:50 -#: users/templates/users/first_login.html:62 -#: users/templates/users/forgot_password.html:44 -#: users/templates/users/user_bulk_update.html:24 -#: users/templates/users/user_list.html:44 -#: users/templates/users/user_password_update.html:59 -#: users/templates/users/user_profile_update.html:64 -#: users/templates/users/user_pubkey_update.html:71 -msgid "Submit" -msgstr "提交" - #: assets/templates/assets/admin_user_assets.html:18 #: assets/templates/assets/admin_user_detail.html:18 -#: assets/templates/assets/cluster_assets.html:20 -#: assets/templates/assets/cluster_detail.html:17 #: assets/templates/assets/system_user_asset.html:17 #: assets/templates/assets/system_user_detail.html:18 #: ops/templates/ops/adhoc_history.html:129 @@ -720,129 +728,59 @@ msgstr "详情" msgid "Assets list" msgstr "资产列表" -#: assets/templates/assets/admin_user_assets.html:24 -#: assets/templates/assets/admin_user_detail.html:24 -#: assets/templates/assets/admin_user_list.html:83 -#: assets/templates/assets/asset_detail.html:24 -#: assets/templates/assets/asset_group_detail.html:18 -#: assets/templates/assets/asset_group_detail.html:177 -#: assets/templates/assets/asset_group_list.html:38 -#: assets/templates/assets/asset_list.html:98 -#: assets/templates/assets/cluster_assets.html:170 -#: assets/templates/assets/cluster_detail.html:25 -#: assets/templates/assets/cluster_list.html:43 -#: assets/templates/assets/system_user_asset.html:25 -#: assets/templates/assets/system_user_detail.html:26 -#: assets/templates/assets/system_user_list.html:84 -#: perms/templates/perms/asset_permission_detail.html:30 -#: perms/templates/perms/asset_permission_list.html:73 -#: terminal/templates/terminal/terminal_detail.html:16 -#: terminal/templates/terminal/terminal_list.html:71 -#: users/templates/users/user_detail.html:25 -#: users/templates/users/user_group_detail.html:28 -#: users/templates/users/user_group_list.html:39 -#: users/templates/users/user_list.html:76 -msgid "Update" -msgstr "更新" - -#: assets/templates/assets/admin_user_assets.html:28 -#: assets/templates/assets/admin_user_detail.html:28 -#: assets/templates/assets/admin_user_list.html:84 -#: assets/templates/assets/asset_detail.html:28 -#: assets/templates/assets/asset_group_detail.html:22 -#: assets/templates/assets/asset_group_list.html:39 -#: assets/templates/assets/asset_list.html:99 -#: assets/templates/assets/cluster_detail.html:29 -#: assets/templates/assets/cluster_list.html:44 -#: assets/templates/assets/system_user_detail.html:30 -#: assets/templates/assets/system_user_list.html:85 -#: ops/templates/ops/task_list.html:71 -#: perms/templates/perms/asset_permission_detail.html:34 -#: perms/templates/perms/asset_permission_list.html:74 -#: terminal/templates/terminal/terminal_list.html:73 -#: users/templates/users/user_detail.html:29 -#: users/templates/users/user_group_detail.html:32 -#: users/templates/users/user_group_list.html:41 -#: users/templates/users/user_list.html:80 -#: users/templates/users/user_list.html:84 -msgid "Delete" -msgstr "删除" - -#: assets/templates/assets/admin_user_assets.html:37 -#: assets/templates/assets/asset_group_detail.html:31 +#: assets/templates/assets/admin_user_assets.html:29 #: perms/templates/perms/asset_permission_asset.html:35 msgid "Asset list of " msgstr "资产列表" -#: assets/templates/assets/admin_user_assets.html:62 -#: assets/templates/assets/asset_group_detail.html:53 -#: assets/templates/assets/cluster_assets.html:54 -#: assets/templates/assets/user_asset_list.html:22 -#: users/templates/users/login_log_list.html:50 -msgid "Type" -msgstr "类型" - -#: assets/templates/assets/admin_user_assets.html:63 -#: assets/templates/assets/admin_user_list.html:25 -#: assets/templates/assets/asset_detail.html:376 -#: assets/templates/assets/asset_list.html:36 -#: assets/templates/assets/system_user_asset.html:55 -#: assets/templates/assets/system_user_list.html:27 -msgid "Reachable" -msgstr "可连接" - -#: assets/templates/assets/admin_user_assets.html:75 -#: assets/templates/assets/cluster_assets.html:68 -#: assets/templates/assets/system_user_asset.html:67 +#: assets/templates/assets/admin_user_assets.html:66 +#: assets/templates/assets/system_user_asset.html:64 #: assets/templates/assets/system_user_detail.html:112 #: perms/templates/perms/asset_permission_detail.html:110 msgid "Quick update" msgstr "快速更新" -#: assets/templates/assets/admin_user_assets.html:81 -#: assets/templates/assets/asset_detail.html:199 +#: assets/templates/assets/admin_user_assets.html:72 +#: assets/templates/assets/asset_detail.html:167 msgid "Test connective" msgstr "测试可连接性" -#: assets/templates/assets/admin_user_assets.html:84 -#: assets/templates/assets/asset_detail.html:202 -#: assets/templates/assets/system_user_asset.html:84 +#: assets/templates/assets/admin_user_assets.html:75 +#: assets/templates/assets/asset_detail.html:170 +#: assets/templates/assets/system_user_asset.html:81 +#: assets/templates/assets/system_user_detail.html:145 msgid "Test" msgstr "测试" -#: assets/templates/assets/admin_user_assets.html:147 +#: assets/templates/assets/admin_user_assets.html:131 msgid "Task has been send, seen left asset status" msgstr "任务已下发,查看左侧资产状态" -#: assets/templates/assets/admin_user_create_update.html:16 -#: assets/templates/assets/admin_user_list.html:14 -#: assets/views/admin_user.py:48 -msgid "Create admin user" -msgstr "创建管理用户" - -#: assets/templates/assets/admin_user_detail.html:84 -msgid "Using this as cluster admin user" -msgstr "使用集群管理用户" +#: assets/templates/assets/admin_user_detail.html:83 +msgid "Replace node assets admin user with this" +msgstr "替换资产的管理员" -#: assets/templates/assets/admin_user_detail.html:101 -#: assets/templates/assets/asset_detail.html:230 -#: assets/templates/assets/asset_group_list.html:81 -#: assets/templates/assets/asset_list.html:220 -#: assets/templates/assets/cluster_assets.html:104 -#: assets/templates/assets/cluster_list.html:89 -#: assets/templates/assets/system_user_detail.html:164 -#: assets/templates/assets/system_user_list.html:134 templates/_modal.html:16 +#: assets/templates/assets/admin_user_detail.html:100 +#: assets/templates/assets/asset_detail.html:198 +#: assets/templates/assets/asset_list.html:541 +#: assets/templates/assets/system_user_detail.html:181 +#: assets/templates/assets/system_user_list.html:135 templates/_modal.html:16 #: terminal/templates/terminal/session_detail.html:108 -#: users/templates/users/user_detail.html:338 -#: users/templates/users/user_detail.html:363 -#: users/templates/users/user_detail.html:386 +#: users/templates/users/user_detail.html:339 +#: users/templates/users/user_detail.html:364 +#: users/templates/users/user_detail.html:387 #: users/templates/users/user_group_create_update.html:32 -#: users/templates/users/user_group_list.html:82 +#: users/templates/users/user_group_list.html:86 #: users/templates/users/user_list.html:196 -#: users/templates/users/user_profile.html:181 +#: users/templates/users/user_profile.html:177 msgid "Confirm" msgstr "确认" +#: assets/templates/assets/admin_user_list.html:14 +#: assets/views/admin_user.py:48 +msgid "Create admin user" +msgstr "创建管理用户" + #: assets/templates/assets/admin_user_list.html:26 #: assets/templates/assets/system_user_list.html:28 msgid "Unreachable" @@ -855,283 +793,129 @@ msgstr "不可达" msgid "Ratio" msgstr "比例" -#: assets/templates/assets/admin_user_list.html:29 -#: assets/templates/assets/asset_group_detail.html:55 -#: assets/templates/assets/asset_group_list.html:18 -#: assets/templates/assets/asset_list.html:37 -#: assets/templates/assets/cluster_assets.html:56 -#: assets/templates/assets/cluster_list.html:23 -#: assets/templates/assets/system_user_list.html:31 -#: ops/templates/ops/adhoc_history.html:59 ops/templates/ops/task_adhoc.html:61 -#: ops/templates/ops/task_history.html:62 ops/templates/ops/task_list.html:41 -#: perms/templates/perms/asset_permission_list.html:32 -#: terminal/templates/terminal/session_list.html:79 -#: terminal/templates/terminal/terminal_list.html:36 -#: users/templates/users/user_group_list.html:15 -#: users/templates/users/user_list.html:28 -msgid "Action" -msgstr "动作" - #: assets/templates/assets/asset_create.html:28 -#: assets/templates/assets/asset_update.html:33 -msgid "Group" -msgstr "组" +#: assets/templates/assets/asset_update.html:33 perms/models.py:74 +#: perms/templates/perms/asset_permission_create_update.html:40 +#: perms/templates/perms/asset_permission_list.html:67 +msgid "Node" +msgstr "节点" -#: assets/templates/assets/asset_detail.html:20 assets/views/asset.py:186 -#: assets/views/cluster.py:103 +#: assets/templates/assets/asset_create.html:34 +#: assets/templates/assets/asset_list.html:75 +#: assets/templates/assets/asset_update.html:39 +msgid "Label" +msgstr "标签" + +#: assets/templates/assets/asset_detail.html:20 assets/views/asset.py:196 msgid "Asset detail" msgstr "资产详情" -#: assets/templates/assets/asset_detail.html:81 -msgid "Remote card IP" -msgstr "远控卡IP" - -#: assets/templates/assets/asset_detail.html:105 +#: assets/templates/assets/asset_detail.html:85 msgid "CPU" msgstr "CPU" -#: assets/templates/assets/asset_detail.html:113 +#: assets/templates/assets/asset_detail.html:93 msgid "Disk" msgstr "硬盘" -#: assets/templates/assets/asset_detail.html:153 -#: users/templates/users/user_detail.html:102 +#: assets/templates/assets/asset_detail.html:121 +#: users/templates/users/user_detail.html:103 #: users/templates/users/user_profile.html:88 msgid "Date joined" msgstr "创建日期" -#: assets/templates/assets/asset_detail.html:169 +#: assets/templates/assets/asset_detail.html:137 #: terminal/templates/terminal/session_detail.html:81 -#: users/templates/users/user_detail.html:121 -#: users/templates/users/user_profile.html:130 +#: users/templates/users/user_detail.html:122 +#: users/templates/users/user_profile.html:126 msgid "Quick modify" msgstr "快速修改" -#: assets/templates/assets/asset_detail.html:175 -#: assets/templates/assets/asset_list.html:35 -#: assets/templates/assets/user_asset_list.html:25 perms/models.py:20 -#: perms/templates/perms/asset_permission_create_update.html:47 -#: perms/templates/perms/asset_permission_detail.html:116 -#: terminal/templates/terminal/terminal_list.html:34 -#: users/templates/users/_select_user_modal.html:18 -#: users/templates/users/user_detail.html:127 -#: users/templates/users/user_list.html:27 -#: users/templates/users/user_profile.html:63 -msgid "Active" -msgstr "激活中" - -#: assets/templates/assets/asset_detail.html:191 +#: assets/templates/assets/asset_detail.html:159 msgid "Refresh hardware" msgstr "更新硬件信息" -#: assets/templates/assets/asset_detail.html:194 +#: assets/templates/assets/asset_detail.html:162 msgid "Refresh" msgstr "刷新" -#: assets/templates/assets/asset_detail.html:221 -msgid "Join asset groups" -msgstr "添加到资产组" - -#: assets/templates/assets/asset_detail.html:318 -#: users/templates/users/user_detail.html:272 +#: assets/templates/assets/asset_detail.html:299 +#: users/templates/users/user_detail.html:273 msgid "Update successfully!" msgstr "更新成功" -#: assets/templates/assets/asset_group_detail.html:16 -msgid "Group assets" -msgstr "组下资产" - -#: assets/templates/assets/asset_group_detail.html:54 -#: assets/templates/assets/cluster_assets.html:55 -#: terminal/templates/terminal/terminal_list.html:35 -msgid "Alive" -msgstr "在线" - -#: assets/templates/assets/asset_group_detail.html:67 -msgid "Add assets to this group" -msgstr "添加资产到该组" - -#: assets/templates/assets/asset_group_detail.html:84 -#: perms/templates/perms/asset_permission_asset.html:97 -#: perms/templates/perms/asset_permission_detail.html:153 -#: perms/templates/perms/asset_permission_user.html:97 -#: perms/templates/perms/asset_permission_user.html:125 -#: users/templates/users/user_group_detail.html:95 -msgid "Add" -msgstr "添加" - -#: assets/templates/assets/asset_group_detail.html:178 -msgid "Remove" -msgstr "移除" - -#: assets/templates/assets/asset_group_list.html:7 assets/views/group.py:35 -#: assets/views/group.py:88 -msgid "Create asset group" -msgstr "创建资产组" - -#: assets/templates/assets/asset_group_list.html:76 -#: assets/templates/assets/asset_list.html:215 -#: assets/templates/assets/cluster_list.html:84 -#: assets/templates/assets/system_user_list.html:129 -#: users/templates/users/user_detail.html:333 -#: users/templates/users/user_detail.html:358 -#: users/templates/users/user_group_list.html:77 -#: users/templates/users/user_list.html:191 -msgid "Are you sure?" -msgstr "你确认吗?" - -#: assets/templates/assets/asset_group_list.html:77 -#: users/templates/users/user_group_list.html:78 -msgid "This will delete the selected groups !!!" -msgstr "删除选择组" - -#: assets/templates/assets/asset_group_list.html:85 -msgid "Group deleted" -msgstr "组已被删除" - -#: assets/templates/assets/asset_group_list.html:86 -#: assets/templates/assets/asset_group_list.html:91 -msgid "Group Delete" -msgstr "删除" - -#: assets/templates/assets/asset_group_list.html:90 -msgid "Group deleting failed." -msgstr "删除失败" - -#: assets/templates/assets/asset_group_list.html:153 -msgid "The selected asset groups has been updated successfully." -msgstr "更新成功" - -#: assets/templates/assets/asset_group_list.html:154 -msgid "AssetGroup Updated" -msgstr "资产组更新" +#: assets/templates/assets/asset_list.html:63 +#: assets/templates/assets/asset_list.html:120 assets/views/asset.py:95 +msgid "Create asset" +msgstr "创建资产" -#: assets/templates/assets/asset_list.html:15 +#: assets/templates/assets/asset_list.html:67 #: users/templates/users/user_list.html:7 msgid "Import" msgstr "导入" -#: assets/templates/assets/asset_list.html:18 +#: assets/templates/assets/asset_list.html:70 #: users/templates/users/user_list.html:10 msgid "Export" msgstr "导出" -#: assets/templates/assets/asset_list.html:25 assets/views/asset.py:85 -msgid "Create asset" -msgstr "创建资产" +#: assets/templates/assets/asset_list.html:102 +msgid "Remove from this node" +msgstr "从节点移除" -#: assets/templates/assets/asset_list.html:34 -#: assets/templates/assets/user_asset_list.html:24 -msgid "Hardware" -msgstr "硬件" +#: assets/templates/assets/asset_list.html:121 +msgid "Add asset" +msgstr "添加资产到节点" -#: assets/templates/assets/asset_list.html:46 -#: users/templates/users/user_list.html:37 -msgid "Delete selected" -msgstr "批量删除" - -#: assets/templates/assets/asset_list.html:47 -#: users/templates/users/user_list.html:38 -msgid "Update selected" -msgstr "批量更新" - -#: assets/templates/assets/asset_list.html:48 -#: users/templates/users/user_list.html:39 -msgid "Deactive selected" -msgstr "禁用所选" - -#: assets/templates/assets/asset_list.html:49 -#: users/templates/users/user_list.html:40 -msgid "Active selected" -msgstr "激活所选" - -#: assets/templates/assets/asset_list.html:216 -msgid "This will delete the selected assets !!!" -msgstr "删除选择资产" - -# msgid "Deleted!" -# msgstr "删除" -#: assets/templates/assets/asset_list.html:224 -msgid "Asset Deleted." -msgstr "已被删除" - -#: assets/templates/assets/asset_list.html:225 -#: assets/templates/assets/asset_list.html:230 -msgid "Asset Delete" -msgstr "删除" - -#: assets/templates/assets/asset_list.html:229 -msgid "Asset Deleting failed." -msgstr "删除失败" - -#: assets/templates/assets/asset_update.html:37 -msgid "Configuration" -msgstr "配置" - -#: assets/templates/assets/asset_update.html:42 -msgid "Location" -msgstr "位置" - -#: assets/templates/assets/cluster_assets.html:23 -#: assets/templates/assets/cluster_assets.html:31 -#: assets/templates/assets/cluster_detail.html:21 -msgid "Cluster assets" -msgstr "集群中资产" - -#: assets/templates/assets/cluster_assets.html:74 -#: assets/templates/assets/system_user_asset.html:81 -msgid "Test assets connective" -msgstr "测试资产可连接性" +#: assets/templates/assets/asset_list.html:123 +msgid "Add node" +msgstr "新建节点" -#: assets/templates/assets/cluster_assets.html:77 -#: ops/templates/ops/task_list.html:70 -msgid "Run" -msgstr "执行" +#: assets/templates/assets/asset_list.html:124 +msgid "Rename node" +msgstr "重命名节点" -#: assets/templates/assets/cluster_assets.html:87 -msgid "Add assets to" -msgstr "添加资产到" - -#: assets/templates/assets/cluster_assets.html:95 -msgid "Select asset" -msgstr "选择资产" - -#: assets/templates/assets/cluster_assets.html:211 -#: assets/templates/assets/system_user_asset.html:162 -msgid "Task has been send, seen left assets status" -msgstr "任务已下发,查看左侧资产状态" +#: assets/templates/assets/asset_list.html:126 +msgid "Delete node" +msgstr "删除节点" -#: assets/templates/assets/cluster_create_update.html:41 -#: users/templates/users/reset_password.html:57 -#: users/templates/users/user_profile.html:20 -msgid "Setting" -msgstr "设置" +#: assets/templates/assets/asset_list.html:201 +msgid "Create node failed" +msgstr "创建节点失败" -#: assets/templates/assets/cluster_list.html:11 assets/views/cluster.py:43 -msgid "Create cluster" -msgstr "创建集群" +#: assets/templates/assets/asset_list.html:214 +msgid "Have child node, cancel" +msgstr "存在子节点,不能删除" -#: assets/templates/assets/cluster_list.html:21 -#: users/templates/users/_select_user_modal.html:17 -msgid "Asset num" -msgstr "资产数量" +#: assets/templates/assets/asset_list.html:536 +#: assets/templates/assets/system_user_list.html:130 +#: users/templates/users/user_detail.html:334 +#: users/templates/users/user_detail.html:359 +#: users/templates/users/user_group_list.html:81 +#: users/templates/users/user_list.html:191 +msgid "Are you sure?" +msgstr "你确认吗?" + +#: assets/templates/assets/asset_list.html:537 +msgid "This will delete the selected assets !!!" +msgstr "删除选择资产" -#: assets/templates/assets/cluster_list.html:85 -msgid "This will delete the selected cluster" -msgstr "删除选择Cluster" +#: assets/templates/assets/asset_list.html:545 +msgid "Asset Deleted." +msgstr "已被删除" -#: assets/templates/assets/cluster_list.html:93 -msgid "Cluster Deleted." -msgstr "集群已被删除" +#: assets/templates/assets/asset_list.html:546 +#: assets/templates/assets/asset_list.html:551 +msgid "Asset Delete" +msgstr "删除" -#: assets/templates/assets/cluster_list.html:94 -#: assets/templates/assets/cluster_list.html:99 -msgid "Cluster delete" -msgstr "删除集群" +#: assets/templates/assets/asset_list.html:550 +msgid "Asset Deleting failed." +msgstr "删除失败" -#: assets/templates/assets/cluster_list.html:98 -msgid "Cluster deleting failed." -msgstr "Cluster删除失败" +#: assets/templates/assets/asset_update.html:58 +msgid "Configuration" +msgstr "配置" #: assets/templates/assets/delete_confirm.html:6 #: perms/templates/perms/delete_confirm.html:6 templates/delete_confirm.html:6 @@ -1143,25 +927,38 @@ msgstr "确认删除" msgid "Are you sure delete" msgstr "您确定删除吗?" -#: assets/templates/assets/system_user_asset.html:33 +#: assets/templates/assets/label_list.html:6 assets/views/label.py:43 +msgid "Create label" +msgstr "创建标签" + +#: assets/templates/assets/system_user_asset.html:30 msgid "Assets of " msgstr "资产" -#: assets/templates/assets/system_user_asset.html:73 +#: assets/templates/assets/system_user_asset.html:70 +#: assets/templates/assets/system_user_detail.html:134 msgid "Push system user manually" msgstr "手动推送系统" -#: assets/templates/assets/system_user_asset.html:76 +#: assets/templates/assets/system_user_asset.html:73 +#: assets/templates/assets/system_user_detail.html:137 msgid "Push" msgstr "推送" -#: assets/templates/assets/system_user_asset.html:150 +#: assets/templates/assets/system_user_asset.html:78 +#: assets/templates/assets/system_user_detail.html:142 +msgid "Test assets connective" +msgstr "测试资产可连接性" + +#: assets/templates/assets/system_user_asset.html:147 +#: assets/templates/assets/system_user_detail.html:301 msgid "Task has been send, Go to ops task list seen result" msgstr "任务已下发,查看ops任务列表" -#: assets/templates/assets/system_user_detail.html:22 -msgid "Attached assets" -msgstr "关联的资产" +#: assets/templates/assets/system_user_asset.html:159 +#: assets/templates/assets/system_user_detail.html:313 +msgid "Task has been send, seen left assets status" +msgstr "任务已下发,查看左侧资产状态" #: assets/templates/assets/system_user_detail.html:81 msgid "Home" @@ -1171,36 +968,33 @@ msgstr "家目录" msgid "Uid" msgstr "Uid" -#: assets/templates/assets/system_user_detail.html:147 -msgid "Clusters" -msgstr "集群" +#: assets/templates/assets/system_user_detail.html:172 +msgid "Add to node" +msgstr "添加到节点" -#: assets/templates/assets/system_user_detail.html:155 -msgid "Add to cluster" -msgstr "添加到集群" +#: assets/templates/assets/system_user_list.html:16 +#: assets/views/system_user.py:45 +msgid "Create system user" +msgstr "创建系统用户" -#: assets/templates/assets/system_user_list.html:130 +#: assets/templates/assets/system_user_list.html:131 msgid "This will delete the selected System Users !!!" msgstr "删除选择系统用户" -#: assets/templates/assets/system_user_list.html:138 +#: assets/templates/assets/system_user_list.html:139 msgid "System Users Deleted." msgstr "已被删除" -#: assets/templates/assets/system_user_list.html:139 -#: assets/templates/assets/system_user_list.html:144 +#: assets/templates/assets/system_user_list.html:140 +#: assets/templates/assets/system_user_list.html:145 msgid "System Users Delete" msgstr "删除系统用户" -#: assets/templates/assets/system_user_list.html:143 +#: assets/templates/assets/system_user_list.html:144 msgid "System Users Deleting failed." msgstr "系统用户删除失败" -#: assets/templates/assets/user_asset_list.html:23 -msgid "Env" -msgstr "环境" - -#: assets/templates/assets/user_asset_list.html:26 +#: assets/templates/assets/user_asset_list.html:24 msgid "Connective" msgstr "连接性" @@ -1212,50 +1006,37 @@ msgstr "管理用户列表" msgid "Update admin user" msgstr "更新管理用户" -#: assets/views/admin_user.py:80 assets/views/admin_user.py:107 +#: assets/views/admin_user.py:79 assets/views/admin_user.py:103 msgid "Admin user detail" msgstr "管理用户详情" -#: assets/views/asset.py:49 assets/views/asset.py:62 +#: assets/views/asset.py:49 templates/_nav.html:23 msgid "Asset list" msgstr "资产列表" -#: assets/views/asset.py:145 +#: assets/views/asset.py:61 templates/_nav_user.html:4 +msgid "My assets" +msgstr "我的资产" + +#: assets/views/asset.py:155 msgid "Bulk update asset" msgstr "批量更新资产" -#: assets/views/asset.py:162 +#: assets/views/asset.py:172 msgid "Update asset" -msgstr "编辑资产" +msgstr "更新资产" -#: assets/views/asset.py:298 +#: assets/views/asset.py:296 msgid "already exists" msgstr "已经存在" -#: assets/views/cluster.py:27 -msgid "Cluster list" -msgstr "集群列表" - -#: assets/views/cluster.py:42 assets/views/cluster.py:70 -#: assets/views/system_user.py:96 -msgid "assets" -msgstr "资产管理" - -#: assets/views/cluster.py:71 -msgid "Update Cluster" -msgstr "更新Cluster" +#: assets/views/label.py:27 +msgid "Label list" +msgstr "标签列表" -#: assets/views/cluster.py:86 -msgid "Cluster detail" -msgstr "集群详情" - -#: assets/views/group.py:53 -msgid "Asset group list" -msgstr "资产组列表" - -#: assets/views/group.py:70 -msgid "Asset group detail" -msgstr "资产组详情" +#: assets/views/label.py:59 +msgid "Update label" +msgstr "更新标签" #: assets/views/system_user.py:29 msgid "System user list" @@ -1265,11 +1046,15 @@ msgstr "系统用户列表" msgid "Update system user" msgstr "更新系统用户" -#: assets/views/system_user.py:76 +#: assets/views/system_user.py:75 msgid "System user detail" msgstr "系统用户详情" -#: assets/views/system_user.py:97 +#: assets/views/system_user.py:95 +msgid "assets" +msgstr "资产管理" + +#: assets/views/system_user.py:96 msgid "System user asset" msgstr "系统用户集群资产" @@ -1281,6 +1066,10 @@ msgstr "邮件已经发送{}, 请检查" msgid "Test ldap success" msgstr "连接LDAP成功" +#: common/api.py:91 +msgid "Match {} s users" +msgstr "匹配 {} 个用户" + #: common/const.py:6 #, python-format msgid "%(name)s was created successfully" @@ -1291,81 +1080,144 @@ msgstr "%(name)s 创建成功" msgid "%(name)s was updated successfully" msgstr "%(name)s 更新成功" -#: common/forms.py:64 +#: common/fields.py:26 +msgid "Not a valid json" +msgstr "不是合法json" + +#: common/fields.py:28 +msgid "Not a string type" +msgstr "不是字符类型" + +#: common/forms.py:70 msgid "Current SITE URL" msgstr "当前站点URL" -#: common/forms.py:68 +#: common/forms.py:74 msgid "User Guide URL" msgstr "用户向导URL" -#: common/forms.py:69 +#: common/forms.py:75 msgid "User first login update profile done redirect to it" msgstr "用户第一次登录,修改profile后重定向到地址" -#: common/forms.py:72 +#: common/forms.py:78 msgid "Email Subject Prefix" msgstr "Email主题前缀" -#: common/forms.py:79 +#: common/forms.py:85 msgid "SMTP host" msgstr "SMTP主机" -#: common/forms.py:81 +#: common/forms.py:87 msgid "SMTP port" msgstr "SMTP端口" -#: common/forms.py:83 +#: common/forms.py:89 msgid "SMTP user" msgstr "SMTP账号" -#: common/forms.py:86 +#: common/forms.py:92 msgid "SMTP password" msgstr "SMTP密码" -#: common/forms.py:87 +#: common/forms.py:93 msgid "Some provider use token except password" msgstr "一些邮件提供商需要输入的是Token" -#: common/forms.py:90 common/forms.py:127 +#: common/forms.py:96 common/forms.py:136 msgid "Use SSL" msgstr "使用SSL" -#: common/forms.py:91 +#: common/forms.py:97 msgid "If SMTP port is 465, may be select" msgstr "如果SMTP端口是465,通常需要启用SSL" -#: common/forms.py:94 +#: common/forms.py:100 msgid "Use TLS" msgstr "使用TLS" -#: common/forms.py:95 +#: common/forms.py:101 msgid "If SMTP port is 587, may be select" msgstr "如果SMTP端口是587,通常需要启用TLS" -#: common/forms.py:101 +#: common/forms.py:107 msgid "LDAP server" msgstr "LDAP地址" -#: common/forms.py:104 +#: common/forms.py:110 msgid "Bind DN" msgstr "绑定DN" -#: common/forms.py:111 +#: common/forms.py:117 msgid "User OU" msgstr "用户OU" -#: common/forms.py:114 +#: common/forms.py:120 msgid "User search filter" msgstr "用户过滤器" -#: common/forms.py:117 +#: common/forms.py:121 +#, python-format +msgid "User search filter must contain ([cn,uid,sAMAccountName,...]=%(user)s)" +msgstr "用户过滤器必须包含([cn,uid,sAMAccountName,...]=%(user)s)" + +#: common/forms.py:124 msgid "User attr map" msgstr "LDAP属性映射" -#: common/forms.py:130 -msgid "Enable LDAP Auth" -msgstr "开启LDAP认证" +#: common/forms.py:131 +msgid "" +"User attr map present how to map LDAP user attr to jumpserver, username,name," +"email is jumpserver attr" +msgstr "" +"用户属性映射代表怎样将LDAP中用户属性映射到jumpserver用户上,username, name," +"email 是jumpserver的属性" + +#: common/forms.py:138 +msgid "Enable LDAP auth" +msgstr "启用LDAP认证" + +#: common/forms.py:147 +msgid "List sort by" +msgstr "资产列表排序" + +#: common/forms.py:150 +msgid "Heartbeat interval" +msgstr "心跳间隔" + +#: common/forms.py:150 ops/models.py:32 +msgid "Units: seconds" +msgstr "单位: 秒" + +#: common/forms.py:153 +msgid "Password auth" +msgstr "密码认证" + +#: common/forms.py:156 +msgid "Public key auth" +msgstr "密钥认证" + +#: common/forms.py:159 common/templates/common/terminal_setting.html:63 +#: terminal/forms.py:30 terminal/models.py:20 +msgid "Command storage" +msgstr "命令存储" + +#: common/forms.py:160 +msgid "" +"Set terminal storage setting, `default` is the using as default,You can set " +"other storage and some terminal using" +msgstr "设置终端命令存储,default是默认用的存储方式" + +#: common/forms.py:165 common/templates/common/terminal_setting.html:81 +#: terminal/forms.py:34 terminal/models.py:21 +msgid "Replay storage" +msgstr "录像存储" + +#: common/forms.py:166 +msgid "" +"Set replay storage setting, `default` is the using as default,You can set " +"other storage and some terminal using" +msgstr "设置终端录像存储,default是默认用的存储方式" #: common/mixins.py:29 msgid "is discard" @@ -1375,43 +1227,56 @@ msgstr "" msgid "discard time" msgstr "" -#: common/models.py:26 -msgid "Value" -msgstr "值" - -#: common/models.py:27 +#: common/models.py:29 msgid "Enabled" msgstr "启用" #: common/templates/common/basic_setting.html:15 #: common/templates/common/email_setting.html:15 -#: common/templates/common/ldap_setting.html:15 common/views.py:18 +#: common/templates/common/ldap_setting.html:15 +#: common/templates/common/terminal_setting.html:16 +#: common/templates/common/terminal_setting.html:42 common/views.py:21 msgid "Basic setting" msgstr "基本设置" #: common/templates/common/basic_setting.html:18 #: common/templates/common/email_setting.html:18 -#: common/templates/common/ldap_setting.html:18 common/views.py:44 +#: common/templates/common/ldap_setting.html:18 +#: common/templates/common/terminal_setting.html:20 common/views.py:47 msgid "Email setting" msgstr "邮件设置" #: common/templates/common/basic_setting.html:21 #: common/templates/common/email_setting.html:21 -#: common/templates/common/ldap_setting.html:21 common/views.py:70 +#: common/templates/common/ldap_setting.html:21 +#: common/templates/common/terminal_setting.html:24 common/views.py:73 msgid "LDAP setting" msgstr "LDAP设置" -#: common/templates/common/email_setting.html:55 -#: common/templates/common/ldap_setting.html:55 +#: common/templates/common/basic_setting.html:24 +#: common/templates/common/email_setting.html:24 +#: common/templates/common/ldap_setting.html:24 +#: common/templates/common/terminal_setting.html:28 common/views.py:103 +msgid "Terminal setting" +msgstr "终端设置" + +#: common/templates/common/email_setting.html:58 +#: common/templates/common/ldap_setting.html:58 msgid "Test connection" msgstr "测试连接" -#: common/views.py:17 common/views.py:43 common/views.py:69 -#: templates/_nav.html:69 +#: common/templates/common/terminal_setting.html:68 +#: common/templates/common/terminal_setting.html:86 +#: users/templates/users/login_log_list.html:50 +msgid "Type" +msgstr "类型" + +#: common/views.py:20 common/views.py:46 common/views.py:72 common/views.py:102 +#: templates/_nav.html:72 msgid "Settings" msgstr "系统设置" -#: common/views.py:28 common/views.py:54 common/views.py:82 +#: common/views.py:31 common/views.py:57 common/views.py:85 common/views.py:115 msgid "Update setting successfully, please restart program" msgstr "更新设置成功, 请手动重启程序" @@ -1419,10 +1284,6 @@ msgstr "更新设置成功, 请手动重启程序" msgid "Interval" msgstr "间隔" -#: ops/models.py:32 -msgid "Units: seconds" -msgstr "单位: 秒" - #: ops/models.py:33 msgid "Crontab" msgstr "Crontab" @@ -1566,7 +1427,7 @@ msgstr "执行历史" #: ops/templates/ops/adhoc_history.html:52 #: ops/templates/ops/adhoc_history_detail.html:58 -#: ops/templates/ops/task_history.html:55 terminal/models.py:101 +#: ops/templates/ops/task_history.html:55 terminal/models.py:132 #: terminal/templates/terminal/session_list.html:77 msgid "Date start" msgstr "开始日期" @@ -1665,6 +1526,10 @@ msgstr "成功" msgid "Date" msgstr "日期" +#: ops/templates/ops/task_list.html:70 +msgid "Run" +msgstr "执行" + #: ops/templates/ops/task_list.html:125 msgid "Task start: " msgstr "任务开始: " @@ -1674,7 +1539,7 @@ msgstr "任务开始: " msgid "Ops" msgstr "作业中心" -#: ops/views.py:37 +#: ops/views.py:37 templates/_nav.html:58 msgid "Task list" msgstr "任务列表" @@ -1682,59 +1547,27 @@ msgstr "任务列表" msgid "Task run history" msgstr "执行历史" -#: perms/forms.py:16 users/forms.py:147 users/forms.py:152 users/forms.py:164 -#: users/forms.py:195 -msgid "Select users" -msgstr "选择用户" - -#: perms/forms.py:18 perms/models.py:15 -#: perms/templates/perms/asset_permission_create_update.html:36 -#: perms/templates/perms/asset_permission_list.html:26 templates/_nav.html:12 -#: terminal/backends/command/models.py:10 terminal/models.py:92 -#: terminal/templates/terminal/command_list.html:32 -#: terminal/templates/terminal/command_list.html:72 -#: terminal/templates/terminal/session_list.html:33 -#: terminal/templates/terminal/session_list.html:71 users/forms.py:191 -#: users/models/user.py:30 users/templates/users/user_group_detail.html:78 -#: users/views/user.py:337 -msgid "User" -msgstr "用户" - -#: perms/forms.py:31 perms/templates/perms/asset_permission_user.html:116 -msgid "Select user groups" -msgstr "选择用户组" - -#: perms/forms.py:52 -msgid "User or group at least one required" -msgstr "用户和组至少需要选一个" - -#: perms/forms.py:60 -msgid "Asset or group at least one required" -msgstr "资产或组至少需要选择一个" - -#: perms/forms.py:78 -msgid "Asset {} of cluster {} not have [{}] system users, please check \n" -msgstr "资产 {} 所在集群 {} 不包含系统用户 [{}] 请检查\n" - -#: perms/forms.py:87 -msgid "" -"Asset {}(group {}) of cluster {} not have [{}] system users, please check \n" -msgstr "资产 {}(组 {}) 所在集群 {} 不包含系统用户 [{}] 请检查\n" - -#: perms/models.py:16 perms/templates/perms/asset_permission_list.html:27 -#: templates/_nav.html:13 users/models/user.py:37 +#: perms/forms.py:22 perms/models.py:16 perms/models.py:75 +#: perms/templates/perms/asset_permission_list.html:68 templates/_nav.html:14 +#: users/models/group.py:25 users/models/user.py:37 #: users/templates/users/_select_user_modal.html:16 -#: users/templates/users/user_detail.html:178 +#: users/templates/users/user_detail.html:179 #: users/templates/users/user_list.html:26 msgid "User group" msgstr "用户组" -#: perms/models.py:21 perms/templates/perms/asset_permission_detail.html:86 -#: users/models/user.py:49 users/templates/users/user_detail.html:94 +#: perms/models.py:21 perms/models.py:78 +#: perms/templates/perms/asset_permission_detail.html:86 +#: perms/templates/perms/asset_permission_list.html:71 users/models/user.py:49 +#: users/templates/users/user_detail.html:95 #: users/templates/users/user_profile.html:96 msgid "Date expired" msgstr "失效日期" +#: perms/models.py:88 templates/_nav.html:33 +msgid "Asset permission" +msgstr "资产授权" + #: perms/templates/perms/asset_permission_asset.html:22 #: perms/templates/perms/asset_permission_detail.html:22 #: perms/templates/perms/asset_permission_user.html:22 @@ -1751,21 +1584,28 @@ msgstr "资产或资产组" msgid "Add asset to this permission" msgstr "添加资产" +#: perms/templates/perms/asset_permission_asset.html:97 +#: perms/templates/perms/asset_permission_detail.html:153 +#: perms/templates/perms/asset_permission_user.html:97 +#: perms/templates/perms/asset_permission_user.html:125 +#: users/templates/users/user_group_detail.html:95 +msgid "Add" +msgstr "添加" + #: perms/templates/perms/asset_permission_asset.html:108 msgid "Add asset group to this permission" msgstr "添加资产组" +#: perms/templates/perms/asset_permission_asset.html:116 users/forms.py:245 +msgid "Select asset groups" +msgstr "选择资产组" + #: perms/templates/perms/asset_permission_asset.html:125 -#: users/templates/users/user_detail.html:195 +#: users/templates/users/user_detail.html:196 msgid "Join" msgstr "加入" -#: perms/templates/perms/asset_permission_create_update.html:17 -msgid "Create asset permission " -msgstr "创建资产权限" - #: perms/templates/perms/asset_permission_detail.html:66 -#: users/templates/users/user_group_list.html:13 msgid "User count" msgstr "用户数量" @@ -1785,14 +1625,14 @@ msgstr "资产组数量" msgid "System user count" msgstr "系统用户数量" -#: perms/templates/perms/asset_permission_list.html:16 +#: perms/templates/perms/asset_permission_detail.html:144 users/forms.py:248 +msgid "Select system users" +msgstr "选择系统用户" + +#: perms/templates/perms/asset_permission_list.html:58 msgid "Create permission" msgstr "创建授权规则" -#: perms/templates/perms/asset_permission_list.html:31 -msgid "Is valid" -msgstr "有效" - #: perms/templates/perms/asset_permission_user.html:35 msgid "User list of " msgstr "用户列表" @@ -1810,71 +1650,62 @@ msgstr "选择用户" msgid "Add user group to asset permission" msgstr "添加用户组" -#: perms/views.py:28 perms/views.py:44 perms/views.py:60 perms/views.py:74 -#: perms/views.py:111 perms/views.py:141 templates/_nav.html:30 +#: perms/templates/perms/asset_permission_user.html:116 +msgid "Select user groups" +msgstr "选择用户组" + +#: perms/views.py:23 perms/views.py:47 perms/views.py:67 templates/_nav.html:30 msgid "Perms" msgstr "权限管理" -#: perms/views.py:29 +#: perms/views.py:24 msgid "Asset permission list" msgstr "资产授权列表" -#: perms/views.py:45 +#: perms/views.py:48 msgid "Create asset permission" msgstr "创建权限规则" -#: perms/views.py:61 +#: perms/views.py:68 msgid "Update asset permission" msgstr "更新资产授权" -#: perms/views.py:75 -msgid "Asset permission detail" -msgstr "资产授权详情" - -#: perms/views.py:112 -msgid "Asset permission user list" -msgstr "资产授权包含用户" - -#: perms/views.py:142 -msgid "Asset permission asset list" -msgstr "资产组授权包含资产" - -#: templates/_header_bar.html:14 -msgid "Welcome to use Jumpserver system" -msgstr "欢迎使用Jumpserver开源跳板机系统" - #: templates/_header_bar.html:18 -msgid "Help" -msgstr "帮助" +msgid "Supports" +msgstr "商业支持" + +#: templates/_header_bar.html:23 +msgid "Docs" +msgstr "文档" -#: templates/_header_bar.html:33 templates/_nav_user.html:9 -#: users/templates/users/_user.html:42 +#: templates/_header_bar.html:37 templates/_nav_user.html:9 +#: users/templates/users/_user.html:36 #: users/templates/users/user_password_update.html:37 #: users/templates/users/user_profile.html:17 #: users/templates/users/user_profile_update.html:37 #: users/templates/users/user_profile_update.html:57 -#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:319 +#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:316 msgid "Profile" msgstr "个人信息" -#: templates/_header_bar.html:37 +#: templates/_header_bar.html:40 msgid "Admin page" msgstr "管理页面" -#: templates/_header_bar.html:39 +#: templates/_header_bar.html:42 msgid "User page" msgstr "用户页面" -#: templates/_header_bar.html:42 +#: templates/_header_bar.html:45 msgid "Logout" msgstr "注销登录" -#: templates/_header_bar.html:46 users/templates/users/login.html:42 -#: users/templates/users/login.html:61 +#: templates/_header_bar.html:49 users/templates/users/login.html:44 +#: users/templates/users/login.html:64 msgid "Login" msgstr "登录" -#: templates/_header_bar.html:59 templates/_nav.html:4 +#: templates/_header_bar.html:62 templates/_nav.html:4 msgid "Dashboard" msgstr "仪表盘" @@ -1891,7 +1722,7 @@ msgstr "" " 补充完整\n" " " -#: templates/_message.html:16 +#: templates/_message.html:17 #, python-format msgid "" "\n" @@ -1900,7 +1731,7 @@ msgid "" " " msgstr "" "\n" -" 您的ssh秘钥没有设置或已失效,请点击 链接 更新\n" " " @@ -1908,63 +1739,54 @@ msgstr "" msgid "Close" msgstr "关闭" -#: templates/_nav.html:9 users/views/group.py:28 users/views/group.py:44 -#: users/views/group.py:62 users/views/group.py:79 users/views/login.py:197 -#: users/views/login.py:246 users/views/user.py:57 users/views/user.py:72 -#: users/views/user.py:91 users/views/user.py:147 users/views/user.py:304 -#: users/views/user.py:318 users/views/user.py:355 users/views/user.py:377 +#: templates/_nav.html:10 users/views/group.py:28 users/views/group.py:44 +#: users/views/group.py:62 users/views/group.py:79 users/views/group.py:95 +#: users/views/login.py:209 users/views/login.py:258 users/views/user.py:59 +#: users/views/user.py:74 users/views/user.py:93 users/views/user.py:149 +#: users/views/user.py:304 users/views/user.py:351 users/views/user.py:373 msgid "Users" msgstr "用户管理" -#: templates/_nav.html:14 +#: templates/_nav.html:13 users/views/user.py:60 +msgid "User list" +msgstr "用户列表" + +#: templates/_nav.html:15 msgid "Login logs" msgstr "登录日志" -#: templates/_nav.html:33 -msgid "Asset permission" -msgstr "资产授权" - #: templates/_nav.html:39 -msgid "Job Center" -msgstr "作业中心" +msgid "Sessions" +msgstr "会话管理" #: templates/_nav.html:42 -msgid "Task" -msgstr "任务" - -#: templates/_nav.html:47 templates/_nav.html:50 -#: terminal/templates/terminal/session_list.html:75 -#: terminal/views/command.py:47 terminal/views/session.py:75 -#: terminal/views/session.py:92 terminal/views/session.py:114 -#: terminal/views/terminal.py:31 terminal/views/terminal.py:46 -#: terminal/views/terminal.py:58 -msgid "Terminal" -msgstr "终端管理" - -#: templates/_nav.html:51 msgid "Session online" msgstr "在线会话" -#: templates/_nav.html:52 +#: templates/_nav.html:43 msgid "Session offline" -msgstr "离线会话" - -#: templates/_nav.html:53 terminal/models.py:99 -#: terminal/templates/terminal/command_list.html:55 -#: terminal/templates/terminal/command_list.html:71 -#: terminal/templates/terminal/session_detail.html:48 -#: terminal/templates/terminal/session_list.html:76 -msgid "Command" -msgstr "命令" +msgstr "历史会话" -#: templates/_nav_user.html:4 -msgid "My assets" -msgstr "我的资产" +#: templates/_nav.html:44 +msgid "Commands" +msgstr "命令记录" -#: templates/_nav_user.html:14 +#: templates/_nav.html:47 templates/_nav_user.html:14 msgid "Web terminal" msgstr "Web终端" +#: templates/_nav.html:50 terminal/templates/terminal/session_list.html:75 +#: terminal/views/command.py:47 terminal/views/session.py:75 +#: terminal/views/session.py:93 terminal/views/session.py:115 +#: terminal/views/terminal.py:31 terminal/views/terminal.py:46 +#: terminal/views/terminal.py:58 +msgid "Terminal" +msgstr "终端管理" + +#: templates/_nav.html:55 +msgid "Job Center" +msgstr "作业中心" + #: templates/captcha/image.html:3 msgid "Play CAPTCHA as audio file" msgstr "语言播放验证码" @@ -1987,67 +1809,79 @@ msgstr "输出" #: terminal/backends/command/models.py:15 #: terminal/templates/terminal/command_list.html:75 +#: terminal/templates/terminal/terminal_list.html:33 msgid "Session" msgstr "会话" -#: terminal/forms.py:15 +#: terminal/forms.py:44 msgid "Coco ssh listen port" msgstr "SSH 监听端口" -#: terminal/forms.py:16 +#: terminal/forms.py:45 msgid "Coco http/ws listen port" msgstr "Http/Websocket 监听端口" -#: terminal/models.py:15 +#: terminal/models.py:17 msgid "Remote Address" msgstr "远端地址" -#: terminal/models.py:16 +#: terminal/models.py:18 msgid "SSH Port" msgstr "SSH端口" -#: terminal/models.py:17 +#: terminal/models.py:19 msgid "HTTP Port" msgstr "HTTP端口" -#: terminal/models.py:68 +#: terminal/models.py:98 msgid "Session Online" msgstr "在线会话" -#: terminal/models.py:69 +#: terminal/models.py:99 msgid "CPU Usage" msgstr "CPU使用" -#: terminal/models.py:70 +#: terminal/models.py:100 msgid "Memory Used" msgstr "内存使用" -#: terminal/models.py:71 +#: terminal/models.py:101 msgid "Connections" msgstr "连接数" -#: terminal/models.py:72 +#: terminal/models.py:102 msgid "Threads" msgstr "线程数" -#: terminal/models.py:73 +#: terminal/models.py:103 msgid "Boot Time" msgstr "运行时间" -#: terminal/models.py:96 terminal/templates/terminal/session_list.html:74 +#: terminal/models.py:126 terminal/templates/terminal/session_list.html:74 #: terminal/templates/terminal/terminal_detail.html:47 msgid "Remote addr" msgstr "远端地址" -#: terminal/models.py:98 terminal/templates/terminal/session_list.html:100 +#: terminal/models.py:128 terminal/templates/terminal/session_list.html:102 msgid "Replay" msgstr "回放" -#: terminal/models.py:102 +#: terminal/models.py:129 terminal/templates/terminal/command_list.html:55 +#: terminal/templates/terminal/command_list.html:71 +#: terminal/templates/terminal/session_detail.html:48 +#: terminal/templates/terminal/session_list.html:76 +msgid "Command" +msgstr "命令" + +#: terminal/models.py:131 +msgid "Date last active" +msgstr "最后活跃日期" + +#: terminal/models.py:133 msgid "Date end" msgstr "结束日期" -#: terminal/models.py:119 +#: terminal/models.py:150 msgid "Args" msgstr "参数" @@ -2056,7 +1890,7 @@ msgid "Goto" msgstr "转到" #: terminal/templates/terminal/session_detail.html:17 -#: terminal/views/session.py:115 +#: terminal/views/session.py:116 msgid "Session detail" msgstr "会话详情" @@ -2086,19 +1920,23 @@ msgstr "监控" msgid "Terminate session" msgstr "终止会话" -#: terminal/templates/terminal/session_list.html:78 +#: terminal/templates/terminal/session_list.html:79 msgid "Duration" msgstr "时长" -#: terminal/templates/terminal/session_list.html:102 +#: terminal/templates/terminal/session_list.html:104 msgid "Monitor" msgstr "监控" -#: terminal/templates/terminal/session_list.html:103 +#: terminal/templates/terminal/session_list.html:105 msgid "Terminate" msgstr "终断" -#: terminal/templates/terminal/session_list.html:119 +#: terminal/templates/terminal/session_list.html:116 +msgid "Terminate selected" +msgstr "终断所选" + +#: terminal/templates/terminal/session_list.html:136 msgid "Terminate task send, waiting ..." msgstr "终断任务已发送,请等待" @@ -2121,9 +1959,9 @@ msgstr "HTTP端口" msgid "Addr" msgstr "地址" -#: terminal/templates/terminal/terminal_list.html:33 -msgid "Sessions" -msgstr "会话" +#: terminal/templates/terminal/terminal_list.html:35 +msgid "Alive" +msgstr "在线" #: terminal/templates/terminal/terminal_list.html:76 msgid "Accept" @@ -2145,7 +1983,7 @@ msgstr "信息" msgid "Session online list" msgstr "在线会话" -#: terminal/views/session.py:93 +#: terminal/views/session.py:94 msgid "Session offline list" msgstr "离线会话" @@ -2221,7 +2059,7 @@ msgstr "" msgid "Invalid token or cache refreshed." msgstr "" -#: users/forms.py:43 users/templates/users/user_detail.html:186 +#: users/forms.py:43 users/templates/users/user_detail.html:187 msgid "Join user groups" msgstr "添加到用户组" @@ -2265,31 +2103,35 @@ msgstr "不能和原来的密钥相同" msgid "Not a valid ssh public key" msgstr "ssh密钥不合法" -#: users/models/authentication.py:35 +#: users/forms.py:147 users/forms.py:152 users/forms.py:164 users/forms.py:194 +msgid "Select users" +msgstr "选择用户" + +#: users/models/authentication.py:36 msgid "Private Token" msgstr "ssh密钥" -#: users/models/authentication.py:45 +#: users/models/authentication.py:46 msgid "Login type" msgstr "登录方式" -#: users/models/authentication.py:46 +#: users/models/authentication.py:47 msgid "Login ip" msgstr "登录IP" -#: users/models/authentication.py:47 +#: users/models/authentication.py:48 msgid "Login city" msgstr "登录城市" -#: users/models/authentication.py:48 +#: users/models/authentication.py:49 msgid "User agent" msgstr "Agent" -#: users/models/authentication.py:49 +#: users/models/authentication.py:50 msgid "Date login" msgstr "登录日期" -#: users/models/user.py:29 users/models/user.py:254 +#: users/models/user.py:29 users/models/user.py:262 msgid "Administrator" msgstr "管理员" @@ -2297,13 +2139,13 @@ msgstr "管理员" msgid "Application" msgstr "应用程序" -#: users/models/user.py:36 users/templates/users/user_detail.html:70 +#: users/models/user.py:36 users/templates/users/user_detail.html:71 #: users/templates/users/user_profile.html:59 msgid "Email" msgstr "邮件" #: users/models/user.py:38 users/templates/users/_select_user_modal.html:15 -#: users/templates/users/user_detail.html:86 +#: users/templates/users/user_detail.html:87 #: users/templates/users/user_list.html:25 #: users/templates/users/user_profile.html:55 msgid "Role" @@ -2313,7 +2155,7 @@ msgstr "角色" msgid "Avatar" msgstr "头像" -#: users/models/user.py:40 users/templates/users/user_detail.html:81 +#: users/models/user.py:40 users/templates/users/user_detail.html:82 msgid "Wechat" msgstr "微信" @@ -2328,7 +2170,7 @@ msgstr "二次验证" msgid "Public key" msgstr "ssh公钥" -#: users/models/user.py:257 +#: users/models/user.py:265 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -2336,6 +2178,10 @@ msgstr "Administrator是初始的超级管理员" msgid "Please Select User" msgstr "选择用户" +#: users/templates/users/_select_user_modal.html:17 +msgid "Asset num" +msgstr "资产数量" + #: users/templates/users/_user.html:13 #: users/templates/users/user_profile_update.html:51 msgid "Account" @@ -2351,7 +2197,7 @@ msgstr "导入" #: users/templates/users/_user_import_modal.html:6 msgid "Download template or use export csv format" -msgstr "下载模板" +msgstr "下载模板或使用导出的csv格式" #: users/templates/users/_user_import_modal.html:14 msgid "Users csv file" @@ -2395,15 +2241,15 @@ msgid " for more information" msgstr "获取更多信息" #: users/templates/users/forgot_password.html:26 -#: users/templates/users/login.html:64 +#: users/templates/users/login.html:73 msgid "Forgot password" msgstr "忘记密码" #: users/templates/users/forgot_password.html:33 msgid "Input your email, that will send a mail to your" -msgstr "输入您的邮箱, 将会发一封重置短信邮件到您的邮箱中" +msgstr "输入您的邮箱, 将会发一封重置邮件到您的邮箱中" -#: users/templates/users/login.html:47 +#: users/templates/users/login.html:50 msgid "Captcha invalid" msgstr "验证码错误" @@ -2416,8 +2262,8 @@ msgid "City" msgstr "城市" #: users/templates/users/reset_password.html:45 -#: users/templates/users/user_detail.html:324 -#: users/templates/users/user_profile.html:136 users/utils.py:68 +#: users/templates/users/user_detail.html:325 +#: users/templates/users/user_profile.html:132 users/utils.py:71 msgid "Reset password" msgstr "重置密码" @@ -2425,8 +2271,13 @@ msgstr "重置密码" msgid "Password again" msgstr "再次输入密码" +#: users/templates/users/reset_password.html:57 +#: users/templates/users/user_profile.html:20 +msgid "Setting" +msgstr "设置" + #: users/templates/users/user_create.html:4 -#: users/templates/users/user_list.html:16 users/views/user.py:72 +#: users/templates/users/user_list.html:16 users/views/user.py:74 msgid "Create user" msgstr "创建用户" @@ -2435,113 +2286,102 @@ msgid "Reset link will be generated and sent to the user. " msgstr "生成重置密码连接,通过邮件发送给用户" #: users/templates/users/user_detail.html:19 -#: users/templates/users/user_granted_asset.html:18 -#: users/templates/users/user_group_granted_asset.html:18 -#: users/views/user.py:148 +#: users/templates/users/user_granted_asset.html:18 users/views/user.py:150 msgid "User detail" msgstr "用户详情" #: users/templates/users/user_detail.html:22 #: users/templates/users/user_granted_asset.html:21 +#: users/templates/users/user_group_detail.html:25 #: users/templates/users/user_group_granted_asset.html:21 msgid "Asset granted" msgstr "授权的资产" -#: users/templates/users/user_detail.html:106 +#: users/templates/users/user_detail.html:107 #: users/templates/users/user_profile.html:92 msgid "Last login" msgstr "最后登录" -#: users/templates/users/user_detail.html:156 +#: users/templates/users/user_detail.html:157 msgid "Send reset password mail" msgstr "发送重置密码邮件" -#: users/templates/users/user_detail.html:159 -#: users/templates/users/user_detail.html:167 +#: users/templates/users/user_detail.html:160 +#: users/templates/users/user_detail.html:168 msgid "Send" msgstr "发送" -#: users/templates/users/user_detail.html:164 +#: users/templates/users/user_detail.html:165 msgid "Send reset ssh key mail" msgstr "发送重置密钥邮件" -#: users/templates/users/user_detail.html:323 +#: users/templates/users/user_detail.html:324 msgid "An e-mail has been sent to the user\\'s mailbox." msgstr "已发送邮件到用户邮箱" -#: users/templates/users/user_detail.html:334 -msgid "" -"This will reset the user's password. A password-reset email will be sent to " -"the user\\'s mailbox." -msgstr "重设密码邮件将会发送到用户邮箱" +#: users/templates/users/user_detail.html:335 +msgid "This will reset the user password and send a reset mail" +msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱" -#: users/templates/users/user_detail.html:348 +#: users/templates/users/user_detail.html:349 msgid "" "The reset-ssh-public-key E-mail has been sent successfully. Please inform " "the user to update his new ssh public key." -msgstr "重设秘钥邮件将会发送到用户邮箱" +msgstr "重设密钥邮件将会发送到用户邮箱" -#: users/templates/users/user_detail.html:349 -#: users/templates/users/user_profile.html:144 +#: users/templates/users/user_detail.html:350 +#: users/templates/users/user_profile.html:140 msgid "Reset SSH public key" msgstr "重置SSH密钥" -#: users/templates/users/user_detail.html:359 -msgid "This will reset the user\\" -msgstr "重置" +#: users/templates/users/user_detail.html:360 +msgid "This will reset the user public key and send a reset mail" +msgstr "将会失效用户当前密钥,并发送重置邮件到用户邮箱" -#: users/templates/users/user_detail.html:376 -#: users/templates/users/user_profile.html:170 +#: users/templates/users/user_detail.html:377 +#: users/templates/users/user_profile.html:166 msgid "Successfully updated the SSH public key." msgstr "更新ssh密钥成功" -#: users/templates/users/user_detail.html:377 -#: users/templates/users/user_detail.html:381 -#: users/templates/users/user_profile.html:171 -#: users/templates/users/user_profile.html:176 +#: users/templates/users/user_detail.html:378 +#: users/templates/users/user_detail.html:382 +#: users/templates/users/user_profile.html:167 +#: users/templates/users/user_profile.html:172 msgid "User SSH public key update" msgstr "ssh密钥" -#: users/templates/users/user_granted_asset.html:29 -#: users/templates/users/user_group_granted_asset.html:29 -msgid "Assets granted of " -msgstr "授权资产" - -#: users/templates/users/user_granted_asset.html:62 -#: users/templates/users/user_group_granted_asset.html:65 -msgid "Asset groups granted of " -msgstr "授权资产组" - #: users/templates/users/user_group_create_update.html:31 msgid "Cancel" msgstr "取消" -#: users/templates/users/user_group_detail.html:22 users/views/group.py:80 +#: users/templates/users/user_group_detail.html:22 +#: users/templates/users/user_group_granted_asset.html:18 +#: users/views/group.py:80 msgid "User group detail" -msgstr "资产组详情" +msgstr "用户组详情" #: users/templates/users/user_group_detail.html:86 msgid "Add user" msgstr "添加用户" -#: users/templates/users/user_group_granted_asset.html:53 -msgid "Valid" -msgstr "可用" - #: users/templates/users/user_group_list.html:5 users/views/group.py:45 msgid "Create user group" msgstr "创建用户组" -#: users/templates/users/user_group_list.html:86 +#: users/templates/users/user_group_list.html:82 +msgid "This will delete the selected groups !!!" +msgstr "删除选择组" + +#: users/templates/users/user_group_list.html:90 msgid "UserGroups Deleted." msgstr "用户组删除" -#: users/templates/users/user_group_list.html:87 -#: users/templates/users/user_group_list.html:92 +#: users/templates/users/user_group_list.html:91 +#: users/templates/users/user_group_list.html:96 msgid "UserGroups Delete" msgstr "用户组删除" -#: users/templates/users/user_group_list.html:91 +#: users/templates/users/user_group_list.html:95 msgid "UserGroup Deleting failed." msgstr "用户组删除失败" @@ -2566,16 +2406,12 @@ msgstr "用户删除失败" msgid "OTP" msgstr "" -#: users/templates/users/user_profile.html:100 users/views/user.py:177 -#: users/views/user.py:229 +#: users/templates/users/user_profile.html:100 users/views/user.py:179 +#: users/views/user.py:233 msgid "User groups" msgstr "用户组" -#: users/templates/users/user_profile.html:114 -msgid "Perm assets" -msgstr "资产" - -#: users/templates/users/user_profile.html:174 +#: users/templates/users/user_profile.html:170 msgid "Failed to update SSH public key." msgstr "更新密钥失败" @@ -2591,9 +2427,9 @@ msgstr "指纹" msgid "Update public key" msgstr "更新密钥" -#: users/templates/users/user_update.html:4 users/views/user.py:91 +#: users/templates/users/user_update.html:4 users/views/user.py:93 msgid "Update user" -msgstr "编辑用户" +msgstr "更新用户" #: users/utils.py:35 msgid "Create account successfully" @@ -2640,7 +2476,7 @@ msgstr "" "
    \n" " " -#: users/utils.py:70 +#: users/utils.py:73 #, python-format msgid "" "\n" @@ -2684,11 +2520,11 @@ msgstr "" "
    \n" " " -#: users/utils.py:101 +#: users/utils.py:104 msgid "SSH Key Reset" msgstr "重置ssh密钥" -#: users/utils.py:103 +#: users/utils.py:106 #, python-format msgid "" "\n" @@ -2713,17 +2549,17 @@ msgstr "" "
    \n" " " -#: users/utils.py:136 +#: users/utils.py:139 msgid "User not exist" msgstr "用户不存在" -#: users/utils.py:138 +#: users/utils.py:141 msgid "Disabled or expired" msgstr "禁用或失效" -#: users/utils.py:151 +#: users/utils.py:154 msgid "Password or SSH public key invalid" -msgstr "密码或秘钥不合法" +msgstr "密码或密钥不合法" #: users/views/group.py:29 msgid "User group list" @@ -2731,66 +2567,66 @@ msgstr "用户组列表" #: users/views/group.py:63 msgid "Update user group" -msgstr "编辑用户组" +msgstr "更新用户组" -#: users/views/login.py:54 +#: users/views/group.py:96 +msgid "User group granted asset" +msgstr "用户组授权资产" + +#: users/views/login.py:57 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: users/views/login.py:87 +#: users/views/login.py:99 msgid "Logout success" msgstr "退出登录成功" -#: users/views/login.py:88 +#: users/views/login.py:100 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: users/views/login.py:104 +#: users/views/login.py:116 msgid "Email address invalid, please input again" msgstr "邮箱地址错误,重新输入" -#: users/views/login.py:117 +#: users/views/login.py:129 msgid "Send reset password message" msgstr "发送重置密码邮件" -#: users/views/login.py:118 +#: users/views/login.py:130 msgid "Send reset password mail success, login your mail box and follow it " msgstr "" "发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)" -#: users/views/login.py:132 +#: users/views/login.py:144 msgid "Reset password success" msgstr "重置密码成功" -#: users/views/login.py:133 +#: users/views/login.py:145 msgid "Reset password success, return to login page" msgstr "重置密码成功,返回到登录页面" -#: users/views/login.py:150 users/views/login.py:163 +#: users/views/login.py:162 users/views/login.py:175 msgid "Token invalid or expired" msgstr "Token错误或失效" -#: users/views/login.py:159 +#: users/views/login.py:171 msgid "Password not same" msgstr "密码不一致" -#: users/views/login.py:197 +#: users/views/login.py:209 msgid "First login" msgstr "首次登陆" -#: users/views/login.py:247 +#: users/views/login.py:259 msgid "Login log list" msgstr "登录日志" -#: users/views/user.py:58 -msgid "User list" -msgstr "用户列表" - -#: users/views/user.py:101 +#: users/views/user.py:103 msgid "Bulk update user success" msgstr "批量更新用户成功" -#: users/views/user.py:206 +#: users/views/user.py:208 msgid "Invalid file." msgstr "文件不合法" @@ -2798,17 +2634,17 @@ msgstr "文件不合法" msgid "User granted assets" msgstr "用户授权资产" -#: users/views/user.py:338 +#: users/views/user.py:334 msgid "Profile setting" msgstr "个人信息设置" -#: users/views/user.py:356 +#: users/views/user.py:352 msgid "Password update" msgstr "密码更新" -#: users/views/user.py:378 +#: users/views/user.py:374 msgid "Public key update" -msgstr "秘钥更新" +msgstr "密钥更新" -#~ msgid "Connect" -#~ msgstr "连接" +#~ msgid "Create asset permission " +#~ msgstr "创建资产权限" diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index 7a8aa422e..04fe210b2 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -17,7 +17,6 @@ import ldap from django_auth_ldap.config import LDAPSearch from django.urls import reverse_lazy - # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.dirname(BASE_DIR) @@ -39,11 +38,9 @@ SECRET_KEY = CONFIG.SECRET_KEY # SECURITY WARNING: don't run with debug turned on in production! DEBUG = CONFIG.DEBUG or False - # Absolute url for some case, for example email link SITE_URL = CONFIG.SITE_URL or 'http://localhost' - # LOG LEVEL LOG_LEVEL = 'DEBUG' if DEBUG else CONFIG.LOG_LEVEL or 'WARNING' @@ -114,7 +111,7 @@ LOGIN_URL = reverse_lazy('users:login') SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN or None CSRF_COOKIE_DOMAIN = CONFIG.CSRF_COOKIE_DOMAIN or None -SESSION_COOKIE_AGE = CONFIG.SESSION_COOKIE_AGE or 3600*24 +SESSION_COOKIE_AGE = CONFIG.SESSION_COOKIE_AGE or 3600 * 24 MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' # Database @@ -241,7 +238,7 @@ USE_L10N = True USE_TZ = True # I18N translation -LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale'), ] +LOCALE_PATHS = [os.path.join(BASE_DIR, 'i18n'), ] # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.10/howto/static-files/ @@ -250,7 +247,6 @@ STATIC_URL = '/static/' STATIC_ROOT = os.path.join(PROJECT_DIR, "data", "static") STATIC_DIR = os.path.join(BASE_DIR, "static") - STATICFILES_DIRS = ( os.path.join(BASE_DIR, "static"), ) @@ -265,7 +261,7 @@ MEDIA_ROOT = os.path.join(PROJECT_DIR, 'data', 'media').replace('\\', '/') + '/' # BOOTSTRAP_COLUMN_COUNT = 11 # Init data or generate fake data source for development -FIXTURE_DIRS = [os.path.join(BASE_DIR, 'fixtures'),] +FIXTURE_DIRS = [os.path.join(BASE_DIR, 'fixtures'), ] # Email config EMAIL_HOST = CONFIG.EMAIL_HOST @@ -298,7 +294,7 @@ REST_FRAMEWORK = { 'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S %z', 'DATETIME_INPUT_FORMATS': ['%Y-%m-%d %H:%M:%S %z'], # 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', - 'PAGE_SIZE': 15 + # 'PAGE_SIZE': 15 } AUTHENTICATION_BACKENDS = [ @@ -308,7 +304,6 @@ AUTHENTICATION_BACKENDS = [ # Custom User Auth model AUTH_USER_MODEL = 'users.User' - # Auth LDAP settings AUTH_LDAP = CONFIG.AUTH_LDAP AUTH_LDAP_SERVER_URI = CONFIG.AUTH_LDAP_SERVER_URI @@ -319,7 +314,7 @@ AUTH_LDAP_SEARCH_FILTER = CONFIG.AUTH_LDAP_SEARCH_FILTER AUTH_LDAP_START_TLS = CONFIG.AUTH_LDAP_START_TLS AUTH_LDAP_USER_ATTR_MAP = CONFIG.AUTH_LDAP_USER_ATTR_MAP AUTH_LDAP_USER_SEARCH = LDAPSearch( - AUTH_LDAP_SEARCH_OU, ldap.SCOPE_SUBTREE, AUTH_LDAP_SEARCH_FILTER, + AUTH_LDAP_SEARCH_OU, ldap.SCOPE_SUBTREE, AUTH_LDAP_SEARCH_FILTER, ) AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER @@ -332,7 +327,6 @@ AUTH_LDAP_BACKEND = 'django_auth_ldap.backend.LDAPBackend' if AUTH_LDAP: AUTHENTICATION_BACKENDS.insert(0, AUTH_LDAP_BACKEND) - # Celery using redis as broker CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/3' % { 'password': CONFIG.REDIS_PASSWORD if CONFIG.REDIS_PASSWORD else '', @@ -354,7 +348,6 @@ CELERY_REDIRECT_STDOUTS = True CELERY_REDIRECT_STDOUTS_LEVEL = "INFO" CELERY_WORKER_HIJACK_ROOT_LOGGER = False - # Cache use redis CACHES = { 'default': { @@ -373,7 +366,25 @@ CAPTCHA_FOREGROUND_COLOR = '#001100' CAPTCHA_NOISE_FUNCTIONS = ('captcha.helpers.noise_dots',) CAPTCHA_TEST_MODE = CONFIG.CAPTCHA_TEST_MODE -COMMAND_STORAGE_BACKEND = 'terminal.backends.command.db' +COMMAND_STORAGE = { + 'ENGINE': 'terminal.backends.command.db', +} + +TERMINAL_COMMAND_STORAGE = { + "default": { + "TYPE": "server", + }, + # 'ali-es': { + # 'TYPE': 'elasticsearch', + # 'HOSTS': ['http://elastic:changeme@localhost:9200'], + # }, +} + +TERMINAL_REPLAY_STORAGE = { + "default": { + "TYPE": "server", + }, +} # Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html BOOTSTRAP3 = { @@ -386,6 +397,6 @@ BOOTSTRAP3 = { } TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION or 3600 -DISPLAY_PER_PAGE = CONFIG.DISPLAY_PER_PAGE +DISPLAY_PER_PAGE = CONFIG.DISPLAY_PER_PAGE or 25 DEFAULT_EXPIRED_YEARS = 70 USER_GUIDE_URL = "" diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index 2eb54d87d..fe8a4417a 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -4,16 +4,16 @@ from __future__ import unicode_literals from django.conf.urls import url, include from django.conf import settings from django.conf.urls.static import static -from django.views.static import serve as static_serve from rest_framework.schemas import get_schema_view from rest_framework_swagger.renderers import SwaggerUIRenderer, OpenAPIRenderer -from .views import IndexView +from .views import IndexView, LunaView schema_view = get_schema_view(title='Users API', renderer_classes=[OpenAPIRenderer, SwaggerUIRenderer]) urlpatterns = [ url(r'^$', IndexView.as_view(), name='index'), + url(r'^luna/', LunaView.as_view(), name='luna-error'), url(r'^users/', include('users.urls.views_urls', namespace='users')), url(r'^assets/', include('assets.urls.views_urls', namespace='assets')), url(r'^perms/', include('perms.urls.views_urls', namespace='perms')), diff --git a/apps/jumpserver/views.py b/apps/jumpserver/views.py index 0ddba94bf..c6a2dc4f3 100644 --- a/apps/jumpserver/views.py +++ b/apps/jumpserver/views.py @@ -1,4 +1,7 @@ -from django.views.generic import TemplateView +import datetime + +from django.http import HttpResponse +from django.views.generic import TemplateView, View from django.utils import timezone from django.db.models import Count from django.contrib.auth.mixins import LoginRequiredMixin @@ -45,15 +48,22 @@ class IndexView(LoginRequiredMixin, TemplateView): return self.session_week.values('user').distinct().count() def get_week_login_asset_count(self): - return self.session_week.values('asset').distinct().count() + return self.session_week.count() + # return self.session_week.values('asset').distinct().count() def get_month_day_metrics(self): month_str = [d.strftime('%m-%d') for d in self.session_month_dates] or ['0'] return month_str def get_month_login_metrics(self): - return [self.session_month.filter(date_start__date=d).count() - for d in self.session_month_dates] + data = [] + time_min = datetime.datetime.min.time() + time_max = datetime.datetime.max.time() + for d in self.session_month_dates: + ds = datetime.datetime.combine(d, time_min).replace(tzinfo=timezone.get_current_timezone()) + de = datetime.datetime.combine(d, time_max).replace(tzinfo=timezone.get_current_timezone()) + data.append(self.session_month.filter(date_start__range=(ds, de)).count()) + return data def get_month_active_user_metrics(self): if self.session_month_dates_archive: @@ -119,10 +129,18 @@ class IndexView(LoginRequiredMixin, TemplateView): self.session_week = Session.objects.filter(date_start__gt=week_ago) self.session_month = Session.objects.filter(date_start__gt=month_ago) self.session_month_dates = self.session_month.dates('date_start', 'day') - self.session_month_dates_archive = [ - self.session_month.filter(date_start__date=d) - for d in self.session_month_dates - ] + + self.session_month_dates_archive = [] + time_min = datetime.datetime.min.time() + time_max = datetime.datetime.max.time() + + for d in self.session_month_dates: + ds = datetime.datetime.combine(d, time_min).replace( + tzinfo=timezone.get_current_timezone()) + de = datetime.datetime.combine(d, time_max).replace( + tzinfo=timezone.get_current_timezone()) + self.session_month_dates_archive.append( + self.session_month.filter(date_start__range=(ds, de))) context = { 'assets_count': self.get_asset_count(), @@ -149,3 +167,12 @@ class IndexView(LoginRequiredMixin, TemplateView): kwargs.update(context) return super(IndexView, self).get_context_data(**kwargs) + + +class LunaView(View): + def get(self, request): + msg = """ + Luna是单独部署的一个程序,你需要部署luna,coco,配置nginx做url分发, + 如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运 + """ + return HttpResponse(msg) \ No newline at end of file diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo deleted file mode 100644 index 7be8a996a99f2451a1f8854e863216f289eaa052..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31661 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 Z=ZmTBevOJA+pkPsM@;YhAa$G){u`YM;LHF3 diff --git a/apps/ops/utils.py b/apps/ops/utils.py index 8ff9c321f..55862dd44 100644 --- a/apps/ops/utils.py +++ b/apps/ops/utils.py @@ -43,7 +43,7 @@ def update_or_create_ansible_task( new_adhoc.become = become_info if not adhoc or adhoc != new_adhoc: - logger.debug("Task create new adhoc: {}".format(task_name)) + print("Task create new adhoc: {}".format(task_name)) new_adhoc.save() task.latest_adhoc = new_adhoc created = True diff --git a/apps/perms/api.py b/apps/perms/api.py index 9165a9098..6b0b15f76 100644 --- a/apps/perms/api.py +++ b/apps/perms/api.py @@ -3,17 +3,14 @@ from django.shortcuts import get_object_or_404 from rest_framework.views import APIView, Response -from rest_framework.generics import ListAPIView, get_object_or_404, RetrieveUpdateAPIView +from rest_framework.generics import ListAPIView, get_object_or_404 from rest_framework import viewsets -from common.utils import get_object_or_none -from users.permissions import IsValidUser, IsSuperUser, IsAppUser, IsSuperUserOrAppUser -from .utils import get_user_granted_assets, get_user_granted_asset_groups, \ - get_user_asset_permissions, get_user_group_asset_permissions, \ - get_user_group_granted_assets, get_user_group_granted_asset_groups -from .models import AssetPermission -from .hands import AssetGrantedSerializer, User, UserGroup, AssetGroup, Asset, \ - AssetGroup, AssetGroupGrantedSerializer, SystemUser, MyAssetGroupGrantedSerializer +from users.permissions import IsValidUser, IsSuperUser, IsSuperUserOrAppUser +from .utils import NodePermissionUtil +from .models import NodePermission +from .hands import AssetGrantedSerializer, User, UserGroup, Asset, \ + NodeGrantedSerializer, SystemUser, NodeSerializer from . import serializers @@ -21,102 +18,23 @@ class AssetPermissionViewSet(viewsets.ModelViewSet): """ 资产授权列表的增删改查api """ - queryset = AssetPermission.objects.all() - serializer_class = serializers.AssetPermissionSerializer + queryset = NodePermission.objects.all() + serializer_class = serializers.AssetPermissionCreateUpdateSerializer permission_classes = (IsSuperUser,) - def get_queryset(self): - queryset = super(AssetPermissionViewSet, self).get_queryset() - user_id = self.request.query_params.get('user', '') - user_group_id = self.request.query_params.get('user_group', '') - - if user_id and user_id.isdigit(): - user = get_object_or_404(User, id=int(user_id)) - queryset = get_user_asset_permissions(user) - - if user_group_id: - user_group = get_object_or_404(UserGroup, id=user_group_id) - queryset = get_user_group_asset_permissions(user_group) - return queryset - def get_serializer_class(self): - if getattr(self, 'user_id', ''): - return serializers.UserAssetPermissionSerializer - return serializers.AssetPermissionSerializer - - -class AssetPermissionRemoveUserApi(RetrieveUpdateAPIView): - """ - 将用户从授权中移除,Detail页面会调用 - """ - permission_classes = (IsSuperUser,) - serializer_class = serializers.AssetPermissionUpdateUserSerializer - queryset = AssetPermission.objects.all() - - def update(self, request, *args, **kwargs): - perm = self.get_object() - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - users = serializer.validated_data.get('users') - if users: - perm.users.remove(*tuple(users)) - return Response({"msg": "ok"}) - else: - return Response({"error": serializer.errors}) - + if self.action in ("list", 'retrieve'): + return serializers.AssetPermissionListSerializer + return self.serializer_class -class AssetPermissionAddUserApi(RetrieveUpdateAPIView): - permission_classes = (IsSuperUser,) - serializer_class = serializers.AssetPermissionUpdateUserSerializer - queryset = AssetPermission.objects.all() - - def update(self, request, *args, **kwargs): - perm = self.get_object() - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - users = serializer.validated_data.get('users') - if users: - perm.users.add(*tuple(users)) - return Response({"msg": "ok"}) - else: - return Response({"error": serializer.errors}) - - -class AssetPermissionRemoveAssetApi(RetrieveUpdateAPIView): - """ - 将用户从授权中移除,Detail页面会调用 - """ - permission_classes = (IsSuperUser,) - serializer_class = serializers.AssetPermissionUpdateAssetSerializer - queryset = AssetPermission.objects.all() - - def update(self, request, *args, **kwargs): - perm = self.get_object() - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - assets = serializer.validated_data.get('assets') - if assets: - perm.assets.remove(*tuple(assets)) - return Response({"msg": "ok"}) - else: - return Response({"error": serializer.errors}) + def get_queryset(self): + queryset = super().get_queryset() + node_id = self.request.query_params.get('node_id') + if node_id: + queryset = queryset.filter(node__id=node_id) -class AssetPermissionAddAssetApi(RetrieveUpdateAPIView): - permission_classes = (IsSuperUser,) - serializer_class = serializers.AssetPermissionUpdateAssetSerializer - queryset = AssetPermission.objects.all() - - def update(self, request, *args, **kwargs): - perm = self.get_object() - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - assets = serializer.validated_data.get('assets') - if assets: - perm.assets.add(*tuple(assets)) - return Response({"msg": "ok"}) - else: - return Response({"error": serializer.errors}) + return queryset class UserGrantedAssetsApi(ListAPIView): @@ -128,41 +46,43 @@ class UserGrantedAssetsApi(ListAPIView): def get_queryset(self): user_id = self.kwargs.get('pk', '') - queryset = [] + if user_id: user = get_object_or_404(User, id=user_id) - for k, v in get_user_granted_assets(user).items(): - k.system_users_granted = v - queryset.append(k) + else: + user = self.request.user + + for k, v in NodePermissionUtil.get_user_assets(user).items(): + if k.is_unixlike(): + system_users_granted = [s for s in v if s.protocol == 'ssh'] + else: + system_users_granted = [s for s in v if s.protocol == 'rdp'] + k.system_users_granted = system_users_granted + queryset.append(k) return queryset + def get_permissions(self): + if self.kwargs.get('pk') is None: + self.permission_classes = (IsValidUser,) + return super().get_permissions() -class UserGrantedAssetGroupsApi(APIView): - permission_classes = (IsValidUser,) - def get(self, request, *args, **kwargs): - asset_groups = {} - user_id = kwargs.get('pk', '') - user = get_object_or_404(User, id=user_id) +class UserGrantedNodesApi(ListAPIView): + permission_classes = (IsSuperUser,) + serializer_class = NodeSerializer + + def get_queryset(self): + user_id = self.kwargs.get('pk', '') + if user_id: + user = get_object_or_404(User, id=user_id) + else: + user = self.request.user + nodes = NodePermissionUtil.get_user_nodes(user) + return nodes.keys() + - assets = get_user_granted_assets(user) - for asset in assets: - for asset_group in asset.groups.all(): - if asset_group.id in asset_groups: - asset_groups[asset_group.id]['assets_amount'] += 1 - else: - asset_groups[asset_group.id] = { - 'id': asset_group.id, - 'name': asset_group.name, - 'comment': asset_group.comment, - 'assets_amount': 1 - } - asset_groups_json = asset_groups.values() - return Response(asset_groups_json, status=200) - - -class UserGrantedAssetGroupsWithAssetsApi(ListAPIView): +class UserGrantedNodesWithAssetsApi(ListAPIView): """ 授权用户的资产组,注:这里的资产组并非是授权列表中授权的, 而是把所有资产取出来,然后反查出所有资产组,然后合并得到, @@ -171,7 +91,7 @@ class UserGrantedAssetGroupsWithAssetsApi(ListAPIView): [ { "id": 1, - "name": "资产组1", + "value": "node", ... 其它属性 "assets_granted": [ { @@ -191,162 +111,89 @@ class UserGrantedAssetGroupsWithAssetsApi(ListAPIView): ] """ permission_classes = (IsSuperUserOrAppUser,) - serializer_class = AssetGroupGrantedSerializer + serializer_class = NodeGrantedSerializer def get_queryset(self): user_id = self.kwargs.get('pk', '') + queryset = [] if not user_id: - return [] - - user = get_object_or_404(User, id=user_id) - asset_groups = get_user_granted_asset_groups(user) + user = self.request.user + else: + user = get_object_or_404(User, id=user_id) - queryset = [] - for asset_group, assets_system_users in asset_groups.items(): - assets = [] - for asset, system_users in assets_system_users: - asset.system_users_granted = system_users - assets.append(asset) - asset_group.assets_granted = assets - queryset.append(asset_group) + nodes = NodePermissionUtil.get_user_nodes_with_assets(user) + assets = {} + for k, v in NodePermissionUtil.get_user_assets(user).items(): + if k.is_unixlike(): + system_users_granted = [s for s in v if s.protocol == 'ssh'] + else: + system_users_granted = [s for s in v if s.protocol == 'rdp'] + assets[k] = system_users_granted + for node, v in nodes.items(): + for asset in v['assets']: + asset.system_users_granted = assets[asset] + node.assets_granted = v['assets'] + queryset.append(node) return queryset + def get_permissions(self): + if self.kwargs.get('pk') is None: + self.permission_classes = (IsValidUser,) + return super().get_permissions() -class MyGrantedAssetsApi(ListAPIView): - """ - 用户自己查询授权的资产列表 - """ - permission_classes = (IsValidUser,) - serializer_class = AssetGrantedSerializer - def get_queryset(self): - queryset = [] - user = self.request.user - if user: - for asset, system_users in get_user_granted_assets(user).items(): - asset.system_users_granted = system_users - queryset.append(asset) - return queryset - - -class MyGrantedAssetGroupsApi(APIView): - """ - 授权的所有资产组,并非是授权列表中的,而是经过计算得来的 - """ - permission_classes = (IsValidUser,) - - def get(self, request, *args, **kwargs): - asset_groups = {} - user = request.user - - if user: - assets = get_user_granted_assets(user) - for asset in assets: - for asset_group in asset.groups.all(): - if asset_group.id in asset_groups: - asset_groups[asset_group.id]['assets_amount'] += 1 - else: - asset_groups[asset_group.id] = { - 'id': asset_group.id, - 'name': asset_group.name, - 'comment': asset_group.comment, - 'assets_amount': 1 - } - asset_groups_json = asset_groups.values() - return Response(asset_groups_json, status=200) - - -class MyGrantedAssetGroupsWithAssetsApi(ListAPIView): - """ - 授权当前用户的资产组,注:这里的资产组并非是授权列表中授权的, - 而是把所有资产取出来,然后反查出所有资产组,然后合并得到, - 结果里也包含资产组下授权的资产 - 数据结构如下: - [ - { - "id": 1, - "name": "资产组1", - ... 其它属性 - "assets_granted": [ - { - "id": 1, - "hostname": "testserver", - "system_users_granted": [ - "id": 1, - "name": "web", - "username": "web", - "protocol": "ssh", - ] - } - ] - } - ] - """ - permission_classes = (IsValidUser,) - serializer_class = MyAssetGroupGrantedSerializer +class UserGroupGrantedAssetsApi(ListAPIView): + permission_classes = (IsSuperUser,) + serializer_class = AssetGrantedSerializer def get_queryset(self): - user = self.request.user - asset_groups = get_user_granted_asset_groups(user) - + user_group_id = self.kwargs.get('pk', '') queryset = [] - for asset_group, assets_system_users in asset_groups.items(): - assets = [] - for asset, system_users in assets_system_users: - asset.system_users_granted = system_users - assets.append(asset) - asset_group.assets_granted = assets - queryset.append(asset_group) - return queryset + if not user_group_id: + return queryset -class MyAssetGroupOfAssetsApi(ListAPIView): - """授权用户资产组下的资产列表, 非该资产组的所有资产,而是被授权的""" - permission_classes = (IsValidUser,) - serializer_class = AssetGrantedSerializer - - def get_queryset(self): - queryset = [] - asset_group_id = self.kwargs.get('pk', -1) - user = self.request.user - asset_group = get_object_or_none(AssetGroup, id=asset_group_id) - - if user and asset_group: - assets = get_user_granted_assets(user) - for asset in asset_group.assets.all(): - if asset in assets: - asset.system_users_granted = assets[asset] - queryset.append(asset) + user_group = get_object_or_404(UserGroup, id=user_group_id) + assets = NodePermissionUtil.get_user_group_assets(user_group) + for k, v in assets.items(): + k.system_users_granted = v + queryset.append(k) return queryset -class UserGroupGrantedAssetsApi(ListAPIView): +class UserGroupGrantedNodesApi(ListAPIView): permission_classes = (IsSuperUser,) - serializer_class = AssetGrantedSerializer + serializer_class = NodeSerializer def get_queryset(self): - user_group_id = self.kwargs.get('pk', '') + group_id = self.kwargs.get('pk', '') + queryset = [] - if user_group_id: - user_group = get_object_or_404(UserGroup, id=user_group_id) - queryset = get_user_group_granted_assets(user_group) - else: - queryset = [] + if group_id: + group = get_object_or_404(UserGroup, id=group_id) + nodes = NodePermissionUtil.get_user_group_nodes(group) + queryset = nodes.keys() return queryset -class UserGroupGrantedAssetGroupsApi(ListAPIView): +class UserGroupGrantedNodesWithAssetsApi(ListAPIView): permission_classes = (IsSuperUser,) - serializer_class = AssetGroupGrantedSerializer + serializer_class = NodeGrantedSerializer def get_queryset(self): user_group_id = self.kwargs.get('pk', '') + queryset = [] - if user_group_id: - user_group = get_object_or_404(UserGroup, id=user_group_id) - queryset = get_user_group_granted_asset_groups(user_group) - else: - queryset = [] + if not user_group_id: + return queryset + + user_group = get_object_or_404(UserGroup, id=user_group_id) + nodes = NodePermissionUtil.get_user_group_nodes_with_assets(user_group) + for node, v in nodes.items(): + for asset in v['assets']: + asset.system_users_granted = v['system_users'] + node.assets_granted = v['assets'] + queryset.append(node) return queryset @@ -363,7 +210,7 @@ class ValidateUserAssetPermissionView(APIView): asset = get_object_or_404(Asset, id=asset_id) system_user = get_object_or_404(SystemUser, id=system_id) - assets_granted = get_user_granted_assets(user) + assets_granted = NodePermissionUtil.get_user_assets(user) if system_user in assets_granted.get(asset, []): return Response({'msg': True}, status=200) else: diff --git a/apps/perms/apps.py b/apps/perms/apps.py index d40373e08..216e9c3d7 100644 --- a/apps/perms/apps.py +++ b/apps/perms/apps.py @@ -5,3 +5,7 @@ from django.apps import AppConfig class PermsConfig(AppConfig): name = 'perms' + + def ready(self): + from . import signals_handler + return super().ready() diff --git a/apps/perms/forms.py b/apps/perms/forms.py index 4e3c0bf72..f84e56693 100644 --- a/apps/perms/forms.py +++ b/apps/perms/forms.py @@ -4,94 +4,27 @@ from __future__ import absolute_import, unicode_literals from django import forms from django.utils.translation import ugettext_lazy as _ -# from .hands import User, UserGroup, Asset, AssetGroup, SystemUser -from .models import AssetPermission -from users.models import User +from .models import NodePermission class AssetPermissionForm(forms.ModelForm): - users = forms.ModelMultipleChoiceField( - queryset=User.objects.exclude(role=User.ROLE_APP), - widget=forms.SelectMultiple( - attrs={'class': 'select2', 'data-placeholder': _('Select users')}, - ), - label=_("User"), - required=False, - ) - class Meta: - model = AssetPermission + model = NodePermission fields = [ - 'name', 'users', 'user_groups', 'assets', 'asset_groups', - 'system_users', 'is_active', 'date_expired', 'comment', + 'node', 'user_group', 'system_user', 'is_active', + 'date_expired', 'comment', ] widgets = { - 'user_groups': forms.SelectMultiple( - attrs={'class': 'select2', - 'data-placeholder': _('Select user groups')}), - 'assets': forms.SelectMultiple( - attrs={'class': 'select2', - 'data-placeholder': _('Select assets')}), - 'asset_groups': forms.SelectMultiple( - attrs={'class': 'select2', - 'data-placeholder': _('Select asset groups')}), - 'system_users': forms.SelectMultiple( - attrs={'class': 'select2', - 'data-placeholder': _('Select system users')}), - } - help_texts = { - 'name': '* required', - 'system_users': '* required', + 'node': forms.Select( + attrs={'style': 'display:none'} + ), + 'user_group': forms.Select( + attrs={'class': 'select2', 'data-placeholder': _("User group")} + ), + 'system_user': forms.Select( + attrs={'class': 'select2', 'data-placeholder': _('System user')} + ), } - def clean_user_groups(self): - users = self.cleaned_data.get('users') - user_groups = self.cleaned_data.get('user_groups') - - if not users and not user_groups: - raise forms.ValidationError(_("User or group at least one required")) - return self.cleaned_data["user_groups"] - - def clean_asset_groups(self): - assets = self.cleaned_data.get('assets') - asset_groups = self.cleaned_data.get('asset_groups') - - if not assets and not asset_groups: - raise forms.ValidationError(_("Asset or group at least one required")) - - return self.cleaned_data["asset_groups"] - - def clean_system_users(self): - from assets.utils import check_assets_have_system_user - - errors = [] - assets = self.cleaned_data['assets'] - asset_groups = self.cleaned_data.get('asset_groups') - system_users = self.cleaned_data.get('system_users') - - if not asset_groups and not assets: - return self.cleaned_data.get("system_users") - - error_data = check_assets_have_system_user(assets, system_users) - if error_data: - for asset, system_users in error_data.items(): - msg = _("Asset {} of cluster {} not have [{}] system users, please check \n") - error = forms.ValidationError(msg.format( - asset.hostname, - asset.cluster.name, - ", ".join(system_user.name for system_user in system_users) - )) - errors.append(error) - - for group in asset_groups: - msg = _("Asset {}(group {}) of cluster {} not have [{}] system users, please check \n") - assets = group.assets.all() - error_data = check_assets_have_system_user(assets, system_users) - for asset, system_users in error_data.items(): - errors.append(msg.format( - asset.hostname, group.name, asset.cluster.name, - ", ".join(system_user.name for system_user in system_users) - )) - if errors: - raise forms.ValidationError(errors) - return self.cleaned_data['system_users'] + def clean_system_user(self): + return self.cleaned_data['system_user'] diff --git a/apps/perms/hands.py b/apps/perms/hands.py index 54af4f788..22f290069 100644 --- a/apps/perms/hands.py +++ b/apps/perms/hands.py @@ -3,8 +3,8 @@ from users.utils import AdminUserRequiredMixin from users.models import User, UserGroup -from assets.models import Asset, AssetGroup, SystemUser -from assets.serializers import AssetGrantedSerializer, AssetGroupGrantedSerializer, MyAssetGroupGrantedSerializer +from assets.models import Asset, AssetGroup, SystemUser, Node +from assets.serializers import AssetGrantedSerializer, NodeGrantedSerializer, NodeSerializer diff --git a/apps/perms/models.py b/apps/perms/models.py index 18e8ab6f2..49825e16d 100644 --- a/apps/perms/models.py +++ b/apps/perms/models.py @@ -67,3 +67,22 @@ class AssetPermission(models.Model): if cluster_remain: errors[system_user] = cluster_remain return errors + + +class NodePermission(models.Model): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + node = models.ForeignKey('assets.Node', on_delete=models.CASCADE, verbose_name=_("Node")) + user_group = models.ForeignKey('users.UserGroup', on_delete=models.CASCADE, verbose_name=_("User group")) + system_user = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE, verbose_name=_("System user")) + is_active = models.BooleanField(default=True, verbose_name=_('Active')) + date_expired = models.DateTimeField(default=date_expired_default, verbose_name=_('Date expired')) + created_by = models.CharField(max_length=128, blank=True, verbose_name=_('Created by')) + date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) + comment = models.TextField(verbose_name=_('Comment'), blank=True) + + def __str__(self): + return "{}:{}:{}".format(self.node.value, self.user_group.name, self.system_user.name) + + class Meta: + unique_together = ('node', 'user_group', 'system_user') + verbose_name = _("Asset permission") diff --git a/apps/perms/serializers.py b/apps/perms/serializers.py index 820a27af6..6decf663b 100644 --- a/apps/perms/serializers.py +++ b/apps/perms/serializers.py @@ -4,40 +4,27 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from common.utils import get_object_or_none -from .models import AssetPermission -from .hands import User +from common.fields import StringIDField +from .models import AssetPermission, NodePermission -class AssetPermissionSerializer(serializers.ModelSerializer): - assets_ = serializers.SerializerMethodField() - asset_groups_ = serializers.SerializerMethodField() - users_ = serializers.SerializerMethodField() - user_groups_ = serializers.SerializerMethodField() - system_users_ = serializers.SerializerMethodField() - +class AssetPermissionCreateUpdateSerializer(serializers.ModelSerializer): class Meta: - model = AssetPermission - fields = '__all__' - - @staticmethod - def get_assets_(obj): - return [asset.hostname for asset in obj.assets.all()] - - @staticmethod - def get_asset_groups_(obj): - return [group.name for group in obj.asset_groups.all()] + model = NodePermission + fields = [ + 'id', 'node', 'user_group', 'system_user', + 'is_active', 'date_expired' + ] - @staticmethod - def get_users_(obj): - return [user.username for user in obj.users.all()] - @staticmethod - def get_user_groups_(obj): - return [group.name for group in obj.user_groups.all()] +class AssetPermissionListSerializer(serializers.ModelSerializer): + node = StringIDField(read_only=True) + user_group = StringIDField(read_only=True) + system_user = StringIDField(read_only=True) - @staticmethod - def get_system_users_(obj): - return [user.username for user in obj.system_users.all()] + class Meta: + model = NodePermission + fields = '__all__' class AssetPermissionUpdateUserSerializer(serializers.ModelSerializer): @@ -54,7 +41,7 @@ class AssetPermissionUpdateAssetSerializer(serializers.ModelSerializer): fields = ['id', 'assets'] -class UserAssetPermissionSerializer(AssetPermissionSerializer): +class UserAssetPermissionCreateUpdateSerializer(AssetPermissionCreateUpdateSerializer): is_inherited = serializers.SerializerMethodField() @staticmethod diff --git a/apps/perms/signals_handler.py b/apps/perms/signals_handler.py new file mode 100644 index 000000000..ab16a85e7 --- /dev/null +++ b/apps/perms/signals_handler.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# + +from django.db.models.signals import post_save, post_delete +from django.dispatch import receiver + +from common.utils import get_logger +from .models import NodePermission + + +logger = get_logger(__file__) + + +@receiver(post_save, sender=NodePermission, dispatch_uid="my_unique_identifier") +def on_asset_permission_create_or_update(sender, instance=None, **kwargs): + if instance and instance.node and instance.system_user: + instance.system_user.nodes.add(instance.node) + diff --git a/apps/perms/tasks.py b/apps/perms/tasks.py index 4b66b5727..b8696e7c8 100644 --- a/apps/perms/tasks.py +++ b/apps/perms/tasks.py @@ -7,7 +7,4 @@ from common.utils import get_logger, encrypt_password logger = get_logger(__file__) -@shared_task(bind=True) -def push_users(self, assets, users): - pass diff --git a/apps/perms/templates/perms/asset_permission_asset.html b/apps/perms/templates/perms/asset_permission_asset.html index 5cfc44c05..12369574d 100644 --- a/apps/perms/templates/perms/asset_permission_asset.html +++ b/apps/perms/templates/perms/asset_permission_asset.html @@ -191,7 +191,7 @@ function updateGroup(groups) { } jumpserver.assets_selected = {}; -jumpserver.groups_selected = {}; +jumpserver.nodes_selected = {}; $(document).ready(function () { $('.select2.asset').select2() @@ -206,11 +206,11 @@ $(document).ready(function () { $('.select2.group').select2() .on('select2:select', function(evt) { var data = evt.params.data; - jumpserver.groups_selected[data.id] = data.text; + jumpserver.nodes_selected[data.id] = data.text; }) .on('select2:unselect', function(evt) { var data = evt.params.data; - delete jumpserver.groups_selected[data.id] + delete jumpserver.nodes_selected[data.id] }) }) .on('click', '.btn-add-assets', function () { @@ -232,7 +232,7 @@ $(document).ready(function () { removeAssets(assets) }) .on('click', '#btn-add-group', function () { - if (Object.keys(jumpserver.groups_selected).length === 0) { + if (Object.keys(jumpserver.nodes_selected).length === 0) { return false; } @@ -240,7 +240,7 @@ $(document).ready(function () { return $(this).data('gid'); }).get(); - $.map(jumpserver.groups_selected, function(group_name, index) { + $.map(jumpserver.nodes_selected, function(group_name, index) { groups.push(index); $('#opt_' + index).remove(); $('.group_edit tbody').append( diff --git a/apps/perms/templates/perms/asset_permission_create_update.html b/apps/perms/templates/perms/asset_permission_create_update.html index 85883a77b..d02b354f5 100644 --- a/apps/perms/templates/perms/asset_permission_create_update.html +++ b/apps/perms/templates/perms/asset_permission_create_update.html @@ -14,7 +14,7 @@ {% bootstrap_field form.comment layout="horizontal" %}
    diff --git a/apps/perms/templates/perms/asset_permission_list.html b/apps/perms/templates/perms/asset_permission_list.html index a8bdab5a1..c1a3e6114 100644 --- a/apps/perms/templates/perms/asset_permission_list.html +++ b/apps/perms/templates/perms/asset_permission_list.html @@ -1,104 +1,238 @@ -{% extends '_base_list.html' %} +{% extends 'base.html' %} +{% load static %} {% load i18n %} -{% block table_search %} -{% endblock %} +{% block custom_head_css_js %} + + + + -{% block help_message %} -
    - 提前规划好集群中的系统用户,授权时选择的资产(组内资产)必须存在该系统用户,否则可能无法成功登录 -
    {% endblock %} -{% block table_container %} -
    - - {% trans "Create permission" %} - +{% block content %} +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + + + + + + + + + + + + + + +
    + + {% trans 'Node' %}{% trans 'User group' %}{% trans 'System user' %}{% trans 'Is active' %}{% trans 'Date expired' %}{% trans 'Action' %}
    +
    +
    +
    - - - - - - - - - - - - - - - - -
    - - {% trans 'Name' %}{% trans 'User' %}{% trans 'User group' %}{% trans 'Asset' %}{% trans 'Asset group' %}{% trans 'System user' %}{% trans 'Is valid' %}{% trans 'Action' %}
    {% endblock %} {% block custom_foot_js %} + {% endblock %} diff --git a/apps/perms/templates/perms/asset_permission_user.html b/apps/perms/templates/perms/asset_permission_user.html index 2e64368aa..bee5b0e35 100644 --- a/apps/perms/templates/perms/asset_permission_user.html +++ b/apps/perms/templates/perms/asset_permission_user.html @@ -150,7 +150,7 @@ {% block custom_foot_js %} \ No newline at end of file diff --git a/apps/templates/_nav.html b/apps/templates/_nav.html index 39b14add8..fad4c6090 100644 --- a/apps/templates/_nav.html +++ b/apps/templates/_nav.html @@ -1,60 +1,63 @@ {% load i18n %}
  • - {% trans 'Dashboard' %} + {% trans 'Dashboard' %}
  • - {% trans 'Users' %} + {% trans 'Users' %}
  • - {% trans 'Assets' %} + {% trans 'Assets' %}
  • - {% trans 'Perms' %} + {% trans 'Perms' %}
  • -
  • +
  • - {% trans 'Job Center' %} + {% trans 'Sessions' %}
  • -
  • +
  • - {% trans 'Terminal' %} + {% trans 'Job Center' %}
  • - - {#
  • #} {# #} {# {% trans 'File' %}#} diff --git a/apps/templates/_nav_user.html b/apps/templates/_nav_user.html index f82ad6776..30e397ebd 100644 --- a/apps/templates/_nav_user.html +++ b/apps/templates/_nav_user.html @@ -1,16 +1,16 @@ {% load i18n %}
  • - {% trans 'My assets' %} + {% trans 'My assets' %}
  • - {% trans 'Profile' %} + {% trans 'Profile' %}
  • - + {% trans 'Web terminal' %}
  • \ No newline at end of file diff --git a/apps/templates/_user_profile.html b/apps/templates/_user_profile.html index 06f793c1a..d7525958b 100644 --- a/apps/templates/_user_profile.html +++ b/apps/templates/_user_profile.html @@ -2,11 +2,8 @@ {% load i18n %}

    {{ users_count }}

    - All user + All users
    @@ -23,7 +23,7 @@

    {{ assets_count }}

    - All host + All hosts
    @@ -36,7 +36,7 @@

    {{ online_user_count }}

    - Online user + Online users
    @@ -57,7 +57,7 @@

    活跃用户TOP5

    - 过去一周共有{{ user_visit_count_weekly }}位用户登录{{ asset_visit_count_weekly }}次服务器. + 过去一周共有{{ user_visit_count_weekly }}位用户登录{{ asset_visit_count_weekly }}次资产.
      {% for data in user_visit_count_top_five %}
    • @@ -164,7 +164,7 @@ {% for login in last_login_ten %}
      - image + image
      {% ifequal login.is_finished 0 %} diff --git a/apps/terminal/api.py b/apps/terminal/api.py index 87311d5b2..88f8fc926 100644 --- a/apps/terminal/api.py +++ b/apps/terminal/api.py @@ -1,19 +1,19 @@ # -*- coding: utf-8 -*- # from collections import OrderedDict -import copy import logging import os import uuid -from rest_framework import viewsets, serializers -from rest_framework.views import APIView, Response -from rest_framework.permissions import AllowAny from django.core.cache import cache from django.shortcuts import get_object_or_404, redirect from django.utils import timezone from django.core.files.storage import default_storage from django.http import HttpResponseNotFound +from rest_framework import viewsets, serializers +from rest_framework.views import APIView, Response +from rest_framework.permissions import AllowAny +from rest_framework_bulk import BulkModelViewSet from common.utils import get_object_or_none from .models import Terminal, Status, Session, Task @@ -21,7 +21,9 @@ from .serializers import TerminalSerializer, StatusSerializer, \ SessionSerializer, TaskSerializer, ReplaySerializer from .hands import IsSuperUserOrAppUser, IsAppUser, \ IsSuperUserOrAppUserOrUserReadonly -from .backends import get_command_store, SessionCommandSerializer +from .backends import get_command_store, get_multi_command_store, \ + SessionCommandSerializer +import boto3 # AWS S3 sdk logger = logging.getLogger(__file__) @@ -56,6 +58,7 @@ class TerminalViewSet(viewsets.ModelViewSet): return Response(data, status=201) else: data = serializer.errors + logger.error("Register terminal error: {}".format(data)) return Response(data, status=400) def get_permissions(self): @@ -173,16 +176,33 @@ class SessionViewSet(viewsets.ModelViewSet): terminal_id = self.kwargs.get("terminal", None) if terminal_id: terminal = get_object_or_404(Terminal, id=terminal_id) - self.queryset = terminal.status_set.all() + self.queryset = terminal.session_set.all() return self.queryset -class TaskViewSet(viewsets.ModelViewSet): +class TaskViewSet(BulkModelViewSet): queryset = Task.objects.all() serializer_class = TaskSerializer permission_classes = (IsSuperUserOrAppUser,) +class KillSessionAPI(APIView): + permission_classes = (IsSuperUserOrAppUser,) + model = Task + + def post(self, request, *args, **kwargs): + validated_session = [] + for session_id in request.data: + session = get_object_or_none(Session, id=session_id) + if session and not session.is_finished: + validated_session.append(session_id) + self.model.objects.create( + name="kill_session", args=session.id, + terminal=session.terminal, + ) + return Response({"ok": validated_session}) + + class CommandViewSet(viewsets.ViewSet): """接受app发送来的command log, 格式如下 { @@ -197,6 +217,7 @@ class CommandViewSet(viewsets.ViewSet): """ command_store = get_command_store() + multi_command_storage = get_multi_command_store() serializer_class = SessionCommandSerializer permission_classes = (IsSuperUserOrAppUser,) @@ -217,7 +238,7 @@ class CommandViewSet(viewsets.ViewSet): return Response({"msg": msg}, status=401) def list(self, request, *args, **kwargs): - queryset = list(self.command_store.all()) + queryset = self.multi_command_storage.filter() serializer = self.serializer_class(queryset, many=True) return Response(serializer.data) @@ -229,7 +250,7 @@ class SessionReplayViewSet(viewsets.ViewSet): def gen_session_path(self): date = self.session.date_start.strftime('%Y-%m-%d') - return os.path.join(date, str(self.session.id)+'.gz') + return os.path.join(date, str(self.session.id) + '.gz') def create(self, request, *args, **kwargs): session_id = kwargs.get('pk') @@ -259,4 +280,39 @@ class SessionReplayViewSet(viewsets.ViewSet): url = default_storage.url(path) return redirect(url) else: + config = self.app.config.get("REPLAY_STORAGE", None) + if config: + for name in config.keys(): + if config[name].get("TYPE", '') == "s3": + client, bucket = self.s3Client(config[name]) + try: + client.head_object(Bucket=bucket, Key=path) + client.download_file(bucket, path, default_storage.base_location + '/' + path) + return redirect(default_storage.url(path)) + except: + pass return HttpResponseNotFound() + + def s3Client(self, config): + bucket = config.get("BUCKET", "jumpserver") + REGION = config.get("REGION", None) + ACCESS_KEY = config.get("ACCESS_KEY", None) + SECRET_KEY = config.get("SECRET_KEY", None) + if self.ACCESS_KEY and REGION and SECRET_KEY: + s3 = boto3.client('s3', + region_name=REGION, + aws_access_key_id=ACCESS_KEY, + aws_secret_access_key=SECRET_KEY) + else: + s3 = boto3.client('s3') + return s3, bucket + + +class TerminalConfig(APIView): + permission_classes = (IsAppUser,) + + def get(self, request): + user = request.user + terminal = user.terminal + configs = terminal.config + return Response(configs, status=200) diff --git a/apps/terminal/backends/__init__.py b/apps/terminal/backends/__init__.py index 6baaed3c4..33f63f4de 100644 --- a/apps/terminal/backends/__init__.py +++ b/apps/terminal/backends/__init__.py @@ -2,9 +2,39 @@ from importlib import import_module from django.conf import settings from .command.serializers import SessionCommandSerializer +TYPE_ENGINE_MAPPING = { + 'elasticsearch': 'terminal.backends.command.es', +} + def get_command_store(): - command_engine = import_module(settings.COMMAND_STORAGE_BACKEND) - command_store = command_engine.CommandStore() - return command_store + params = settings.COMMAND_STORAGE + engine_class = import_module(params['ENGINE']) + storage = engine_class.CommandStore(params) + return storage + + +def get_terminal_command_store(): + storage_list = {} + for name, params in settings.TERMINAL_COMMAND_STORAGE.items(): + tp = params['TYPE'] + if tp == 'server': + storage = get_command_store() + else: + if not TYPE_ENGINE_MAPPING.get(tp): + raise AssertionError("Command storage type should in {}".format( + ', '.join(TYPE_ENGINE_MAPPING.keys())) + ) + engine_class = import_module(TYPE_ENGINE_MAPPING[tp]) + storage = engine_class.CommandStore(params) + storage_list[name] = storage + return storage_list + + +def get_multi_command_store(): + from .command.multi import CommandStore + storage_list = get_terminal_command_store().values() + storage = CommandStore(storage_list) + return storage + diff --git a/apps/terminal/backends/command/base.py b/apps/terminal/backends/command/base.py index 0aa689738..585930a5d 100644 --- a/apps/terminal/backends/command/base.py +++ b/apps/terminal/backends/command/base.py @@ -19,3 +19,9 @@ class CommandBase(object): input=None, session=None): pass + @abc.abstractmethod + def count(self, date_from=None, date_to=None, + user=None, asset=None, system_user=None, + input=None, session=None): + pass + diff --git a/apps/terminal/backends/command/db.py b/apps/terminal/backends/command/db.py index 27ee19a14..18652a26f 100644 --- a/apps/terminal/backends/command/db.py +++ b/apps/terminal/backends/command/db.py @@ -8,7 +8,7 @@ from .base import CommandBase class CommandStore(CommandBase): - def __init__(self): + def __init__(self, params): from terminal.models import Command self.model = Command @@ -37,9 +37,11 @@ class CommandStore(CommandBase): )) return self.model.objects.bulk_create(_commands) - def filter(self, date_from=None, date_to=None, - user=None, asset=None, system_user=None, - input=None, session=None): + @staticmethod + def make_filter_kwargs( + date_from=None, date_to=None, + user=None, asset=None, system_user=None, + input=None, session=None): filter_kwargs = {} date_from_default = timezone.now() - datetime.timedelta(days=7) date_to_default = timezone.now() @@ -60,10 +62,28 @@ class CommandStore(CommandBase): if session: filter_kwargs['session'] = session + return filter_kwargs + + def filter(self, date_from=None, date_to=None, + user=None, asset=None, system_user=None, + input=None, session=None): + filter_kwargs = self.make_filter_kwargs( + date_from=date_from, date_to=date_to, user=user, + asset=asset, system_user=system_user, input=input, + session=session, + ) queryset = self.model.objects.filter(**filter_kwargs) - return queryset + return [command.to_dict() for command in queryset] + + def count(self, date_from=None, date_to=None, + user=None, asset=None, system_user=None, + input=None, session=None): + filter_kwargs = self.make_filter_kwargs( + date_from=date_from, date_to=date_to, user=user, + asset=asset, system_user=system_user, input=input, + session=session, + ) + count = self.model.objects.filter(**filter_kwargs).count() + return count - def all(self): - """返回所有数据""" - return self.model.objects.iterator() diff --git a/apps/terminal/backends/command/es.py b/apps/terminal/backends/command/es.py new file mode 100644 index 000000000..2e0995ad9 --- /dev/null +++ b/apps/terminal/backends/command/es.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# + +from jms_es_sdk import ESStore +from .base import CommandBase + + +class CommandStore(CommandBase, ESStore): + def __init__(self, params): + hosts = params.get('HOSTS', ['http://localhost']) + ESStore.__init__(self, hosts=hosts) + + def save(self, command): + return ESStore.save(self, command) + + def bulk_save(self, commands): + return ESStore.bulk_save(self, commands) + + def filter(self, date_from=None, date_to=None, + user=None, asset=None, system_user=None, + input=None, session=None): + + data = ESStore.filter( + self, date_from=date_from, date_to=date_to, + user=user, asset=asset, system_user=system_user, + input=input, session=session + ) + return [item["_source"] for item in data["hits"] if item] + + def count(self, date_from=None, date_to=None, + user=None, asset=None, system_user=None, + input=None, session=None): + amount = ESStore.count( + self, date_from=date_from, date_to=date_to, + user=user, asset=asset, system_user=system_user, + input=input, session=session + ) + return amount diff --git a/apps/terminal/backends/command/models.py b/apps/terminal/backends/command/models.py index 2d1387427..186769b48 100644 --- a/apps/terminal/backends/command/models.py +++ b/apps/terminal/backends/command/models.py @@ -18,5 +18,26 @@ class AbstractSessionCommand(models.Model): class Meta: abstract = True + @classmethod + def from_dict(cls, d): + self = cls() + for k, v in d.items(): + setattr(self, k, v) + return self + + @classmethod + def from_multi_dict(cls, l): + commands = [] + for d in l: + command = cls.from_dict(d) + commands.append(command) + return commands + + def to_dict(self): + d = {} + for field in self._meta.fields: + d[field.name] = getattr(self, field.name) + return d + def __str__(self): return self.input diff --git a/apps/terminal/backends/command/multi.py b/apps/terminal/backends/command/multi.py new file mode 100644 index 000000000..1bd1623f8 --- /dev/null +++ b/apps/terminal/backends/command/multi.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# + +from .base import CommandBase + + +class CommandStore(CommandBase): + def __init__(self, storage_list): + self.storage_list = storage_list + + def filter(self, **kwargs): + queryset = [] + for storage in self.storage_list: + queryset.extend(storage.filter(**kwargs)) + return sorted(queryset, key=lambda command: command["timestamp"], reverse=True) + + def count(self, **kwargs): + amount = 0 + for storage in self.storage_list: + amount += storage.count(**kwargs) + return amount + + def save(self, command): + pass + + def bulk_save(self, commands): + pass diff --git a/apps/terminal/forms.py b/apps/terminal/forms.py index 5b6278c02..ab4520b03 100644 --- a/apps/terminal/forms.py +++ b/apps/terminal/forms.py @@ -2,19 +2,45 @@ # from django import forms +from django.conf import settings from django.utils.translation import ugettext_lazy as _ from .models import Terminal +def get_all_command_storage(): + # storage_choices = [] + from common.models import Setting + Setting.refresh_all_settings() + for k, v in settings.TERMINAL_COMMAND_STORAGE.items(): + yield (k, k) + + +def get_all_replay_storage(): + # storage_choices = [] + from common.models import Setting + Setting.refresh_all_settings() + for k, v in settings.TERMINAL_REPLAY_STORAGE.items(): + yield (k, k) + + class TerminalForm(forms.ModelForm): + command_storage = forms.ChoiceField( + choices=get_all_command_storage(), + label=_("Command storage") + ) + replay_storage = forms.ChoiceField( + choices=get_all_replay_storage(), + label=_("Replay storage") + ) + class Meta: model = Terminal - fields = ['name', 'remote_addr', 'ssh_port', 'http_port', 'comment'] + fields = [ + 'name', 'remote_addr', 'ssh_port', 'http_port', 'comment', + 'command_storage', 'replay_storage', + ] help_texts = { 'ssh_port': _("Coco ssh listen port"), 'http_port': _("Coco http/ws listen port"), } - widgets = { - 'name': forms.TextInput(attrs={'readonly': 'readonly'}) - } diff --git a/apps/terminal/models.py b/apps/terminal/models.py index f545baa5f..91ac8e052 100644 --- a/apps/terminal/models.py +++ b/apps/terminal/models.py @@ -4,6 +4,8 @@ import uuid from django.db import models from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone +from django.conf import settings from users.models import User from .backends.command.models import AbstractSessionCommand @@ -15,6 +17,8 @@ class Terminal(models.Model): remote_addr = models.CharField(max_length=128, verbose_name=_('Remote Address')) ssh_port = models.IntegerField(verbose_name=_('SSH Port'), default=2222) http_port = models.IntegerField(verbose_name=_('HTTP Port'), default=5000) + command_storage = models.CharField(max_length=128, verbose_name=_("Command storage"), default='default') + replay_storage = models.CharField(max_length=128, verbose_name=_("Replay storage"), default='default') user = models.OneToOneField(User, related_name='terminal', verbose_name='Application User', null=True, on_delete=models.CASCADE) is_accepted = models.BooleanField(default=False, verbose_name='Is Accepted') is_deleted = models.BooleanField(default=False) @@ -33,6 +37,32 @@ class Terminal(models.Model): self.user.is_active = active self.user.save() + def get_common_storage(self): + storage_all = settings.TERMINAL_COMMAND_STORAGE + if self.command_storage in storage_all: + storage = storage_all.get(self.command_storage) + else: + storage = storage_all.get('default') + return {"TERMINAL_COMMAND_STORAGE": storage} + + def get_replay_storage(self): + storage_all = settings.TERMINAL_REPLAY_STORAGE + if self.replay_storage in storage_all: + storage = storage_all.get(self.replay_storage) + else: + storage = storage_all.get('default') + return {"TERMINAL_REPLAY_STORAGE": storage} + + @property + def config(self): + configs = {} + for k in dir(settings): + if k.startswith('TERMINAL'): + configs[k] = getattr(settings, k) + configs.update(self.get_common_storage()) + configs.update(self.get_replay_storage()) + return configs + def create_app_user(self): random = uuid.uuid4().hex[:6] user, access_key = User.create_app_user(name="{}-{}".format(self.name, random), comment=self.comment) @@ -98,7 +128,8 @@ class Session(models.Model): has_replay = models.BooleanField(default=False, verbose_name=_("Replay")) has_command = models.BooleanField(default=False, verbose_name=_("Command")) terminal = models.ForeignKey(Terminal, null=True, on_delete=models.CASCADE) - date_start = models.DateTimeField(verbose_name=_("Date start")) + date_last_active = models.DateTimeField(verbose_name=_("Date last active"), default=timezone.now) + date_start = models.DateTimeField(verbose_name=_("Date start"), db_index=True) date_end = models.DateTimeField(verbose_name=_("Date end"), null=True) class Meta: diff --git a/apps/terminal/serializers.py b/apps/terminal/serializers.py index 52b4d2b3c..8c40315a7 100644 --- a/apps/terminal/serializers.py +++ b/apps/terminal/serializers.py @@ -3,9 +3,13 @@ from django.utils import timezone from rest_framework import serializers +from rest_framework_bulk.serializers import BulkListSerializer + +from common.mixins import BulkSerializerMixin +from common.utils import get_object_or_none from .models import Terminal, Status, Session, Task -from .backends import get_command_store +from .backends import get_multi_command_store class TerminalSerializer(serializers.ModelSerializer): @@ -43,14 +47,15 @@ class TerminalSerializer(serializers.ModelSerializer): class SessionSerializer(serializers.ModelSerializer): command_amount = serializers.SerializerMethodField() - command_store = get_command_store() + command_store = get_multi_command_store() class Meta: model = Session + list_serializer_class = BulkListSerializer fields = '__all__' def get_command_amount(self, obj): - return len(self.command_store.filter(session=obj.session)) + return self.command_store.count(session=str(obj.id)) class StatusSerializer(serializers.ModelSerializer): @@ -60,11 +65,12 @@ class StatusSerializer(serializers.ModelSerializer): model = Status -class TaskSerializer(serializers.ModelSerializer): +class TaskSerializer(BulkSerializerMixin, serializers.ModelSerializer): class Meta: fields = '__all__' model = Task + list_serializer_class = BulkListSerializer class ReplaySerializer(serializers.Serializer): diff --git a/apps/terminal/tasks.py b/apps/terminal/tasks.py index ed404a54b..89f9cc5cc 100644 --- a/apps/terminal/tasks.py +++ b/apps/terminal/tasks.py @@ -1,20 +1,36 @@ # -*- coding: utf-8 -*- # +import datetime + from celery import shared_task +from django.utils import timezone + +from common.celery import register_as_period_task, after_app_ready_start, \ + after_app_shutdown_clean +from .models import Status, Session CACHE_REFRESH_INTERVAL = 10 RUNNING = False -# Todo: 定期清理上报history @shared_task -def clean_terminal_history(): - pass - - - - +@register_as_period_task(interval=3600) +@after_app_ready_start +@after_app_shutdown_clean +def delete_terminal_status_period(): + yesterday = timezone.now() - datetime.timedelta(days=3) + Status.objects.filter(date_created__lt=yesterday).delete() +@shared_task +@register_as_period_task(interval=3600) +@after_app_ready_start +@after_app_shutdown_clean +def clean_orphan_session(): + active_sessions = Session.objects.filter(is_finished=False) + for session in active_sessions: + if not session.terminal.is_active: + session.is_finished = True + session.save() diff --git a/apps/terminal/templates/terminal/session_list.html b/apps/terminal/templates/terminal/session_list.html index 3bf2600c8..c7f29f7b1 100644 --- a/apps/terminal/templates/terminal/session_list.html +++ b/apps/terminal/templates/terminal/session_list.html @@ -75,6 +75,7 @@ {% trans 'Terminal' %} {% trans 'Command' %} {% trans 'Date start' %} +{# {% trans 'Date last active' %}#} {% trans 'Duration' %} {% trans 'Action' %} {% endblock %} @@ -94,6 +95,7 @@ {{ session.id | get_session_command_amount }} {{ session.date_start }} +{# {{ session.date_last_active }}#} {{ session.date_start|time_util_with_seconds:session.date_end }} {% if session.is_finished %} @@ -107,6 +109,21 @@ {% endfor %} {% endblock %} +{% block content_bottom_left %} + +{% endblock %} + {% block custom_foot_js %} {% endblock %} diff --git a/apps/terminal/templates/terminal/terminal_list.html b/apps/terminal/templates/terminal/terminal_list.html index f040f92ee..a56843852 100644 --- a/apps/terminal/templates/terminal/terminal_list.html +++ b/apps/terminal/templates/terminal/terminal_list.html @@ -30,7 +30,7 @@ {% trans 'Addr' %} {% trans 'SSH port' %} {% trans 'Http port' %} - {% trans 'Sessions' %} + {% trans 'Session' %} {% trans 'Active' %} {% trans 'Alive' %} {% trans 'Action' %} diff --git a/apps/terminal/templates/terminal/terminal_modal_accept.html b/apps/terminal/templates/terminal/terminal_modal_accept.html index b4a18b17c..e846a63e1 100644 --- a/apps/terminal/templates/terminal/terminal_modal_accept.html +++ b/apps/terminal/templates/terminal/terminal_modal_accept.html @@ -12,6 +12,8 @@ {% bootstrap_field form.remote_addr layout="horizontal" %} {% bootstrap_field form.ssh_port layout="horizontal" %} {% bootstrap_field form.http_port layout="horizontal" %} + {% bootstrap_field form.command_storage layout="horizontal" %} + {% bootstrap_field form.replay_storage layout="horizontal" %} {% bootstrap_field form.comment layout="horizontal" %} diff --git a/apps/terminal/templates/terminal/terminal_update.html b/apps/terminal/templates/terminal/terminal_update.html index 9ebd9d2d8..a40e3c362 100644 --- a/apps/terminal/templates/terminal/terminal_update.html +++ b/apps/terminal/templates/terminal/terminal_update.html @@ -35,6 +35,8 @@ {% bootstrap_field form.remote_addr layout="horizontal" %} {% bootstrap_field form.ssh_port layout="horizontal" %} {% bootstrap_field form.http_port layout="horizontal" %} + {% bootstrap_field form.command_storage layout="horizontal" %} + {% bootstrap_field form.replay_storage layout="horizontal" %}

      {% trans 'Other' %}

      diff --git a/apps/terminal/templatetags/terminal_tags.py b/apps/terminal/templatetags/terminal_tags.py index 22b517880..cd7120fec 100644 --- a/apps/terminal/templatetags/terminal_tags.py +++ b/apps/terminal/templatetags/terminal_tags.py @@ -1,13 +1,12 @@ # ~*~ coding: utf-8 ~*~ from django import template -from ..backends import get_command_store +from ..backends import get_multi_command_store register = template.Library() -command_store = get_command_store() +command_store = get_multi_command_store() @register.filter def get_session_command_amount(session_id): - return len(command_store.filter(session=str(session_id))) - + return command_store.count(session=session_id) diff --git a/apps/terminal/urls/api_urls.py b/apps/terminal/urls/api_urls.py index 55d77ee00..0d9da427d 100644 --- a/apps/terminal/urls/api_urls.py +++ b/apps/terminal/urls/api_urls.py @@ -15,12 +15,16 @@ router.register(r'v1/terminal/(?P[a-zA-Z0-9\-]{36})?/?sessions', api.S router.register(r'v1/tasks', api.TaskViewSet, 'tasks') router.register(r'v1/terminal', api.TerminalViewSet, 'terminal') router.register(r'v1/command', api.CommandViewSet, 'command') +router.register(r'v1/sessions', api.SessionViewSet, 'session') +router.register(r'v1/status', api.StatusViewSet, 'session') urlpatterns = [ url(r'^v1/sessions/(?P[0-9a-zA-Z\-]{36})/replay/$', api.SessionReplayViewSet.as_view({'get': 'retrieve', 'post': 'create'}), name='session-replay'), - url(r'^v1/terminal/(?P[a-zA-Z0-9\-]{36})/access-key', api.TerminalTokenApi.as_view(), name='terminal-access-key') + url(r'^v1/tasks/kill-session/', api.KillSessionAPI.as_view(), name='kill-session'), + url(r'^v1/terminal/(?P[a-zA-Z0-9\-]{36})/access-key', api.TerminalTokenApi.as_view(), name='terminal-access-key'), + url(r'^v1/terminal/config', api.TerminalConfig.as_view(), name='terminal-config'), ] urlpatterns += router.urls diff --git a/apps/terminal/views/command.py b/apps/terminal/views/command.py index 8b0479d3e..0af0b5bfd 100644 --- a/apps/terminal/views/command.py +++ b/apps/terminal/views/command.py @@ -6,16 +6,16 @@ from django.conf import settings from django.utils import timezone from django.utils.translation import ugettext as _ -from common.mixins import DatetimeSearchMixin +from common.mixins import DatetimeSearchMixin, AdminUserRequiredMixin from ..models import Command from .. import utils -from ..backends import get_command_store +from ..backends import get_multi_command_store __all__ = ['CommandListView'] -command_store = get_command_store() +common_storage = get_multi_command_store() -class CommandListView(DatetimeSearchMixin, ListView): +class CommandListView(DatetimeSearchMixin, AdminUserRequiredMixin, ListView): model = Command template_name = "terminal/command_list.html" context_object_name = 'command_list' @@ -39,7 +39,7 @@ class CommandListView(DatetimeSearchMixin, ListView): filter_kwargs['system_user'] = self.system_user if self.command: filter_kwargs['input'] = self.command - queryset = command_store.filter(**filter_kwargs) + queryset = common_storage.filter(**filter_kwargs) return queryset def get_context_data(self, **kwargs): diff --git a/apps/terminal/views/session.py b/apps/terminal/views/session.py index 22d04fbaf..3b66baff7 100644 --- a/apps/terminal/views/session.py +++ b/apps/terminal/views/session.py @@ -10,7 +10,7 @@ from django.conf import settings from users.utils import AdminUserRequiredMixin from common.mixins import DatetimeSearchMixin from ..models import Session, Command, Terminal -from ..backends import get_command_store +from ..backends import get_multi_command_store from .. import utils @@ -19,7 +19,7 @@ __all__ = [ 'SessionDetailView', ] -command_store = get_command_store() +command_store = get_multi_command_store() class SessionListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): @@ -74,6 +74,7 @@ class SessionOnlineListView(SessionListView): context = { 'app': _('Terminal'), 'action': _('Session online list'), + 'type': 'online', 'now': timezone.now(), } kwargs.update(context) @@ -97,7 +98,7 @@ class SessionOfflineListView(SessionListView): return super().get_context_data(**kwargs) -class SessionDetailView(SingleObjectMixin, ListView): +class SessionDetailView(SingleObjectMixin, AdminUserRequiredMixin, ListView): template_name = 'terminal/session_detail.html' model = Session object = None diff --git a/apps/users/api.py b/apps/users/api.py index 2dfd89fba..b485a593e 100644 --- a/apps/users/api.py +++ b/apps/users/api.py @@ -1,33 +1,48 @@ # ~*~ coding: utf-8 ~*~ +import uuid + +from django.core.cache import cache from rest_framework import generics -from rest_framework.permissions import AllowAny +from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from rest_framework_bulk import BulkModelViewSet from .serializers import UserSerializer, UserGroupSerializer, \ UserGroupUpdateMemeberSerializer, UserPKUpdateSerializer, \ - UserUpdateGroupSerializer + UserUpdateGroupSerializer, ChangeUserPasswordSerializer from .tasks import write_login_log_async from .models import User, UserGroup -from .permissions import IsSuperUser, IsValidUser, IsCurrentUserOrReadOnly +from .permissions import IsSuperUser, IsValidUser, IsCurrentUserOrReadOnly, \ + IsSuperUserOrAppUser from .utils import check_user_valid, generate_token -from common.mixins import CustomFilterMixin +from common.mixins import IDInFilterMixin from common.utils import get_logger logger = get_logger(__name__) -class UserViewSet(CustomFilterMixin, BulkModelViewSet): +class UserViewSet(IDInFilterMixin, BulkModelViewSet): queryset = User.objects.exclude(role="App") # queryset = User.objects.all().exclude(role="App").order_by("date_joined") serializer_class = UserSerializer - permission_classes = (IsSuperUser,) + permission_classes = (IsSuperUser, IsAuthenticated) filter_fields = ('username', 'email', 'name', 'id') +class ChangeUserPasswordApi(generics.RetrieveUpdateAPIView): + permission_classes = (IsSuperUser,) + queryset = User.objects.all() + serializer_class = ChangeUserPasswordSerializer + + def perform_update(self, serializer): + user = self.get_object() + user.password_raw = serializer.validated_data["password"] + user.save() + + class UserUpdateGroupApi(generics.RetrieveUpdateAPIView): queryset = User.objects.all() serializer_class = UserUpdateGroupSerializer @@ -37,6 +52,7 @@ class UserUpdateGroupApi(generics.RetrieveUpdateAPIView): class UserResetPasswordApi(generics.UpdateAPIView): queryset = User.objects.all() serializer_class = UserSerializer + permission_classes = (IsAuthenticated,) def perform_update(self, serializer): # Note: we are not updating the user object here. @@ -72,7 +88,7 @@ class UserUpdatePKApi(generics.UpdateAPIView): user.save() -class UserGroupViewSet(CustomFilterMixin, BulkModelViewSet): +class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet): queryset = UserGroup.objects.all() serializer_class = UserGroupSerializer @@ -129,7 +145,8 @@ class UserAuthApi(APIView): if not login_ip: x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',') - if x_forwarded_for: + + if x_forwarded_for and x_forwarded_for[0]: login_ip = x_forwarded_for[0] else: login_ip = request.META.get("REMOTE_ADDR") @@ -148,3 +165,30 @@ class UserAuthApi(APIView): return Response({'token': token, 'user': user.to_json()}) else: return Response({'msg': msg}, status=401) + + +class UserConnectionTokenApi(APIView): + permission_classes = (IsSuperUserOrAppUser,) + + def post(self, request): + user_id = request.data.get('user', '') + asset_id = request.data.get('asset', '') + system_user_id = request.data.get('system_user', '') + token = str(uuid.uuid4()) + value = { + 'user': user_id, + 'asset': asset_id, + 'system_user': system_user_id + } + cache.set(token, value, timeout=3600) + return Response({"token": token}, status=201) + + def get(self, request): + token = request.query_params.get('token') + value = cache.get(token, None) + if value: + cache.delete(token) + return Response(value) + + + diff --git a/apps/users/apps.py b/apps/users/apps.py index af3dddf8e..f37fa6cf5 100644 --- a/apps/users/apps.py +++ b/apps/users/apps.py @@ -6,3 +6,6 @@ from django.apps import AppConfig class UsersConfig(AppConfig): name = 'users' + def ready(self): + from . import signals_handler + super().ready() diff --git a/apps/users/forms.py b/apps/users/forms.py index 018c39a18..b239f58ee 100644 --- a/apps/users/forms.py +++ b/apps/users/forms.py @@ -172,7 +172,6 @@ class UserBulkUpdateForm(forms.ModelForm): if self.data.get(field) is not None: changed_fields.append(field) - print(changed_fields) cleaned_data = {k: v for k, v in self.cleaned_data.items() if k in changed_fields} users = cleaned_data.pop('users', '') diff --git a/apps/users/hands.py b/apps/users/hands.py index bf47d775a..56435a5a4 100644 --- a/apps/users/hands.py +++ b/apps/users/hands.py @@ -6,7 +6,7 @@ Other module of this app shouldn't connect with other app. - :copyright: (c) 2014-2017 by Jumpserver Team. + :copyright: (c) 2014-2018 by Jumpserver Team. :license: GPL v2, see LICENSE for more details. """ diff --git a/apps/users/migrations/0001_initial.py b/apps/users/migrations/0001_initial.py index 521f2efe2..01edf24b6 100644 --- a/apps/users/migrations/0001_initial.py +++ b/apps/users/migrations/0001_initial.py @@ -28,6 +28,9 @@ def add_default_admin(apps, schema_editor): email="admin@mycomany.com", role="Admin", password=make_password("admin"), ) + group_model = apps.get_model("users", "UserGroup") + default_group = group_model.objects.using(db_alias).get(name="Default") + admin.groups.add(default_group) class Migration(migrations.Migration): diff --git a/apps/users/models/authentication.py b/apps/users/models/authentication.py index f023d3d59..5169a79d2 100644 --- a/apps/users/models/authentication.py +++ b/apps/users/models/authentication.py @@ -16,7 +16,8 @@ class AccessKey(models.Model): default=uuid.uuid4, editable=False) secret = models.UUIDField(verbose_name='AccessKeySecret', default=uuid.uuid4, editable=False) - user = models.ForeignKey(User, verbose_name='User', on_delete=models.CASCADE, related_name='access_key') + user = models.ForeignKey(User, verbose_name='User', + on_delete=models.CASCADE, related_name='access_key') def get_id(self): return str(self.id) diff --git a/apps/users/models/group.py b/apps/users/models/group.py index b4b7aacc1..128fbdbcb 100644 --- a/apps/users/models/group.py +++ b/apps/users/models/group.py @@ -15,13 +15,14 @@ class UserGroup(NoDeleteModelMixin): comment = models.TextField(blank=True, verbose_name=_('Comment')) date_created = models.DateTimeField(auto_now_add=True, null=True, verbose_name=_('Date created')) - created_by = models.CharField(max_length=100) + created_by = models.CharField(max_length=100, null=True, blank=True) def __str__(self): return self.name class Meta: ordering = ['name'] + verbose_name = _("User group") @classmethod def initial(cls): diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 4ae0e62c4..4c051db22 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -49,6 +49,9 @@ class User(AbstractUser): date_expired = models.DateTimeField(default=date_expired_default, blank=True, null=True, verbose_name=_('Date expired')) created_by = models.CharField(max_length=30, default='', verbose_name=_('Created by')) + def __str__(self): + return self.username + @property def password_raw(self): raise AttributeError('Password raw is not a readable attribute') @@ -148,6 +151,10 @@ class User(AbstractUser): def save(self, *args, **kwargs): if not self.name: self.name = self.username + if self.username == 'admin': + self.role = 'Admin' + self.is_active = True + super().save(*args, **kwargs) @property @@ -244,6 +251,7 @@ class User(AbstractUser): class Meta: ordering = ['username'] + verbose_name = _("User") #: Use this method initial user @classmethod diff --git a/apps/users/serializers.py b/apps/users/serializers.py index f086da12b..e72f97407 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -52,7 +52,7 @@ class UserUpdateGroupSerializer(serializers.ModelSerializer): class UserGroupSerializer(BulkSerializerMixin, serializers.ModelSerializer): - user_amount = serializers.SerializerMethodField() + users = serializers.SerializerMethodField() class Meta: model = UserGroup @@ -60,8 +60,8 @@ class UserGroupSerializer(BulkSerializerMixin, serializers.ModelSerializer): fields = '__all__' @staticmethod - def get_user_amount(obj): - return obj.users.count() + def get_users(obj): + return [user.name for user in obj.users.all()] class UserGroupUpdateMemeberSerializer(serializers.ModelSerializer): @@ -71,3 +71,9 @@ class UserGroupUpdateMemeberSerializer(serializers.ModelSerializer): model = UserGroup fields = ['id', 'users'] + +class ChangeUserPasswordSerializer(serializers.ModelSerializer): + + class Meta: + model = User + fields = ['password'] diff --git a/apps/users/signals.py b/apps/users/signals.py index 39e57f5a5..324cd1627 100644 --- a/apps/users/signals.py +++ b/apps/users/signals.py @@ -1,20 +1,5 @@ -# -*- coding: utf-8 -*- -# +from django.dispatch import Signal -from django.dispatch import Signal, receiver -from django.db.models.signals import post_save -from common.utils import get_logger -from .models import User - -logger = get_logger(__file__) - - -@receiver(post_save, sender=User) -def on_user_created(sender, instance=None, created=False, **kwargs): - if created: - logger.debug("Receive user `{}` create signal".format(instance.name)) - from .utils import send_user_created_mail - logger.info(" - Sending welcome mail ...".format(instance.name)) - send_user_created_mail(instance) +post_user_create = Signal(providing_args=('user',)) diff --git a/apps/users/signals_handler.py b/apps/users/signals_handler.py new file mode 100644 index 000000000..9e1abf350 --- /dev/null +++ b/apps/users/signals_handler.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# + +from django.dispatch import receiver +from django.db.models.signals import post_save + +from common.utils import get_logger +from .models import User + +logger = get_logger(__file__) + + +@receiver(post_save, sender=User) +def on_user_created(sender, instance=None, created=False, **kwargs): + if created: + logger.debug("Receive user `{}` create signal".format(instance.name)) + from .utils import send_user_created_mail + logger.info(" - Sending welcome mail ...".format(instance.name)) + if instance.email: + send_user_created_mail(instance) \ No newline at end of file diff --git a/apps/users/templates/users/_user.html b/apps/users/templates/users/_user.html index b33abce51..fe76954f0 100644 --- a/apps/users/templates/users/_user.html +++ b/apps/users/templates/users/_user.html @@ -11,8 +11,8 @@
      {% csrf_token %}

      {% trans 'Account' %}

      - {% bootstrap_field form.username layout="horizontal" %} {% bootstrap_field form.name layout="horizontal" %} + {% bootstrap_field form.username layout="horizontal" %} {% bootstrap_field form.email layout="horizontal" %} {% bootstrap_field form.groups layout="horizontal" %} @@ -32,12 +32,6 @@ {{ form.date_expired.errors }}
      -{#
      #} -{# #} -{#
      #} -{# {{ form.enable_otp }}#} -{#
      #} -{#
      #}

      {% trans 'Profile' %}

      {% bootstrap_field form.phone layout="horizontal" %} diff --git a/apps/users/templates/users/forgot_password.html b/apps/users/templates/users/forgot_password.html index 2788ba580..0ceed556e 100644 --- a/apps/users/templates/users/forgot_password.html +++ b/apps/users/templates/users/forgot_password.html @@ -51,11 +51,8 @@

    -
    - Copyright Jumpserver.org -
    -
    - © 2014-2017 +
    + {% include '_copyright.html' %}
    diff --git a/apps/users/templates/users/login.html b/apps/users/templates/users/login.html index 284b6bc43..13276f6fe 100644 --- a/apps/users/templates/users/login.html +++ b/apps/users/templates/users/login.html @@ -22,24 +22,27 @@
    -

    欢迎使用Jumpserver开源跳板机

    +

    欢迎使用Jumpserver开源堡垒机

    - Jumpserver是一款使用Python, Django开发的开源跳板机系统, 助力互联网企业高效 用户、资产、权限、审计 管理 + 全球首款完全开源的堡垒机,使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计系统。

    - 我们自五湖四海,我们对开源精神无比敬仰和崇拜,我们对完美、整洁、优雅 无止境的追求 + 使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。

    - 专注自动化运维,努力打造 易用、稳定、安全、自动化 的跳板机, 这是我们的不懈的追求和动力 + 采纳分布式架构,支持多机房跨区域部署,中心节点提供 API,各机房部署登录节点,可横向扩展、无并发访问限制。

    - 永远年轻,永远热泪盈眶 stay foolish stay hungry + 改变世界,从一点点开始。

    -
    {% trans 'Login' %}
    +
    + + {% trans 'Login' %} +
    {% csrf_token %} {% if form.errors %} @@ -60,12 +63,16 @@
    + {% if demo_mode %} +

    + Demo账号: admin 密码: admin +

    + {% endif %} + {% trans 'Forgot password' %}? -

    -

    @@ -74,11 +81,8 @@

    -
    - Copyright Jumpserver.org -
    -
    - © 2014-2017 +
    + {% include '_copyright.html' %}
    diff --git a/apps/users/templates/users/reset_password.html b/apps/users/templates/users/reset_password.html index cf8003a86..0bab32809 100644 --- a/apps/users/templates/users/reset_password.html +++ b/apps/users/templates/users/reset_password.html @@ -70,11 +70,8 @@

    -
    - Copyright Jumpserver.org -
    -
    - © 2014-2017 +
    + {% include '_copyright.html' %}
    diff --git a/apps/users/templates/users/user_detail.html b/apps/users/templates/users/user_detail.html index 78c5ac542..573276bcb 100644 --- a/apps/users/templates/users/user_detail.html +++ b/apps/users/templates/users/user_detail.html @@ -24,8 +24,9 @@
  • {% trans 'Update' %}
  • +
  • - + {% trans 'Delete' %}
  • @@ -128,7 +129,7 @@
    - +