diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index 096c65939..b1e01c9c1 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -5,11 +5,13 @@ from collections import namedtuple, defaultdict from rest_framework import status from rest_framework.serializers import ValidationError from rest_framework.response import Response +from rest_framework.decorators import action from django.utils.translation import ugettext_lazy as _ from django.shortcuts import get_object_or_404, Http404 from django.utils.decorators import method_decorator from django.db.models.signals import m2m_changed +from common.const.http import POST from common.exceptions import SomeoneIsDoingThis from common.const.signals import PRE_REMOVE, POST_REMOVE from assets.models import Asset @@ -19,6 +21,7 @@ from common.const.distributed_lock_key import UPDATE_NODE_TREE_LOCK_KEY from orgs.mixins.api import OrgModelViewSet from orgs.mixins import generics from orgs.lock import org_level_transaction_lock +from assets.tasks import check_node_assets_amount_period_task from ..hands import IsOrgAdmin from ..models import Node from ..tasks import ( @@ -46,6 +49,11 @@ class NodeViewSet(OrgModelViewSet): permission_classes = (IsOrgAdmin,) serializer_class = serializers.NodeSerializer + @action(methods=[POST], detail=False, url_name='launch-check-assets-amount-task') + def launch_check_assets_amount_task(self, request): + task = check_node_assets_amount_period_task.delay() + return Response(data={'task': task.id}) + # 仅支持根节点指直接创建,子节点下的节点需要通过children接口创建 def perform_create(self, serializer): child_key = Node.org_root().get_next_child_key() diff --git a/apps/assets/tasks/nodes_amount.py b/apps/assets/tasks/nodes_amount.py index 3ae191788..cd929d131 100644 --- a/apps/assets/tasks/nodes_amount.py +++ b/apps/assets/tasks/nodes_amount.py @@ -1,13 +1,19 @@ from celery import shared_task +from django.utils.translation import gettext_lazy as _ from ops.celery.decorator import register_as_period_task from assets.utils import check_node_assets_amount + +from common.utils.lock import AcquireFailed from common.utils import get_logger logger = get_logger(__file__) -@register_as_period_task(crontab='0 2 * * *') @shared_task(queue='celery_heavy_tasks') -def check_node_assets_amount_celery_task(): - check_node_assets_amount() +@register_as_period_task(crontab='0 2 * * *') +def check_node_assets_amount_period_task(): + try: + check_node_assets_amount() + except AcquireFailed: + logger.error(_('The task of self-checking is already running and cannot be started repeatedly')) diff --git a/apps/assets/utils.py b/apps/assets/utils.py index c4c112c16..2805ac034 100644 --- a/apps/assets/utils.py +++ b/apps/assets/utils.py @@ -5,6 +5,7 @@ import time from django.db.models import Q from common.utils import get_logger, dict_get_any, is_uuid, get_object_or_none +from common.utils.lock import DistributedLock from common.http import is_true from .models import Asset, Node @@ -12,6 +13,7 @@ from .models import Asset, Node logger = get_logger(__file__) +@DistributedLock(name="assets.node.check_node_assets_amount", blocking=False) def check_node_assets_amount(): for node in Node.objects.all(): logger.info(f'Check node assets amount: {node}') diff --git a/apps/common/utils/inspect.py b/apps/common/utils/inspect.py new file mode 100644 index 000000000..650d3b434 --- /dev/null +++ b/apps/common/utils/inspect.py @@ -0,0 +1,10 @@ +import inspect + + +def copy_function_args(func, locals_dict: dict): + signature = inspect.signature(func) + keys = signature.parameters.keys() + kwargs = {} + for k in keys: + kwargs[k] = locals_dict.get(k) + return kwargs diff --git a/apps/common/utils/lock.py b/apps/common/utils/lock.py new file mode 100644 index 000000000..9041a2578 --- /dev/null +++ b/apps/common/utils/lock.py @@ -0,0 +1,55 @@ +from functools import wraps + +from redis_lock import Lock as RedisLock +from redis import Redis + +from common.utils import get_logger +from common.utils.inspect import copy_function_args +from apps.jumpserver.const import CONFIG + +logger = get_logger(__file__) + + +class AcquireFailed(RuntimeError): + pass + + +class DistributedLock(RedisLock): + def __init__(self, name, blocking=True, expire=60*2, auto_renewal=True): + """ + 使用 redis 构造的分布式锁 + + :param name: + 锁的名字,要全局唯一 + :param blocking: + 该参数只在锁作为装饰器或者 `with` 时有效。 + :param expire: + 锁的过期时间,注意不一定是锁到这个时间就释放了,分两种情况 + 当 `auto_renewal=False` 时,锁会释放 + 当 `auto_renewal=True` 时,如果过期之前程序还没释放锁,我们会延长锁的存活时间。 + 这里的作用是防止程序意外终止没有释放锁,导致死锁。 + """ + self.kwargs_copy = copy_function_args(self.__init__, locals()) + redis = Redis(host=CONFIG.REDIS_HOST, port=CONFIG.REDIS_PORT, password=CONFIG.REDIS_PASSWORD) + super().__init__(redis_client=redis, name=name, expire=expire, auto_renewal=auto_renewal) + self._blocking = blocking + + def __enter__(self): + acquired = self.acquire(blocking=self._blocking) + if self._blocking and not acquired: + raise EnvironmentError("Lock wasn't acquired, but blocking=True") + if not acquired: + raise AcquireFailed + return self + + def __exit__(self, exc_type=None, exc_value=None, traceback=None): + self.release() + + def __call__(self, func): + @wraps(func) + def inner(*args, **kwds): + # 要创建一个新的锁对象 + with self.__class__(**self.kwargs_copy): + return func(*args, **kwds) + + return inner diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 211388dd8..73c802715 100644 Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 7ddf14999..1b82453bb 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: 2020-12-09 18:14+0800\n" +"POT-Creation-Date: 2020-12-10 17:04+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -41,7 +41,7 @@ msgstr "远程应用" #: orgs/models.py:23 perms/models/base.py:48 settings/models.py:27 #: terminal/models.py:28 terminal/models.py:372 terminal/models.py:404 #: terminal/models.py:441 users/forms/profile.py:20 users/models/group.py:15 -#: users/models/user.py:501 users/templates/users/_select_user_modal.html:13 +#: users/models/user.py:495 users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:154 #: users/templates/users/user_database_app_permission.html:36 @@ -94,7 +94,7 @@ msgstr "类型" #: assets/models/label.py:23 ops/models/adhoc.py:37 orgs/models.py:26 #: perms/models/base.py:56 settings/models.py:32 terminal/models.py:38 #: terminal/models.py:411 terminal/models.py:448 tickets/models/ticket.py:43 -#: users/models/group.py:16 users/models/user.py:534 +#: users/models/group.py:16 users/models/user.py:528 #: users/templates/users/user_detail.html:115 #: users/templates/users/user_granted_database_app.html:38 #: users/templates/users/user_granted_remote_app.html:37 @@ -182,7 +182,7 @@ msgstr "参数" #: assets/models/cmd_filter.py:26 assets/models/cmd_filter.py:60 #: assets/models/group.py:21 common/db/models.py:67 common/mixins/models.py:49 #: orgs/models.py:24 orgs/models.py:400 perms/models/base.py:54 -#: users/models/user.py:542 users/serializers/group.py:35 +#: users/models/user.py:536 users/serializers/group.py:35 #: users/templates/users/user_detail.html:97 #: xpack/plugins/change_auth_plan/models.py:81 xpack/plugins/cloud/models.py:58 #: xpack/plugins/cloud/models.py:156 xpack/plugins/gathered_user/models.py:30 @@ -237,7 +237,7 @@ msgstr "目标URL" #: authentication/forms.py:11 #: authentication/templates/authentication/login.html:21 #: authentication/templates/authentication/xpack_login.html:101 -#: ops/models/adhoc.py:148 users/forms/profile.py:19 users/models/user.py:499 +#: ops/models/adhoc.py:148 users/forms/profile.py:19 users/models/user.py:493 #: users/templates/users/_select_user_modal.html:14 #: users/templates/users/user_detail.html:53 #: users/templates/users/user_list.html:15 @@ -251,7 +251,8 @@ msgstr "用户名" #: applications/serializers/remote_app.py:71 #: applications/serializers/remote_app.py:79 #: applications/serializers/remote_app.py:86 assets/models/base.py:236 -#: assets/serializers/asset_user.py:71 authentication/forms.py:13 +#: assets/serializers/asset_user.py:71 audits/signals_handler.py:42 +#: authentication/forms.py:13 #: authentication/templates/authentication/login.html:29 #: authentication/templates/authentication/xpack_login.html:109 #: users/forms/user.py:22 users/forms/user.py:193 @@ -295,15 +296,15 @@ msgstr "删除失败,存在关联资产" msgid "Number required" msgstr "需要为数字" -#: assets/api/node.py:58 +#: assets/api/node.py:66 msgid "You can't update the root node name" msgstr "不能修改根节点名称" -#: assets/api/node.py:65 +#: assets/api/node.py:73 msgid "You can't delete the root node ({})" msgstr "不能删除根节点 ({})" -#: assets/api/node.py:68 +#: assets/api/node.py:76 msgid "Deletion failed and the node contains children or assets" msgstr "删除失败,节点包含子节点或资产" @@ -364,7 +365,7 @@ msgstr "节点" #: assets/models/asset.py:200 assets/models/cmd_filter.py:22 #: assets/models/domain.py:56 assets/models/label.py:22 -#: authentication/models.py:48 +#: authentication/models.py:46 msgid "Is active" msgstr "激活" @@ -484,7 +485,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:520 +#: assets/models/cluster.py:22 users/models/user.py:514 #: users/templates/users/user_detail.html:62 msgid "Phone" msgstr "手机" @@ -510,7 +511,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:661 +#: users/models/user.py:655 msgid "System" msgstr "系统" @@ -611,8 +612,8 @@ msgid "Default asset group" msgstr "默认资产组" #: assets/models/label.py:15 audits/models.py:36 audits/models.py:56 -#: audits/models.py:69 audits/serializers.py:80 authentication/models.py:46 -#: authentication/models.py:90 orgs/models.py:18 orgs/models.py:396 +#: audits/models.py:69 audits/serializers.py:81 authentication/models.py:44 +#: authentication/models.py:88 orgs/models.py:18 orgs/models.py:396 #: perms/forms/asset_permission.py:83 perms/forms/database_app_permission.py:38 #: perms/forms/remote_app_permission.py:40 perms/models/asset_permission.py:169 #: perms/models/base.py:49 templates/index.html:78 @@ -621,7 +622,7 @@ msgstr "默认资产组" #: tickets/models/ticket.py:30 tickets/models/ticket.py:136 #: tickets/serializers/request_asset_perm.py:66 #: tickets/serializers/ticket.py:31 users/forms/group.py:15 -#: users/models/user.py:159 users/models/user.py:649 +#: users/models/user.py:159 users/models/user.py:643 #: users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 @@ -715,7 +716,7 @@ msgstr "登录模式" msgid "SFTP Root" msgstr "SFTP根路径" -#: assets/models/user.py:110 authentication/models.py:88 +#: assets/models/user.py:110 authentication/models.py:86 msgid "Token" msgstr "" @@ -809,14 +810,14 @@ msgid "Backend" msgstr "后端" #: assets/serializers/asset_user.py:75 users/forms/profile.py:148 -#: users/models/user.py:531 users/templates/users/user_password_update.html:48 +#: users/models/user.py:525 users/templates/users/user_password_update.html:48 #: users/templates/users/user_profile.html:69 #: users/templates/users/user_profile_update.html:46 #: users/templates/users/user_pubkey_update.html:46 msgid "Public key" msgstr "SSH公钥" -#: assets/serializers/asset_user.py:79 users/models/user.py:528 +#: assets/serializers/asset_user.py:79 users/models/user.py:522 msgid "Private key" msgstr "ssh私钥" @@ -936,6 +937,11 @@ msgstr "更新节点资产硬件信息: {}" msgid "Gather assets users" msgstr "收集资产上的用户" +#: assets/tasks/nodes_amount.py:19 +msgid "" +"The task of self-checking is already running and cannot be started repeatedly" +msgstr "自检程序已经在运行,不能重复启动" + #: assets/tasks/push_system_user.py:184 #: assets/tasks/system_user_connectivity.py:89 msgid "System user is dynamic: {}" @@ -1125,14 +1131,14 @@ msgstr "登录IP" msgid "Login city" msgstr "登录城市" -#: audits/models.py:103 audits/serializers.py:37 +#: audits/models.py:103 audits/serializers.py:38 msgid "User agent" msgstr "用户代理" #: audits/models.py:104 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: authentication/templates/authentication/login_otp.html:6 -#: users/forms/profile.py:52 users/models/user.py:523 +#: users/forms/profile.py:52 users/models/user.py:517 #: users/serializers/user.py:232 users/templates/users/user_detail.html:77 #: users/templates/users/user_profile.html:87 msgid "MFA" @@ -1153,6 +1159,10 @@ msgstr "状态" msgid "Date login" msgstr "登录日期" +#: audits/models.py:108 +msgid "Login backend" +msgstr "登录引擎" + #: audits/serializers.py:15 msgid "Operate for display" msgstr "操作(显示名称)" @@ -1165,32 +1175,40 @@ msgstr "状态(显示名称)" msgid "MFA for display" msgstr "多因子认证状态(显示名称)" -#: audits/serializers.py:65 audits/serializers.py:77 ops/models/adhoc.py:244 +#: audits/serializers.py:66 audits/serializers.py:78 ops/models/adhoc.py:244 #: terminal/serializers/session.py:34 msgid "Is success" msgstr "是否成功" -#: audits/serializers.py:76 ops/models/command.py:24 +#: audits/serializers.py:77 ops/models/command.py:24 #: xpack/plugins/cloud/models.py:222 msgid "Result" msgstr "结果" -#: audits/serializers.py:78 +#: audits/serializers.py:79 msgid "Hosts" msgstr "主机" -#: audits/serializers.py:79 +#: audits/serializers.py:80 msgid "Run as" msgstr "运行用户" -#: audits/serializers.py:81 +#: audits/serializers.py:82 msgid "Run as for display" msgstr "运行用户(显示名称)" -#: audits/serializers.py:82 +#: audits/serializers.py:83 msgid "User for display" msgstr "用户(显示名称)" +#: audits/signals_handler.py:38 +msgid "SSH Key" +msgstr "SSH 密钥" + +#: audits/signals_handler.py:43 +msgid "SSO" +msgstr "" + #: authentication/api/mfa.py:60 msgid "Code is invalid" msgstr "Code无效" @@ -1333,7 +1351,7 @@ msgstr "你的密码过于简单,为了安全,请修改" #: authentication/errors.py:227 authentication/views/login.py:262 msgid "Your password has expired, please reset before logging in" -msgstr "您的密码已过期,请先修改再登录" +msgstr "您的密码已过期,先修改再登录" #: authentication/forms.py:26 authentication/forms.py:34 #: authentication/templates/authentication/login.html:39 @@ -1342,7 +1360,7 @@ msgstr "您的密码已过期,请先修改再登录" msgid "MFA code" msgstr "多因子认证验证码" -#: authentication/models.py:22 +#: authentication/models.py:20 #: authentication/templates/authentication/_access_key_modal.html:32 #: perms/models/base.py:51 users/templates/users/_select_user_modal.html:18 #: users/templates/users/user_detail.html:132 @@ -1350,24 +1368,24 @@ msgstr "多因子认证验证码" msgid "Active" msgstr "激活中" -#: authentication/models.py:42 +#: authentication/models.py:40 msgid "Private Token" msgstr "SSH密钥" -#: authentication/models.py:47 users/templates/users/user_detail.html:258 +#: authentication/models.py:45 users/templates/users/user_detail.html:258 msgid "Reviewers" msgstr "审批人" -#: authentication/models.py:56 tickets/models/ticket.py:23 +#: authentication/models.py:54 tickets/models/ticket.py:23 #: users/templates/users/user_detail.html:250 msgid "Login confirm" msgstr "登录复核" -#: authentication/models.py:66 +#: authentication/models.py:64 msgid "City" msgstr "城市" -#: authentication/models.py:89 +#: authentication/models.py:87 msgid "Expired" msgstr "过期时间" @@ -1857,7 +1875,7 @@ msgstr "组织管理员" msgid "Organization auditor" msgstr "组织审计员" -#: orgs/models.py:397 users/forms/user.py:27 users/models/user.py:511 +#: orgs/models.py:397 users/forms/user.py:27 users/models/user.py:505 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:73 #: users/templates/users/user_list.html:16 @@ -1890,7 +1908,7 @@ msgstr "提示:RDP 协议不支持单独控制上传或下载文件" #: perms/forms/asset_permission.py:86 perms/forms/database_app_permission.py:41 #: perms/forms/remote_app_permission.py:43 perms/models/base.py:50 #: templates/_nav.html:21 users/forms/user.py:168 users/models/group.py:31 -#: users/models/user.py:507 users/templates/users/_select_user_modal.html:16 +#: users/models/user.py:501 users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_asset_permission.html:39 #: users/templates/users/user_asset_permission.html:67 #: users/templates/users/user_database_app_permission.html:38 @@ -1964,7 +1982,7 @@ msgid "Asset permission" msgstr "资产授权" #: perms/models/base.py:53 tickets/serializers/request_asset_perm.py:31 -#: users/models/user.py:539 users/templates/users/user_detail.html:93 +#: users/models/user.py:533 users/templates/users/user_detail.html:93 #: users/templates/users/user_profile.html:120 msgid "Date expired" msgstr "失效日期" @@ -3116,7 +3134,7 @@ msgstr "确认密码" msgid "Password does not match" msgstr "密码不一致" -#: users/forms/profile.py:89 users/models/user.py:503 +#: users/forms/profile.py:89 users/models/user.py:497 #: users/templates/users/user_detail.html:57 #: users/templates/users/user_profile.html:59 msgid "Email" @@ -3157,7 +3175,7 @@ msgstr "不能和原来的密钥相同" msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/forms/user.py:31 users/models/user.py:546 +#: users/forms/user.py:31 users/models/user.py:540 #: users/templates/users/user_detail.html:89 #: users/templates/users/user_list.html:18 #: users/templates/users/user_profile.html:102 @@ -3203,27 +3221,27 @@ msgstr "系统审计员" msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:490 +#: users/models/user.py:485 msgid "Local" msgstr "数据库" -#: users/models/user.py:514 +#: users/models/user.py:508 msgid "Avatar" msgstr "头像" -#: users/models/user.py:517 users/templates/users/user_detail.html:68 +#: users/models/user.py:511 users/templates/users/user_detail.html:68 msgid "Wechat" msgstr "微信" -#: users/models/user.py:550 +#: users/models/user.py:544 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:657 +#: users/models/user.py:651 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:660 +#: users/models/user.py:654 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员"