diff --git a/apps/audits/signal_handlers/operate_log.py b/apps/audits/signal_handlers/operate_log.py index c6a8948ab..2d4524198 100644 --- a/apps/audits/signal_handlers/operate_log.py +++ b/apps/audits/signal_handlers/operate_log.py @@ -187,7 +187,7 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs): 'PermedAsset', 'PermedAccount', 'MenuPermission', 'Permission', 'TicketSession', 'ApplyLoginTicket', 'ApplyCommandTicket', 'ApplyLoginAssetTicket', - 'FavoriteAsset', 'ChangeSecretRecord', 'AppProvider', + 'FavoriteAsset', 'ChangeSecretRecord', 'AppProvider', 'Variable' } include_models = {'UserSession'} for i, app in enumerate(apps.get_models(), 1): diff --git a/apps/i18n/core/ja/LC_MESSAGES/django.po b/apps/i18n/core/ja/LC_MESSAGES/django.po index 8173572a7..627df1a2a 100644 --- a/apps/i18n/core/ja/LC_MESSAGES/django.po +++ b/apps/i18n/core/ja/LC_MESSAGES/django.po @@ -5098,11 +5098,11 @@ msgstr "パラメータ定義" #: ops/models/job.py:155 msgid "Run as" -msgstr "ユーザーとして実行" +msgstr "実行アカウント (じっこうアカウント)" #: ops/models/job.py:157 msgid "Run as policy" -msgstr "ユーザー ポリシー" +msgstr "アカウントポリシー " #: ops/models/job.py:222 ops/serializers/job.py:92 #: terminal/notifications.py:182 diff --git a/apps/i18n/core/zh/LC_MESSAGES/django.po b/apps/i18n/core/zh/LC_MESSAGES/django.po index eb1e9d19c..b1e0716c6 100644 --- a/apps/i18n/core/zh/LC_MESSAGES/django.po +++ b/apps/i18n/core/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-11-06 11:27+0800\n" +"POT-Creation-Date: 2024-11-06 16:37+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -35,7 +35,7 @@ msgstr "生成资产或应用相关备份信息文件" #: accounts/automations/backup_account/handlers.py:156 #: accounts/automations/backup_account/handlers.py:295 -#: accounts/automations/backup_account/manager.py:40 ops/serializers/job.py:76 +#: accounts/automations/backup_account/manager.py:40 ops/serializers/job.py:82 #: settings/templates/ldap/_msg_import_ldap_user.html:7 msgid "Time cost" msgstr "花费时间" @@ -467,7 +467,7 @@ msgstr "账号备份计划" #: accounts/models/automations/backup_account.py:120 #: assets/models/automations/base.py:115 audits/models.py:65 -#: ops/models/base.py:55 ops/models/celery.py:89 ops/models/job.py:242 +#: ops/models/base.py:55 ops/models/celery.py:89 ops/models/job.py:247 #: ops/templates/ops/celery_task_log.html:101 #: perms/models/asset_permission.py:78 settings/serializers/feature.py:25 #: settings/templates/ldap/_msg_import_ldap_user.html:5 @@ -506,7 +506,7 @@ msgstr "原因" #: accounts/models/automations/backup_account.py:136 #: accounts/serializers/automations/change_secret.py:117 #: accounts/serializers/automations/change_secret.py:152 -#: ops/serializers/job.py:74 terminal/serializers/session.py:54 +#: ops/serializers/job.py:80 terminal/serializers/session.py:54 msgid "Is success" msgstr "是否成功" @@ -581,7 +581,7 @@ msgstr "开始日期" #: accounts/models/automations/change_secret.py:42 #: assets/models/automations/base.py:116 ops/models/base.py:56 -#: ops/models/celery.py:90 ops/models/job.py:243 +#: ops/models/celery.py:90 ops/models/job.py:248 #: terminal/models/applet/host.py:142 msgid "Date finished" msgstr "结束日期" @@ -589,7 +589,7 @@ msgstr "结束日期" #: accounts/models/automations/change_secret.py:44 #: assets/models/automations/base.py:113 #: assets/serializers/automations/base.py:39 audits/models.py:208 -#: audits/serializers.py:54 ops/models/base.py:49 ops/models/job.py:234 +#: audits/serializers.py:54 ops/models/base.py:49 ops/models/job.py:239 #: terminal/models/applet/applet.py:331 terminal/models/applet/host.py:140 #: terminal/models/component/status.py:30 #: terminal/models/virtualapp/virtualapp.py:99 @@ -664,8 +664,8 @@ msgstr "触发方式" #: audits/models.py:92 audits/serializers.py:84 #: authentication/serializers/connect_token_secret.py:119 #: authentication/templates/authentication/_access_key_modal.html:34 -#: behemoth/serializers/environment.py:13 perms/serializers/permission.py:52 -#: perms/serializers/permission.py:74 tickets/serializers/ticket/ticket.py:21 +#: perms/serializers/permission.py:52 perms/serializers/permission.py:74 +#: tickets/serializers/ticket/ticket.py:21 msgid "Action" msgstr "动作" @@ -718,8 +718,8 @@ msgstr "密码规则" #: authentication/serializers/connect_token_secret.py:113 #: authentication/serializers/connect_token_secret.py:169 labels/models.py:11 #: ops/mixin.py:28 ops/models/adhoc.py:19 ops/models/celery.py:15 -#: ops/models/celery.py:81 ops/models/job.py:142 ops/models/playbook.py:30 -#: ops/serializers/job.py:18 orgs/models.py:82 +#: ops/models/celery.py:81 ops/models/job.py:145 ops/models/playbook.py:28 +#: ops/models/variable.py:9 ops/serializers/job.py:19 orgs/models.py:82 #: perms/models/asset_permission.py:61 rbac/models/role.py:29 #: rbac/serializers/role.py:28 settings/models.py:35 settings/models.py:184 #: settings/serializers/msg.py:89 settings/serializers/terminal.py:9 @@ -882,7 +882,7 @@ msgstr "类别" #: assets/serializers/asset/common.py:146 assets/serializers/platform.py:159 #: assets/serializers/platform.py:171 audits/serializers.py:53 #: audits/serializers.py:170 -#: authentication/serializers/connect_token_secret.py:126 ops/models/job.py:150 +#: authentication/serializers/connect_token_secret.py:126 ops/models/job.py:153 #: perms/serializers/user_permission.py:27 terminal/models/applet/applet.py:40 #: terminal/models/component/storage.py:58 #: terminal/models/component/storage.py:152 terminal/serializers/applet.py:29 @@ -918,10 +918,8 @@ msgstr "已修改" #: assets/models/automations/base.py:19 #: assets/serializers/automations/base.py:20 assets/serializers/domain.py:34 #: assets/serializers/platform.py:180 assets/serializers/platform.py:212 -#: authentication/api/connection_token.py:410 -#: behemoth/serializers/environment.py:11 -#: behemoth/serializers/environment.py:22 ops/models/base.py:17 -#: ops/models/job.py:152 ops/serializers/job.py:19 +#: authentication/api/connection_token.py:410 ops/models/base.py:17 +#: ops/models/job.py:155 ops/serializers/job.py:20 #: perms/serializers/permission.py:46 #: terminal/templates/terminal/_msg_command_execute_alert.html:16 #: xpack/plugins/cloud/manager.py:89 @@ -1071,7 +1069,7 @@ msgstr "关联平台,可配置推送参数,如果不关联,将使用默认 #: accounts/serializers/account/virtual.py:19 assets/models/cmd_filter.py:40 #: assets/models/cmd_filter.py:88 common/db/models.py:36 ops/models/adhoc.py:25 -#: ops/models/job.py:158 ops/models/playbook.py:33 rbac/models/role.py:37 +#: ops/models/job.py:163 ops/models/playbook.py:31 rbac/models/role.py:37 #: settings/models.py:40 terminal/models/applet/applet.py:46 #: terminal/models/applet/applet.py:332 terminal/models/applet/host.py:143 #: terminal/models/component/endpoint.py:26 @@ -1095,7 +1093,8 @@ msgstr "" #: accounts/serializers/automations/base.py:23 #: assets/models/asset/common.py:176 assets/serializers/asset/common.py:172 -#: assets/serializers/automations/base.py:21 perms/serializers/permission.py:47 +#: assets/serializers/automations/base.py:21 ops/serializers/job.py:21 +#: perms/serializers/permission.py:47 msgid "Nodes" msgstr "节点" @@ -1396,7 +1395,7 @@ msgid "Accounts" msgstr "账号" #: acls/models/command_acl.py:16 assets/models/cmd_filter.py:60 -#: ops/serializers/job.py:73 terminal/const.py:86 +#: ops/serializers/job.py:79 terminal/const.py:86 #: terminal/models/session/session.py:43 terminal/serializers/command.py:18 #: terminal/templates/terminal/_msg_command_alert.html:12 #: terminal/templates/terminal/_msg_command_execute_alert.html:10 @@ -2032,13 +2031,14 @@ msgid "Proxy" msgstr "代理" #: assets/models/automations/base.py:18 assets/models/cmd_filter.py:32 -#: assets/models/node.py:553 perms/models/asset_permission.py:72 -#: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:388 +#: assets/models/node.py:553 ops/models/job.py:156 +#: perms/models/asset_permission.py:72 tickets/models/ticket/apply_asset.py:14 +#: xpack/plugins/cloud/models.py:388 msgid "Node" msgstr "节点" -#: assets/models/automations/base.py:22 ops/models/job.py:237 -#: settings/serializers/auth/sms.py:108 +#: assets/models/automations/base.py:22 ops/models/job.py:242 +#: ops/serializers/job.py:23 settings/serializers/auth/sms.py:108 msgid "Parameters" msgstr "参数" @@ -2053,7 +2053,7 @@ msgstr "资产自动化任务" # msgid "Comment" # msgstr "备注" #: assets/models/automations/base.py:114 assets/models/cmd_filter.py:41 -#: common/db/models.py:34 ops/models/base.py:54 ops/models/job.py:241 +#: common/db/models.py:34 ops/models/base.py:54 ops/models/job.py:246 #: users/models/user/__init__.py:311 msgid "Date created" msgstr "创建日期" @@ -2185,7 +2185,7 @@ msgstr "可以匹配节点" msgid "Primary" msgstr "主要的" -#: assets/models/platform.py:18 +#: assets/models/platform.py:18 ops/models/variable.py:20 msgid "Required" msgstr "必须的" @@ -2728,7 +2728,7 @@ msgstr "标签" msgid "operate_log_id" msgstr "操作日志ID" -#: audits/backends/db.py:111 +#: audits/backends/db.py:111 ops/models/variable.py:19 msgid "Tips" msgstr "提示" @@ -2972,8 +2972,8 @@ msgid "Offline user session" msgstr "下线用户会话" #: audits/serializers.py:33 ops/models/adhoc.py:24 ops/models/base.py:16 -#: ops/models/base.py:53 ops/models/celery.py:87 ops/models/job.py:151 -#: ops/models/job.py:240 ops/models/playbook.py:32 +#: ops/models/base.py:53 ops/models/celery.py:87 ops/models/job.py:154 +#: ops/models/job.py:245 ops/models/playbook.py:30 ops/models/variable.py:17 #: terminal/models/session/sharing.py:25 msgid "Creator" msgstr "创建者" @@ -4664,7 +4664,7 @@ msgid "" " work orders, and other notifications" msgstr "系统一些告警,工单等需要发送站内信时执行该任务" -#: ops/ansible/inventory.py:116 ops/models/job.py:65 +#: ops/ansible/inventory.py:116 ops/models/job.py:68 msgid "No account available" msgstr "无可用账号" @@ -4688,34 +4688,34 @@ msgstr "任务 {} 不存在" msgid "Task {} args or kwargs error" msgstr "任务 {} 执行参数错误" -#: ops/api/job.py:83 +#: ops/api/job.py:68 #, python-brace-format msgid "" "Asset ({asset}) must have at least one of the following protocols added: " "SSH, SFTP, or WinRM" msgstr "资产({asset})至少要添加ssh,sftp,winrm其中一种协议" -#: ops/api/job.py:84 +#: ops/api/job.py:69 #, python-brace-format msgid "Asset ({asset}) authorization is missing SSH, SFTP, or WinRM protocol" msgstr "资产({asset})授权缺少ssh,sftp或winrm协议" -#: ops/api/job.py:85 +#: ops/api/job.py:70 #, python-brace-format msgid "Asset ({asset}) authorization lacks upload permissions" msgstr "资产({asset})授权缺少上传权限" -#: ops/api/job.py:170 +#: ops/api/job.py:157 msgid "Duplicate file exists" msgstr "存在同名文件" -#: ops/api/job.py:175 +#: ops/api/job.py:162 #, python-brace-format msgid "" "File size exceeds maximum limit. Please select a file smaller than {limit}MB" msgstr "文件大小超过最大限制。请选择小于 {limit}MB 的文件。" -#: ops/api/job.py:244 +#: ops/api/job.py:231 msgid "" "The task is being created and cannot be interrupted. Please try again later." msgstr "正在创建任务,无法中断,请稍后重试。" @@ -4784,11 +4784,13 @@ msgstr "空白" msgid "VCS" msgstr "VCS" -#: ops/const.py:38 ops/models/adhoc.py:44 settings/serializers/feature.py:123 +#: ops/const.py:38 ops/models/adhoc.py:44 ops/models/variable.py:26 +#: settings/serializers/feature.py:123 msgid "Adhoc" msgstr "命令" -#: ops/const.py:39 ops/models/job.py:149 ops/models/playbook.py:91 +#: ops/const.py:39 ops/models/job.py:152 ops/models/playbook.py:89 +#: ops/models/variable.py:23 msgid "Playbook" msgstr "Playbook" @@ -4861,6 +4863,14 @@ msgstr "公有" msgid "Private" msgstr "私有" +#: ops/const.py:91 +msgid "Text" +msgstr "文本框" + +#: ops/const.py:92 +msgid "Select" +msgstr "选择框" + #: ops/exception.py:6 msgid "no valid program entry found." msgstr "没有可用程序入口" @@ -4900,16 +4910,16 @@ msgstr "需要周期或定期设置" msgid "Pattern" msgstr "模式" -#: ops/models/adhoc.py:22 ops/models/job.py:146 +#: ops/models/adhoc.py:22 ops/models/job.py:149 msgid "Module" msgstr "模块" -#: ops/models/adhoc.py:23 ops/models/celery.py:82 ops/models/job.py:144 +#: ops/models/adhoc.py:23 ops/models/celery.py:82 ops/models/job.py:147 #: terminal/models/component/task.py:14 msgid "Args" -msgstr "参数" +msgstr "内容" -#: ops/models/adhoc.py:26 ops/models/playbook.py:36 ops/serializers/mixin.py:10 +#: ops/models/adhoc.py:26 ops/models/playbook.py:34 ops/serializers/mixin.py:10 #: rbac/models/role.py:31 rbac/models/rolebinding.py:46 #: rbac/serializers/role.py:12 settings/serializers/auth/oauth2.py:37 msgid "Scope" @@ -4923,16 +4933,16 @@ msgstr "账号策略" msgid "Last execution" msgstr "最后执行" -#: ops/models/base.py:22 ops/serializers/job.py:17 +#: ops/models/base.py:22 ops/serializers/job.py:18 msgid "Date last run" msgstr "最后运行日期" -#: ops/models/base.py:51 ops/models/job.py:238 +#: ops/models/base.py:51 ops/models/job.py:243 #: xpack/plugins/cloud/models.py:225 msgid "Result" msgstr "结果" -#: ops/models/base.py:52 ops/models/job.py:239 +#: ops/models/base.py:52 ops/models/job.py:244 #: xpack/plugins/cloud/manager.py:99 msgid "Summary" msgstr "汇总" @@ -4961,55 +4971,87 @@ msgstr "发布日期" msgid "Celery Task Execution" msgstr "Celery 任务执行" -#: ops/models/job.py:147 +#: ops/models/job.py:150 msgid "Run dir" msgstr "运行目录" -#: ops/models/job.py:148 +#: ops/models/job.py:151 msgid "Timeout (Seconds)" msgstr "超时时间 (秒)" -#: ops/models/job.py:153 +#: ops/models/job.py:157 msgid "Use Parameter Define" msgstr "使用参数定义" -#: ops/models/job.py:154 +#: ops/models/job.py:158 msgid "Parameters define" msgstr "参数定义" -#: ops/models/job.py:155 +#: ops/models/job.py:159 +msgid "Periodic variable" +msgstr "定时任务参数" + +#: ops/models/job.py:160 msgid "Run as" -msgstr "运行用户" +msgstr "运行账号" -#: ops/models/job.py:157 +#: ops/models/job.py:162 msgid "Run as policy" -msgstr "用户策略" +msgstr "账户策略" -#: ops/models/job.py:222 ops/serializers/job.py:92 +#: ops/models/job.py:227 ops/models/variable.py:28 ops/serializers/job.py:98 #: terminal/notifications.py:182 msgid "Job" msgstr "作业" -#: ops/models/job.py:245 +#: ops/models/job.py:250 msgid "Material" msgstr "Material" -#: ops/models/job.py:247 +#: ops/models/job.py:252 msgid "Material Type" msgstr "Material 类型" -#: ops/models/job.py:558 +#: ops/models/job.py:564 msgid "Job Execution" msgstr "作业执行" -#: ops/models/playbook.py:35 +#: ops/models/playbook.py:33 msgid "CreateMethod" msgstr "创建方式" -#: ops/models/playbook.py:37 +#: ops/models/playbook.py:35 msgid "VCS URL" msgstr "VCS URL" +#: ops/models/variable.py:11 +msgid "Variable name" +msgstr "参数名" + +#: ops/models/variable.py:12 +msgid "" +"The variable name used in the script has a fixed prefix 'jms_' followed by " +"the input variable name. For example, if the variable name is 'name,' the " +"final generated environment variable will be 'jms_name'." +msgstr "在脚本使用的变量名称,固定前缀 jms_ + 输入的变量名,例如变量名name,则最终生成环境变量为 jms_name" + +#: ops/models/variable.py:16 +msgid "Default Value" +msgstr "默认值" + +#: ops/models/variable.py:18 +msgid "Variable type" +msgstr "变量名" + +#: ops/models/variable.py:21 ops/serializers/variable.py:23 +msgid "ExtraVars" +msgstr "额外参数" + +#: ops/models/variable.py:49 ops/serializers/adhoc.py:16 +#: ops/serializers/job.py:22 ops/serializers/playbook.py:21 +msgid "Variable" +msgstr "变量" + #: ops/notifications.py:20 msgid "Server performance" msgstr "监控告警" @@ -5046,61 +5088,72 @@ msgstr "周期执行" msgid "Next execution time" msgstr "下次执行时间" -#: ops/serializers/job.py:15 +#: ops/serializers/job.py:17 msgid "Execute after saving" msgstr "保存后执行" -#: ops/serializers/job.py:52 terminal/serializers/session.py:49 +#: ops/serializers/job.py:58 terminal/serializers/session.py:49 msgid "Duration" msgstr "时长" -#: ops/serializers/job.py:72 +#: ops/serializers/job.py:78 msgid "Job type" msgstr "任务类型" -#: ops/serializers/job.py:75 terminal/serializers/session.py:58 +#: ops/serializers/job.py:81 terminal/serializers/session.py:58 msgid "Is finished" msgstr "是否完成" -#: ops/serializers/job.py:89 +#: ops/serializers/job.py:95 msgid "Task id" msgstr "任务 ID" -#: ops/serializers/job.py:98 +#: ops/serializers/job.py:104 msgid "You do not have permission for the current job." msgstr "你没有当前作业的权限。" -#: ops/tasks.py:51 +#: ops/serializers/variable.py:20 +msgid "Variable Type" +msgstr "参数类型" + +#: ops/serializers/variable.py:25 +msgid "" +"Each item is on a separate line, with each line separated by a colon. The " +"part before the colon is the display content, and the part after the colon " +"is the value." +msgstr "每项单独一行,每行可以用英文冒号分割前边是显示的内容后边是值" + +#: ops/tasks.py:52 msgid "Run ansible task" msgstr "运行 Ansible 任务" -#: ops/tasks.py:54 +#: ops/tasks.py:55 msgid "" "Execute scheduled adhoc and playbooks, periodically invoking the task for " "execution" msgstr "当执行定时的快捷命令,playbook,定时调用该任务执行" -#: ops/tasks.py:82 +#: ops/tasks.py:85 msgid "Run ansible task execution" msgstr "开始执行 Ansible 任务" -#: ops/tasks.py:85 +#: ops/tasks.py:88 msgid "Execute the task when manually adhoc or playbooks" msgstr "手动执行快捷命令,playbook时执行该任务" -#: ops/tasks.py:99 +#: ops/tasks.py:102 msgid "Clear celery periodic tasks" msgstr "清理周期任务" -#: ops/tasks.py:101 +#: ops/tasks.py:104 msgid "At system startup, clean up celery tasks that no longer exist" msgstr "系统启动时,清理已经不存在的celery任务" -#: ops/tasks.py:125 +#: ops/tasks.py:128 msgid "Create or update periodic tasks" msgstr "创建或更新周期任务" -#: ops/tasks.py:127 +#: ops/tasks.py:130 msgid "" "With version iterations, new tasks may be added, or task names and execution " "times may \n" @@ -5111,11 +5164,11 @@ msgstr "" "随着版本迭代,可能会新增任务或者修改任务的名称,执行时间,所以在系统启动时," "将会注册任务或者更新定时任务参数" -#: ops/tasks.py:140 +#: ops/tasks.py:143 msgid "Periodic check service performance" msgstr "周期检测服务性能" -#: ops/tasks.py:142 +#: ops/tasks.py:145 msgid "" "Check every hour whether each component is offline and whether the CPU, " "memory, \n" @@ -5125,11 +5178,11 @@ msgstr "" "每小时检测各组件是否离线,cpu,内存,硬盘使用率是否超过阈值,向管理员发送消息" "预警" -#: ops/tasks.py:152 +#: ops/tasks.py:155 msgid "Clean up unexpected jobs" msgstr "清理异常作业" -#: ops/tasks.py:154 +#: ops/tasks.py:157 msgid "" "Due to exceptions caused by executing adhoc and playbooks in the Job " "Center, \n" @@ -5142,11 +5195,11 @@ msgstr "" "由于作业中心执行快捷命令,playbook会产生异常,任务状态未更新完成,系统将每小" "时执行清理超3小时未完成的异常作业,并将任务标记失败" -#: ops/tasks.py:167 +#: ops/tasks.py:170 msgid "Clean job_execution db record" msgstr "清理作业中心执行历史" -#: ops/tasks.py:169 +#: ops/tasks.py:172 msgid "" "Due to the execution of adhoc and playbooks in the Job Center, execution " "records will \n" @@ -9600,6 +9653,14 @@ msgstr "账号保护已开启,请根据提示完成以下操作" msgid "Open MFA Authenticator and enter the 6-bit dynamic code" msgstr "请打开 MFA 验证器,输入 6 位动态码" +#: users/utils.py:60 +msgid "Auth success" +msgstr "认证成功" + +#: users/utils.py:61 +msgid "Redirecting to JumpServer Client" +msgstr "" + #: users/views/profile/otp.py:106 msgid "Already bound" msgstr "已经绑定" diff --git a/apps/i18n/core/zh_Hant/LC_MESSAGES/django.po b/apps/i18n/core/zh_Hant/LC_MESSAGES/django.po index 231a3b317..1b4bd5cd3 100644 --- a/apps/i18n/core/zh_Hant/LC_MESSAGES/django.po +++ b/apps/i18n/core/zh_Hant/LC_MESSAGES/django.po @@ -4982,11 +4982,11 @@ msgstr "參數定義" #: ops/models/job.py:155 msgid "Run as" -msgstr "執行使用者" +msgstr "運行賬號" #: ops/models/job.py:157 msgid "Run as policy" -msgstr "使用者策略" +msgstr "賬號策略" #: ops/models/job.py:222 ops/serializers/job.py:92 #: terminal/notifications.py:182 diff --git a/apps/i18n/lina/en.json b/apps/i18n/lina/en.json index c494af5ec..2f980d1a0 100644 --- a/apps/i18n/lina/en.json +++ b/apps/i18n/lina/en.json @@ -70,7 +70,7 @@ "AdhocCreate": "Create the command", "AdhocDetail": "Command details", "AdhocManage": "Script", - "AdhocUpdate": "Update the command", + "AdhocUpdate": "Update Script", "Advanced": "Advanced settings", "AfterChange": "After changes", "AjaxError404": "404 request error", @@ -1401,5 +1401,12 @@ "ZoneUpdate": "Update the zone", "disallowSelfUpdateFields": "Not allowed to modify the current fields yourself", "forceEnableMFAHelpText": "If force enable, user can not disable by themselves", - "removeWarningMsg": "Are you sure you want to remove" + "removeWarningMsg": "Are you sure you want to remove", + "DefaultValue": "Default value", + "AddVariable": "Add Variable", + "VariableName": "Variable name", + "LoadTemplate": "Load template", + "Templates": "Templates", + "ExtraArgsPlaceholder": "One option per line, for example:\nOption 1: Value 1\nOption 2: Value 2", + "setVariable": "Set variable" } \ No newline at end of file diff --git a/apps/i18n/lina/ja.json b/apps/i18n/lina/ja.json index 8ce5d40c4..d67fff442 100644 --- a/apps/i18n/lina/ja.json +++ b/apps/i18n/lina/ja.json @@ -70,7 +70,7 @@ "AdhocCreate": "アドホックコマンドを作成", "AdhocDetail": "コマンド詳細", "AdhocManage": "スクリプト管理", - "AdhocUpdate": "コマンドを更新", + "AdhocUpdate": "更新スクリプト", "Advanced": "高度な設定", "AfterChange": "変更後", "AjaxError404": "404 リクエストエラー", @@ -1053,7 +1053,7 @@ "Rules": "規則", "Run": "Action", "RunAgain": "再実行", - "RunAs": "実行ユーザー", + "RunAs": "実行アカウント (じっこうアカウント)", "RunCommand": "コマンドの実行", "RunJob": "ジョブを実行", "RunSucceed": "タスクが成功", diff --git a/apps/i18n/lina/zh.json b/apps/i18n/lina/zh.json index 5ffaae08a..c7c3b299c 100644 --- a/apps/i18n/lina/zh.json +++ b/apps/i18n/lina/zh.json @@ -70,7 +70,7 @@ "AdhocCreate": "创建命令", "AdhocDetail": "命令详情", "AdhocManage": "脚本管理", - "AdhocUpdate": "更新命令", + "AdhocUpdate": "更新脚本", "Advanced": "高级设置", "AfterChange": "变更后", "AjaxError404": "404 请求错误", @@ -1022,7 +1022,7 @@ "Rules": "规则", "Run": "执行", "RunAgain": "再次执行", - "RunAs": "运行用户", + "RunAs": "运行账号", "RunCommand": "运行命令", "RunJob": "运行作业", "RunSucceed": "任务执行成功", @@ -1405,5 +1405,13 @@ "ZoneUpdate": "更新网域", "disallowSelfUpdateFields": "不允许自己修改当前字段", "forceEnableMFAHelpText": "如果强制启用,用户无法自行禁用", - "removeWarningMsg": "你确定要移除" + "removeWarningMsg": "你确定要移除", + "VariableName": "变量名", + "ExecuteAfterSaving": "保存后执行", + "DefaultValue": "默认值", + "AddVariable": "添加参数", + "LoadTemplate": "从模板中加载", + "Templates": "模板", + "ExtraArgsPlaceholder": "每行一个选项,例如:\n选项1:值1\n选项2:值2\n", + "setVariable": "设置参数" } \ No newline at end of file diff --git a/apps/i18n/lina/zh_hant.json b/apps/i18n/lina/zh_hant.json index 7a5cf44eb..17c790a77 100644 --- a/apps/i18n/lina/zh_hant.json +++ b/apps/i18n/lina/zh_hant.json @@ -88,7 +88,7 @@ "AdhocCreate": "創建命令", "AdhocDetail": "命令詳情", "AdhocManage": "腳本管理", - "AdhocUpdate": "更新命令", + "AdhocUpdate": "更新腳本", "Admin": "管理員", "AdminUser": "特權用戶", "AdminUserCreate": "創建管理用戶", @@ -1356,7 +1356,7 @@ "Rules": "規則", "Run": "運行", "RunAgain": "再次執行", - "RunAs": "執行使用者", + "RunAs": "運行賬號", "RunCommand": "運行命令", "RunJob": "運行作業", "RunSucceed": "任務執行成功", @@ -2161,7 +2161,7 @@ "riskLevel": "風險等級", "rows": "行", "run": "執行", - "runAs": "運行用戶", + "runAs": "運行賬號", "runSucceed": "任務執行成功", "runTimes": "執行次數", "running": "運行中", diff --git a/apps/i18n/luna/en.json b/apps/i18n/luna/en.json index 3d7869858..adb97db1f 100644 --- a/apps/i18n/luna/en.json +++ b/apps/i18n/luna/en.json @@ -222,5 +222,6 @@ "start time": "start time", "success": "success", "system user": "system user", - "user": "user" + "user": "user", + "Command": "Command" } \ No newline at end of file diff --git a/apps/i18n/luna/zh.json b/apps/i18n/luna/zh.json index b8a4d812f..9f46403a2 100644 --- a/apps/i18n/luna/zh.json +++ b/apps/i18n/luna/zh.json @@ -220,5 +220,6 @@ "start time": "开始时间", "success": "成功", "system user": "系统用户", - "user": "用户" + "user": "用户", + "Command": "命令" } \ No newline at end of file diff --git a/apps/ops/ansible/runner.py b/apps/ops/ansible/runner.py index 29ccf53f9..df0c338fd 100644 --- a/apps/ops/ansible/runner.py +++ b/apps/ops/ansible/runner.py @@ -78,7 +78,7 @@ class AdHocRunner: class PlaybookRunner: - def __init__(self, inventory, playbook, project_dir='/tmp/', callback=None): + def __init__(self, inventory, playbook, project_dir='/tmp/', callback=None, extra_vars=None, ): self.id = uuid.uuid4() self.inventory = inventory @@ -89,6 +89,9 @@ class PlaybookRunner: self.cb = callback self.isolate = True self.envs = {} + if extra_vars is None: + extra_vars = {} + self.extra_vars = extra_vars def copy_playbook(self): entry = os.path.basename(self.playbook) @@ -119,6 +122,7 @@ class PlaybookRunner: status_handler=self.cb.status_handler, host_cwd=self.project_dir, envvars=self.envs, + extravars=self.extra_vars, **kwargs ) return self.cb diff --git a/apps/ops/api/__init__.py b/apps/ops/api/__init__.py index e82f3fb2e..0a96bbbdb 100644 --- a/apps/ops/api/__init__.py +++ b/apps/ops/api/__init__.py @@ -4,3 +4,4 @@ from .adhoc import * from .celery import * from .job import * from .playbook import * +from .variable import * diff --git a/apps/ops/api/job.py b/apps/ops/api/job.py index dcb7ee85f..3322124ca 100644 --- a/apps/ops/api/job.py +++ b/apps/ops/api/job.py @@ -22,6 +22,7 @@ from ops.models import Job, JobExecution from ops.serializers.job import ( JobSerializer, JobExecutionSerializer, FileSerializer, JobTaskStopSerializer ) +from ops.utils import merge_nodes_and_assets __all__ = [ 'JobViewSet', 'JobExecutionViewSet', 'JobRunVariableHelpAPIView', 'JobExecutionTaskDetail', 'UsernameHintsAPI' @@ -36,8 +37,6 @@ from accounts.models import Account from assets.const import Protocol from perms.const import ActionChoices from perms.utils.asset_perm import PermAssetDetailUtil -from perms.models import PermNode -from perms.utils import UserPermAssetUtil from jumpserver.settings import get_file_md5 @@ -47,26 +46,12 @@ def set_task_to_serializer_data(serializer, task_id): setattr(serializer, "_data", data) -def merge_nodes_and_assets(nodes, assets, user): - if not nodes: - return assets - perm_util = UserPermAssetUtil(user=user) - for node_id in nodes: - if node_id == PermNode.FAVORITE_NODE_KEY: - node_assets = perm_util.get_favorite_assets() - elif node_id == PermNode.UNGROUPED_NODE_KEY: - node_assets = perm_util.get_ungroup_assets() - else: - _, node_assets = perm_util.get_node_all_assets(node_id) - assets.extend(node_assets.exclude(id__in=[asset.id for asset in assets])) - return assets - - class JobViewSet(OrgBulkModelViewSet): serializer_class = JobSerializer filterset_fields = ('name', 'type') search_fields = ('name', 'comment') model = Job + _parameters = None def check_permissions(self, request): # job: upload_file @@ -106,10 +91,10 @@ class JobViewSet(OrgBulkModelViewSet): def perform_create(self, serializer): run_after_save = serializer.validated_data.pop('run_after_save', False) - node_ids = serializer.validated_data.pop('nodes', []) - assets = serializer.validated_data.get('assets') - assets = merge_nodes_and_assets(node_ids, assets, self.request.user) - serializer.validated_data['assets'] = assets + self._parameters = serializer.validated_data.pop('parameters', None) + nodes = serializer.validated_data.pop('nodes', []) + assets = serializer.validated_data.get('assets', []) + assets = merge_nodes_and_assets(nodes, assets, self.request.user) if serializer.validated_data.get('type') == Types.upload_file: account_name = serializer.validated_data.get('runas') self.check_upload_permission(assets, account_name) @@ -126,6 +111,8 @@ class JobViewSet(OrgBulkModelViewSet): def run_job(self, job, serializer): execution = job.create_execution() + if self._parameters: + execution.parameters = JobExecutionSerializer.validate_parameters(self._parameters) execution.creator = self.request.user execution.save() @@ -300,7 +287,7 @@ class UsernameHintsAPI(APIView): permission_classes = [IsValidUser] def post(self, request, **kwargs): - node_ids = request.data.get('nodes', None) + node_ids = request.data.get('nodes', []) asset_ids = request.data.get('assets', []) query = request.data.get('query', None) diff --git a/apps/ops/api/variable.py b/apps/ops/api/variable.py new file mode 100644 index 000000000..a3ea5ee4e --- /dev/null +++ b/apps/ops/api/variable.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from rest_framework.decorators import action +from rest_framework.response import Response + +from common.api.generic import JMSModelViewSet +from common.const.http import OPTIONS, GET +from common.permissions import IsValidUser +from ..models import Variable +from ..serializers import VariableSerializer, VariableFormDataSerializer + +__all__ = [ + 'VariableViewSet' +] + + +class VariableViewSet(JMSModelViewSet): + queryset = Variable.objects.all() + serializer_class = VariableSerializer + http_method_names = ['options', 'get'] + + @action(methods=[GET], detail=False, serializer_class=VariableFormDataSerializer, + permission_classes=[IsValidUser, ], url_path='form_data') + def form_data(self, request, *args, **kwargs): + # 只是为了动态返回serializer fields info + return Response({}) diff --git a/apps/ops/const.py b/apps/ops/const.py index b2b1c63ca..5b37f85ff 100644 --- a/apps/ops/const.py +++ b/apps/ops/const.py @@ -85,3 +85,8 @@ COMMAND_EXECUTION_DISABLED = _('Command execution disabled') class Scope(models.TextChoices): public = 'public', pgettext_lazy("scope", 'Public') private = 'private', _('Private') + + +class FieldType(models.TextChoices): + text = 'text', _('Text') + select = 'select', _('Select') diff --git a/apps/ops/migrations/0004_job_nodes_alter_job_assets.py b/apps/ops/migrations/0004_job_nodes_alter_job_assets.py new file mode 100644 index 000000000..09f82322c --- /dev/null +++ b/apps/ops/migrations/0004_job_nodes_alter_job_assets.py @@ -0,0 +1,28 @@ +# Generated by Django 4.1.13 on 2024-10-21 08:02 +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('assets', '0006_database_pg_ssl_mode'), + ('ops', '0003_alter_adhoc_unique_together_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='job', + name='nodes', + field=models.ManyToManyField(blank=True, to='assets.node', verbose_name='Node'), + ), + migrations.AlterField( + model_name='job', + name='assets', + field=models.ManyToManyField(blank=True, to='assets.asset', verbose_name='Assets'), + ), + migrations.AlterUniqueTogether( + name='job', + unique_together={('name', 'org_id', 'creator', 'type')}, + ), + ] diff --git a/apps/ops/migrations/0005_historicaljob_periodic_variable_and_more.py b/apps/ops/migrations/0005_historicaljob_periodic_variable_and_more.py new file mode 100644 index 000000000..1b09246ce --- /dev/null +++ b/apps/ops/migrations/0005_historicaljob_periodic_variable_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 4.1.13 on 2024-10-30 09:38 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('ops', '0004_job_nodes_alter_job_assets'), + ] + + operations = [ + migrations.AddField( + model_name='historicaljob', + name='periodic_variable', + field=models.JSONField(default=dict, verbose_name='Periodic variable'), + ), + migrations.AddField( + model_name='job', + name='periodic_variable', + field=models.JSONField(default=dict, verbose_name='Periodic variable'), + ), + migrations.CreateModel( + name='Variable', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=1024, null=True, verbose_name='Name')), + ('var_name', models.CharField(help_text="The variable name used in the script has a fixed prefix 'jms_' followed by the input variable name. For example, if the variable name is 'name,' the final generated environment variable will be 'jms_name'.", max_length=1024, null=True, verbose_name='Variable name')), + ('default_value', models.CharField(max_length=2048, null=True, verbose_name='Default Value')), + ('type', models.CharField(default='text', max_length=64, verbose_name='Variable type')), + ('tips', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Tips')), + ('required', models.BooleanField(default=False, verbose_name='Required')), + ('extra_args', models.JSONField(default=dict, verbose_name='ExtraVars')), + ('adhoc', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='variable', to='ops.adhoc', verbose_name='Adhoc')), + ('creator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Creator')), + ('job', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='variable', to='ops.job', verbose_name='Job')), + ('playbook', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='variable', to='ops.playbook', verbose_name='Playbook')), + ], + options={ + 'verbose_name': 'Variable', + 'ordering': ['date_created'], + }, + ), + ] diff --git a/apps/ops/models/__init__.py b/apps/ops/models/__init__.py index b6edb768d..cba6bf22e 100644 --- a/apps/ops/models/__init__.py +++ b/apps/ops/models/__init__.py @@ -5,3 +5,4 @@ from .adhoc import * from .celery import * from .playbook import * from .job import * +from .variable import * diff --git a/apps/ops/models/job.py b/apps/ops/models/job.py index 6c1132f3e..433f4158d 100644 --- a/apps/ops/models/job.py +++ b/apps/ops/models/job.py @@ -29,6 +29,7 @@ from ops.ansible.exception import CommandInBlackListException from ops.mixin import PeriodTaskModelMixin from ops.variables import * from ops.const import Types, RunasPolicies, JobStatus, JobModules +from ops.utils import merge_nodes_and_assets from orgs.mixins.models import JMSOrgBaseModel from perms.models import AssetPermission from perms.utils import UserPermAssetUtil @@ -50,11 +51,13 @@ def get_parent_keys(key, include_self=True): class JMSPermedInventory(JMSInventory): def __init__(self, assets, + nodes, account_policy='privileged_first', account_prefer='root,Administrator', module=None, host_callback=None, user=None): + assets = merge_nodes_and_assets(list(nodes), list(assets), user) super().__init__(assets, account_policy, account_prefer, host_callback, exclude_localhost=True) self.user = user self.module = module @@ -149,9 +152,11 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin): playbook = models.ForeignKey('ops.Playbook', verbose_name=_("Playbook"), null=True, on_delete=models.SET_NULL) type = models.CharField(max_length=128, choices=Types.choices, default=Types.adhoc, verbose_name=_("Type")) creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) - assets = models.ManyToManyField('assets.Asset', verbose_name=_("Assets")) + assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets")) + nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Node")) use_parameter_define = models.BooleanField(default=False, verbose_name=(_('Use Parameter Define'))) parameters_define = models.JSONField(default=dict, verbose_name=_('Parameters define')) + periodic_variable = models.JSONField(default=dict, verbose_name=_('Periodic variable')) runas = models.CharField(max_length=128, default='root', verbose_name=_('Run as')) runas_policy = models.CharField(max_length=128, choices=RunasPolicies.choices, default=RunasPolicies.skip, verbose_name=_('Run as policy')) @@ -203,7 +208,7 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin): @property def inventory(self): - return JMSPermedInventory(self.assets.all(), + return JMSPermedInventory(self.assets.all(), self.nodes.all(), self.runas_policy, self.runas, user=self.creator, module=self.module) @@ -220,7 +225,7 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin): class Meta: verbose_name = _("Job") - unique_together = [('name', 'org_id', 'creator')] + unique_together = [('name', 'org_id', 'creator', 'type')] ordering = ['date_created'] @@ -328,7 +333,7 @@ class JobExecution(JMSOrgBaseModel): if isinstance(self.parameters, str): extra_vars = json.loads(self.parameters) else: - extra_vars = {} + extra_vars = self.parameters if self.parameters else {} static_variables = self.gather_static_variables() extra_vars.update(static_variables) @@ -349,7 +354,8 @@ class JobExecution(JMSOrgBaseModel): runner = PlaybookRunner( self.inventory_path, self.current_job.playbook.entry, - self.private_dir + self.private_dir, + extra_vars=extra_vars, ) elif self.current_job.type == Types.upload_file: job_id = self.current_job.id diff --git a/apps/ops/models/playbook.py b/apps/ops/models/playbook.py index ccc7223d2..02da093bb 100644 --- a/apps/ops/models/playbook.py +++ b/apps/ops/models/playbook.py @@ -23,8 +23,6 @@ dangerous_keywords = ( ) - - class Playbook(JMSBaseModel): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, verbose_name=_('Name'), null=True) diff --git a/apps/ops/models/variable.py b/apps/ops/models/variable.py new file mode 100644 index 000000000..55044d758 --- /dev/null +++ b/apps/ops/models/variable.py @@ -0,0 +1,50 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from common.db.models import JMSBaseModel +from ops.const import FieldType + + +class Variable(JMSBaseModel): + name = models.CharField(max_length=1024, verbose_name=_('Name'), null=True) + var_name = models.CharField( + max_length=1024, null=True, verbose_name=_('Variable name'), + help_text=_("The variable name used in the script has a fixed prefix 'jms_' followed by the input variable " + "name. For example, if the variable name is 'name,' the final generated environment variable will " + "be 'jms_name'.") + ) + default_value = models.CharField(max_length=2048, verbose_name=_('Default Value'), null=True) + creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) + type = models.CharField(max_length=64, default=FieldType.text, verbose_name=_('Variable type')) + tips = models.CharField(max_length=1024, default='', verbose_name=_('Tips'), null=True, blank=True) + required = models.BooleanField(default=False, verbose_name=_('Required')) + extra_args = models.JSONField(default=dict, verbose_name=_('ExtraVars')) + playbook = models.ForeignKey( + 'ops.Playbook', verbose_name=_("Playbook"), null=True, on_delete=models.CASCADE, related_name='variable' + ) + adhoc = models.ForeignKey( + 'ops.AdHoc', verbose_name=_("Adhoc"), null=True, on_delete=models.CASCADE, related_name='variable' + ) + job = models.ForeignKey('ops.Job', verbose_name=_("Job"), null=True, on_delete=models.CASCADE, + related_name='variable') + + def __str__(self): + return self.name + + @property + def form_data(self): + return { + 'var_name': self.var_name, + 'label': self.name, + 'help_text': self.tips, + 'read_only': False, + 'required': self.required, + 'type': self.type, + 'write_only': False, + 'default': self.default_value, + 'extra_args': self.extra_args, + } + + class Meta: + verbose_name = _("Variable") + ordering = ['date_created'] diff --git a/apps/ops/serializers/__init__.py b/apps/ops/serializers/__init__.py index 166827b4b..c4b8f4982 100644 --- a/apps/ops/serializers/__init__.py +++ b/apps/ops/serializers/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- # - from .celery import * +from .variable import * from .adhoc import * diff --git a/apps/ops/serializers/adhoc.py b/apps/ops/serializers/adhoc.py index 4c73a01ea..8724be42d 100644 --- a/apps/ops/serializers/adhoc.py +++ b/apps/ops/serializers/adhoc.py @@ -3,17 +3,20 @@ from __future__ import unicode_literals from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from common.serializers.fields import ReadableHiddenField, LabeledChoiceField +from common.serializers import WritableNestedModelSerializer +from common.serializers.fields import ReadableHiddenField from common.serializers.mixin import CommonBulkModelSerializer from .mixin import ScopeSerializerMixin -from ..const import Scope from ..models import AdHoc +from ops.serializers import AdhocVariableSerializer -class AdHocSerializer(ScopeSerializerMixin, CommonBulkModelSerializer): +class AdHocSerializer(ScopeSerializerMixin, CommonBulkModelSerializer, WritableNestedModelSerializer): creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) + variable = AdhocVariableSerializer(many=True, required=False, allow_null=True, label=_('Variable')) class Meta: model = AdHoc read_only_field = ["id", "creator", "date_created", "date_updated", "created_by"] - fields = read_only_field + ["id", "name", "scope", "module", "args", "comment"] + fields_m2m = ['variable'] + fields = read_only_field + fields_m2m + ["id", "name", "scope", "module", "args", "comment"] diff --git a/apps/ops/serializers/job.py b/apps/ops/serializers/job.py index 431b062d0..fd4b28aef 100644 --- a/apps/ops/serializers/job.py +++ b/apps/ops/serializers/job.py @@ -3,20 +3,25 @@ import uuid from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from assets.models import Asset -from common.serializers.fields import ReadableHiddenField +from assets.models import Asset, Node +from common.serializers import WritableNestedModelSerializer +from common.serializers.fields import ReadableHiddenField, ObjectRelatedField from ops.mixin import PeriodTaskSerializerMixin from ops.models import Job, JobExecution from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from ops.serializers import JobVariableSerializer -class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin): +class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin, WritableNestedModelSerializer): creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) run_after_save = serializers.BooleanField(label=_("Execute after saving"), default=False, required=False) - nodes = serializers.ListField(required=False, child=serializers.CharField()) date_last_run = serializers.DateTimeField(label=_('Date last run'), read_only=True) name = serializers.CharField(label=_('Name'), max_length=128, allow_blank=True, required=False) assets = serializers.PrimaryKeyRelatedField(label=_('Assets'), queryset=Asset.objects, many=True, required=False) + nodes = ObjectRelatedField(label=_('Nodes'), queryset=Node.objects, many=True, required=False) + variable = JobVariableSerializer(many=True, required=False, allow_null=True, label=_('Variable')) + parameters = serializers.JSONField(label=_('Parameters'), default={}, write_only=True, required=False, + allow_null=True) def to_internal_value(self, data): instant = data.get('instant', False) @@ -39,6 +44,7 @@ class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin): "id", "date_last_run", "date_created", "date_updated", "average_time_cost" ] + fields_m2m = ['variable'] fields = read_only_fields + [ "name", "instant", "type", "module", "args", "playbook", "assets", @@ -46,8 +52,8 @@ class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin): "use_parameter_define", "parameters_define", "timeout", "chdir", "comment", "summary", "is_periodic", "interval", "crontab", "nodes", - "run_after_save" - ] + "run_after_save", "parameters", "periodic_variable" + ] + fields_m2m extra_kwargs = { 'average_time_cost': {'label': _('Duration')}, } @@ -97,3 +103,13 @@ class JobExecutionSerializer(BulkOrgResourceModelSerializer): if job_obj.creator != self.context['request'].user: raise serializers.ValidationError(_("You do not have permission for the current job.")) return job_obj + + @staticmethod + def validate_parameters(parameters): + prefix = "jms_" + new_parameters = {} + for key, value in parameters.items(): + if not key.startswith("jms_"): + key = prefix + key + new_parameters[key] = value + return new_parameters diff --git a/apps/ops/serializers/playbook.py b/apps/ops/serializers/playbook.py index e22e33068..8eff11191 100644 --- a/apps/ops/serializers/playbook.py +++ b/apps/ops/serializers/playbook.py @@ -1,11 +1,13 @@ import os - +from django.utils.translation import gettext_lazy as _ from rest_framework import serializers +from common.serializers import WritableNestedModelSerializer from common.serializers.fields import ReadableHiddenField from common.serializers.mixin import CommonBulkModelSerializer from ops.models import Playbook from .mixin import ScopeSerializerMixin +from ops.serializers.variable import PlaybookVariableSerializer def parse_playbook_name(path): @@ -13,10 +15,11 @@ def parse_playbook_name(path): return file_name.split(".")[-2] -class PlaybookSerializer(ScopeSerializerMixin, CommonBulkModelSerializer): +class PlaybookSerializer(ScopeSerializerMixin, CommonBulkModelSerializer, WritableNestedModelSerializer): creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) path = serializers.FileField(required=False) - + variable = PlaybookVariableSerializer(many=True, required=False, allow_null=True, label=_('Variable')) + def to_internal_value(self, data): name = data.get('name', False) if not name and data.get('path'): @@ -26,7 +29,8 @@ class PlaybookSerializer(ScopeSerializerMixin, CommonBulkModelSerializer): class Meta: model = Playbook read_only_fields = ["id", "date_created", "date_updated", "created_by"] - fields = read_only_fields + [ + fields_m2m = ['variable'] + fields = read_only_fields + fields_m2m + [ "id", 'path', 'scope', "name", "comment", "creator", 'create_method', 'vcs_url', ] diff --git a/apps/ops/serializers/variable.py b/apps/ops/serializers/variable.py new file mode 100644 index 000000000..4fb2413bf --- /dev/null +++ b/apps/ops/serializers/variable.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +from django.db import models +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from common.serializers.fields import ReadableHiddenField, LabeledChoiceField, EncryptedField +from common.serializers.mixin import CommonBulkModelSerializer +from ops.const import FieldType +from ops.models import Variable, AdHoc, Job, Playbook + +__all__ = [ + 'VariableSerializer', 'AdhocVariableSerializer', 'JobVariableSerializer', 'PlaybookVariableSerializer', + 'VariableFormDataSerializer' +] + + +class VariableSerializer(CommonBulkModelSerializer): + creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) + type = LabeledChoiceField( + choices=FieldType.choices, default=FieldType.text, label=_("Variable Type") + ) + extra_args = serializers.CharField( + max_length=1024, label=_("ExtraVars"), required=False, allow_blank=True, + help_text=_( + "Each item is on a separate line, with each line separated by a colon. The part before the colon is the " + "display content, and the part after the colon is the value.") + ) + + class Meta: + model = Variable + read_only_fields = ["id", "date_created", "date_updated", "created_by", "creator"] + fields = read_only_fields + [ + "name", "var_name", "type", 'required', 'default_value', 'tips', 'adhoc', 'playbook', 'job', 'form_data', + 'extra_args' + ] + + def validate(self, attrs): + attrs = super().validate(attrs) + type = attrs.get('type') + attrs['extra_args'] = {} + if type == FieldType.text: + attrs['default_value'] = self.initial_data.get('text_default_value') + elif type == FieldType.select: + attrs['default_value'] = self.initial_data.get('select_default_value') + options = self.initial_data.get('extra_args', '') + attrs['extra_args'] = {"options": options} + return attrs + + def to_representation(self, instance): + data = super().to_representation(instance) + if instance.type == FieldType.select: + data['extra_args'] = instance.extra_args.get('options', '') + data['select_default_value'] = instance.default_value + if instance.type == FieldType.text: + data['text_default_value'] = instance.default_value + return data + + @classmethod + def setup_eager_loading(cls, queryset): + queryset = queryset.prefetch_related('adhoc', 'job', 'playbook') + return queryset + + +class AdhocVariableSerializer(VariableSerializer): + adhoc = serializers.PrimaryKeyRelatedField(queryset=AdHoc.objects, required=False) + + class Meta(VariableSerializer.Meta): + fields = VariableSerializer.Meta.fields + + +class JobVariableSerializer(VariableSerializer): + job = serializers.PrimaryKeyRelatedField(queryset=Job.objects, required=False) + + class Meta(VariableSerializer.Meta): + fields = VariableSerializer.Meta.fields + + +class PlaybookVariableSerializer(VariableSerializer): + playbook = serializers.PrimaryKeyRelatedField(queryset=Playbook.objects, required=False) + + class Meta(VariableSerializer.Meta): + fields = VariableSerializer.Meta.fields + + +def create_dynamic_text_choices(options): + """ + 动态创建一个 TextChoices 子类。`options` 应该是一个列表, + 格式为 [(value1, display1), (value2, display2), ...] + """ + attrs = { + key.upper(): value for value, key in options + } + attrs['choices'] = options + return type('DynamicTextChoices', (models.TextChoices,), attrs) + + +class VariableFormDataSerializer(serializers.Serializer): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + request = self.context.get('request') + if not request: + return + params = request.query_params + job = params.get('job') + adhoc = params.get('adhoc') + playbook = params.get('playbook') + if job: + variables = Variable.objects.filter(job=job).all() + elif adhoc: + variables = Variable.objects.filter(adhoc=adhoc).all() + else: + variables = Variable.objects.filter(playbook=playbook).all() + dynamic_fields = [var.form_data for var in variables] + + if dynamic_fields: + for field in dynamic_fields: + field_type = field['type'] + required = field['required'] + var_name = field["var_name"] + label = field["label"] + help_text = field['help_text'] + default = field['default'] + if field_type == FieldType.text: + self.fields[var_name] = serializers.CharField( + max_length=1024, label=label, help_text=help_text, required=required + ) + elif field_type == FieldType.select: + extra_args = field.get('extra_args', {}) + options = extra_args.get('options', '').splitlines() + + DynamicFieldType = models.TextChoices( + 'DynamicFieldType', + { + option.split(':')[0]: option.split(':')[1] for option in + options + } + ) + self.fields[var_name] = LabeledChoiceField( + choices=DynamicFieldType.choices, required=required, label=label, + help_text=help_text + ) + if required and default is not None: + self.fields[var_name].default = default diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py index 31d9ee38d..283c4d95d 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -10,6 +10,7 @@ from django_celery_beat.models import PeriodicTask from common.const.crontab import CRONTAB_AT_AM_TWO from common.utils import get_logger, get_object_or_none, get_log_keep_day from ops.celery import app +from ops.serializers.job import JobExecutionSerializer from orgs.utils import tmp_to_org, tmp_to_root_org from .celery.decorator import ( register_as_period_task, after_app_ready_start @@ -64,6 +65,8 @@ def run_ops_job(job_id): with tmp_to_org(job.org): execution = job.create_execution() execution.creator = job.creator + if job.periodic_variable: + execution.parameters = JobExecutionSerializer.validate_parameters(job.periodic_variable) _run_ops_job_execution(execution) diff --git a/apps/ops/urls/api_urls.py b/apps/ops/urls/api_urls.py index a02f582fa..28157972a 100644 --- a/apps/ops/urls/api_urls.py +++ b/apps/ops/urls/api_urls.py @@ -15,8 +15,8 @@ bulk_router = BulkRouter() bulk_router.register(r'adhocs', api.AdHocViewSet, 'adhoc') bulk_router.register(r'playbooks', api.PlaybookViewSet, 'playbook') bulk_router.register(r'jobs', api.JobViewSet, 'job') +bulk_router.register(r'variable', api.VariableViewSet, 'variable') bulk_router.register(r'job-executions', api.JobExecutionViewSet, 'job-execution') - router.register(r'celery/period-tasks', api.CeleryPeriodTaskViewSet, 'celery-period-task') router.register(r'tasks', api.CeleryTaskViewSet, 'task') diff --git a/apps/ops/utils.py b/apps/ops/utils.py index 539d6b66c..2e970b4d9 100644 --- a/apps/ops/utils.py +++ b/apps/ops/utils.py @@ -5,6 +5,9 @@ from django.conf import settings from common.utils import get_logger, make_dirs from jumpserver.const import PROJECT_DIR +from perms.models import PermNode +from perms.utils import UserPermAssetUtil +from assets.models import Asset, Node logger = get_logger(__file__) @@ -29,3 +32,19 @@ def get_ansible_log_verbosity(verbosity=0): return 1 return verbosity + +def merge_nodes_and_assets(nodes, assets, user): + if not nodes: + return assets + perm_util = UserPermAssetUtil(user=user) + for node_id in nodes: + if isinstance(node_id, Node): + node_id = node_id.id + if node_id == PermNode.FAVORITE_NODE_KEY: + node_assets = perm_util.get_favorite_assets() + elif node_id == PermNode.UNGROUPED_NODE_KEY: + node_assets = perm_util.get_ungroup_assets() + else: + _, node_assets = perm_util.get_node_all_assets(node_id) + assets.extend(node_assets.exclude(id__in=[asset.id for asset in assets])) + return assets diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py index e3441a04d..1d108bb92 100644 --- a/apps/perms/serializers/user_permission.py +++ b/apps/perms/serializers/user_permission.py @@ -51,7 +51,7 @@ class NodePermedSerializer(serializers.ModelSerializer): class Meta: model = Node fields = [ - 'id', 'name', 'key', 'value', + 'id', 'name', 'key', 'value', 'full_value', 'org_id', "assets_amount" ] read_only_fields = fields