From c7ac93fcc1eb9b94ed1cdbd78104ba5439665337 Mon Sep 17 00:00:00 2001 From: ibuler Date: Mon, 17 Dec 2018 10:21:16 +0800 Subject: [PATCH 01/13] =?UTF-8?q?[Bugfix]=20=E4=BF=AE=E5=A4=8D=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jms | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jms b/jms index 1bb6bb56c..613de2fe3 100755 --- a/jms +++ b/jms @@ -26,8 +26,8 @@ LOG_DIR = os.path.join(BASE_DIR, 'logs') TMP_DIR = os.path.join(BASE_DIR, 'tmp') HTTP_HOST = CONFIG.HTTP_BIND_HOST or '127.0.0.1' HTTP_PORT = CONFIG.HTTP_LISTEN_PORT or 8080 -DEBUG = CONFIG.DEBUG -LOG_LEVEL = CONFIG.LOG_LEVEL +DEBUG = CONFIG.DEBUG or False +LOG_LEVEL = CONFIG.LOG_LEVEL or 'INFO' START_TIMEOUT = 40 WORKERS = 4 From 374039d287fbbc297cd11de6dd4511679e76a9e0 Mon Sep 17 00:00:00 2001 From: BaiJiangJie <32935519+BaiJiangJie@users.noreply.github.com> Date: Mon, 17 Dec 2018 11:18:55 +0800 Subject: [PATCH 02/13] =?UTF-8?q?[Update]=20=E7=BB=88=E7=AB=AF=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=EF=BC=8C=E6=B7=BB=E5=8A=A0coco=E7=AB=AF=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E5=88=97=E8=A1=A8=E9=A1=B5=E9=9D=A2=E5=A4=A7=E5=B0=8F?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E9=A1=B9=20(#2182)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 终端设置,添加coco端资产列表页面大小配置项 * [Update] 添加页面大小选项 --- apps/common/forms.py | 11 + apps/jumpserver/conf.py | 7 +- apps/jumpserver/settings.py | 6 + apps/locale/zh/LC_MESSAGES/django.mo | Bin 60010 -> 60078 bytes apps/locale/zh/LC_MESSAGES/django.po | 349 ++++++++++++++++----------- 5 files changed, 227 insertions(+), 146 deletions(-) diff --git a/apps/common/forms.py b/apps/common/forms.py index ff599062f..d052819b6 100644 --- a/apps/common/forms.py +++ b/apps/common/forms.py @@ -134,6 +134,14 @@ class TerminalSettingForm(BaseForm): ('hostname', _('Hostname')), ('ip', _('IP')), ) + PAGE_SIZE_CHOICES = ( + ('all', _('All')), + ('auto', _('Auto')), + (10, 10), + (15, 15), + (25, 25), + (50, 50), + ) TERMINAL_PASSWORD_AUTH = forms.BooleanField( initial=True, required=False, label=_("Password auth") ) @@ -146,6 +154,9 @@ class TerminalSettingForm(BaseForm): TERMINAL_ASSET_LIST_SORT_BY = forms.ChoiceField( choices=SORT_BY_CHOICES, initial='hostname', label=_("List sort by") ) + TERMINAL_ASSET_LIST_PAGE_SIZE = forms.ChoiceField( + choices=PAGE_SIZE_CHOICES, initial='auto', label=_("List page size"), + ) class TerminalCommandStorage(BaseForm): diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 08716f93b..b1d33c3cd 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -312,7 +312,12 @@ defaults = { 'SESSION_COOKIE_AGE': 3600 * 24, 'SESSION_EXPIRE_AT_BROWSER_CLOSE': False, 'AUTH_OPENID': False, - 'EMAIL_SUFFIX': 'jumpserver.org' + 'EMAIL_SUFFIX': 'jumpserver.org', + 'TERMINAL_PASSWORD_AUTH': True, + 'TERMINAL_PUBLIC_KEY_AUTH': True, + 'TERMINAL_HEARTBEAT_INTERVAL': 5, + 'TERMINAL_ASSET_LIST_SORT_BY': 'hostname', + 'TERMINAL_ASSET_LIST_PAGE_SIZE': 'auto', } diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index f1d1f8e8f..fb595a4df 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -484,6 +484,12 @@ SECURITY_PASSWORD_RULES = [ 'SECURITY_PASSWORD_SPECIAL_CHAR' ] +TERMINAL_PASSWORD_AUTH = CONFIG.TERMINAL_PASSWORD_AUTH +TERMINAL_PUBLIC_KEY_AUTH = CONFIG.TERMINAL_PUBLIC_KEY_AUTH +TERMINAL_HEARTBEAT_INTERVAL = CONFIG.TERMINAL_HEARTBEAT_INTERVAL +TERMINAL_ASSET_LIST_SORT_BY = CONFIG.TERMINAL_ASSET_LIST_SORT_BY +TERMINAL_ASSET_LIST_PAGE_SIZE = CONFIG.TERMINAL_ASSET_LIST_PAGE_SIZE + # Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html BOOTSTRAP3 = { 'horizontal_label_class': 'col-md-2', diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 65dbbaee802a31e7fe8db5f3cbec742b30d05839..7c398c4ac4bccaaac775f7c0456b087d6bea835a 100644 GIT binary patch delta 17946 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 delta 17896 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 G!v6uJp0L;e diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 5de533806..f218e1995 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/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-12-07 18:11+0800\n" +"POT-Creation-Date: 2018-12-17 10:14+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: Jumpserver team\n" @@ -34,8 +34,8 @@ msgid "Test if the assets under the node are connectable: {}" msgstr "测试节点下资产是否可连接: {}" #: assets/forms/asset.py:27 assets/models/asset.py:83 assets/models/user.py:113 -#: assets/templates/assets/asset_detail.html:187 -#: assets/templates/assets/asset_detail.html:195 +#: assets/templates/assets/asset_detail.html:191 +#: assets/templates/assets/asset_detail.html:199 #: assets/templates/assets/system_user_asset.html:95 perms/models.py:32 msgid "Nodes" msgstr "节点管理" @@ -62,7 +62,8 @@ msgid "Label" msgstr "标签" #: assets/forms/asset.py:37 assets/forms/asset.py:76 assets/models/asset.py:79 -#: assets/models/domain.py:24 assets/models/domain.py:50 +#: assets/models/domain.py:26 assets/models/domain.py:52 +#: assets/templates/assets/asset_detail.html:81 #: assets/templates/assets/user_asset_list.html:157 #: xpack/plugins/orgs/templates/orgs/org_list.html:17 msgid "Domain" @@ -105,7 +106,7 @@ msgid "Select assets" msgstr "选择资产" #: assets/forms/asset.py:108 assets/models/asset.py:76 -#: assets/models/domain.py:48 assets/templates/assets/admin_user_assets.html:53 +#: assets/models/domain.py:50 assets/templates/assets/admin_user_assets.html:53 #: assets/templates/assets/asset_detail.html:69 #: assets/templates/assets/domain_gateway_list.html:58 #: assets/templates/assets/system_user_asset.html:52 @@ -137,13 +138,13 @@ msgstr "端口" msgid "Asset" msgstr "资产" -#: assets/forms/domain.py:42 +#: assets/forms/domain.py:46 msgid "Password should not contain special characters" msgstr "不能包含特殊字符" -#: assets/forms/domain.py:59 assets/forms/user.py:80 assets/forms/user.py:143 +#: assets/forms/domain.py:63 assets/forms/user.py:80 assets/forms/user.py:143 #: assets/models/base.py:22 assets/models/cluster.py:18 -#: assets/models/cmd_filter.py:20 assets/models/domain.py:18 +#: assets/models/cmd_filter.py:20 assets/models/domain.py:20 #: assets/models/group.py:20 assets/models/label.py:18 #: assets/templates/assets/admin_user_detail.html:56 #: assets/templates/assets/admin_user_list.html:26 @@ -183,7 +184,7 @@ msgstr "不能包含特殊字符" msgid "Name" msgstr "名称" -#: assets/forms/domain.py:60 assets/forms/user.py:81 assets/forms/user.py:144 +#: assets/forms/domain.py:64 assets/forms/user.py:81 assets/forms/user.py:144 #: assets/models/base.py:23 assets/templates/assets/admin_user_detail.html:60 #: assets/templates/assets/admin_user_list.html:27 #: assets/templates/assets/domain_gateway_list.html:60 @@ -256,7 +257,7 @@ msgid "" "password." msgstr "如果选择手动登录模式,用户名和密码可以不填写" -#: assets/models/asset.py:73 assets/models/domain.py:47 +#: assets/models/asset.py:73 assets/models/domain.py:49 #: assets/templates/assets/_asset_list_modal.html:46 #: assets/templates/assets/admin_user_assets.html:52 #: assets/templates/assets/asset_detail.html:61 @@ -285,7 +286,7 @@ msgstr "IP" msgid "Hostname" msgstr "主机名" -#: assets/models/asset.py:75 assets/models/domain.py:49 +#: assets/models/asset.py:75 assets/models/domain.py:51 #: assets/models/user.py:117 assets/templates/assets/asset_detail.html:73 #: assets/templates/assets/domain_gateway_list.html:59 #: assets/templates/assets/system_user_detail.html:70 @@ -295,14 +296,14 @@ msgstr "主机名" msgid "Protocol" msgstr "协议" -#: assets/models/asset.py:77 assets/templates/assets/asset_detail.html:101 +#: assets/models/asset.py:77 assets/templates/assets/asset_detail.html:105 #: assets/templates/assets/user_asset_list.html:154 msgid "Platform" msgstr "系统平台" #: assets/models/asset.py:84 assets/models/cmd_filter.py:21 -#: assets/models/domain.py:52 assets/models/label.py:21 -#: assets/templates/assets/asset_detail.html:109 +#: assets/models/domain.py:54 assets/models/label.py:22 +#: assets/templates/assets/asset_detail.html:113 #: assets/templates/assets/user_asset_list.html:158 msgid "Is active" msgstr "激活" @@ -311,19 +312,19 @@ msgstr "激活" msgid "Public IP" msgstr "公网IP" -#: assets/models/asset.py:92 assets/templates/assets/asset_detail.html:117 +#: assets/models/asset.py:92 assets/templates/assets/asset_detail.html:121 msgid "Asset number" msgstr "资产编号" -#: assets/models/asset.py:96 assets/templates/assets/asset_detail.html:81 +#: assets/models/asset.py:96 assets/templates/assets/asset_detail.html:85 msgid "Vendor" msgstr "制造商" -#: assets/models/asset.py:98 assets/templates/assets/asset_detail.html:85 +#: assets/models/asset.py:98 assets/templates/assets/asset_detail.html:89 msgid "Model" msgstr "型号" -#: assets/models/asset.py:100 assets/templates/assets/asset_detail.html:113 +#: assets/models/asset.py:100 assets/templates/assets/asset_detail.html:117 msgid "Serial number" msgstr "序列号" @@ -343,7 +344,7 @@ msgstr "CPU核数" msgid "CPU vcpus" msgstr "CPU总数" -#: assets/models/asset.py:108 assets/templates/assets/asset_detail.html:93 +#: assets/models/asset.py:108 assets/templates/assets/asset_detail.html:97 msgid "Memory" msgstr "内存" @@ -355,7 +356,7 @@ msgstr "硬盘大小" msgid "Disk info" msgstr "硬盘信息" -#: assets/models/asset.py:115 assets/templates/assets/asset_detail.html:105 +#: assets/models/asset.py:115 assets/templates/assets/asset_detail.html:109 #: assets/templates/assets/user_asset_list.html:155 msgid "OS" msgstr "操作系统" @@ -373,7 +374,7 @@ msgid "Hostname raw" msgstr "主机名原始" #: assets/models/asset.py:125 assets/templates/assets/asset_create.html:34 -#: assets/templates/assets/asset_detail.html:224 +#: assets/templates/assets/asset_detail.html:228 #: assets/templates/assets/asset_update.html:39 templates/_nav.html:26 msgid "Labels" msgstr "标签管理" @@ -382,7 +383,7 @@ msgstr "标签管理" #: assets/models/cluster.py:28 assets/models/cmd_filter.py:25 #: assets/models/cmd_filter.py:55 assets/models/group.py:21 #: assets/templates/assets/admin_user_detail.html:68 -#: assets/templates/assets/asset_detail.html:121 +#: assets/templates/assets/asset_detail.html:125 #: assets/templates/assets/cmd_filter_detail.html:77 #: assets/templates/assets/domain_detail.html:72 #: assets/templates/assets/system_user_detail.html:100 @@ -394,8 +395,8 @@ msgid "Created by" msgstr "创建者" #: assets/models/asset.py:130 assets/models/cluster.py:26 -#: assets/models/domain.py:21 assets/models/group.py:22 -#: assets/models/label.py:24 assets/templates/assets/admin_user_detail.html:64 +#: assets/models/domain.py:23 assets/models/group.py:22 +#: assets/models/label.py:25 assets/templates/assets/admin_user_detail.html:64 #: assets/templates/assets/cmd_filter_detail.html:69 #: assets/templates/assets/domain_detail.html:68 #: assets/templates/assets/system_user_detail.html:96 @@ -413,11 +414,11 @@ msgstr "创建日期" #: assets/models/asset.py:132 assets/models/base.py:27 #: assets/models/cluster.py:29 assets/models/cmd_filter.py:22 -#: assets/models/cmd_filter.py:52 assets/models/domain.py:19 -#: assets/models/domain.py:51 assets/models/group.py:23 -#: assets/models/label.py:22 assets/templates/assets/admin_user_detail.html:72 +#: assets/models/cmd_filter.py:52 assets/models/domain.py:21 +#: assets/models/domain.py:53 assets/models/group.py:23 +#: assets/models/label.py:23 assets/templates/assets/admin_user_detail.html:72 #: assets/templates/assets/admin_user_list.html:32 -#: assets/templates/assets/asset_detail.html:129 +#: assets/templates/assets/asset_detail.html:133 #: assets/templates/assets/cmd_filter_detail.html:65 #: assets/templates/assets/cmd_filter_list.html:27 #: assets/templates/assets/cmd_filter_rule_list.html:62 @@ -487,7 +488,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:422 +#: users/models/user.py:420 msgid "System" msgstr "系统" @@ -516,7 +517,7 @@ msgid "Regex" msgstr "正则表达式" #: assets/models/cmd_filter.py:36 ops/models/command.py:19 -#: ops/templates/ops/command_execution_list.html:46 terminal/models.py:144 +#: ops/templates/ops/command_execution_list.html:60 terminal/models.py:144 #: terminal/templates/terminal/command_list.html:55 #: terminal/templates/terminal/command_list.html:71 #: terminal/templates/terminal/session_detail.html:48 @@ -591,7 +592,7 @@ msgstr "每行一个命令" msgid "Action" msgstr "动作" -#: assets/models/domain.py:59 assets/templates/assets/domain_detail.html:21 +#: assets/models/domain.py:61 assets/templates/assets/domain_detail.html:21 #: assets/templates/assets/domain_detail.html:64 #: assets/templates/assets/domain_gateway_list.html:21 #: assets/templates/assets/domain_list.html:27 @@ -613,8 +614,8 @@ msgstr "默认资产组" #: audits/templates/audits/operate_log_list.html:66 #: audits/templates/audits/password_change_log_list.html:33 #: audits/templates/audits/password_change_log_list.html:50 -#: ops/templates/ops/command_execution_list.html:22 -#: ops/templates/ops/command_execution_list.html:45 perms/forms.py:28 +#: ops/templates/ops/command_execution_list.html:34 +#: ops/templates/ops/command_execution_list.html:59 perms/forms.py:28 #: perms/models.py:29 #: perms/templates/perms/asset_permission_create_update.html:36 #: perms/templates/perms/asset_permission_list.html:54 @@ -623,10 +624,10 @@ msgstr "默认资产组" #: 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:310 -#: users/models/user.py:33 users/models/user.py:410 +#: terminal/templates/terminal/session_list.html:71 users/forms.py:314 +#: users/models/user.py:33 users/models/user.py:408 #: users/templates/users/user_group_detail.html:78 -#: users/templates/users/user_group_list.html:13 users/views/user.py:384 +#: users/templates/users/user_group_list.html:13 users/views/user.py:386 #: xpack/plugins/orgs/forms.py:26 #: xpack/plugins/orgs/templates/orgs/org_detail.html:113 #: xpack/plugins/orgs/templates/orgs/org_list.html:14 @@ -638,7 +639,7 @@ msgstr "用户" msgid "Value" msgstr "值" -#: assets/models/label.py:20 +#: assets/models/label.py:21 msgid "Category" msgstr "分类" @@ -899,9 +900,9 @@ msgstr "其它" #: common/templates/common/replay_storage_create.html:139 #: common/templates/common/security_setting.html:70 #: common/templates/common/terminal_setting.html:68 -#: perms/templates/perms/asset_permission_create_update.html:69 +#: perms/templates/perms/asset_permission_create_update.html:75 #: terminal/templates/terminal/terminal_update.html:47 -#: users/templates/users/_user.html:46 +#: users/templates/users/_user.html:50 #: users/templates/users/user_bulk_update.html:23 #: users/templates/users/user_detail.html:176 #: users/templates/users/user_password_update.html:71 @@ -932,11 +933,11 @@ msgstr "重置" #: common/templates/common/replay_storage_create.html:140 #: common/templates/common/security_setting.html:71 #: common/templates/common/terminal_setting.html:70 -#: perms/templates/perms/asset_permission_create_update.html:70 +#: perms/templates/perms/asset_permission_create_update.html:76 #: terminal/templates/terminal/command_list.html:103 #: terminal/templates/terminal/session_list.html:127 #: terminal/templates/terminal/terminal_update.html:48 -#: users/templates/users/_user.html:47 +#: users/templates/users/_user.html:51 #: users/templates/users/forgot_password.html:45 #: users/templates/users/user_bulk_update.html:24 #: users/templates/users/user_list.html:45 @@ -999,12 +1000,12 @@ msgid "Quick update" msgstr "快速更新" #: assets/templates/assets/admin_user_assets.html:72 -#: assets/templates/assets/asset_detail.html:172 +#: assets/templates/assets/asset_detail.html:176 msgid "Test connective" msgstr "测试可连接性" #: assets/templates/assets/admin_user_assets.html:75 -#: assets/templates/assets/asset_detail.html:175 +#: assets/templates/assets/asset_detail.html:179 #: assets/templates/assets/system_user_asset.html:75 #: assets/templates/assets/system_user_asset.html:161 #: assets/templates/assets/system_user_detail.html:151 @@ -1087,7 +1088,7 @@ msgid "Select nodes" msgstr "选择节点" #: assets/templates/assets/admin_user_detail.html:100 -#: assets/templates/assets/asset_detail.html:204 +#: assets/templates/assets/asset_detail.html:208 #: assets/templates/assets/asset_list.html:633 #: assets/templates/assets/cmd_filter_detail.html:106 #: assets/templates/assets/system_user_asset.html:112 @@ -1153,28 +1154,28 @@ msgstr "选择需要修改属性" msgid "Select all" msgstr "全选" -#: assets/templates/assets/asset_detail.html:89 +#: assets/templates/assets/asset_detail.html:93 msgid "CPU" msgstr "CPU" -#: assets/templates/assets/asset_detail.html:97 +#: assets/templates/assets/asset_detail.html:101 msgid "Disk" msgstr "硬盘" -#: assets/templates/assets/asset_detail.html:125 +#: assets/templates/assets/asset_detail.html:129 #: users/templates/users/user_detail.html:115 #: users/templates/users/user_profile.html:104 msgid "Date joined" msgstr "创建日期" -#: assets/templates/assets/asset_detail.html:141 +#: assets/templates/assets/asset_detail.html:145 #: terminal/templates/terminal/session_detail.html:81 #: users/templates/users/user_detail.html:138 #: users/templates/users/user_profile.html:146 msgid "Quick modify" msgstr "快速修改" -#: assets/templates/assets/asset_detail.html:147 +#: assets/templates/assets/asset_detail.html:151 #: assets/templates/assets/asset_list.html:95 #: assets/templates/assets/user_asset_list.html:47 perms/models.py:34 #: perms/models.py:82 @@ -1191,15 +1192,15 @@ msgstr "快速修改" msgid "Active" msgstr "激活中" -#: assets/templates/assets/asset_detail.html:164 +#: assets/templates/assets/asset_detail.html:168 msgid "Refresh hardware" msgstr "更新硬件信息" -#: assets/templates/assets/asset_detail.html:167 +#: assets/templates/assets/asset_detail.html:171 msgid "Refresh" msgstr "刷新" -#: assets/templates/assets/asset_detail.html:304 +#: assets/templates/assets/asset_detail.html:308 #: users/templates/users/user_detail.html:305 #: users/templates/users/user_detail.html:332 msgid "Update successfully!" @@ -1680,7 +1681,7 @@ msgid "Filename" msgstr "文件名" #: audits/models.py:22 audits/templates/audits/ftp_log_list.html:76 -#: ops/templates/ops/command_execution_list.html:49 +#: ops/templates/ops/command_execution_list.html:64 #: ops/templates/ops/task_list.html:39 users/models/authentication.py:73 #: users/templates/users/user_detail.html:456 xpack/plugins/cloud/api.py:61 msgid "Success" @@ -1706,7 +1707,7 @@ msgstr "修改者" #: audits/templates/audits/ftp_log_list.html:77 #: ops/templates/ops/adhoc_history.html:52 #: ops/templates/ops/adhoc_history_detail.html:61 -#: ops/templates/ops/command_execution_list.html:50 +#: ops/templates/ops/command_execution_list.html:65 #: ops/templates/ops/task_history.html:58 perms/models.py:35 #: perms/templates/perms/asset_permission_detail.html:86 terminal/models.py:148 #: terminal/templates/terminal/session_list.html:78 @@ -1722,8 +1723,8 @@ msgstr "选择用户" #: audits/templates/audits/login_log_list.html:40 #: audits/templates/audits/operate_log_list.html:58 #: audits/templates/audits/password_change_log_list.html:42 -#: ops/templates/ops/command_execution_list.html:30 -#: ops/templates/ops/command_execution_list.html:35 +#: ops/templates/ops/command_execution_list.html:42 +#: ops/templates/ops/command_execution_list.html:47 #: ops/templates/ops/task_list.html:21 ops/templates/ops/task_list.html:26 #: templates/_base_list.html:43 templates/_header_bar.html:8 #: terminal/templates/terminal/command_list.html:60 @@ -1751,7 +1752,7 @@ msgstr "Agent" msgid "City" msgstr "城市" -#: audits/templates/audits/login_log_list.html:54 users/forms.py:168 +#: audits/templates/audits/login_log_list.html:54 users/forms.py:172 #: users/models/authentication.py:82 users/models/user.py:75 #: users/templates/users/first_login.html:45 msgid "MFA" @@ -1785,23 +1786,23 @@ msgid "Datetime" msgstr "日期" #: audits/views.py:68 audits/views.py:112 audits/views.py:148 -#: audits/views.py:192 audits/views.py:223 templates/_nav.html:71 +#: audits/views.py:192 audits/views.py:223 templates/_nav.html:72 msgid "Audits" msgstr "日志审计" -#: audits/views.py:69 templates/_nav.html:75 +#: audits/views.py:69 templates/_nav.html:76 msgid "FTP log" msgstr "FTP日志" -#: audits/views.py:113 templates/_nav.html:76 +#: audits/views.py:113 templates/_nav.html:77 msgid "Operate log" msgstr "操作日志" -#: audits/views.py:149 templates/_nav.html:77 +#: audits/views.py:149 templates/_nav.html:78 msgid "Password change log" msgstr "改密日志" -#: audits/views.py:193 templates/_nav.html:74 +#: audits/views.py:193 templates/_nav.html:75 msgid "Login log" msgstr "登录日志" @@ -1958,64 +1959,76 @@ msgid "Enable LDAP auth" msgstr "启用LDAP认证" #: common/forms.py:138 +msgid "All" +msgstr "全部" + +#: common/forms.py:139 +msgid "Auto" +msgstr "自动" + +#: common/forms.py:144 msgid "Password auth" msgstr "密码认证" -#: common/forms.py:141 +#: common/forms.py:147 msgid "Public key auth" msgstr "密钥认证" -#: common/forms.py:144 +#: common/forms.py:150 msgid "Heartbeat interval" msgstr "心跳间隔" -#: common/forms.py:144 ops/models/adhoc.py:38 +#: common/forms.py:150 ops/models/adhoc.py:38 msgid "Units: seconds" msgstr "单位: 秒" -#: common/forms.py:147 +#: common/forms.py:153 msgid "List sort by" msgstr "资产列表排序" -#: common/forms.py:159 +#: common/forms.py:156 +msgid "List page size" +msgstr "资产列表页面大小" + +#: common/forms.py:168 msgid "MFA Secondary certification" msgstr "MFA 二次认证" -#: common/forms.py:161 +#: common/forms.py:170 msgid "" "After opening, the user login must use MFA secondary authentication (valid " "for all users, including administrators)" msgstr "开启后,用户登录必须使用MFA二次认证(对所有用户有效,包括管理员)" -#: common/forms.py:168 +#: common/forms.py:177 msgid "Limit the number of login failures" msgstr "限制登录失败次数" -#: common/forms.py:173 +#: common/forms.py:182 msgid "No logon interval" msgstr "禁止登录时间间隔" -#: common/forms.py:175 +#: common/forms.py:184 msgid "" "Tip: (unit/minute) if the user has failed to log in for a limited number of " "times, no login is allowed during this time interval." msgstr "" "提示:(单位:分)当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录" -#: common/forms.py:182 +#: common/forms.py:191 msgid "Connection max idle time" msgstr "SSH最大空闲时间" -#: common/forms.py:184 +#: common/forms.py:193 msgid "" "If idle time more than it, disconnect connection(only ssh now) Unit: minute" msgstr "提示:(单位:分)如果超过该配置没有操作,连接会被断开(仅ssh)" -#: common/forms.py:190 +#: common/forms.py:199 msgid "Password expiration time" msgstr "密码过期时间" -#: common/forms.py:193 +#: common/forms.py:202 msgid "" "Tip: (unit: day) If the user does not update the password during the time, " "the user password will expire failure;The password expiration reminder mail " @@ -2025,45 +2038,45 @@ msgstr "" "提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期" "提醒邮件将在密码过期前5天内由系统(每天)自动发送给用户" -#: common/forms.py:202 +#: common/forms.py:211 msgid "Password minimum length" msgstr "密码最小长度 " -#: common/forms.py:208 +#: common/forms.py:217 msgid "Must contain capital letters" msgstr "必须包含大写字母" -#: common/forms.py:210 +#: common/forms.py:219 msgid "" "After opening, the user password changes and resets must contain uppercase " "letters" msgstr "开启后,用户密码修改、重置必须包含大写字母" -#: common/forms.py:216 +#: common/forms.py:225 msgid "Must contain lowercase letters" msgstr "必须包含小写字母" -#: common/forms.py:217 +#: common/forms.py:226 msgid "" "After opening, the user password changes and resets must contain lowercase " "letters" msgstr "开启后,用户密码修改、重置必须包含小写字母" -#: common/forms.py:223 +#: common/forms.py:232 msgid "Must contain numeric characters" msgstr "必须包含数字字符" -#: common/forms.py:224 +#: common/forms.py:233 msgid "" "After opening, the user password changes and resets must contain numeric " "characters" msgstr "开启后,用户密码修改、重置必须包含数字字符" -#: common/forms.py:230 +#: common/forms.py:239 msgid "Must contain special characters" msgstr "必须包含特殊字符" -#: common/forms.py:231 +#: common/forms.py:240 msgid "" "After opening, the user password changes and resets must contain special " "characters" @@ -2126,7 +2139,7 @@ msgstr "安全设置" #: common/templates/common/command_storage_create.html:50 #: ops/models/adhoc.py:161 ops/templates/ops/adhoc_detail.html:53 -#: ops/templates/ops/command_execution_list.html:44 +#: ops/templates/ops/command_execution_list.html:58 #: ops/templates/ops/task_adhoc.html:59 ops/templates/ops/task_list.html:38 msgid "Hosts" msgstr "主机" @@ -2227,7 +2240,7 @@ msgstr "不能包含特殊字符" #: common/views.py:18 common/views.py:44 common/views.py:70 common/views.py:99 #: common/views.py:126 common/views.py:138 common/views.py:151 -#: templates/_nav.html:106 +#: templates/_nav.html:107 msgid "Settings" msgstr "系统设置" @@ -2339,15 +2352,15 @@ msgstr "汇总" msgid "Result" msgstr "结果" -#: ops/models/command.py:52 +#: ops/models/command.py:55 msgid "Task start" msgstr "任务开始" -#: ops/models/command.py:64 +#: ops/models/command.py:67 msgid "Command `{}` is forbidden ........" msgstr "命令 `{}` 不允许被执行 ......." -#: ops/models/command.py:70 +#: ops/models/command.py:73 msgid "Task end" msgstr "任务结束" @@ -2362,7 +2375,9 @@ msgid "Version run history" msgstr "执行历史" #: ops/templates/ops/adhoc_detail.html:72 -#: ops/templates/ops/adhoc_detail.html:77 ops/templates/ops/task_adhoc.html:61 +#: ops/templates/ops/adhoc_detail.html:77 +#: ops/templates/ops/command_execution_list.html:61 +#: ops/templates/ops/task_adhoc.html:61 msgid "Run as" msgstr "运行用户" @@ -2424,7 +2439,7 @@ msgid "Run history detail" msgstr "执行历史详情" #: ops/templates/ops/adhoc_history_detail.html:22 -#: ops/templates/ops/command_execution_list.html:47 +#: ops/templates/ops/command_execution_list.html:62 #: terminal/backends/command/models.py:16 msgid "Output" msgstr "输出" @@ -2450,24 +2465,20 @@ msgstr "没有资产" msgid "Success assets" msgstr "成功资产" -#: ops/templates/ops/command_execution_create.html:67 +#: ops/templates/ops/command_execution_create.html:71 #: terminal/templates/terminal/session_detail.html:91 #: terminal/templates/terminal/session_detail.html:100 msgid "Go" msgstr "" -#: ops/templates/ops/command_execution_create.html:244 +#: ops/templates/ops/command_execution_create.html:253 msgid "Pending" msgstr "" -#: ops/templates/ops/command_execution_list.html:48 +#: ops/templates/ops/command_execution_list.html:63 msgid "Finished" msgstr "结束" -#: ops/templates/ops/command_execution_list.html:51 -msgid "Date finished" -msgstr "结束日期" - #: ops/templates/ops/task_adhoc.html:19 ops/templates/ops/task_detail.html:20 #: ops/templates/ops/task_history.html:19 ops/views/adhoc.py:72 msgid "Task detail" @@ -2535,7 +2546,8 @@ msgstr "任务列表" msgid "Task run history" msgstr "执行历史" -#: ops/views/command.py:68 templates/_nav.html:78 templates/_nav_user.html:9 +#: ops/views/command.py:68 templates/_nav.html:67 templates/_nav.html:79 +#: templates/_nav_user.html:9 msgid "Command execution" msgstr "命令执行" @@ -2546,7 +2558,7 @@ msgstr "组织管理" #: perms/forms.py:31 perms/models.py:30 perms/models.py:80 #: perms/templates/perms/asset_permission_list.html:55 #: perms/templates/perms/asset_permission_list.html:145 templates/_nav.html:14 -#: users/forms.py:280 users/models/group.py:26 users/models/user.py:59 +#: users/forms.py:284 users/models/group.py:26 users/models/user.py:59 #: users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_detail.html:211 #: users/templates/users/user_list.html:26 @@ -2688,14 +2700,14 @@ msgstr "文档" msgid "Commercial support" msgstr "商业支持" -#: templates/_header_bar.html:89 templates/_nav_user.html:14 users/forms.py:147 -#: users/templates/users/_user.html:39 +#: templates/_header_bar.html:89 templates/_nav_user.html:14 users/forms.py:151 +#: users/templates/users/_user.html:43 #: users/templates/users/first_login.html:39 #: users/templates/users/user_password_update.html:40 #: 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:366 +#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:368 msgid "Profile" msgstr "个人信息" @@ -2783,8 +2795,8 @@ msgstr "" #: templates/_nav.html:10 users/views/group.py:28 users/views/group.py:44 #: users/views/group.py:60 users/views/group.py:76 users/views/group.py:92 #: users/views/login.py:346 users/views/user.py:68 users/views/user.py:83 -#: users/views/user.py:111 users/views/user.py:192 users/views/user.py:353 -#: users/views/user.py:403 users/views/user.py:437 +#: users/views/user.py:113 users/views/user.py:194 users/views/user.py:355 +#: users/views/user.py:405 users/views/user.py:439 msgid "Users" msgstr "用户管理" @@ -2831,15 +2843,15 @@ msgstr "终端管理" msgid "Job Center" msgstr "作业中心" -#: templates/_nav.html:84 +#: templates/_nav.html:85 msgid "XPack" msgstr "" -#: templates/_nav.html:92 xpack/plugins/cloud/views.py:26 +#: templates/_nav.html:93 xpack/plugins/cloud/views.py:26 msgid "Account list" msgstr "账户列表" -#: templates/_nav.html:93 +#: templates/_nav.html:94 msgid "Sync instance" msgstr "同步实例" @@ -3309,11 +3321,11 @@ msgstr "MFA 验证码" msgid "Role" msgstr "角色" -#: users/forms.py:55 users/forms.py:226 +#: users/forms.py:55 users/forms.py:230 msgid "ssh public key" msgstr "ssh公钥" -#: users/forms.py:56 users/forms.py:227 +#: users/forms.py:56 users/forms.py:231 msgid "ssh-rsa AAAA..." msgstr "" @@ -3325,15 +3337,15 @@ msgstr "复制用户公钥到这里" msgid "Join user groups" msgstr "添加到用户组" -#: users/forms.py:110 users/forms.py:241 +#: users/forms.py:110 users/forms.py:245 msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" -#: users/forms.py:114 users/forms.py:245 users/serializers/v1.py:51 +#: users/forms.py:114 users/forms.py:249 users/serializers/v1.py:51 msgid "Not a valid ssh public key" msgstr "ssh密钥不合法" -#: users/forms.py:153 +#: users/forms.py:157 msgid "" "Tip: when enabled, you will enter the MFA binding process the next time you " "log in. you can also directly bind in \"personal information -> quick " @@ -3342,11 +3354,11 @@ msgstr "" "提示:启用之后您将会在下次登录时进入MFA绑定流程;您也可以在(个人信息->快速修" "改->更改MFA设置)中直接绑定!" -#: users/forms.py:163 +#: users/forms.py:167 msgid "* Enable MFA authentication to make the account more secure." msgstr "* 启用MFA认证,使账号更加安全." -#: users/forms.py:173 +#: users/forms.py:177 msgid "" "In order to protect you and your company, please keep your account, password " "and key sensitive information properly. (for example: setting complex " @@ -3355,41 +3367,41 @@ msgstr "" "为了保护您和公司的安全,请妥善保管您的账户、密码和密钥等重要敏感信息;(如:" "设置复杂密码,启用MFA认证)" -#: users/forms.py:180 users/templates/users/first_login.html:48 +#: users/forms.py:184 users/templates/users/first_login.html:48 #: users/templates/users/first_login.html:107 #: users/templates/users/first_login.html:130 msgid "Finish" msgstr "完成" -#: users/forms.py:186 +#: users/forms.py:190 msgid "Old password" msgstr "原来密码" -#: users/forms.py:191 +#: users/forms.py:195 msgid "New password" msgstr "新密码" -#: users/forms.py:196 +#: users/forms.py:200 msgid "Confirm password" msgstr "确认密码" -#: users/forms.py:206 +#: users/forms.py:210 msgid "Old password error" msgstr "原来密码错误" -#: users/forms.py:214 +#: users/forms.py:218 msgid "Password does not match" msgstr "密码不一致" -#: users/forms.py:224 +#: users/forms.py:228 msgid "Automatically configure and download the SSH key" msgstr "自动配置并下载SSH密钥" -#: users/forms.py:228 +#: users/forms.py:232 msgid "Paste your id_rsa.pub here." msgstr "复制你的公钥到这里" -#: users/forms.py:256 users/models/user.py:83 +#: users/forms.py:260 users/models/user.py:83 #: users/templates/users/first_login.html:42 #: users/templates/users/user_password_update.html:46 #: users/templates/users/user_profile.html:68 @@ -3398,7 +3410,7 @@ msgstr "复制你的公钥到这里" msgid "Public key" msgstr "ssh公钥" -#: users/forms.py:263 users/forms.py:268 users/forms.py:314 +#: users/forms.py:267 users/forms.py:272 users/forms.py:318 #: xpack/plugins/orgs/forms.py:30 msgid "Select users" msgstr "选择用户" @@ -3456,7 +3468,7 @@ msgstr "Agent" msgid "Date login" msgstr "登录日期" -#: users/models/user.py:32 users/models/user.py:418 +#: users/models/user.py:32 users/models/user.py:416 msgid "Administrator" msgstr "管理员" @@ -3502,7 +3514,7 @@ msgstr "用户来源" msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:421 +#: users/models/user.py:419 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -3790,7 +3802,7 @@ 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/views/user.py:193 +#: users/templates/users/user_granted_asset.html:18 users/views/user.py:195 msgid "User detail" msgstr "用户详情" @@ -3985,8 +3997,8 @@ msgstr "安装完成后点击下一步进入绑定页面(如已安装,直接 msgid "Administrator Settings force MFA login" msgstr "管理员设置强制使用MFA登录" -#: users/templates/users/user_profile.html:120 users/views/user.py:229 -#: users/views/user.py:283 +#: users/templates/users/user_profile.html:120 users/views/user.py:231 +#: users/views/user.py:285 msgid "User groups" msgstr "用户组" @@ -4036,7 +4048,7 @@ msgid "" "corresponding private key." msgstr "新的公钥已设置成功,请下载对应的私钥" -#: users/templates/users/user_update.html:4 users/views/user.py:112 +#: users/templates/users/user_update.html:4 users/views/user.py:114 msgid "Update user" msgstr "更新用户" @@ -4243,7 +4255,7 @@ msgstr "用户组授权资产" msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: users/views/login.py:191 users/views/user.py:524 users/views/user.py:549 +#: users/views/login.py:191 users/views/user.py:526 users/views/user.py:551 msgid "MFA code invalid, or ntp sync server time" msgstr "MFA验证码不正确,或者服务器端时间不对" @@ -4288,7 +4300,7 @@ msgstr "Token错误或失效" msgid "Password not same" msgstr "密码不一致" -#: users/views/login.py:308 users/views/user.py:126 users/views/user.py:420 +#: users/views/login.py:308 users/views/user.py:128 users/views/user.py:422 msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" @@ -4296,51 +4308,51 @@ msgstr "* 您的密码不符合要求" msgid "First login" msgstr "首次登陆" -#: users/views/user.py:143 +#: users/views/user.py:145 msgid "Bulk update user success" msgstr "批量更新用户成功" -#: users/views/user.py:173 +#: users/views/user.py:175 msgid "Bulk update user" msgstr "批量更新用户" -#: users/views/user.py:258 +#: users/views/user.py:260 msgid "Invalid file." msgstr "文件不合法" -#: users/views/user.py:354 +#: users/views/user.py:356 msgid "User granted assets" msgstr "用户授权资产" -#: users/views/user.py:385 +#: users/views/user.py:387 msgid "Profile setting" msgstr "个人信息设置" -#: users/views/user.py:404 +#: users/views/user.py:406 msgid "Password update" msgstr "密码更新" -#: users/views/user.py:438 +#: users/views/user.py:440 msgid "Public key update" msgstr "密钥更新" -#: users/views/user.py:479 +#: users/views/user.py:481 msgid "Password invalid" msgstr "用户名或密码无效" -#: users/views/user.py:579 +#: users/views/user.py:581 msgid "MFA enable success" msgstr "MFA 绑定成功" -#: users/views/user.py:580 +#: users/views/user.py:582 msgid "MFA enable success, return login page" msgstr "MFA 绑定成功,返回到登录页面" -#: users/views/user.py:582 +#: users/views/user.py:584 msgid "MFA disable success" msgstr "MFA 解绑成功" -#: users/views/user.py:583 +#: users/views/user.py:585 msgid "MFA disable success, return login page" msgstr "MFA 解绑成功,返回登录页面" @@ -4474,6 +4486,50 @@ msgstr "AWS (中国)" msgid "AWS (International)" msgstr "AWS (国际)" +#: xpack/plugins/cloud/providers/base.py:76 +msgid "任务执行开始: {}\n" +msgstr "" + +#: xpack/plugins/cloud/providers/base.py:79 +msgid "检测账户有效性: {}" +msgstr "" + +#: xpack/plugins/cloud/providers/base.py:82 +msgid "账户无效!\n" +msgstr "" + +#: xpack/plugins/cloud/providers/base.py:86 +msgid "账户有效!\n" +msgstr "" + +#: xpack/plugins/cloud/providers/base.py:90 +msgid "" +"\n" +"任务执行结束!\n" +msgstr "" + +#: xpack/plugins/cloud/providers/base.py:91 +msgid "" +"查看任务详细信息路径: XPack -> 云管中心 -> 任务列表 -> 任务详情(点击任务名" +"称) -> 查看同步历史列表/实例列表\n" +msgstr "" + +#: xpack/plugins/cloud/providers/base.py:126 +msgid "同步实例列表: {}" +msgstr "" + +#: xpack/plugins/cloud/providers/base.py:135 +msgid "同步地域列表: {}\n" +msgstr "" + +#: xpack/plugins/cloud/providers/base.py:138 +msgid "地域: {}" +msgstr "" + +#: xpack/plugins/cloud/providers/base.py:148 +msgid "实例: {}, 地域: {}" +msgstr "" + #: xpack/plugins/cloud/providers/qcloud.py:14 msgid "Qcloud" msgstr "腾讯云" @@ -4605,6 +4661,9 @@ msgstr "创建组织" msgid "Update org" msgstr "更新组织" +#~ msgid "Date finished" +#~ msgstr "结束日期" + #, fuzzy #~| msgid "Audits" #~ msgid "Audit" From 985bd6fc8260349b79d07ca0c323a9986fe8be8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=B9=BF?= Date: Mon, 17 Dec 2018 11:44:43 +0800 Subject: [PATCH 03/13] Bugfix2 (#2183) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Bugfix] 修复错误 * [Bugfix] 修复一些bug --- apps/assets/tasks.py | 21 +++++++++++---------- apps/common/models.py | 2 ++ apps/ops/serializers.py | 2 +- apps/ops/urls/api_urls.py | 1 + apps/ops/views/adhoc.py | 7 +++++-- jms | 4 ++-- 6 files changed, 22 insertions(+), 15 deletions(-) diff --git a/apps/assets/tasks.py b/apps/assets/tasks.py index 4b4ec2867..846964d94 100644 --- a/apps/assets/tasks.py +++ b/apps/assets/tasks.py @@ -141,11 +141,11 @@ def update_assets_hardware_info_period(): logger.debug("Period task disabled, update assets hardware info pass") return - from ops.utils import update_or_create_ansible_task - from orgs.models import Organization - orgs = Organization.objects.all().values_list('id', flat=True) - orgs.append('') - task_name = _("Update assets hardware info period") + # from ops.utils import update_or_create_ansible_task + # from orgs.models import Organization + # orgs = Organization.objects.all().values_list('id', flat=True) + # orgs.append('') + # task_name = _("Update assets hardware info period") # for org_id in orgs: # org_id = str(org_id) # hostname_list = [ @@ -354,11 +354,12 @@ def test_system_user_connectability_period(): if PERIOD_TASK != "on": logger.debug("Period task disabled, test system user connectability pass") return - system_users = SystemUser.objects.all() - for system_user in system_users: - task_name = _("Test system user connectability period: {}").format(system_user) - # task_name = _("定期测试系统用户可连接性: {}".format(system_user)) - test_system_user_connectability_util(system_user, task_name) + # Todo: 暂时禁用定期测试 + # system_users = SystemUser.objects.all() + # for system_user in system_users: + # task_name = _("Test system user connectability period: {}").format(system_user) + # # task_name = _("定期测试系统用户可连接性: {}".format(system_user)) + # test_system_user_connectability_util(system_user, task_name) #### Push system user tasks #### diff --git a/apps/common/models.py b/apps/common/models.py index cc5ba8fc5..cb97b8988 100644 --- a/apps/common/models.py +++ b/apps/common/models.py @@ -45,6 +45,8 @@ class Setting(models.Model): def cleaned_value(self): try: value = self.value + if not isinstance(value, (str, bytes)): + return value if self.encrypted: value = signer.unsign(value) value = json.loads(value) diff --git a/apps/ops/serializers.py b/apps/ops/serializers.py index 13423486f..5eb16c5a8 100644 --- a/apps/ops/serializers.py +++ b/apps/ops/serializers.py @@ -53,7 +53,7 @@ class AdHocRunHistorySerializer(serializers.ModelSerializer): @staticmethod def get_stat(obj): return { - "total": len(obj.adhoc.hosts), + "total": obj.adhoc.hosts.count(), "success": len(obj.summary.get("contacted", [])), "failed": len(obj.summary.get("dark", [])), } diff --git a/apps/ops/urls/api_urls.py b/apps/ops/urls/api_urls.py index 615f53a8b..5f955540d 100644 --- a/apps/ops/urls/api_urls.py +++ b/apps/ops/urls/api_urls.py @@ -11,6 +11,7 @@ app_name = "ops" router = DefaultRouter() router.register(r'tasks', api.TaskViewSet, 'task') router.register(r'adhoc', api.AdHocViewSet, 'adhoc') +router.register(r'history', api.AdHocRunHistoryViewSet, 'history') router.register(r'command-executions', api.CommandExecutionViewSet, 'command-execution') urlpatterns = [ diff --git a/apps/ops/views/adhoc.py b/apps/ops/views/adhoc.py index 737047290..f3efbcc70 100644 --- a/apps/ops/views/adhoc.py +++ b/apps/ops/views/adhoc.py @@ -27,7 +27,7 @@ class TaskListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): def get_queryset(self): queryset = super().get_queryset() - if current_org.is_real(): + if current_org: queryset = queryset.filter(created_by=current_org.id) else: queryset = queryset.filter(created_by='') @@ -62,8 +62,11 @@ class TaskDetailView(AdminUserRequiredMixin, DetailView): def get_queryset(self): queryset = super().get_queryset() - if current_org: + # Todo: 需要整理默认组织等东西 + if current_org.is_real(): queryset = queryset.filter(created_by=current_org.id) + else: + queryset = queryset.filter(created_by='') return queryset def get_context_data(self, **kwargs): diff --git a/jms b/jms index 1bb6bb56c..613de2fe3 100755 --- a/jms +++ b/jms @@ -26,8 +26,8 @@ LOG_DIR = os.path.join(BASE_DIR, 'logs') TMP_DIR = os.path.join(BASE_DIR, 'tmp') HTTP_HOST = CONFIG.HTTP_BIND_HOST or '127.0.0.1' HTTP_PORT = CONFIG.HTTP_LISTEN_PORT or 8080 -DEBUG = CONFIG.DEBUG -LOG_LEVEL = CONFIG.LOG_LEVEL +DEBUG = CONFIG.DEBUG or False +LOG_LEVEL = CONFIG.LOG_LEVEL or 'INFO' START_TIMEOUT = 40 WORKERS = 4 From b95f8a7d6b55e74b29cf7a345f42c3d771f6bcd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=B9=BF?= Date: Mon, 17 Dec 2018 11:49:57 +0800 Subject: [PATCH 04/13] =?UTF-8?q?[Update]=20=E4=BF=AE=E5=A4=8D=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E8=8A=82=E7=82=B9=E6=95=B0=E9=87=8F=E6=AF=94=E8=BE=83?= =?UTF-8?q?=E6=85=A2=E7=9A=84=E9=97=AE=E9=A2=98=20(#2184)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/node.py | 3 +- apps/assets/models/node.py | 79 ++++++++++++------- apps/assets/serializers/node.py | 18 ++--- apps/assets/signals_handler.py | 20 ++++- .../templates/assets/_asset_list_modal.html | 1 + apps/assets/templates/assets/asset_list.html | 7 ++ 6 files changed, 83 insertions(+), 45 deletions(-) diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index 831c85e8a..73313f222 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -20,11 +20,10 @@ from rest_framework.response import Response from rest_framework_bulk import BulkModelViewSet from django.utils.translation import ugettext_lazy as _ from django.shortcuts import get_object_or_404 -from django.db.models import Count from common.utils import get_logger, get_object_or_none from ..hands import IsOrgAdmin -from ..models import Node +from ..models import Node, Asset from ..tasks import update_assets_hardware_info_util, test_asset_connectability_util from .. import serializers diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index af4ffd839..47a835861 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -22,7 +22,9 @@ class Node(OrgModelMixin): date_create = models.DateTimeField(auto_now_add=True) is_node = True - _full_value_cache_key_prefix = '_NODE_VALUE_{}' + _assets_amount = None + _full_value_cache_key = '_NODE_VALUE_{}' + _assets_amount_cache_key = '_NODE_ASSETS_AMOUNT_{}' class Meta: verbose_name = _("Node") @@ -49,30 +51,56 @@ class Node(OrgModelMixin): def name(self): return self.value + @property + def assets_amount(self): + """ + 获取节点下所有资产数量速度太慢,所以需要重写,使用cache等方案 + :return: + """ + if self._assets_amount is not None: + return self._assets_amount + cache_key = self._assets_amount_cache_key.format(self.key) + cached = cache.get(cache_key) + if cached is not None: + return cached + assets_amount = self.get_all_assets().count() + cache.set(cache_key, assets_amount, 3600) + return assets_amount + + @assets_amount.setter + def assets_amount(self, value): + self._assets_amount = value + + def expire_assets_amount(self): + ancestor_keys = self.get_ancestor_keys(with_self=True) + cache_keys = [self._assets_amount_cache_key.format(k) for k in ancestor_keys] + cache.delete_many(cache_keys) + + @classmethod + def expire_nodes_assets_amount(cls, nodes=None): + if nodes: + for node in nodes: + node.expire_assets_amount() + return + key = cls._assets_amount_cache_key.format('*') + cache.delete_pattern(key) + @property def full_value(self): - key = self._full_value_cache_key_prefix.format(self.key) + key = self._full_value_cache_key.format(self.key) cached = cache.get(key) if cached: return cached - value = self.get_full_value() - self.cache_full_value(value) - return value - - def get_full_value(self): - # ancestor = [a.value for a in self.get_ancestor(with_self=True)] if self.is_root(): return self.value parent_full_value = self.parent.full_value value = parent_full_value + ' / ' + self.value + key = self._full_value_cache_key.format(self.key) + cache.set(key, value, 3600) return value - def cache_full_value(self, value): - key = self._full_value_cache_key_prefix.format(self.key) - cache.set(key, value, 3600) - def expire_full_value(self): - key = self._full_value_cache_key_prefix.format(self.key) + key = self._full_value_cache_key.format(self.key) cache.delete_pattern(key+'*') @property @@ -182,17 +210,18 @@ class Node(OrgModelMixin): child.save() self.save() - def get_ancestor(self, with_self=False): - if self.is_root(): - root = self.__class__.root() - return [root] - _key = self.key.split(':') + def get_ancestor_keys(self, with_self=False): + parent_keys = [] + key_list = self.key.split(":") if not with_self: - _key.pop() - ancestor_keys = [] - for i in range(len(_key)): - ancestor_keys.append(':'.join(_key)) - _key.pop() + key_list.pop() + for i in range(len(key_list)): + parent_keys.append(":".join(key_list)) + key_list.pop() + return parent_keys + + def get_ancestor(self, with_self=False): + ancestor_keys = self.get_ancestor_keys(with_self=with_self) ancestor = self.__class__.objects.filter( key__in=ancestor_keys ).order_by('key') @@ -227,10 +256,6 @@ class Node(OrgModelMixin): defaults = {'value': 'Default'} return cls.objects.get_or_create(defaults=defaults, key='1') - @classmethod - def get_tree_name_ref(cls): - pass - @classmethod def generate_fake(cls, count=100): import random diff --git a/apps/assets/serializers/node.py b/apps/assets/serializers/node.py index f1be42d06..f44ff44d6 100644 --- a/apps/assets/serializers/node.py +++ b/apps/assets/serializers/node.py @@ -43,7 +43,7 @@ class NodeGrantedSerializer(BulkSerializerMixin, serializers.ModelSerializer): class NodeSerializer(serializers.ModelSerializer): - assets_amount = serializers.SerializerMethodField() + assets_amount = serializers.IntegerField() tree_id = serializers.SerializerMethodField() tree_parent = serializers.SerializerMethodField() @@ -53,6 +53,10 @@ class NodeSerializer(serializers.ModelSerializer): 'id', 'key', 'value', 'assets_amount', 'is_node', 'org_id', 'tree_id', 'tree_parent', ] + read_only_fields = [ + 'id', 'key', 'assets_amount', 'is_node', + 'org_id', + ] list_serializer_class = BulkListSerializer def validate(self, data): @@ -66,12 +70,6 @@ class NodeSerializer(serializers.ModelSerializer): ) return data - @staticmethod - def get_assets_amount(obj): - if hasattr(obj, 'assets_amount'): - return obj.assets_amount - return obj.get_all_assets().count() - @staticmethod def get_tree_id(obj): return obj.key @@ -80,12 +78,6 @@ class NodeSerializer(serializers.ModelSerializer): def get_tree_parent(obj): return obj.parent_key - def get_fields(self): - fields = super().get_fields() - field = fields["key"] - field.required = False - return fields - class NodeAssetsSerializer(serializers.ModelSerializer): assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all()) diff --git a/apps/assets/signals_handler.py b/apps/assets/signals_handler.py index 9028f52c3..08ee6e670 100644 --- a/apps/assets/signals_handler.py +++ b/apps/assets/signals_handler.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # from collections import defaultdict -from django.db.models.signals import post_save, m2m_changed +from django.db.models.signals import post_save, m2m_changed, post_delete from django.dispatch import receiver from common.utils import get_logger @@ -35,6 +35,17 @@ def on_asset_created_or_update(sender, instance=None, created=False, **kwargs): update_asset_hardware_info_on_created(instance) test_asset_conn_on_created(instance) + # 过期节点资产数量 + nodes = instance.nodes.all() + Node.expire_nodes_assets_amount(nodes) + + +@receiver(post_delete, sender=Asset, dispatch_uid="my_unique_identifier") +def on_asset_delete(sender, instance=None, **kwargs): + # 过期节点资产数量 + nodes = instance.nodes.all() + Node.expire_nodes_assets_amount(nodes) + @receiver(post_save, sender=SystemUser, dispatch_uid="my_unique_identifier") def on_system_user_update(sender, instance=None, created=True, **kwargs): @@ -63,10 +74,11 @@ def on_system_user_assets_change(sender, instance=None, **kwargs): @receiver(m2m_changed, sender=Asset.nodes.through) def on_asset_node_changed(sender, instance=None, **kwargs): + logger.debug("Asset node change signal received") if isinstance(instance, Asset): if kwargs['action'] == 'post_add': - logger.debug("Asset node change signal received") nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) + Node.expire_nodes_assets_amount(nodes) system_users_assets = defaultdict(set) system_users = SystemUser.objects.filter(nodes__in=nodes) # 清理节点缓存 @@ -79,9 +91,11 @@ def on_asset_node_changed(sender, instance=None, **kwargs): @receiver(m2m_changed, sender=Asset.nodes.through) def on_node_assets_changed(sender, instance=None, **kwargs): if isinstance(instance, Node): + logger.debug("Node assets change signal received") + # 当节点和资产关系发生改变时,过期资产数量缓存 + instance.expire_assets_amount() assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) if kwargs['action'] == 'post_add': - logger.debug("Node assets change signal received") # 重新关联系统用户和资产的关系 system_users = SystemUser.objects.filter(nodes=instance) for system_user in system_users: diff --git a/apps/assets/templates/assets/_asset_list_modal.html b/apps/assets/templates/assets/_asset_list_modal.html index ea8d59e49..fe50ac2b3 100644 --- a/apps/assets/templates/assets/_asset_list_modal.html +++ b/apps/assets/templates/assets/_asset_list_modal.html @@ -116,6 +116,7 @@ function initTree2() { $(document).ready(function(){ +}).on('show.bs.modal', function () { initTable2(); initTree2(); }) diff --git a/apps/assets/templates/assets/asset_list.html b/apps/assets/templates/assets/asset_list.html index 7e093688f..bd3a776b3 100644 --- a/apps/assets/templates/assets/asset_list.html +++ b/apps/assets/templates/assets/asset_list.html @@ -305,6 +305,9 @@ function onSelected(event, treeNode) { } function selectQueryNode() { + // TODO: 是否应该添加 + // 暂时忽略之前选中的内容 + return var query_node_id = $.getUrlParam("node"); var cookie_node_id = getCookie('node_selected'); var node; @@ -355,6 +358,9 @@ function onDrop(event, treeId, treeNodes, targetNode, moveType) { } function initTree() { + if (zTree) { + return + } var setting = { view: { dblClickExpand: false, @@ -387,6 +393,7 @@ function initTree() { }; var zNodes = []; + console.log("Get assets") $.get("{% url 'api-assets:node-list' %}", function(data, status){ $.each(data, function (index, value) { value["node_id"] = value["id"]; From ab6c88823d39f240fcdb3d9c01f3284c3f75e7b9 Mon Sep 17 00:00:00 2001 From: vkill Date: Mon, 17 Dec 2018 14:26:00 +0800 Subject: [PATCH 05/13] Support for TOTP valid_window configuration (#2187) --- apps/jumpserver/settings.py | 1 + apps/users/utils.py | 3 ++- config_docker.py | 7 +++++++ config_example.py | 3 +++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index fb595a4df..a58642877 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -356,6 +356,7 @@ FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755 # OTP settings OTP_ISSUER_NAME = CONFIG.OTP_ISSUER_NAME +OTP_VALID_WINDOW = CONFIG.OTP_VALID_WINDOW # Auth LDAP settings AUTH_LDAP = False diff --git a/apps/users/utils.py b/apps/users/utils.py index eac1c6f99..c998774b0 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -292,7 +292,8 @@ def check_otp_code(otp_secret_key, otp_code): if not otp_secret_key or not otp_code: return False totp = pyotp.TOTP(otp_secret_key) - return totp.verify(otp_code) + otp_valid_window = settings.OTP_VALID_WINDOW or 0 + return totp.verify(otp=otp_code, valid_window=otp_valid_window) def get_password_check_rules(): diff --git a/config_docker.py b/config_docker.py index ca322b4b8..643c11055 100644 --- a/config_docker.py +++ b/config_docker.py @@ -100,6 +100,9 @@ class Config: } AUTH_LDAP_START_TLS = False + # + # OTP_VALID_WINDOW = 0 + def __init__(self): pass @@ -200,6 +203,10 @@ class DockerConfig(Config): AUTH_LDAP_START_TLS = False + # + OTP_VALID_WINDOW = int(os.environ.get("OTP_VALID_WINDOW")) if os.environ.get("OTP_VALID_WINDOW") else 0 + + # Default using Config settings, you can write if/else for different env config = DockerConfig() diff --git a/config_example.py b/config_example.py index dfcc876a3..e37df23b0 100644 --- a/config_example.py +++ b/config_example.py @@ -90,6 +90,9 @@ class Config: # AUTH_OPENID_CLIENT_ID = 'client-id' # AUTH_OPENID_CLIENT_SECRET = 'client-secret' + # + # OTP_VALID_WINDOW = 0 + def __init__(self): pass From 517a27ea337a6b49d42cf9829901dd5419acb26d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=B9=BF?= Date: Mon, 17 Dec 2018 18:20:44 +0800 Subject: [PATCH 06/13] Node asset amount (#2191) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Bugfix] 修复错误 * [Update] 修改树结构,统一api --- apps/assets/api/asset.py | 48 ++-- apps/assets/api/node.py | 138 ++++++++---- apps/assets/models/asset.py | 30 +++ apps/assets/models/node.py | 43 +++- apps/assets/serializers/asset.py | 14 ++ apps/assets/serializers/node.py | 56 +---- apps/assets/templates/assets/asset_list.html | 132 +++++------ apps/assets/urls/api_urls.py | 2 + apps/assets/views/asset.py | 1 - apps/locale/zh/LC_MESSAGES/django.mo | Bin 60078 -> 59786 bytes apps/locale/zh/LC_MESSAGES/django.po | 210 +++++++----------- apps/perms/api.py | 6 +- apps/perms/hands.py | 2 +- apps/perms/serializers.py | 29 +++ .../perms/asset_permission_list.html | 79 ++----- 15 files changed, 393 insertions(+), 397 deletions(-) diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index 92a1775d0..986829def 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -41,40 +41,46 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet): pagination_class = LimitOffsetPagination permission_classes = (IsOrgAdminOrAppUser,) - def filter_node(self): + def filter_node(self, queryset): node_id = self.request.query_params.get("node_id") if not node_id: - return + return queryset node = get_object_or_404(Node, id=node_id) show_current_asset = self.request.query_params.get("show_current_asset") in ('1', 'true') - if node.is_root(): - if show_current_asset: - self.queryset = self.queryset.filter( - Q(nodes=node_id) | Q(nodes__isnull=True) - ) - return - if show_current_asset: - self.queryset = self.queryset.filter(nodes=node) + if node.is_root() and show_current_asset: + queryset = queryset.filter( + Q(nodes=node_id) | Q(nodes__isnull=True) + ) + elif node.is_root() and not show_current_asset: + pass + elif not node.is_root() and show_current_asset: + queryset = queryset.filter(nodes=node) else: - self.queryset = self.queryset.filter( + queryset = queryset.filter( nodes__key__regex='^{}(:[0-9]+)*$'.format(node.key), ) + return queryset - def filter_admin_user_id(self): + def filter_admin_user_id(self, queryset): admin_user_id = self.request.query_params.get('admin_user_id') - if admin_user_id: - admin_user = get_object_or_404(AdminUser, id=admin_user_id) - self.queryset = self.queryset.filter(admin_user=admin_user) + if not admin_user_id: + return queryset + admin_user = get_object_or_404(AdminUser, id=admin_user_id) + queryset = queryset.filter(admin_user=admin_user) + return queryset + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + queryset = self.filter_node(queryset) + queryset = self.filter_admin_user_id(queryset) + return queryset def get_queryset(self): - self.queryset = super().get_queryset()\ - .prefetch_related('labels', 'nodes')\ - .select_related('admin_user') - self.filter_admin_user_id() - self.filter_node() - return self.queryset.distinct() + queryset = super().get_queryset().distinct() + queryset = self.get_serializer_class().setup_eager_loading(queryset) + return queryset class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView): diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index 73313f222..84ba4c69f 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -17,13 +17,13 @@ from rest_framework import generics, mixins, viewsets from rest_framework.serializers import ValidationError from rest_framework.views import APIView from rest_framework.response import Response -from rest_framework_bulk import BulkModelViewSet from django.utils.translation import ugettext_lazy as _ from django.shortcuts import get_object_or_404 from common.utils import get_logger, get_object_or_none +from common.tree import TreeNodeSerializer from ..hands import IsOrgAdmin -from ..models import Node, Asset +from ..models import Node from ..tasks import update_assets_hardware_info_util, test_asset_connectability_util from .. import serializers @@ -33,7 +33,8 @@ __all__ = [ 'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi', 'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi', 'NodeAddChildrenApi', 'RefreshNodeHardwareInfoApi', - 'TestNodeConnectiveApi' + 'TestNodeConnectiveApi', 'NodeListAsTreeApi', + 'NodeChildrenAsTreeApi', ] @@ -42,22 +43,89 @@ class NodeViewSet(viewsets.ModelViewSet): permission_classes = (IsOrgAdmin,) 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() - def update(self, request, *args, **kwargs): - node = self.get_object() - if node.is_root(): - node_value = node.value - post_value = request.data.get('value') - if node_value != post_value: - return Response( - {"msg": _("You can't update the root node name")}, - status=400 - ) - return super().update(request, *args, **kwargs) +class NodeListAsTreeApi(generics.ListAPIView): + """ + 获取节点列表树 + [ + { + "id": "", + "name": "", + "pId": "", + "meta": "" + } + ] + """ + permission_classes = (IsOrgAdmin,) + serializer_class = TreeNodeSerializer + + def get_queryset(self): + queryset = [node.as_tree_node() for node in Node.objects.all()] + return queryset + + def filter_queryset(self, queryset): + if self.request.query_params.get('refresh', '0') == '1': + queryset = self.refresh_nodes(queryset) + return queryset + + @staticmethod + def refresh_nodes(queryset): + Node.expire_nodes_assets_amount() + Node.expire_nodes_full_value() + return queryset + + +class NodeChildrenAsTreeApi(generics.ListAPIView): + """ + 节点子节点作为树返回, + [ + { + "id": "", + "name": "", + "pId": "", + "meta": "" + } + ] + + """ + permission_classes = (IsOrgAdmin,) + serializer_class = TreeNodeSerializer + node = None + is_root = False + + def get_queryset(self): + node_key = self.request.query_params.get('key') + if node_key: + self.node = Node.objects.get(key=node_key) + queryset = self.node.get_children(with_self=False) + else: + self.is_root = True + self.node = Node.root() + queryset = list(self.node.get_children(with_self=True)) + nodes_invalid = Node.objects.exclude(key__startswith=self.node.key) + queryset.extend(list(nodes_invalid)) + queryset = [node.as_tree_node() for node in queryset] + return queryset + + def filter_assets(self, queryset): + include_assets = self.request.query_params.get('assets', '0') == '1' + if not include_assets: + return queryset + assets = self.node.get_assets() + for asset in assets: + queryset.append(asset.as_tree_node(self.node)) + return queryset + + def filter_queryset(self, queryset): + queryset = self.filter_assets(queryset) + queryset = self.filter_refresh_nodes(queryset) + return queryset + + def filter_refresh_nodes(self, queryset): + if self.request.query_params.get('refresh', '0') == '1': + Node.expire_nodes_assets_amount() + Node.expire_nodes_full_value() + return queryset class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView): @@ -66,19 +134,10 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView): serializer_class = serializers.NodeSerializer instance = None - def counter(self): - values = [ - child.value[child.value.rfind(' '):] - for child in self.get_object().get_children() - if child.value.startswith("新节点 ") - ] - values = [int(value) for value in values if value.strip().isdigit()] - count = max(values)+1 if values else 1 - return count - def post(self, request, *args, **kwargs): + instance = self.get_object() if not request.data.get("value"): - request.data["value"] = _("New node {}").format(self.counter()) + request.data["value"] = instance.get_next_child_preset_name() return super().post(request, *args, **kwargs) def create(self, request, *args, **kwargs): @@ -90,10 +149,7 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView): 'The same level node name cannot be the same' ) node = instance.create_child(value=value) - return Response( - {"id": node.id, "key": node.key, "value": node.value}, - status=201, - ) + return Response(self.serializer_class(instance=node).data, status=201) def get_object(self): pk = self.kwargs.get('pk') or self.request.query_params.get('id') @@ -106,7 +162,6 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView): def get_queryset(self): queryset = [] query_all = self.request.query_params.get("all") - query_assets = self.request.query_params.get('assets') node = self.get_object() if node is None: @@ -119,23 +174,8 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView): else: children = node.get_children() queryset.extend(list(children)) - - if query_assets: - assets = node.get_assets() - for asset in assets: - node_fake = Node() - node_fake.assets__count = 0 - node_fake.id = asset.id - node_fake.is_node = False - node_fake.key = node.key + ':0' - node_fake.value = asset.hostname - queryset.append(node_fake) - queryset = sorted(queryset, key=lambda x: x.is_node, reverse=True) return queryset - def get(self, request, *args, **kwargs): - return super().list(request, *args, **kwargs) - class NodeAssetsApi(generics.ListAPIView): permission_classes = (IsOrgAdmin,) diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index cde9cde2e..06fb29a51 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -255,6 +255,36 @@ class Asset(OrgModelMixin): }) return data + def as_tree_node(self, parent_node): + from common.tree import TreeNode + icon_skin = 'file' + if self.platform.lower() == 'windows': + icon_skin = 'windows' + elif self.platform.lower() == 'linux': + icon_skin = 'linux' + data = { + 'id': str(self.id), + 'name': self.hostname, + 'title': self.ip, + 'pId': parent_node.key, + 'isParent': False, + 'open': False, + 'iconSkin': icon_skin, + 'meta': { + 'type': 'asset', + 'asset': { + 'id': self.id, + 'hostname': self.hostname, + 'ip': self.ip, + 'port': self.port, + 'platform': self.platform, + 'protocol': self.protocol, + } + } + } + tree_node = TreeNode(**data) + return tree_node + class Meta: unique_together = [('org_id', 'hostname')] verbose_name = _("Asset") diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index 47a835861..881b041d7 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -5,6 +5,7 @@ import uuid from django.db import models, transaction from django.db.models import Q from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext from django.core.cache import cache from orgs.mixins import OrgModelMixin @@ -103,6 +104,15 @@ class Node(OrgModelMixin): key = self._full_value_cache_key.format(self.key) cache.delete_pattern(key+'*') + @classmethod + def expire_nodes_full_value(cls, nodes=None): + if nodes: + for node in nodes: + node.expire_full_value() + return + key = cls._full_value_cache_key.format('*') + cache.delete_pattern(key+'*') + @property def level(self): return len(self.key.split(':')) @@ -113,6 +123,17 @@ class Node(OrgModelMixin): self.save() return "{}:{}".format(self.key, mark) + def get_next_child_preset_name(self): + name = ugettext("New node") + values = [ + child.value[child.value.rfind(' '):] + for child in self.get_children() + if child.value.startswith(name) + ] + values = [int(value) for value in values if value.strip().isdigit()] + count = max(values) + 1 if values else 1 + return '{} {}'.format(name, count) + def create_child(self, value): with transaction.atomic(): child_key = self.get_next_child_key() @@ -162,7 +183,7 @@ class Node(OrgModelMixin): pattern = r'^{0}$|^{0}:'.format(self.key) args = [] kwargs = {} - if self.is_default_node(): + if self.is_root(): args.append(Q(nodes__key__regex=pattern) | Q(nodes=None)) else: kwargs['nodes__key__regex'] = pattern @@ -256,6 +277,26 @@ class Node(OrgModelMixin): defaults = {'value': 'Default'} return cls.objects.get_or_create(defaults=defaults, key='1') + def as_tree_node(self): + from common.tree import TreeNode + from ..serializers import NodeSerializer + name = '{} ({})'.format(self.value, self.assets_amount) + node_serializer = NodeSerializer(instance=self) + data = { + 'id': self.key, + 'name': name, + 'title': name, + 'pId': self.parent_key, + 'isParent': True, + 'open': self.is_root(), + 'meta': { + 'node': node_serializer.data, + 'type': 'node' + } + } + tree_node = TreeNode(**data) + return tree_node + @classmethod def generate_fake(cls, count=100): import random diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index dae9ab9af..1066ae0b7 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -9,6 +9,7 @@ from .system_user import AssetSystemUserSerializer __all__ = [ 'AssetSerializer', 'AssetGrantedSerializer', 'MyAssetGrantedSerializer', + 'AssetAsNodeSerializer', ] @@ -22,6 +23,13 @@ class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer): fields = '__all__' validators = [] + @classmethod + def setup_eager_loading(cls, queryset): + """ Perform necessary eager loading of data. """ + queryset = queryset.prefetch_related('labels', 'nodes')\ + .select_related('admin_user') + return queryset + def get_field_names(self, declared_fields, info): fields = super().get_field_names(declared_fields, info) fields.extend([ @@ -30,6 +38,12 @@ class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer): return fields +class AssetAsNodeSerializer(serializers.ModelSerializer): + class Meta: + model = Asset + fields = ['id', 'hostname', 'ip', 'port', 'platform', 'protocol'] + + class AssetGrantedSerializer(serializers.ModelSerializer): """ 被授权资产的数据结构 diff --git a/apps/assets/serializers/node.py b/apps/assets/serializers/node.py index f44ff44d6..79c573c60 100644 --- a/apps/assets/serializers/node.py +++ b/apps/assets/serializers/node.py @@ -8,76 +8,33 @@ from .asset import AssetGrantedSerializer __all__ = [ - 'NodeSerializer', "NodeGrantedSerializer", "NodeAddChildrenSerializer", + 'NodeSerializer', "NodeAddChildrenSerializer", "NodeAssetsSerializer", ] -class NodeGrantedSerializer(BulkSerializerMixin, serializers.ModelSerializer): - """ - 授权资产组 - """ - assets_granted = AssetGrantedSerializer(many=True, read_only=True) - assets_amount = serializers.SerializerMethodField() - parent = serializers.SerializerMethodField() - name = serializers.SerializerMethodField() - - class Meta: - model = Node - fields = [ - 'id', 'key', 'name', 'value', 'parent', - 'assets_granted', 'assets_amount', 'org_id', - ] - - @staticmethod - def get_assets_amount(obj): - return len(obj.assets_granted) - - @staticmethod - def get_name(obj): - return obj.name - - @staticmethod - def get_parent(obj): - return obj.parent.id - - class NodeSerializer(serializers.ModelSerializer): - assets_amount = serializers.IntegerField() - tree_id = serializers.SerializerMethodField() - tree_parent = serializers.SerializerMethodField() + assets_amount = serializers.IntegerField(read_only=True) class Meta: model = Node fields = [ - 'id', 'key', 'value', 'assets_amount', - 'is_node', 'org_id', 'tree_id', 'tree_parent', + 'id', 'key', 'value', 'assets_amount', 'org_id', ] read_only_fields = [ - 'id', 'key', 'assets_amount', 'is_node', - 'org_id', + 'id', 'key', 'assets_amount', 'org_id', ] - list_serializer_class = BulkListSerializer - def validate(self, data): - value = data.get('value') + def validate_value(self, data): instance = self.instance if self.instance else Node.root() children = instance.parent.get_children().exclude(key=instance.key) values = [child.value for child in children] - if value in values: + if data in values: raise serializers.ValidationError( 'The same level node name cannot be the same' ) return data - @staticmethod - def get_tree_id(obj): - return obj.key - - @staticmethod - def get_tree_parent(obj): - return obj.parent_key - class NodeAssetsSerializer(serializers.ModelSerializer): assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all()) @@ -89,3 +46,4 @@ class NodeAssetsSerializer(serializers.ModelSerializer): class NodeAddChildrenSerializer(serializers.Serializer): nodes = serializers.ListField() + diff --git a/apps/assets/templates/assets/asset_list.html b/apps/assets/templates/assets/asset_list.html index bd3a776b3..4a77f1de5 100644 --- a/apps/assets/templates/assets/asset_list.html +++ b/apps/assets/templates/assets/asset_list.html @@ -136,6 +136,7 @@
  • +
  • {% trans 'Refresh' %}
  • @@ -147,6 +148,8 @@ {% endblock %} From 1293d72189fdd8d3b565ddccc9052d2809661a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=B9=BF?= Date: Tue, 18 Dec 2018 11:29:21 +0800 Subject: [PATCH 08/13] Session task (#2196) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Bugfix] 修复错误 * [Update] 增加会话定期清理 --- apps/common/forms.py | 9 ++--- apps/common/signals_handler.py | 9 +++-- apps/jumpserver/conf.py | 1 + apps/jumpserver/settings.py | 2 ++ apps/locale/zh/LC_MESSAGES/django.mo | Bin 59907 -> 60196 bytes apps/locale/zh/LC_MESSAGES/django.po | 50 ++++++++++++++++----------- apps/terminal/api/v1/session.py | 41 ++++------------------ apps/terminal/models.py | 32 +++++++++++++++++ apps/terminal/tasks.py | 32 ++++++++++++++++- 9 files changed, 111 insertions(+), 65 deletions(-) diff --git a/apps/common/forms.py b/apps/common/forms.py index d052819b6..f29d19ec3 100644 --- a/apps/common/forms.py +++ b/apps/common/forms.py @@ -15,8 +15,6 @@ class BaseForm(forms.Form): super().__init__(*args, **kwargs) for name, field in self.fields.items(): value = getattr(settings, name, None) - # django_value = getattr(settings, name) if hasattr(settings, name) else None - if value is None: # and django_value is None: continue @@ -24,8 +22,6 @@ class BaseForm(forms.Form): if isinstance(value, dict): value = json.dumps(value) initial_value = value - # elif django_value is False or django_value: - # initial_value = django_value else: initial_value = '' field.initial = initial_value @@ -157,6 +153,11 @@ class TerminalSettingForm(BaseForm): TERMINAL_ASSET_LIST_PAGE_SIZE = forms.ChoiceField( choices=PAGE_SIZE_CHOICES, initial='auto', label=_("List page size"), ) + TERMINAL_SESSION_KEEP_DURATION = forms.IntegerField( + label=_("Session keep duration"), + help_text=_("Units: days, Session, record, command will be delete " + "if more than duration, only in database") + ) class TerminalCommandStorage(BaseForm): diff --git a/apps/common/signals_handler.py b/apps/common/signals_handler.py index 207dd2ce5..96142e394 100644 --- a/apps/common/signals_handler.py +++ b/apps/common/signals_handler.py @@ -26,21 +26,20 @@ def refresh_settings_on_changed(sender, instance=None, **kwargs): def refresh_all_settings_on_django_ready(sender, **kwargs): logger.debug("Receive django ready signal") logger.debug(" - fresh all settings") - CACHE_KEY_PREFIX = '_SETTING_' + cache_key_prefix = '_SETTING_' def monkey_patch_getattr(self, name): - key = CACHE_KEY_PREFIX + name + key = cache_key_prefix + name cached = cache.get(key) if cached is not None: return cached if self._wrapped is empty: self._setup(name) val = getattr(self._wrapped, name) - # self.__dict__[name] = val # Never set it return val def monkey_patch_setattr(self, name, value): - key = CACHE_KEY_PREFIX + name + key = cache_key_prefix + name cache.set(key, value, None) if name == '_wrapped': self.__dict__.clear() @@ -51,7 +50,7 @@ def refresh_all_settings_on_django_ready(sender, **kwargs): def monkey_patch_delattr(self, name): super(LazySettings, self).__delattr__(name) self.__dict__.pop(name, None) - key = CACHE_KEY_PREFIX + name + key = cache_key_prefix + name cache.delete(key) try: diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index b1d33c3cd..d537b9e16 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -318,6 +318,7 @@ defaults = { 'TERMINAL_HEARTBEAT_INTERVAL': 5, 'TERMINAL_ASSET_LIST_SORT_BY': 'hostname', 'TERMINAL_ASSET_LIST_PAGE_SIZE': 'auto', + 'TERMINAL_SESSION_KEEP_DURATION': 9999, } diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index a58642877..167de3180 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -467,6 +467,7 @@ DEFAULT_TERMINAL_REPLAY_STORAGE = { TERMINAL_REPLAY_STORAGE = { } + SECURITY_MFA_AUTH = False SECURITY_LOGIN_LIMIT_COUNT = 7 SECURITY_LOGIN_LIMIT_TIME = 30 # Unit: minute @@ -490,6 +491,7 @@ TERMINAL_PUBLIC_KEY_AUTH = CONFIG.TERMINAL_PUBLIC_KEY_AUTH TERMINAL_HEARTBEAT_INTERVAL = CONFIG.TERMINAL_HEARTBEAT_INTERVAL TERMINAL_ASSET_LIST_SORT_BY = CONFIG.TERMINAL_ASSET_LIST_SORT_BY TERMINAL_ASSET_LIST_PAGE_SIZE = CONFIG.TERMINAL_ASSET_LIST_PAGE_SIZE +TERMINAL_SESSION_KEEP_DURATION = CONFIG.TERMINAL_SESSION_KEEP_DURATION # Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html BOOTSTRAP3 = { diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index bbbee180bb402790b79c2a3e0ad2573935d7d393..2192ac16e338d572c34aa4c8f2faf64acb2b7b62 100644 GIT binary patch delta 15877 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 QGG5R8GdSbg<+^$P4=%nw82|tP diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 7f42c17be..fda3df537 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/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-12-17 20:06+0800\n" +"POT-Creation-Date: 2018-12-18 10:13+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: Jumpserver team\n" @@ -1976,47 +1976,57 @@ msgstr "资产列表排序" #: common/forms.py:158 msgid "List page size" -msgstr "资产列表页面大小" +msgstr "资产分页每页数量" -#: common/forms.py:170 +#: common/forms.py:161 +msgid "Session keep duration" +msgstr "会话保留时长" + +#: common/forms.py:162 +msgid "" +"Units: days, Session, record, command will be delete if more than duration, " +"only in database" +msgstr "单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss等不受影响)" + +#: common/forms.py:175 msgid "MFA Secondary certification" msgstr "MFA 二次认证" -#: common/forms.py:172 +#: common/forms.py:177 msgid "" "After opening, the user login must use MFA secondary authentication (valid " "for all users, including administrators)" msgstr "开启后,用户登录必须使用MFA二次认证(对所有用户有效,包括管理员)" -#: common/forms.py:179 +#: common/forms.py:184 msgid "Limit the number of login failures" msgstr "限制登录失败次数" -#: common/forms.py:184 +#: common/forms.py:189 msgid "No logon interval" msgstr "禁止登录时间间隔" -#: common/forms.py:186 +#: common/forms.py:191 msgid "" "Tip: (unit/minute) if the user has failed to log in for a limited number of " "times, no login is allowed during this time interval." msgstr "" "提示:(单位:分)当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录" -#: common/forms.py:193 +#: common/forms.py:198 msgid "Connection max idle time" msgstr "SSH最大空闲时间" -#: common/forms.py:195 +#: common/forms.py:200 msgid "" "If idle time more than it, disconnect connection(only ssh now) Unit: minute" msgstr "提示:(单位:分)如果超过该配置没有操作,连接会被断开(仅ssh)" -#: common/forms.py:201 +#: common/forms.py:206 msgid "Password expiration time" msgstr "密码过期时间" -#: common/forms.py:204 +#: common/forms.py:209 msgid "" "Tip: (unit: day) If the user does not update the password during the time, " "the user password will expire failure;The password expiration reminder mail " @@ -2026,45 +2036,45 @@ msgstr "" "提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期" "提醒邮件将在密码过期前5天内由系统(每天)自动发送给用户" -#: common/forms.py:213 +#: common/forms.py:218 msgid "Password minimum length" msgstr "密码最小长度 " -#: common/forms.py:219 +#: common/forms.py:224 msgid "Must contain capital letters" msgstr "必须包含大写字母" -#: common/forms.py:221 +#: common/forms.py:226 msgid "" "After opening, the user password changes and resets must contain uppercase " "letters" msgstr "开启后,用户密码修改、重置必须包含大写字母" -#: common/forms.py:227 +#: common/forms.py:232 msgid "Must contain lowercase letters" msgstr "必须包含小写字母" -#: common/forms.py:228 +#: common/forms.py:233 msgid "" "After opening, the user password changes and resets must contain lowercase " "letters" msgstr "开启后,用户密码修改、重置必须包含小写字母" -#: common/forms.py:234 +#: common/forms.py:239 msgid "Must contain numeric characters" msgstr "必须包含数字字符" -#: common/forms.py:235 +#: common/forms.py:240 msgid "" "After opening, the user password changes and resets must contain numeric " "characters" msgstr "开启后,用户密码修改、重置必须包含数字字符" -#: common/forms.py:241 +#: common/forms.py:246 msgid "Must contain special characters" msgstr "必须包含特殊字符" -#: common/forms.py:242 +#: common/forms.py:247 msgid "" "After opening, the user password changes and resets must contain special " "characters" diff --git a/apps/terminal/api/v1/session.py b/apps/terminal/api/v1/session.py index de3e09a55..5788df775 100644 --- a/apps/terminal/api/v1/session.py +++ b/apps/terminal/api/v1/session.py @@ -94,44 +94,15 @@ class SessionReplayViewSet(viewsets.ViewSet): serializer_class = serializers.ReplaySerializer permission_classes = (IsOrgAdminOrAppUser,) session = None - upload_to = 'replay' # 仅添加到本地存储中 - - def get_session_path(self, version=2): - """ - 获取session日志的文件路径 - :param version: 原来后缀是 .gz,为了统一新版本改为 .replay.gz - :return: - """ - suffix = '.replay.gz' - if version == 1: - suffix = '.gz' - date = self.session.date_start.strftime('%Y-%m-%d') - return os.path.join(date, str(self.session.id) + suffix) - - def get_local_path(self, version=2): - session_path = self.get_session_path(version=version) - if version == 2: - local_path = os.path.join(self.upload_to, session_path) - else: - local_path = session_path - return local_path - - def save_to_storage(self, f): - local_path = self.get_local_path() - try: - name = default_storage.save(local_path, f) - return name, None - except OSError as e: - return None, e def create(self, request, *args, **kwargs): session_id = kwargs.get('pk') - self.session = get_object_or_404(Session, id=session_id) + session = get_object_or_404(Session, id=session_id) serializer = self.serializer_class(data=request.data) if serializer.is_valid(): file = serializer.validated_data['file'] - name, err = self.save_to_storage(file) + name, err = session.save_to_storage(file) if not name: msg = "Failed save replay `{}`: {}".format(session_id, err) logger.error(msg) @@ -145,7 +116,7 @@ class SessionReplayViewSet(viewsets.ViewSet): def retrieve(self, request, *args, **kwargs): session_id = kwargs.get('pk') - self.session = get_object_or_404(Session, id=session_id) + session = get_object_or_404(Session, id=session_id) data = { 'type': 'guacamole' if self.session.protocol == 'rdp' else 'json', @@ -153,9 +124,9 @@ class SessionReplayViewSet(viewsets.ViewSet): } # 新版本和老版本的文件后缀不同 - session_path = self.get_session_path() # 存在外部存储上的路径 - local_path = self.get_local_path() - local_path_v1 = self.get_local_path(version=1) + session_path = session.get_rel_replay_path() # 存在外部存储上的路径 + local_path = session.get_local_path() + local_path_v1 = session.get_local_path(version=1) # 去default storage中查找 for _local_path in (local_path, local_path_v1, session_path): diff --git a/apps/terminal/models.py b/apps/terminal/models.py index 661b4a57d..6491bdf35 100644 --- a/apps/terminal/models.py +++ b/apps/terminal/models.py @@ -1,11 +1,13 @@ from __future__ import unicode_literals +import os 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 django.core.files.storage import default_storage from users.models import User from orgs.mixins import OrgModelMixin @@ -148,6 +150,36 @@ class Session(OrgModelMixin): date_start = models.DateTimeField(verbose_name=_("Date start"), db_index=True, default=timezone.now) date_end = models.DateTimeField(verbose_name=_("Date end"), null=True) + upload_to = 'replay' + + def get_rel_replay_path(self, version=2): + """ + 获取session日志的文件路径 + :param version: 原来后缀是 .gz,为了统一新版本改为 .replay.gz + :return: + """ + suffix = '.replay.gz' + if version == 1: + suffix = '.gz' + date = self.date_start.strftime('%Y-%m-%d') + return os.path.join(date, str(self.id) + suffix) + + def get_local_path(self, version=2): + rel_path = self.get_rel_replay_path(version=version) + if version == 2: + local_path = os.path.join(self.upload_to, rel_path) + else: + local_path = rel_path + return local_path + + def save_to_storage(self, f): + local_path = self.get_local_path() + try: + name = default_storage.save(local_path, f) + return name, None + except OSError as e: + return None, e + class Meta: db_table = "terminal_session" ordering = ["-date_start"] diff --git a/apps/terminal/tasks.py b/apps/terminal/tasks.py index 4e57c5f5e..77aa66226 100644 --- a/apps/terminal/tasks.py +++ b/apps/terminal/tasks.py @@ -4,15 +4,20 @@ import datetime from celery import shared_task +from celery.utils.log import get_task_logger from django.utils import timezone +from django.conf import settings +from django.core.files.storage import default_storage + from ops.celery.utils import register_as_period_task, after_app_ready_start, \ after_app_shutdown_clean -from .models import Status, Session +from .models import Status, Session, Command CACHE_REFRESH_INTERVAL = 10 RUNNING = False +logger = get_task_logger(__name__) @shared_task @@ -34,3 +39,28 @@ def clean_orphan_session(): if not session.terminal or not session.terminal.is_active: session.is_finished = True session.save() + + +@shared_task +@register_as_period_task(interval=3600*24) +@after_app_ready_start +@after_app_shutdown_clean +def clean_expired_session_period(): + logger.info("Start clean expired session record, commands and replay") + days = settings.TERMINAL_SESSION_KEEP_DURATION + dt = timezone.now() - timezone.timedelta(days=days) + expired_sessions = Session.objects.filter(date_start__lt=dt) + for session in expired_sessions: + logger.info("Clean session: {}".format(session.id)) + Command.objects.filter(session=str(session.id)).delete() + # 删除录像文件 + session_path = session.get_rel_replay_path() + local_path = session.get_local_path() + local_path_v1 = session.get_local_path(version=1) + + # 去default storage中查找 + for _local_path in (local_path, local_path_v1, session_path): + if default_storage.exists(_local_path): + default_storage.delete(_local_path) + # 删除session记录 + session.delete() From a609f1707834aee935096bdb8802acc2cd857bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=B9=BF?= Date: Tue, 18 Dec 2018 17:28:45 +0800 Subject: [PATCH 09/13] [Update] Stash it (#2197) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] Stash it * [Bugfix] 修复错误 * [Update] 修改jms --- apps/assets/api/admin_user.py | 25 +- apps/assets/api/asset.py | 6 +- apps/assets/api/node.py | 4 +- apps/assets/api/system_user.py | 15 +- apps/assets/forms/user.py | 6 +- apps/assets/models/asset.py | 107 +++---- apps/assets/models/base.py | 7 + apps/assets/models/user.py | 99 ++++-- apps/assets/serializers/asset.py | 10 +- apps/assets/serializers/system_user.py | 15 +- apps/assets/signals_handler.py | 6 +- apps/assets/tasks.py | 289 +++++++----------- .../templates/assets/admin_user_assets.html | 47 ++- .../templates/assets/system_user_asset.html | 10 +- .../templates/assets/system_user_list.html | 2 +- apps/assets/urls/api_urls.py | 14 +- apps/assets/views/admin_user.py | 2 +- apps/jumpserver/conf.py | 3 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 60196 -> 60280 bytes apps/locale/zh/LC_MESSAGES/django.po | 18 +- apps/ops/api/adhoc.py | 4 +- apps/ops/views/adhoc.py | 2 +- apps/users/api/user.py | 3 + 23 files changed, 361 insertions(+), 333 deletions(-) diff --git a/apps/assets/api/admin_user.py b/apps/assets/api/admin_user.py index 8d30ee9d9..263d669fd 100644 --- a/apps/assets/api/admin_user.py +++ b/apps/assets/api/admin_user.py @@ -14,6 +14,7 @@ # limitations under the License. from django.db import transaction +from django.shortcuts import get_object_or_404 from rest_framework import generics from rest_framework.response import Response from rest_framework_bulk import BulkModelViewSet @@ -24,13 +25,14 @@ from common.utils import get_logger from ..hands import IsOrgAdmin from ..models import AdminUser, Asset from .. import serializers -from ..tasks import test_admin_user_connectability_manual +from ..tasks import test_admin_user_connectivity_manual logger = get_logger(__file__) __all__ = [ 'AdminUserViewSet', 'ReplaceNodesAdminUserApi', 'AdminUserTestConnectiveApi', 'AdminUserAuthApi', + 'AdminUserAssetsListView', ] @@ -81,12 +83,29 @@ class ReplaceNodesAdminUserApi(generics.UpdateAPIView): class AdminUserTestConnectiveApi(generics.RetrieveAPIView): """ - Test asset admin user connectivity + Test asset admin user assets_connectivity """ queryset = AdminUser.objects.all() permission_classes = (IsOrgAdmin,) def retrieve(self, request, *args, **kwargs): admin_user = self.get_object() - task = test_admin_user_connectability_manual.delay(admin_user) + task = test_admin_user_connectivity_manual.delay(admin_user) return Response({"task": task.id}) + + +class AdminUserAssetsListView(generics.ListAPIView): + permission_classes = (IsOrgAdmin,) + serializer_class = serializers.AssetSimpleSerializer + pagination_class = LimitOffsetPagination + filter_fields = ("hostname", "ip") + http_method_names = ['get'] + search_fields = filter_fields + + def get_object(self): + pk = self.kwargs.get('pk') + return get_object_or_404(AdminUser, pk=pk) + + def get_queryset(self): + admin_user = self.get_object() + return admin_user.get_related_assets() diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index 986829def..cd343b2a5 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -17,7 +17,7 @@ from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser from ..models import Asset, AdminUser, Node from .. import serializers from ..tasks import update_asset_hardware_info_manual, \ - test_asset_connectability_manual + test_asset_connectivity_manual from ..utils import LabelFilter @@ -109,7 +109,7 @@ class AssetRefreshHardwareApi(generics.RetrieveAPIView): class AssetAdminUserTestApi(generics.RetrieveAPIView): """ - Test asset admin user connectivity + Test asset admin user assets_connectivity """ queryset = Asset.objects.all() permission_classes = (IsOrgAdmin,) @@ -117,7 +117,7 @@ class AssetAdminUserTestApi(generics.RetrieveAPIView): def retrieve(self, request, *args, **kwargs): asset_id = kwargs.get('pk') asset = get_object_or_404(Asset, pk=asset_id) - task = test_asset_connectability_manual.delay(asset) + task = test_asset_connectivity_manual.delay(asset) return Response({"task": task.id}) diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index 84ba4c69f..4295b2618 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -24,7 +24,7 @@ from common.utils import get_logger, get_object_or_none from common.tree import TreeNodeSerializer from ..hands import IsOrgAdmin from ..models import Node -from ..tasks import update_assets_hardware_info_util, test_asset_connectability_util +from ..tasks import update_assets_hardware_info_util, test_asset_connectivity_util from .. import serializers @@ -273,5 +273,5 @@ class TestNodeConnectiveApi(APIView): assets = node.assets.all() # task_name = _("测试节点下资产是否可连接: {}".format(node.name)) task_name = _("Test if the assets under the node are connectable: {}".format(node.name)) - task = test_asset_connectability_util.delay(assets, task_name=task_name) + task = test_asset_connectivity_util.delay(assets, task_name=task_name) return Response({"task": task.id}) diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index 3c1d0b3bd..e66e4bfc9 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -24,8 +24,8 @@ from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser from ..models import SystemUser, Asset from .. import serializers from ..tasks import push_system_user_to_assets_manual, \ - test_system_user_connectability_manual, push_system_user_a_asset_manual, \ - test_system_user_connectability_a_asset + test_system_user_connectivity_manual, push_system_user_a_asset_manual, \ + test_system_user_connectivity_a_asset logger = get_logger(__file__) @@ -33,7 +33,7 @@ __all__ = [ 'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserPushApi', 'SystemUserTestConnectiveApi', 'SystemUserAssetsListView', 'SystemUserPushToAssetApi', - 'SystemUserTestAssetConnectabilityApi', 'SystemUserCommandFilterRuleListApi', + 'SystemUserTestAssetConnectivityApi', 'SystemUserCommandFilterRuleListApi', ] @@ -93,15 +93,16 @@ class SystemUserTestConnectiveApi(generics.RetrieveAPIView): def retrieve(self, request, *args, **kwargs): system_user = self.get_object() - task = test_system_user_connectability_manual.delay(system_user) + task = test_system_user_connectivity_manual.delay(system_user) return Response({"task": task.id}) class SystemUserAssetsListView(generics.ListAPIView): permission_classes = (IsOrgAdmin,) - serializer_class = serializers.AssetSerializer + serializer_class = serializers.AssetSimpleSerializer pagination_class = LimitOffsetPagination filter_fields = ("hostname", "ip") + http_method_names = ['get'] search_fields = filter_fields def get_object(self): @@ -125,7 +126,7 @@ class SystemUserPushToAssetApi(generics.RetrieveAPIView): return Response({"task": task.id}) -class SystemUserTestAssetConnectabilityApi(generics.RetrieveAPIView): +class SystemUserTestAssetConnectivityApi(generics.RetrieveAPIView): queryset = SystemUser.objects.all() permission_classes = (IsOrgAdmin,) @@ -133,7 +134,7 @@ class SystemUserTestAssetConnectabilityApi(generics.RetrieveAPIView): system_user = self.get_object() asset_id = self.kwargs.get('aid') asset = get_object_or_404(Asset, id=asset_id) - task = test_system_user_connectability_a_asset.delay(system_user, asset) + task = test_system_user_connectivity_a_asset.delay(system_user, asset) return Response({"task": task.id}) diff --git a/apps/assets/forms/user.py b/apps/assets/forms/user.py index f5c62a4ff..70fcdafad 100644 --- a/apps/assets/forms/user.py +++ b/apps/assets/forms/user.py @@ -99,8 +99,8 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm): auto_generate_key = self.cleaned_data.get('auto_generate_key', False) private_key, public_key = super().gen_keys() - if login_mode == SystemUser.MANUAL_LOGIN or \ - protocol in [SystemUser.RDP_PROTOCOL, SystemUser.TELNET_PROTOCOL]: + if login_mode == SystemUser.LOGIN_MANUAL or \ + protocol in [SystemUser.PROTOCOL_RDP, SystemUser.PROTOCOL_TELNET]: system_user.auto_push = 0 auto_generate_key = False system_user.save() @@ -124,7 +124,7 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm): validated = super().is_valid() username = self.cleaned_data.get('username') login_mode = self.cleaned_data.get('login_mode') - if login_mode == SystemUser.AUTO_LOGIN and not username: + if login_mode == SystemUser.LOGIN_AUTO and not username: self.add_error( "username", _('* Automatic login mode,' ' must fill in the username.') diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index 06fb29a51..ccff0fff5 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -13,7 +13,6 @@ from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from django.core.cache import cache -from ..const import ASSET_ADMIN_CONN_CACHE_KEY from .user import AdminUser, SystemUser from orgs.mixins import OrgModelMixin, OrgManager @@ -75,63 +74,48 @@ class Asset(OrgModelMixin): protocol = models.CharField(max_length=128, default=SSH_PROTOCOL, choices=PROTOCOL_CHOICES, verbose_name=_('Protocol')) port = models.IntegerField(default=22, verbose_name=_('Port')) platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform')) - domain = models.ForeignKey("assets.Domain", null=True, blank=True, - related_name='assets', verbose_name=_("Domain"), - on_delete=models.SET_NULL) - nodes = models.ManyToManyField('assets.Node', default=default_node, - related_name='assets', - verbose_name=_("Nodes")) + domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), on_delete=models.SET_NULL) + nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes")) is_active = models.BooleanField(default=True, verbose_name=_('Is active')) # Auth - admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, - null=True, verbose_name=_("Admin user")) + admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, null=True, verbose_name=_("Admin user")) # Some information public_ip = models.GenericIPAddressField(max_length=32, blank=True, null=True, verbose_name=_('Public IP')) number = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Asset number')) # Collect - vendor = models.CharField(max_length=64, null=True, blank=True, - verbose_name=_('Vendor')) - model = models.CharField(max_length=54, null=True, blank=True, - verbose_name=_('Model')) - sn = models.CharField(max_length=128, null=True, blank=True, - verbose_name=_('Serial number')) + vendor = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Vendor')) + model = models.CharField(max_length=54, null=True, blank=True, verbose_name=_('Model')) + sn = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Serial number')) - cpu_model = models.CharField(max_length=64, null=True, blank=True, - verbose_name=_('CPU model')) + cpu_model = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('CPU model')) cpu_count = models.IntegerField(null=True, verbose_name=_('CPU count')) cpu_cores = models.IntegerField(null=True, verbose_name=_('CPU cores')) cpu_vcpus = models.IntegerField(null=True, verbose_name=_('CPU vcpus')) - memory = models.CharField(max_length=64, null=True, blank=True, - verbose_name=_('Memory')) - disk_total = models.CharField(max_length=1024, null=True, blank=True, - verbose_name=_('Disk total')) - disk_info = models.CharField(max_length=1024, null=True, blank=True, - verbose_name=_('Disk info')) + memory = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Memory')) + disk_total = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk total')) + disk_info = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk info')) - os = models.CharField(max_length=128, null=True, blank=True, - verbose_name=_('OS')) - os_version = models.CharField(max_length=16, null=True, blank=True, - verbose_name=_('OS version')) - os_arch = models.CharField(max_length=16, blank=True, null=True, - verbose_name=_('OS arch')) - hostname_raw = models.CharField(max_length=128, blank=True, null=True, - verbose_name=_('Hostname raw')) + os = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('OS')) + os_version = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('OS version')) + os_arch = models.CharField(max_length=16, blank=True, null=True, verbose_name=_('OS arch')) + hostname_raw = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Hostname raw')) - labels = models.ManyToManyField('assets.Label', blank=True, - related_name='assets', - verbose_name=_("Labels")) - created_by = models.CharField(max_length=32, null=True, blank=True, - verbose_name=_('Created by')) - date_created = models.DateTimeField(auto_now_add=True, null=True, - blank=True, - verbose_name=_('Date created')) - comment = models.TextField(max_length=128, default='', blank=True, - verbose_name=_('Comment')) + labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels")) + created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by')) + date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) + comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) objects = OrgManager.from_queryset(AssetQuerySet)() + CONNECTIVITY_CACHE_KEY = '_JMS_ASSET_CONNECTIVITY_{}' + UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3) + CONNECTIVITY_CHOICES = ( + (UNREACHABLE, _("Unreachable")), + (REACHABLE, _('Reachable')), + (UNKNOWN, _("Unknown")), + ) def __str__(self): return '{0.hostname}({0.ip})'.format(self) @@ -197,25 +181,17 @@ class Asset(OrgModelMixin): return '' @property - def is_connective(self): + def connectivity(self): if not self.is_unixlike(): - return True - val = cache.get(ASSET_ADMIN_CONN_CACHE_KEY.format(self.hostname)) - if val == 1: - return True - else: - return False + return self.UNKNOWN + key = self.CONNECTIVITY_CACHE_KEY.format(str(self.id)) + cached = cache.get(key, None) + return cached if cached is not None else self.UNKNOWN - def to_json(self): - info = { - 'id': self.id, - 'hostname': self.hostname, - 'ip': self.ip, - 'port': self.port, - } - if self.domain and self.domain.gateway_set.all(): - info["gateways"] = [d.id for d in self.domain.gateway_set.all()] - return info + @connectivity.setter + def connectivity(self, value): + key = self.CONNECTIVITY_CACHE_KEY.format(str(self.id)) + cache.set(key, value, 3600*2) def get_auth_info(self): if self.admin_user: @@ -236,11 +212,20 @@ class Asset(OrgModelMixin): fake_node.is_node = False return fake_node + def to_json(self): + info = { + 'id': self.id, + 'hostname': self.hostname, + 'ip': self.ip, + 'port': self.port, + } + if self.domain and self.domain.gateway_set.all(): + info["gateways"] = [d.id for d in self.domain.gateway_set.all()] + return info + def _to_secret_json(self): """ - Ansible use it create inventory, First using asset user, - otherwise using cluster admin user - + Ansible use it create inventory Todo: May be move to ops implements it """ data = self.to_json() diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index af59f000c..37e099e99 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -29,6 +29,13 @@ class AssetUser(OrgModelMixin): date_updated = models.DateTimeField(auto_now=True) created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by')) + UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3) + CONNECTIVITY_CHOICES = ( + (UNREACHABLE, _("Unreachable")), + (REACHABLE, _('Reachable')), + (UNKNOWN, _("Unknown")), + ) + @property def password(self): if self._password: diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index f5c8e17a1..147687471 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -14,7 +14,7 @@ from ..const import SYSTEM_USER_CONN_CACHE_KEY from .base import AssetUser -__all__ = ['AdminUser', 'SystemUser',] +__all__ = ['AdminUser', 'SystemUser'] logger = logging.getLogger(__name__) signer = get_signer() @@ -31,6 +31,7 @@ class AdminUser(AssetUser): become_method = models.CharField(choices=BECOME_METHOD_CHOICES, default='sudo', max_length=4) become_user = models.CharField(default='root', max_length=64) _become_pass = models.CharField(default='', max_length=128) + CONNECTIVE_CACHE_KEY = '_JMS_ADMIN_USER_CONNECTIVE_{}' def __str__(self): return self.name @@ -67,6 +68,23 @@ class AdminUser(AssetUser): def assets_amount(self): return self.get_related_assets().count() + @property + def connectivity(self): + from .asset import Asset + assets = self.get_related_assets().values_list('id', 'hostname', flat=True) + data = { + 'unreachable': [], + 'reachable': [], + } + for asset_id, hostname in assets: + key = Asset.CONNECTIVITY_CACHE_KEY.format(str(self.id)) + value = cache.get(key, Asset.UNKNOWN) + if value == Asset.REACHABLE: + data['reachable'].append(hostname) + elif value == Asset.UNREACHABLE: + data['unreachable'].append(hostname) + return data + class Meta: ordering = ['name'] unique_together = [('name', 'org_id')] @@ -94,34 +112,34 @@ class AdminUser(AssetUser): class SystemUser(AssetUser): - SSH_PROTOCOL = 'ssh' - RDP_PROTOCOL = 'rdp' - TELNET_PROTOCOL = 'telnet' + PROTOCOL_SSH = 'ssh' + PROTOCOL_RDP = 'rdp' + PROTOCOL_TELNET = 'telnet' PROTOCOL_CHOICES = ( - (SSH_PROTOCOL, 'ssh'), - (RDP_PROTOCOL, 'rdp'), - (TELNET_PROTOCOL, 'telnet (beta)'), + (PROTOCOL_SSH, 'ssh'), + (PROTOCOL_RDP, 'rdp'), + (PROTOCOL_TELNET, 'telnet (beta)'), ) - AUTO_LOGIN = 'auto' - MANUAL_LOGIN = 'manual' + LOGIN_AUTO = 'auto' + LOGIN_MANUAL = 'manual' LOGIN_MODE_CHOICES = ( - (AUTO_LOGIN, _('Automatic login')), - (MANUAL_LOGIN, _('Manually login')) + (LOGIN_AUTO, _('Automatic login')), + (LOGIN_MANUAL, _('Manually login')) ) nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes")) assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets")) - priority = models.IntegerField(default=20, verbose_name=_("Priority"), - validators=[MinValueValidator(1), MaxValueValidator(100)]) + priority = models.IntegerField(default=20, verbose_name=_("Priority"), validators=[MinValueValidator(1), MaxValueValidator(100)]) protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol')) auto_push = models.BooleanField(default=True, verbose_name=_('Auto push')) sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo')) shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell')) - login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=AUTO_LOGIN, max_length=10, verbose_name=_('Login mode')) + login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode')) cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True) - cache_key = "__SYSTEM_USER_CACHED_{}" + SYSTEM_USER_CACHE_KEY = "__SYSTEM_USER_CACHED_{}" + CONNECTIVE_CACHE_KEY = '_JMS_SYSTEM_USER_CONNECTIVE_{}' def __str__(self): return '{0.name}({0.username})'.format(self) @@ -136,34 +154,61 @@ class SystemUser(AssetUser): 'auto_push': self.auto_push, } - def get_assets(self): + def get_related_assets(self): assets = set(self.assets.all()) return assets @property - def assets_connective(self): - _result = cache.get(SYSTEM_USER_CONN_CACHE_KEY.format(self.name), {}) - return _result + def connectivity(self): + cache_key = self.CONNECTIVE_CACHE_KEY.format(str(self.id)) + value = cache.get(cache_key, None) + if not value or 'unreachable' not in value: + return {'unreachable': [], 'reachable': []} + else: + return value + + @connectivity.setter + def connectivity(self, value): + data = self.connectivity + unreachable = data['unreachable'] + reachable = data['reachable'] + + for host in value.get('dark', {}).keys(): + if host not in unreachable: + unreachable.append(host) + if host in reachable: + reachable.remove(host) + for host in value.get('contacted'): + if host not in reachable: + reachable.append(host) + if host in unreachable: + unreachable.remove(host) + cache_key = self.CONNECTIVE_CACHE_KEY.format(str(self.id)) + cache.set(cache_key, data, 3600) @property - def unreachable_assets(self): - return list(self.assets_connective.get('dark', {}).keys()) + def assets_unreachable(self): + return self.connectivity.get('unreachable') @property - def reachable_assets(self): - return self.assets_connective.get('contacted', []) + def assets_reachable(self): + return self.connectivity.get('reachable') + + @property + def login_mode_display(self): + return self.get_login_mode_display() def is_need_push(self): - if self.auto_push and self.protocol == self.__class__.SSH_PROTOCOL: + if self.auto_push and self.protocol == self.PROTOCOL_SSH: return True else: return False def set_cache(self): - cache.set(self.cache_key.format(self.id), self, 3600) + cache.set(self.SYSTEM_USER_CACHE_KEY.format(self.id), self, 3600) def expire_cache(self): - cache.delete(self.cache_key.format(self.id)) + cache.delete(self.SYSTEM_USER_CACHE_KEY.format(self.id)) @property def cmd_filter_rules(self): @@ -184,7 +229,7 @@ class SystemUser(AssetUser): @classmethod def get_system_user_by_id_or_cached(cls, sid): - cached = cache.get(cls.cache_key.format(sid)) + cached = cache.get(cls.SYSTEM_USER_CACHE_KEY.format(sid)) if cached: return cached try: diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index 1066ae0b7..9640aff7f 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -9,7 +9,7 @@ from .system_user import AssetSystemUserSerializer __all__ = [ 'AssetSerializer', 'AssetGrantedSerializer', 'MyAssetGrantedSerializer', - 'AssetAsNodeSerializer', + 'AssetAsNodeSerializer', 'AssetSimpleSerializer', ] @@ -33,7 +33,7 @@ class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer): def get_field_names(self, declared_fields, info): fields = super().get_field_names(declared_fields, info) fields.extend([ - 'hardware_info', 'is_connective', 'org_name' + 'hardware_info', 'connectivity', 'org_name' ]) return fields @@ -78,3 +78,9 @@ class MyAssetGrantedSerializer(AssetGrantedSerializer): "is_active", "system_users_join", "org_name", "os", "platform", "comment", "org_id", "protocol" ) + + +class AssetSimpleSerializer(serializers.ModelSerializer): + class Meta: + model = Asset + fields = ['id', 'hostname', 'port', 'ip', 'connectivity'] diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index a295f245c..be1f594ec 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from ..models import SystemUser +from ..models import SystemUser, Asset from .base import AuthSerializer @@ -21,17 +21,17 @@ class SystemUserSerializer(serializers.ModelSerializer): def get_field_names(self, declared_fields, info): fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info) fields.extend([ - 'get_login_mode_display', + 'login_mode_display', ]) return fields @staticmethod def get_unreachable_assets(obj): - return obj.unreachable_assets + return obj.assets_unreachable @staticmethod def get_reachable_assets(obj): - return obj.reachable_assets + return obj.assets_reachable def get_unreachable_amount(self, obj): return len(self.get_unreachable_assets(obj)) @@ -41,7 +41,7 @@ class SystemUserSerializer(serializers.ModelSerializer): @staticmethod def get_assets_amount(obj): - return len(obj.get_assets()) + return len(obj.get_related_assets()) class SystemUserAuthSerializer(AuthSerializer): @@ -75,4 +75,7 @@ class SystemUserSimpleSerializer(serializers.ModelSerializer): """ class Meta: model = SystemUser - fields = ('id', 'name', 'username') \ No newline at end of file + fields = ('id', 'name', 'username') + + + diff --git a/apps/assets/signals_handler.py b/apps/assets/signals_handler.py index 08ee6e670..85156c60d 100644 --- a/apps/assets/signals_handler.py +++ b/apps/assets/signals_handler.py @@ -7,7 +7,7 @@ from django.dispatch import receiver from common.utils import get_logger from .models import Asset, SystemUser, Node from .tasks import update_assets_hardware_info_util, \ - test_asset_connectability_util, push_system_user_to_assets + test_asset_connectivity_util, push_system_user_to_assets logger = get_logger(__file__) @@ -19,8 +19,8 @@ def update_asset_hardware_info_on_created(asset): def test_asset_conn_on_created(asset): - logger.debug("Test asset `{}` connectability".format(asset)) - test_asset_connectability_util.delay([asset]) + logger.debug("Test asset `{}` connectivity".format(asset)) + test_asset_connectivity_util.delay([asset]) def set_asset_root_node(asset): diff --git a/apps/assets/tasks.py b/apps/assets/tasks.py index 846964d94..0e6a6ec99 100644 --- a/apps/assets/tasks.py +++ b/apps/assets/tasks.py @@ -26,6 +26,23 @@ disk_pattern = re.compile(r'^hd|sd|xvd|vd') PERIOD_TASK = os.environ.get("PERIOD_TASK", "off") +def clean_hosts(assets): + clean_assets = [] + for asset in assets: + if not asset.is_active: + msg = _("Asset has been disabled, skipped: {}").format(asset) + logger.info(msg) + continue + if not asset.support_ansible(): + msg = _("Asset may not be support ansible, skipped: {}").format(asset) + logger.info(msg) + continue + clean_assets.append(asset) + if not clean_assets: + logger.info(_("No assets matched, stop task")) + return clean_assets + + @shared_task def set_assets_hardware_info(assets, result, **kwargs): """ @@ -60,9 +77,12 @@ def set_assets_hardware_info(assets, result, **kwargs): ___cpu_model = 'Unknown' ___cpu_model = ___cpu_model[:64] ___cpu_count = info.get('ansible_processor_count', 0) - ___cpu_cores = info.get('ansible_processor_cores', None) or len(info.get('ansible_processor', [])) + ___cpu_cores = info.get('ansible_processor_cores', None) or \ + len(info.get('ansible_processor', [])) ___cpu_vcpus = info.get('ansible_processor_vcpus', 0) - ___memory = '%s %s' % capacity_convert('{} MB'.format(info.get('ansible_memtotal_mb'))) + ___memory = '%s %s' % capacity_convert( + '{} MB'.format(info.get('ansible_memtotal_mb')) + ) disk_info = {} for dev, dev_info in info.get('ansible_devices', {}).items(): if disk_pattern.match(dev) and dev_info['removable'] == '0': @@ -96,19 +116,8 @@ def update_assets_hardware_info_util(assets, task_name=None): if task_name is None: task_name = _("Update some assets hardware info") tasks = const.UPDATE_ASSETS_HARDWARE_TASKS - hosts = [] - for asset in assets: - if not asset.is_active: - msg = _("Asset has been disabled, skipped: {}").format(asset) - logger.info(msg) - continue - if not asset.support_ansible(): - msg = _("Asset may not be support ansible, skipped: {}").format(asset) - logger.info(msg) - continue - hosts.append(asset) + hosts = clean_hosts(assets) if not hosts: - logger.info(_("No assets matched, stop task")) return {} created_by = str(assets[0].org_id) task, created = update_or_create_ansible_task( @@ -125,7 +134,6 @@ def update_assets_hardware_info_util(assets, task_name=None): @shared_task def update_asset_hardware_info_manual(asset): task_name = _("Update asset hardware info: {}").format(asset.hostname) - # task_name = _("更新资产硬件信息") return update_assets_hardware_info_util( [asset], task_name=task_name ) @@ -141,123 +149,18 @@ def update_assets_hardware_info_period(): logger.debug("Period task disabled, update assets hardware info pass") return - # from ops.utils import update_or_create_ansible_task - # from orgs.models import Organization - # orgs = Organization.objects.all().values_list('id', flat=True) - # orgs.append('') - # task_name = _("Update assets hardware info period") - # for org_id in orgs: - # org_id = str(org_id) - # hostname_list = [ - # asset for asset in Asset.objects.all() - # if asset.is_active and asset.is_unixlike() - # ] - # tasks = const.UPDATE_ASSETS_HARDWARE_TASKS - # - # # Only create, schedule by celery beat - # update_or_create_ansible_task( - # task_name, hosts=hostname_list, tasks=tasks, pattern='all', - # options=const.TASK_OPTIONS, run_as_admin=True, created_by='System', - # interval=60*60*24, is_periodic=True, callback=set_assets_hardware_info.name, - # ) - ## ADMIN USER CONNECTIVE ## -def set_admin_user_connectability_info(result, **kwargs): - admin_user = kwargs.get("admin_user") - task_name = kwargs.get("task_name") - if admin_user is None and task_name is not None: - admin_user = task_name.split(":")[-1] - - raw, summary = result - cache_key = const.ADMIN_USER_CONN_CACHE_KEY.format(admin_user) - cache.set(cache_key, summary, CACHE_MAX_TIME) - - for i in summary.get('contacted', []): - asset_conn_cache_key = const.ASSET_ADMIN_CONN_CACHE_KEY.format(i) - cache.set(asset_conn_cache_key, 1, CACHE_MAX_TIME) - - for i, msg in summary.get('dark', {}).items(): - asset_conn_cache_key = const.ASSET_ADMIN_CONN_CACHE_KEY.format(i) - cache.set(asset_conn_cache_key, 0, CACHE_MAX_TIME) - logger.error(msg) - @shared_task -def test_admin_user_connectability_util(admin_user, task_name): - """ - Test asset admin user can connect or not. Using ansible api do that - :param admin_user: - :param task_name: - :return: - """ - from ops.utils import update_or_create_ansible_task - - assets = admin_user.get_related_assets() - hosts = [] - for asset in assets: - if not asset.is_active: - msg = _("Asset has been disabled, skipped: {}").format(asset) - logger.info(msg) - continue - if not asset.support_ansible(): - msg = _("Asset may not be support ansible, skipped: {}").format(asset) - logger.info(msg) - continue - hosts.append(asset) - if not hosts: - logger.info(_("No assets matched, stop task")) - return {} - tasks = const.TEST_ADMIN_USER_CONN_TASKS - task, created = update_or_create_ansible_task( - task_name=task_name, hosts=hosts, tasks=tasks, pattern='all', - options=const.TASK_OPTIONS, run_as_admin=True, created_by=admin_user.org_id, - ) - result = task.run() - set_admin_user_connectability_info(result, admin_user=admin_user.name) - return result - - -@shared_task -@register_as_period_task(interval=3600) -def test_admin_user_connectability_period(): - """ - A period task that update the ansible task period - """ - admin_users = AdminUser.objects.all() - for admin_user in admin_users: - task_name = _("Test admin user connectability period: {}").format(admin_user.name) - test_admin_user_connectability_util(admin_user, task_name) - - -@shared_task -def test_admin_user_connectability_manual(admin_user): - task_name = _("Test admin user connectability: {}").format(admin_user.name) - # task_name = _("测试管理行号可连接性: {}").format(admin_user.name) - return test_admin_user_connectability_util(admin_user, task_name) - - -@shared_task -def test_asset_connectability_util(assets, task_name=None): +def test_asset_connectivity_util(assets, task_name=None): from ops.utils import update_or_create_ansible_task if task_name is None: - task_name = _("Test assets connectability") - # task_name = _("测试资产可连接性") - hosts = [] - for asset in assets: - if not asset.is_active: - msg = _("Asset has been disabled, skip: {}").format(asset) - logger.info(msg) - continue - if not asset.support_ansible(): - msg = _("Asset may not be support ansible, skip: {}").format(asset) - logger.info(msg) - continue - hosts.append(asset) + task_name = _("Test assets connectivity") + hosts = clean_hosts(assets) if not hosts: - logger.info(_("No assets, task stop")) return {} tasks = const.TEST_ADMIN_USER_CONN_TASKS created_by = assets[0].org_id @@ -267,18 +170,20 @@ def test_asset_connectability_util(assets, task_name=None): ) result = task.run() summary = result[1] - for k in summary.get('dark'): - cache.set(const.ASSET_ADMIN_CONN_CACHE_KEY.format(k), 0, CACHE_MAX_TIME) - - for k in summary.get('contacted'): - cache.set(const.ASSET_ADMIN_CONN_CACHE_KEY.format(k), 1, CACHE_MAX_TIME) + for asset in assets: + if asset.hostname in summary.get('dark', {}): + asset.connectivity = asset.UNREACHABLE + elif asset.hostname in summary.get('contacted', []): + asset.connectivity = asset.REACHABLE + else: + asset.connectivity = asset.UNKNOWN return summary @shared_task -def test_asset_connectability_manual(asset): - task_name = _("Test assets connectability: {}").format(asset) - summary = test_asset_connectability_util([asset], task_name=task_name) +def test_asset_connectivity_manual(asset): + task_name = _("Test assets connectivity: {}").format(asset) + summary = test_asset_connectivity_util([asset], task_name=task_name) if summary.get('dark'): return False, summary['dark'] @@ -286,21 +191,50 @@ def test_asset_connectability_manual(asset): return True, "" +@shared_task +def test_admin_user_connectivity_util(admin_user, task_name): + """ + Test asset admin user can connect or not. Using ansible api do that + :param admin_user: + :param task_name: + :return: + """ + assets = admin_user.get_related_assets() + hosts = clean_hosts(assets) + if not hosts: + return {} + summary = test_asset_connectivity_util(hosts, task_name) + return summary + + +@shared_task +@register_as_period_task(interval=3600) +def test_admin_user_connectivity_period(): + """ + A period task that update the ansible task period + """ + admin_users = AdminUser.objects.all() + for admin_user in admin_users: + task_name = _("Test admin user connectivity period: {}").format(admin_user.name) + test_admin_user_connectivity_util(admin_user, task_name) + + +@shared_task +def test_admin_user_connectivity_manual(admin_user): + task_name = _("Test admin user connectivity: {}").format(admin_user.name) + return test_admin_user_connectivity_util(admin_user, task_name) + + ## System user connective ## @shared_task -def set_system_user_connectablity_info(result, **kwargs): +def set_system_user_connectivity_info(system_user, result): summary = result[1] - task_name = kwargs.get("task_name") - system_user = kwargs.get("system_user") - if system_user is None: - system_user = task_name.split(":")[-1] - cache_key = const.SYSTEM_USER_CONN_CACHE_KEY.format(str(system_user.id)) - cache.set(cache_key, summary, CACHE_MAX_TIME) + system_user.connectivity = summary @shared_task -def test_system_user_connectability_util(system_user, assets, task_name): +def test_system_user_connectivity_util(system_user, assets, task_name): """ Test system cant connect his assets or not. :param system_user: @@ -309,20 +243,9 @@ def test_system_user_connectability_util(system_user, assets, task_name): :return: """ from ops.utils import update_or_create_ansible_task - hosts = [] tasks = const.TEST_SYSTEM_USER_CONN_TASKS - for asset in assets: - if not asset.is_active: - msg = _("Asset has been disabled, skip: {}").format(asset) - logger.info(msg) - continue - if not asset.support_ansible(): - msg = _("Asset may not be support ansible, skip: {}").format(asset) - logger.info(msg) - continue - hosts.append(asset) + hosts = clean_hosts(assets) if not hosts: - logger.info(_("No assets matched, stop task")) return {} task, created = update_or_create_ansible_task( task_name, hosts=hosts, tasks=tasks, pattern='all', @@ -330,36 +253,35 @@ def test_system_user_connectability_util(system_user, assets, task_name): run_as=system_user, created_by=system_user.org_id, ) result = task.run() - set_system_user_connectablity_info(result, system_user=system_user) + set_system_user_connectivity_info(system_user, result) return result @shared_task -def test_system_user_connectability_manual(system_user): - task_name = _("Test system user connectability: {}").format(system_user) - assets = system_user.get_assets() - return test_system_user_connectability_util(system_user, assets, task_name) +def test_system_user_connectivity_manual(system_user): + task_name = _("Test system user connectivity: {}").format(system_user) + assets = system_user.get_related_assets() + return test_system_user_connectivity_util(system_user, assets, task_name) @shared_task -def test_system_user_connectability_a_asset(system_user, asset): - task_name = _("Test system user connectability: {} => {}").format( +def test_system_user_connectivity_a_asset(system_user, asset): + task_name = _("Test system user connectivity: {} => {}").format( system_user, asset ) - return test_system_user_connectability_util(system_user, [asset], task_name) + return test_system_user_connectivity_util(system_user, [asset], task_name) @shared_task -def test_system_user_connectability_period(): +def test_system_user_connectivity_period(): if PERIOD_TASK != "on": - logger.debug("Period task disabled, test system user connectability pass") + logger.debug("Period task disabled, test system user connectivity pass") return - # Todo: 暂时禁用定期测试 - # system_users = SystemUser.objects.all() - # for system_user in system_users: - # task_name = _("Test system user connectability period: {}").format(system_user) - # # task_name = _("定期测试系统用户可连接性: {}".format(system_user)) - # test_system_user_connectability_util(system_user, task_name) + system_users = SystemUser.objects.all() + for system_user in system_users: + task_name = _("Test system user connectivity period: {}").format(system_user) + assets = system_user.get_related_assets() + test_system_user_connectivity_util(system_user, assets, task_name) #### Push system user tasks #### @@ -381,6 +303,24 @@ def get_push_system_user_tasks(system_user): ), } }) + tasks.extend([ + { + 'name': 'Check home dir exists', + 'action': { + 'module': 'stat', + 'args': 'path=/home/{}'.format(system_user.username) + }, + 'register': 'home_existed' + }, + { + 'name': "Set home dir permission", + 'action': { + 'module': 'file', + 'args': "path=/home/{0} owner={0} group={0} mode=700".format(system_user.username) + }, + 'when': 'home_existed.stat.exists == true' + } + ]) if system_user.public_key: tasks.append({ 'name': 'Set {} authorized key'.format(system_user.username), @@ -417,19 +357,8 @@ def push_system_user_util(system_user, assets, task_name): return tasks = get_push_system_user_tasks(system_user) - hosts = [] - for asset in assets: - if not asset.is_active: - msg = _("Asset has been disabled, skip: {}").format(asset) - logger.info(msg) - continue - if not asset.support_ansible(): - msg = _("Asset may not be support ansible, skip: {}").format(asset) - logger.info(msg) - continue - hosts.append(asset) + hosts = clean_hosts(assets) if not hosts: - logger.info(_("No assets matched, stop task")) return {} task, created = update_or_create_ansible_task( task_name=task_name, hosts=hosts, tasks=tasks, pattern='all', @@ -441,7 +370,7 @@ def push_system_user_util(system_user, assets, task_name): @shared_task def push_system_user_to_assets_manual(system_user): - assets = system_user.get_assets() + assets = system_user.get_related_assets() task_name = _("Push system users to assets: {}").format(system_user.name) return push_system_user_util(system_user, assets, task_name=task_name) diff --git a/apps/assets/templates/assets/admin_user_assets.html b/apps/assets/templates/assets/admin_user_assets.html index 31314392f..7a9882563 100644 --- a/apps/assets/templates/assets/admin_user_assets.html +++ b/apps/assets/templates/assets/admin_user_assets.html @@ -45,13 +45,11 @@ - + @@ -91,26 +89,36 @@
    - - {% trans 'Hostname' %} {% trans 'IP' %} {% trans 'Port' %} {% trans 'Reachable' %}{% trans 'Action' %}