From 3681bfc015fdb9020ecc99e6ef3d6effc4a032af Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 17 Sep 2019 17:46:18 +0800 Subject: [PATCH 1/7] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=BB=84=E6=8E=88=E6=9D=83api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/perms/api/mixin.py | 13 +++++++++++-- apps/perms/api/user_group_permission.py | 13 +------------ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/apps/perms/api/mixin.py b/apps/perms/api/mixin.py index dd5678a7c..fba8155ae 100644 --- a/apps/perms/api/mixin.py +++ b/apps/perms/api/mixin.py @@ -8,14 +8,14 @@ from assets.utils import LabelFilterMixin from common.permissions import IsValidUser, IsOrgAdminOrAppUser from common.utils import get_logger from orgs.utils import set_to_root_org -from ..hands import User, Asset, SystemUser +from ..hands import User, UserGroup, Asset, SystemUser from .. import serializers logger = get_logger(__name__) __all__ = [ - 'UserPermissionMixin', + 'UserPermissionMixin', 'UserGroupPermissionMixin', ] @@ -45,6 +45,15 @@ class UserPermissionMixin: return super().get_permissions() +class UserGroupPermissionMixin: + obj = None + + def get_obj(self): + user_group_id = self.kwargs.get('pk', '') + user_group = get_object_or_404(UserGroup, id=user_group_id) + return user_group + + class GrantAssetsMixin(LabelFilterMixin): serializer_class = serializers.AssetGrantedSerializer diff --git a/apps/perms/api/user_group_permission.py b/apps/perms/api/user_group_permission.py index 7d93724c3..73e191bc7 100644 --- a/apps/perms/api/user_group_permission.py +++ b/apps/perms/api/user_group_permission.py @@ -1,11 +1,9 @@ # -*- coding: utf-8 -*- # -from django.shortcuts import get_object_or_404 - -from ..hands import UserGroup from . import user_permission as uapi +from .mixin import UserGroupPermissionMixin __all__ = [ 'UserGroupGrantedAssetsApi', 'UserGroupGrantedNodesApi', @@ -17,15 +15,6 @@ __all__ = [ ] -class UserGroupPermissionMixin: - obj = None - - def get_object(self): - user_group_id = self.kwargs.get('pk', '') - user_group = get_object_or_404(UserGroup, id=user_group_id) - return user_group - - class UserGroupGrantedAssetsApi(UserGroupPermissionMixin, uapi.UserGrantedAssetsApi): pass From 0db3e41bde84171c0d400b5924b3581d8f4f034c Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 17 Sep 2019 18:59:54 +0800 Subject: [PATCH 2/7] =?UTF-8?q?[Update]=20=E6=8B=86=E5=88=86asset=20tasks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/tasks.py | 634 ------------------ apps/assets/tasks/__init__.py | 10 + apps/assets/tasks/admin_user_connectivity.py | 65 ++ apps/assets/tasks/asset_connectivity.py | 81 +++ apps/assets/tasks/asset_user_connectivity.py | 77 +++ apps/assets/{ => tasks}/const.py | 23 + .../tasks/gather_asset_hardware_info.py | 125 ++++ apps/assets/tasks/gather_asset_users.py | 42 ++ apps/assets/tasks/push_system_user.py | 202 ++++++ apps/assets/tasks/system_user_connectivity.py | 101 +++ apps/assets/tasks/utils.py | 45 ++ apps/jumpserver/conf.py | 1 + apps/jumpserver/settings.py | 1 + apps/ops/inventory.py | 4 +- 14 files changed, 776 insertions(+), 635 deletions(-) create mode 100644 apps/assets/tasks/__init__.py create mode 100644 apps/assets/tasks/admin_user_connectivity.py create mode 100644 apps/assets/tasks/asset_connectivity.py create mode 100644 apps/assets/tasks/asset_user_connectivity.py rename apps/assets/{ => tasks}/const.py (76%) create mode 100644 apps/assets/tasks/gather_asset_hardware_info.py create mode 100644 apps/assets/tasks/gather_asset_users.py create mode 100644 apps/assets/tasks/push_system_user.py create mode 100644 apps/assets/tasks/system_user_connectivity.py create mode 100644 apps/assets/tasks/utils.py diff --git a/apps/assets/tasks.py b/apps/assets/tasks.py index 20bbcd5e6..e69de29bb 100644 --- a/apps/assets/tasks.py +++ b/apps/assets/tasks.py @@ -1,634 +0,0 @@ -# ~*~ coding: utf-8 ~*~ -import json -import re -import os - -from collections import defaultdict -from celery import shared_task -from django.utils.translation import ugettext as _ -from django.core.cache import cache - -from common.utils import ( - capacity_convert, sum_capacity, encrypt_password, get_logger -) -from ops.celery.decorator import ( - register_as_period_task, after_app_shutdown_clean_periodic -) - -from .models import SystemUser, AdminUser -from .models.utils import Connectivity -from . import const - - -FORKS = 10 -TIMEOUT = 60 -logger = get_logger(__file__) -CACHE_MAX_TIME = 60*60*2 -disk_pattern = re.compile(r'^hd|sd|xvd|vd|nv') -PERIOD_TASK = os.environ.get("PERIOD_TASK", "on") - - -def check_asset_can_run_ansible(asset): - if not asset.is_active: - msg = _("Asset has been disabled, skipped: {}").format(asset) - logger.info(msg) - return False - if not asset.is_support_ansible(): - msg = _("Asset may not be support ansible, skipped: {}").format(asset) - logger.info(msg) - return False - return True - - -def clean_hosts(assets): - clean_assets = [] - for asset in assets: - if not check_asset_can_run_ansible(asset): - continue - clean_assets.append(asset) - if not clean_assets: - logger.info(_("No assets matched, stop task")) - return clean_assets - - -def clean_hosts_by_protocol(system_user, assets): - hosts = [ - asset for asset in assets - if asset.has_protocol(system_user.protocol) - ] - if not hosts: - msg = _("No assets matched related system user protocol, stop task") - logger.info(msg) - return hosts - - -@shared_task(queue="ansible") -def set_assets_hardware_info(assets, result, **kwargs): - """ - Using ops task run result, to update asset info - - @shared_task must be exit, because we using it as a task callback, is must - be a celery task also - :param assets: - :param result: - :param kwargs: {task_name: ""} - :return: - """ - result_raw = result[0] - assets_updated = [] - success_result = result_raw.get('ok', {}) - - for asset in assets: - hostname = asset.hostname - info = success_result.get(hostname, {}) - info = info.get('setup', {}).get('ansible_facts', {}) - if not info: - logger.error(_("Get asset info failed: {}").format(hostname)) - continue - ___vendor = info.get('ansible_system_vendor', 'Unknown') - ___model = info.get('ansible_product_name', 'Unknown') - ___sn = info.get('ansible_product_serial', 'Unknown') - - for ___cpu_model in info.get('ansible_processor', []): - if ___cpu_model.endswith('GHz') or ___cpu_model.startswith("Intel"): - break - else: - ___cpu_model = 'Unknown' - ___cpu_model = ___cpu_model[:48] - ___cpu_count = info.get('ansible_processor_count', 0) - ___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')) - ) - disk_info = {} - for dev, dev_info in info.get('ansible_devices', {}).items(): - if disk_pattern.match(dev) and dev_info['removable'] == '0': - disk_info[dev] = dev_info['size'] - ___disk_total = '%.1f %s' % sum_capacity(disk_info.values()) - ___disk_info = json.dumps(disk_info) - - # ___platform = info.get('ansible_system', 'Unknown') - ___os = info.get('ansible_distribution', 'Unknown') - ___os_version = info.get('ansible_distribution_version', 'Unknown') - ___os_arch = info.get('ansible_architecture', 'Unknown') - ___hostname_raw = info.get('ansible_hostname', 'Unknown') - - for k, v in locals().items(): - if k.startswith('___'): - setattr(asset, k.strip('_'), v) - asset.save() - assets_updated.append(asset) - return assets_updated - - -@shared_task -def update_assets_hardware_info_util(assets, task_name=None): - """ - Using ansible api to update asset hardware info - :param assets: asset seq - :param task_name: task_name running - :return: result summary ['contacted': {}, 'dark': {}] - """ - from ops.utils import update_or_create_ansible_task - if task_name is None: - task_name = _("Update some assets hardware info") - tasks = const.UPDATE_ASSETS_HARDWARE_TASKS - hosts = clean_hosts(assets) - if not hosts: - return {} - created_by = str(assets[0].org_id) - task, created = update_or_create_ansible_task( - task_name, hosts=hosts, tasks=tasks, created_by=created_by, - pattern='all', options=const.TASK_OPTIONS, run_as_admin=True, - ) - result = task.run() - set_assets_hardware_info(assets, result) - return result - - -@shared_task(queue="ansible") -def update_asset_hardware_info_manual(asset): - task_name = _("Update asset hardware info: {}").format(asset.hostname) - update_assets_hardware_info_util( - [asset], task_name=task_name - ) - - -@shared_task(queue="ansible") -def update_assets_hardware_info_period(): - """ - Update asset hardware period task - :return: - """ - if PERIOD_TASK != "on": - logger.debug("Period task disabled, update assets hardware info pass") - return - - -## ADMIN USER CONNECTIVE ## - - -@shared_task(queue="ansible") -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 connectivity") - - hosts = clean_hosts(assets) - if not hosts: - return {} - - hosts_category = { - 'linux': { - 'hosts': [], - 'tasks': const.TEST_ADMIN_USER_CONN_TASKS - }, - 'windows': { - 'hosts': [], - 'tasks': const.TEST_WINDOWS_ADMIN_USER_CONN_TASKS - } - } - for host in hosts: - hosts_list = hosts_category['windows']['hosts'] if host.is_windows() \ - else hosts_category['linux']['hosts'] - hosts_list.append(host) - - results_summary = dict( - contacted=defaultdict(dict), dark=defaultdict(dict), success=True - ) - created_by = assets[0].org_id - for k, value in hosts_category.items(): - if not value['hosts']: - continue - task, created = update_or_create_ansible_task( - task_name=task_name, hosts=value['hosts'], tasks=value['tasks'], - pattern='all', options=const.TASK_OPTIONS, run_as_admin=True, - created_by=created_by, - ) - raw, summary = task.run() - success = summary.get('success', False) - contacted = summary.get('contacted', {}) - dark = summary.get('dark', {}) - - results_summary['success'] &= success - results_summary['contacted'].update(contacted) - results_summary['dark'].update(dark) - - for asset in assets: - if asset.hostname in results_summary.get('dark', {}).keys(): - asset.connectivity = Connectivity.unreachable() - elif asset.hostname in results_summary.get('contacted', {}).keys(): - asset.connectivity = Connectivity.reachable() - else: - asset.connectivity = Connectivity.unknown() - return results_summary - - -@shared_task(queue="ansible") -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'] - else: - return True, "" - - -@shared_task(queue="ansible") -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(queue="ansible") -@register_as_period_task(interval=3600) -def test_admin_user_connectivity_period(): - """ - A period task that update the ansible task period - """ - if PERIOD_TASK != "on": - logger.debug('Period task off, skip') - return - key = '_JMS_TEST_ADMIN_USER_CONNECTIVITY_PERIOD' - prev_execute_time = cache.get(key) - if prev_execute_time: - logger.debug("Test admin user connectivity, less than 40 minutes, skip") - return - cache.set(key, 1, 60*40) - 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) - cache.set(key, 1, 60*40) - - -@shared_task(queue="ansible") -def test_admin_user_connectivity_manual(admin_user): - task_name = _("Test admin user connectivity: {}").format(admin_user.name) - test_admin_user_connectivity_util(admin_user, task_name) - return True - - -## System user connective ## - - -@shared_task(queue="ansible") -def test_system_user_connectivity_util(system_user, assets, task_name): - """ - Test system cant connect his assets or not. - :param system_user: - :param assets: - :param task_name: - :return: - """ - from ops.utils import update_or_create_ansible_task - - hosts = clean_hosts(assets) - if not hosts: - return {} - - hosts = clean_hosts_by_protocol(system_user, hosts) - if not hosts: - return {} - - hosts_category = { - 'linux': { - 'hosts': [], - 'tasks': const.TEST_SYSTEM_USER_CONN_TASKS - }, - 'windows': { - 'hosts': [], - 'tasks': const.TEST_WINDOWS_SYSTEM_USER_CONN_TASKS - } - } - for host in hosts: - hosts_list = hosts_category['windows']['hosts'] if host.is_windows() \ - else hosts_category['linux']['hosts'] - hosts_list.append(host) - - results_summary = dict( - contacted=defaultdict(dict), dark=defaultdict(dict), success=True - ) - for k, value in hosts_category.items(): - if not value['hosts']: - continue - task, created = update_or_create_ansible_task( - task_name=task_name, hosts=value['hosts'], tasks=value['tasks'], - pattern='all', options=const.TASK_OPTIONS, - run_as=system_user.username, created_by=system_user.org_id, - ) - raw, summary = task.run() - success = summary.get('success', False) - contacted = summary.get('contacted', {}) - dark = summary.get('dark', {}) - - results_summary['success'] &= success - results_summary['contacted'].update(contacted) - results_summary['dark'].update(dark) - - system_user.set_connectivity(results_summary) - return results_summary - - -@shared_task(queue="ansible") -def test_system_user_connectivity_manual(system_user): - task_name = _("Test system user connectivity: {}").format(system_user) - assets = system_user.get_all_assets() - return test_system_user_connectivity_util(system_user, assets, task_name) - - -@shared_task(queue="ansible") -def test_system_user_connectivity_a_asset(system_user, asset): - task_name = _("Test system user connectivity: {} => {}").format( - system_user, asset - ) - return test_system_user_connectivity_util(system_user, [asset], task_name) - - -@shared_task(queue="ansible") -def test_system_user_connectivity_period(): - if PERIOD_TASK != "on": - logger.debug("Period task disabled, test system user connectivity pass") - return - 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_all_assets() - test_system_user_connectivity_util(system_user, assets, task_name) - - -#### Push system user tasks #### - -def get_push_linux_system_user_tasks(system_user): - tasks = [ - { - 'name': 'Add user {}'.format(system_user.username), - 'action': { - 'module': 'user', - 'args': 'name={} shell={} state=present'.format( - system_user.username, system_user.shell, - ), - } - }, - { - 'name': 'Add group {}'.format(system_user.username), - 'action': { - 'module': 'group', - 'args': 'name={} state=present'.format( - system_user.username, - ), - } - }, - { - '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.password: - tasks.append({ - 'name': 'Set {} password'.format(system_user.username), - 'action': { - 'module': 'user', - 'args': 'name={} shell={} state=present password={}'.format( - system_user.username, system_user.shell, - encrypt_password(system_user.password, salt="K3mIlKK"), - ), - } - }) - if system_user.public_key: - tasks.append({ - 'name': 'Set {} authorized key'.format(system_user.username), - 'action': { - 'module': 'authorized_key', - 'args': "user={} state=present key='{}'".format( - system_user.username, system_user.public_key - ) - } - }) - if system_user.sudo: - sudo = system_user.sudo.replace('\r\n', '\n').replace('\r', '\n') - sudo_list = sudo.split('\n') - sudo_tmp = [] - for s in sudo_list: - sudo_tmp.append(s.strip(',')) - sudo = ','.join(sudo_tmp) - tasks.append({ - 'name': 'Set {} sudo setting'.format(system_user.username), - 'action': { - 'module': 'lineinfile', - 'args': "dest=/etc/sudoers state=present regexp='^{0} ALL=' " - "line='{0} ALL=(ALL) NOPASSWD: {1}' " - "validate='visudo -cf %s'".format( - system_user.username, sudo, - ) - } - }) - - return tasks - - -def get_push_windows_system_user_tasks(system_user): - tasks = [] - if system_user.password: - tasks.append({ - 'name': 'Add user {}'.format(system_user.username), - 'action': { - 'module': 'win_user', - 'args': 'fullname={} ' - 'name={} ' - 'password={} ' - 'state=present ' - 'update_password=always ' - 'password_expired=no ' - 'password_never_expires=yes ' - 'groups="Users,Remote Desktop Users" ' - 'groups_action=add ' - ''.format(system_user.name, - system_user.username, - system_user.password), - } - }) - return tasks - - -def get_push_system_user_tasks(host, system_user): - if host.is_unixlike(): - tasks = get_push_linux_system_user_tasks(system_user) - elif host.is_windows(): - tasks = get_push_windows_system_user_tasks(system_user) - else: - msg = _( - "The asset {} system platform {} does not " - "support run Ansible tasks".format(host.hostname, host.platform) - ) - logger.info(msg) - tasks = [] - return tasks - - -@shared_task(queue="ansible") -def push_system_user_util(system_user, assets, task_name): - from ops.utils import update_or_create_ansible_task - if not system_user.is_need_push(): - msg = _("Push system user task skip, auto push not enable or " - "protocol is not ssh or rdp: {}").format(system_user.name) - logger.info(msg) - return {} - - # Set root as system user is dangerous - if system_user.username.lower() in ["root", "administrator"]: - msg = _("For security, do not push user {}".format(system_user.username)) - logger.info(msg) - return {} - - hosts = clean_hosts(assets) - if not hosts: - return {} - - hosts = clean_hosts_by_protocol(system_user, hosts) - if not hosts: - return {} - - for host in hosts: - system_user.load_specific_asset_auth(host) - tasks = get_push_system_user_tasks(host, system_user) - if not tasks: - continue - task, created = update_or_create_ansible_task( - task_name=task_name, hosts=[host], tasks=tasks, pattern='all', - options=const.TASK_OPTIONS, run_as_admin=True, - created_by=system_user.org_id, - ) - task.run() - - -@shared_task(queue="ansible") -def push_system_user_to_assets_manual(system_user): - assets = system_user.get_all_assets() - task_name = _("Push system users to assets: {}").format(system_user.name) - return push_system_user_util(system_user, assets, task_name=task_name) - - -@shared_task(queue="ansible") -def push_system_user_a_asset_manual(system_user, asset): - task_name = _("Push system users to asset: {} => {}").format( - system_user.name, asset - ) - return push_system_user_util(system_user, [asset], task_name=task_name) - - -@shared_task(queue="ansible") -def push_system_user_to_assets(system_user, assets): - task_name = _("Push system users to assets: {}").format(system_user.name) - return push_system_user_util(system_user, assets, task_name) - - -@shared_task -@after_app_shutdown_clean_periodic -def test_system_user_connectability_period(): - pass - - -@shared_task -@after_app_shutdown_clean_periodic -def test_admin_user_connectability_period(): - pass - - -#### Test Asset user connectivity task #### - -def get_test_asset_user_connectivity_tasks(asset): - if asset.is_unixlike(): - tasks = const.TEST_ASSET_USER_CONN_TASKS - elif asset.is_windows(): - tasks = const.TEST_WINDOWS_ASSET_USER_CONN_TASKS - else: - msg = _( - "The asset {} system platform {} does not " - "support run Ansible tasks".format(asset.hostname, asset.platform) - ) - logger.info(msg) - tasks = [] - return tasks - - -@shared_task(queue="ansible") -def test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=False): - """ - :param asset_user: 对象 - :param task_name: - :param run_as_admin: - :return: - """ - from ops.utils import update_or_create_ansible_task - - if not check_asset_can_run_ansible(asset_user.asset): - return - - tasks = get_test_asset_user_connectivity_tasks(asset_user.asset) - if not tasks: - logger.debug("No tasks ") - return - - args = (task_name,) - kwargs = { - 'hosts': [asset_user.asset], 'tasks': tasks, - 'pattern': 'all', 'options': const.TASK_OPTIONS, - 'created_by': asset_user.org_id, - } - if run_as_admin: - kwargs["run_as_admin"] = True - else: - kwargs["run_as"] = asset_user.username - task, created = update_or_create_ansible_task(*args, **kwargs) - raw, summary = task.run() - asset_user.set_connectivity(summary) - - -@shared_task(queue="ansible") -def test_asset_users_connectivity_manual(asset_users, run_as_admin=False): - """ - :param asset_users: 对象 - """ - for asset_user in asset_users: - task_name = _("Test asset user connectivity: {}").format(asset_user) - test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=run_as_admin) - - -# @shared_task -# @register_as_period_task(interval=3600) -# @after_app_ready_start -# @after_app_shutdown_clean_periodic -# def push_system_user_period(): -# for system_user in SystemUser.objects.all(): -# push_system_user_related_nodes(system_user) - - - - diff --git a/apps/assets/tasks/__init__.py b/apps/assets/tasks/__init__.py new file mode 100644 index 000000000..eb8c5a7b9 --- /dev/null +++ b/apps/assets/tasks/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# +from .utils import * +from .admin_user_connectivity import * +from .asset_connectivity import * +from .asset_user_connectivity import * +from .gather_asset_users import * +from .gather_asset_hardware_info import * +from .push_system_user import * +from .system_user_connectivity import * diff --git a/apps/assets/tasks/admin_user_connectivity.py b/apps/assets/tasks/admin_user_connectivity.py new file mode 100644 index 000000000..c94b38d90 --- /dev/null +++ b/apps/assets/tasks/admin_user_connectivity.py @@ -0,0 +1,65 @@ +# ~*~ coding: utf-8 ~*~ + +from celery import shared_task +from django.utils.translation import ugettext as _ +from django.core.cache import cache + +from common.utils import get_logger +from ops.celery.decorator import register_as_period_task + +from ..models import AdminUser +from .utils import clean_hosts +from .asset_connectivity import test_asset_connectivity_util +from . import const + + +logger = get_logger(__file__) +__all__ = [ + 'test_admin_user_connectivity_util', 'test_admin_user_connectivity_manual', + 'test_admin_user_connectivity_period' +] + + +@shared_task(queue="ansible") +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(queue="ansible") +@register_as_period_task(interval=3600) +def test_admin_user_connectivity_period(): + """ + A period task that update the ansible task period + """ + if const.PERIOD_TASK_ENABLED: + logger.debug('Period task off, skip') + return + key = '_JMS_TEST_ADMIN_USER_CONNECTIVITY_PERIOD' + prev_execute_time = cache.get(key) + if prev_execute_time: + logger.debug("Test admin user connectivity, less than 40 minutes, skip") + return + cache.set(key, 1, 60*40) + 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) + cache.set(key, 1, 60*40) + + +@shared_task(queue="ansible") +def test_admin_user_connectivity_manual(admin_user): + task_name = _("Test admin user connectivity: {}").format(admin_user.name) + test_admin_user_connectivity_util(admin_user, task_name) + return True diff --git a/apps/assets/tasks/asset_connectivity.py b/apps/assets/tasks/asset_connectivity.py new file mode 100644 index 000000000..b777dde7c --- /dev/null +++ b/apps/assets/tasks/asset_connectivity.py @@ -0,0 +1,81 @@ +# ~*~ coding: utf-8 ~*~ +from collections import defaultdict +from celery import shared_task +from django.utils.translation import ugettext as _ + +from common.utils import get_logger +from ..models.utils import Connectivity +from . import const +from .utils import clean_hosts + + +logger = get_logger(__file__) +__all__ = ['test_asset_connectivity_util', 'test_asset_connectivity_manual'] + + +@shared_task(queue="ansible") +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 connectivity") + + hosts = clean_hosts(assets) + if not hosts: + return {} + + hosts_category = { + 'linux': { + 'hosts': [], + 'tasks': const.TEST_ADMIN_USER_CONN_TASKS + }, + 'windows': { + 'hosts': [], + 'tasks': const.TEST_WINDOWS_ADMIN_USER_CONN_TASKS + } + } + for host in hosts: + hosts_list = hosts_category['windows']['hosts'] if host.is_windows() \ + else hosts_category['linux']['hosts'] + hosts_list.append(host) + + results_summary = dict( + contacted=defaultdict(dict), dark=defaultdict(dict), success=True + ) + created_by = assets[0].org_id + for k, value in hosts_category.items(): + if not value['hosts']: + continue + task, created = update_or_create_ansible_task( + task_name=task_name, hosts=value['hosts'], tasks=value['tasks'], + pattern='all', options=const.TASK_OPTIONS, run_as_admin=True, + created_by=created_by, + ) + raw, summary = task.run() + success = summary.get('success', False) + contacted = summary.get('contacted', {}) + dark = summary.get('dark', {}) + + results_summary['success'] &= success + results_summary['contacted'].update(contacted) + results_summary['dark'].update(dark) + + for asset in assets: + if asset.hostname in results_summary.get('dark', {}).keys(): + asset.connectivity = Connectivity.unreachable() + elif asset.hostname in results_summary.get('contacted', {}).keys(): + asset.connectivity = Connectivity.reachable() + else: + asset.connectivity = Connectivity.unknown() + return results_summary + + +@shared_task(queue="ansible") +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'] + else: + return True, "" diff --git a/apps/assets/tasks/asset_user_connectivity.py b/apps/assets/tasks/asset_user_connectivity.py new file mode 100644 index 000000000..5a4bbcc43 --- /dev/null +++ b/apps/assets/tasks/asset_user_connectivity.py @@ -0,0 +1,77 @@ +# ~*~ coding: utf-8 ~*~ + +from celery import shared_task +from django.utils.translation import ugettext as _ + +from common.utils import get_logger +from . import const +from .utils import check_asset_can_run_ansible + + +logger = get_logger(__file__) + + +__all__ = [ + 'test_asset_user_connectivity_util', 'test_asset_users_connectivity_manual', + 'get_test_asset_user_connectivity_tasks', +] + + +def get_test_asset_user_connectivity_tasks(asset): + if asset.is_unixlike(): + tasks = const.TEST_ASSET_USER_CONN_TASKS + elif asset.is_windows(): + tasks = const.TEST_WINDOWS_ASSET_USER_CONN_TASKS + else: + msg = _( + "The asset {} system platform {} does not " + "support run Ansible tasks".format(asset.hostname, asset.platform) + ) + logger.info(msg) + tasks = [] + return tasks + + +@shared_task(queue="ansible") +def test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=False): + """ + :param asset_user: 对象 + :param task_name: + :param run_as_admin: + :return: + """ + from ops.utils import update_or_create_ansible_task + + if not check_asset_can_run_ansible(asset_user.asset): + return + + tasks = get_test_asset_user_connectivity_tasks(asset_user.asset) + if not tasks: + logger.debug("No tasks ") + return + + args = (task_name,) + kwargs = { + 'hosts': [asset_user.asset], 'tasks': tasks, + 'pattern': 'all', 'options': const.TASK_OPTIONS, + 'created_by': asset_user.org_id, + } + if run_as_admin: + kwargs["run_as_admin"] = True + else: + kwargs["run_as"] = asset_user.username + task, created = update_or_create_ansible_task(*args, **kwargs) + raw, summary = task.run() + asset_user.set_connectivity(summary) + + +@shared_task(queue="ansible") +def test_asset_users_connectivity_manual(asset_users, run_as_admin=False): + """ + :param asset_users: 对象 + """ + for asset_user in asset_users: + task_name = _("Test asset user connectivity: {}").format(asset_user) + test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=run_as_admin) + + diff --git a/apps/assets/const.py b/apps/assets/tasks/const.py similarity index 76% rename from apps/assets/const.py rename to apps/assets/tasks/const.py index e5f65fce0..83775d908 100644 --- a/apps/assets/const.py +++ b/apps/assets/tasks/const.py @@ -1,7 +1,11 @@ # -*- coding: utf-8 -*- # +import os from django.utils.translation import ugettext_lazy as _ + +PERIOD_TASK_ENABLED = os.environ.get("PERIOD_TASK", "on") == 'on' + UPDATE_ASSETS_HARDWARE_TASKS = [ { 'name': "setup", @@ -79,3 +83,22 @@ CONNECTIVITY_CHOICES = ( (CONN_UNKNOWN, _("Unknown")), ) +GATHER_ASSET_USERS_TASKS = [ + { + "name": "gather host users", + "action": { + "module": 'getent', + "args": "database=passwd" + }, + }, +] + +GATHER_ASSET_USERS_TASKS_WINDOWS = [ + { + "name": "gather windows host users", + "action": { + "module": 'win_shell', + "args": "net user" + } + } +] diff --git a/apps/assets/tasks/gather_asset_hardware_info.py b/apps/assets/tasks/gather_asset_hardware_info.py new file mode 100644 index 000000000..31262f699 --- /dev/null +++ b/apps/assets/tasks/gather_asset_hardware_info.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +# +import json +import re + +from celery import shared_task +from django.utils.translation import ugettext as _ + +from common.utils import ( + capacity_convert, sum_capacity, get_logger +) +from . import const +from .utils import clean_hosts + + +logger = get_logger(__file__) +disk_pattern = re.compile(r'^hd|sd|xvd|vd|nv') +__all__ = [ + 'update_assets_hardware_info_util', 'update_asset_hardware_info_manual', + 'update_assets_hardware_info_period', +] + + +def set_assets_hardware_info(assets, result, **kwargs): + """ + Using ops task run result, to update asset info + + @shared_task must be exit, because we using it as a task callback, is must + be a celery task also + :param assets: + :param result: + :param kwargs: {task_name: ""} + :return: + """ + result_raw = result[0] + assets_updated = [] + success_result = result_raw.get('ok', {}) + + for asset in assets: + hostname = asset.hostname + info = success_result.get(hostname, {}) + info = info.get('setup', {}).get('ansible_facts', {}) + if not info: + logger.error(_("Get asset info failed: {}").format(hostname)) + continue + ___vendor = info.get('ansible_system_vendor', 'Unknown') + ___model = info.get('ansible_product_name', 'Unknown') + ___sn = info.get('ansible_product_serial', 'Unknown') + + for ___cpu_model in info.get('ansible_processor', []): + if ___cpu_model.endswith('GHz') or ___cpu_model.startswith("Intel"): + break + else: + ___cpu_model = 'Unknown' + ___cpu_model = ___cpu_model[:48] + ___cpu_count = info.get('ansible_processor_count', 0) + ___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')) + ) + disk_info = {} + for dev, dev_info in info.get('ansible_devices', {}).items(): + if disk_pattern.match(dev) and dev_info['removable'] == '0': + disk_info[dev] = dev_info['size'] + ___disk_total = '%.1f %s' % sum_capacity(disk_info.values()) + ___disk_info = json.dumps(disk_info) + + # ___platform = info.get('ansible_system', 'Unknown') + ___os = info.get('ansible_distribution', 'Unknown') + ___os_version = info.get('ansible_distribution_version', 'Unknown') + ___os_arch = info.get('ansible_architecture', 'Unknown') + ___hostname_raw = info.get('ansible_hostname', 'Unknown') + + for k, v in locals().items(): + if k.startswith('___'): + setattr(asset, k.strip('_'), v) + asset.save() + assets_updated.append(asset) + return assets_updated + + +@shared_task +def update_assets_hardware_info_util(assets, task_name=None): + """ + Using ansible api to update asset hardware info + :param assets: asset seq + :param task_name: task_name running + :return: result summary ['contacted': {}, 'dark': {}] + """ + from ops.utils import update_or_create_ansible_task + if task_name is None: + task_name = _("Update some assets hardware info") + tasks = const.UPDATE_ASSETS_HARDWARE_TASKS + hosts = clean_hosts(assets) + if not hosts: + return {} + created_by = str(assets[0].org_id) + task, created = update_or_create_ansible_task( + task_name, hosts=hosts, tasks=tasks, created_by=created_by, + pattern='all', options=const.TASK_OPTIONS, run_as_admin=True, + ) + result = task.run() + set_assets_hardware_info(assets, result) + return result + + +@shared_task(queue="ansible") +def update_asset_hardware_info_manual(asset): + task_name = _("Update asset hardware info: {}").format(asset.hostname) + update_assets_hardware_info_util( + [asset], task_name=task_name + ) + + +@shared_task(queue="ansible") +def update_assets_hardware_info_period(): + """ + Update asset hardware period task + :return: + """ + if not const.PERIOD_TASK_ENABLED: + logger.debug("Period task disabled, update assets hardware info pass") + return diff --git a/apps/assets/tasks/gather_asset_users.py b/apps/assets/tasks/gather_asset_users.py new file mode 100644 index 000000000..a48049d5a --- /dev/null +++ b/apps/assets/tasks/gather_asset_users.py @@ -0,0 +1,42 @@ +# ~*~ coding: utf-8 ~*~ + +from collections import defaultdict +from celery import shared_task +from django.utils.translation import ugettext as _ + +from . import const + + +@shared_task(queue="ansible") +def gather_asset_all_users(assets, task_name=None): + from ops.utils import update_or_create_ansible_task + if task_name is None: + task_name = _("Gather assets users") + hosts_category = { + 'linux': { + 'hosts': [], + 'tasks': const.GATHER_ASSET_USERS_TASKS + }, + 'windows': { + 'hosts': [], + 'tasks': const.GATHER_ASSET_USERS_TASKS_WINDOWS + } + } + for asset in assets: + hosts_list = hosts_category['windows']['hosts'] if asset.is_windows() \ + else hosts_category['linux']['hosts'] + hosts_list.append(asset) + + results = {'linux': defaultdict(dict), 'windows': defaultdict(dict)} + for k, value in hosts_category.items(): + if not value['hosts']: + continue + task, created = update_or_create_ansible_task( + task_name=task_name, hosts=value['hosts'], tasks=value['tasks'], + pattern='all', options=const.TASK_OPTIONS, + run_as_admin=True, created_by=value['hosts'][0].org_id, + ) + raw, summary = task.run() + results[k].update(raw['ok']) + return results + diff --git a/apps/assets/tasks/push_system_user.py b/apps/assets/tasks/push_system_user.py new file mode 100644 index 000000000..01348a0fe --- /dev/null +++ b/apps/assets/tasks/push_system_user.py @@ -0,0 +1,202 @@ +# ~*~ coding: utf-8 ~*~ + +from celery import shared_task +from django.utils.translation import ugettext as _ + +from common.utils import encrypt_password, get_logger +from . import const +from .utils import clean_hosts_by_protocol, clean_hosts + + +logger = get_logger(__file__) +__all__ = [ + 'push_system_user_util', 'push_system_user_to_assets', + 'push_system_user_to_assets_manual', 'push_system_user_a_asset_manual', +] + + +def get_push_linux_system_user_tasks(system_user): + tasks = [ + { + 'name': 'Add user {}'.format(system_user.username), + 'action': { + 'module': 'user', + 'args': 'name={} shell={} state=present'.format( + system_user.username, system_user.shell, + ), + } + }, + { + 'name': 'Add group {}'.format(system_user.username), + 'action': { + 'module': 'group', + 'args': 'name={} state=present'.format( + system_user.username, + ), + } + }, + { + '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.password: + tasks.append({ + 'name': 'Set {} password'.format(system_user.username), + 'action': { + 'module': 'user', + 'args': 'name={} shell={} state=present password={}'.format( + system_user.username, system_user.shell, + encrypt_password(system_user.password, salt="K3mIlKK"), + ), + } + }) + if system_user.public_key: + tasks.append({ + 'name': 'Set {} authorized key'.format(system_user.username), + 'action': { + 'module': 'authorized_key', + 'args': "user={} state=present key='{}'".format( + system_user.username, system_user.public_key + ) + } + }) + if system_user.sudo: + sudo = system_user.sudo.replace('\r\n', '\n').replace('\r', '\n') + sudo_list = sudo.split('\n') + sudo_tmp = [] + for s in sudo_list: + sudo_tmp.append(s.strip(',')) + sudo = ','.join(sudo_tmp) + tasks.append({ + 'name': 'Set {} sudo setting'.format(system_user.username), + 'action': { + 'module': 'lineinfile', + 'args': "dest=/etc/sudoers state=present regexp='^{0} ALL=' " + "line='{0} ALL=(ALL) NOPASSWD: {1}' " + "validate='visudo -cf %s'".format( + system_user.username, sudo, + ) + } + }) + + return tasks + + +def get_push_windows_system_user_tasks(system_user): + tasks = [] + if not system_user.password: + return tasks + tasks.append({ + 'name': 'Add user {}'.format(system_user.username), + 'action': { + 'module': 'win_user', + 'args': 'fullname={} ' + 'name={} ' + 'password={} ' + 'state=present ' + 'update_password=always ' + 'password_expired=no ' + 'password_never_expires=yes ' + 'groups="Users,Remote Desktop Users" ' + 'groups_action=add ' + ''.format(system_user.name, + system_user.username, + system_user.password), + } + }) + return tasks + + +def get_push_system_user_tasks(host, system_user): + if host.is_unixlike(): + tasks = get_push_linux_system_user_tasks(system_user) + elif host.is_windows(): + tasks = get_push_windows_system_user_tasks(system_user) + else: + msg = _( + "The asset {} system platform {} does not " + "support run Ansible tasks".format(host.hostname, host.platform) + ) + logger.info(msg) + tasks = [] + return tasks + + +@shared_task(queue="ansible") +def push_system_user_util(system_user, assets, task_name): + from ops.utils import update_or_create_ansible_task + if not system_user.is_need_push(): + msg = _("Push system user task skip, auto push not enable or " + "protocol is not ssh or rdp: {}").format(system_user.name) + logger.info(msg) + return {} + + # Set root as system user is dangerous + if system_user.username.lower() in ["root", "administrator"]: + msg = _("For security, do not push user {}".format(system_user.username)) + logger.info(msg) + return {} + + hosts = clean_hosts(assets) + if not hosts: + return {} + + hosts = clean_hosts_by_protocol(system_user, hosts) + if not hosts: + return {} + + for host in hosts: + system_user.load_specific_asset_auth(host) + tasks = get_push_system_user_tasks(host, system_user) + if not tasks: + continue + task, created = update_or_create_ansible_task( + task_name=task_name, hosts=[host], tasks=tasks, pattern='all', + options=const.TASK_OPTIONS, run_as_admin=True, + created_by=system_user.org_id, + ) + task.run() + + +@shared_task(queue="ansible") +def push_system_user_to_assets_manual(system_user): + assets = system_user.get_all_assets() + task_name = _("Push system users to assets: {}").format(system_user.name) + return push_system_user_util(system_user, assets, task_name=task_name) + + +@shared_task(queue="ansible") +def push_system_user_a_asset_manual(system_user, asset): + task_name = _("Push system users to asset: {} => {}").format( + system_user.name, asset + ) + return push_system_user_util(system_user, [asset], task_name=task_name) + + +@shared_task(queue="ansible") +def push_system_user_to_assets(system_user, assets): + task_name = _("Push system users to assets: {}").format(system_user.name) + return push_system_user_util(system_user, assets, task_name) + + + +# @shared_task +# @register_as_period_task(interval=3600) +# @after_app_ready_start +# @after_app_shutdown_clean_periodic +# def push_system_user_period(): +# for system_user in SystemUser.objects.all(): +# push_system_user_related_nodes(system_user) \ No newline at end of file diff --git a/apps/assets/tasks/system_user_connectivity.py b/apps/assets/tasks/system_user_connectivity.py new file mode 100644 index 000000000..ffe618a59 --- /dev/null +++ b/apps/assets/tasks/system_user_connectivity.py @@ -0,0 +1,101 @@ + +from collections import defaultdict +from celery import shared_task +from django.utils.translation import ugettext as _ + +from common.utils import get_logger + +from ..models import SystemUser +from . import const +from .utils import clean_hosts, clean_hosts_by_protocol + +logger = get_logger(__name__) +__all__ = [ + 'test_system_user_connectivity_util', 'test_system_user_connectivity_manual', + 'test_system_user_connectivity_period', 'test_system_user_connectivity_a_asset', +] + + +@shared_task(queue="ansible") +def test_system_user_connectivity_util(system_user, assets, task_name): + """ + Test system cant connect his assets or not. + :param system_user: + :param assets: + :param task_name: + :return: + """ + from ops.utils import update_or_create_ansible_task + + hosts = clean_hosts(assets) + if not hosts: + return {} + + hosts = clean_hosts_by_protocol(system_user, hosts) + if not hosts: + return {} + + hosts_category = { + 'linux': { + 'hosts': [], + 'tasks': const.TEST_SYSTEM_USER_CONN_TASKS + }, + 'windows': { + 'hosts': [], + 'tasks': const.TEST_WINDOWS_SYSTEM_USER_CONN_TASKS + } + } + for host in hosts: + hosts_list = hosts_category['windows']['hosts'] if host.is_windows() \ + else hosts_category['linux']['hosts'] + hosts_list.append(host) + + results_summary = dict( + contacted=defaultdict(dict), dark=defaultdict(dict), success=True + ) + for k, value in hosts_category.items(): + if not value['hosts']: + continue + task, created = update_or_create_ansible_task( + task_name=task_name, hosts=value['hosts'], tasks=value['tasks'], + pattern='all', options=const.TASK_OPTIONS, + run_as=system_user.username, created_by=system_user.org_id, + ) + raw, summary = task.run() + success = summary.get('success', False) + contacted = summary.get('contacted', {}) + dark = summary.get('dark', {}) + + results_summary['success'] &= success + results_summary['contacted'].update(contacted) + results_summary['dark'].update(dark) + + system_user.set_connectivity(results_summary) + return results_summary + + +@shared_task(queue="ansible") +def test_system_user_connectivity_manual(system_user): + task_name = _("Test system user connectivity: {}").format(system_user) + assets = system_user.get_all_assets() + return test_system_user_connectivity_util(system_user, assets, task_name) + + +@shared_task(queue="ansible") +def test_system_user_connectivity_a_asset(system_user, asset): + task_name = _("Test system user connectivity: {} => {}").format( + system_user, asset + ) + return test_system_user_connectivity_util(system_user, [asset], task_name) + + +@shared_task(queue="ansible") +def test_system_user_connectivity_period(): + if not const.PERIOD_TASK_ENABLED: + logger.debug("Period task disabled, test system user connectivity pass") + return + 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_all_assets() + test_system_user_connectivity_util(system_user, assets, task_name) diff --git a/apps/assets/tasks/utils.py b/apps/assets/tasks/utils.py new file mode 100644 index 000000000..e5983143f --- /dev/null +++ b/apps/assets/tasks/utils.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# +from django.utils.translation import ugettext as _ + +from common.utils import get_logger + + +logger = get_logger(__file__) +__all__ = [ + 'check_asset_can_run_ansible', 'clean_hosts', 'clean_hosts_by_protocol' +] + + +def check_asset_can_run_ansible(asset): + if not asset.is_active: + msg = _("Asset has been disabled, skipped: {}").format(asset) + logger.info(msg) + return False + if not asset.is_support_ansible(): + msg = _("Asset may not be support ansible, skipped: {}").format(asset) + logger.info(msg) + return False + return True + + +def clean_hosts(assets): + clean_assets = [] + for asset in assets: + if not check_asset_can_run_ansible(asset): + continue + clean_assets.append(asset) + if not clean_assets: + logger.info(_("No assets matched, stop task")) + return clean_assets + + +def clean_hosts_by_protocol(system_user, assets): + hosts = [ + asset for asset in assets + if asset.has_protocol(system_user.protocol) + ] + if not hosts: + msg = _("No assets matched related system user protocol, stop task") + logger.info(msg) + return hosts diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index a7fb5fcb5..dc3d43b78 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -382,6 +382,7 @@ defaults = { 'SYSLOG_ADDR': '', # '192.168.0.1:514' 'SYSLOG_FACILITY': 'user', 'PERM_SINGLE_ASSET_TO_UNGROUP_NODE': False, + 'WINDOWS_SSH_DEFAULT_SHELL': 'cmd' } diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index f211eef82..d6ce267df 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -622,3 +622,4 @@ ASSETS_PERM_CACHE_TIME = CONFIG.ASSETS_PERM_CACHE_TIME BACKEND_ASSET_USER_AUTH_VAULT = False PERM_SINGLE_ASSET_TO_UNGROUP_NODE = CONFIG.PERM_SINGLE_ASSET_TO_UNGROUP_NODE +WINDOWS_SSH_DEFAULT_SHELL = CONFIG.WINDOWS_SSH_DEFAULT_SHELL diff --git a/apps/ops/inventory.py b/apps/ops/inventory.py index 9cb1027ef..b6c302e41 100644 --- a/apps/ops/inventory.py +++ b/apps/ops/inventory.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # +from django.conf import settings from .ansible.inventory import BaseInventory from common.utils import get_logger @@ -14,6 +15,7 @@ logger = get_logger(__file__) class JMSBaseInventory(BaseInventory): + windows_ssh_default_ssh = settings.WINDOWS_SSH_DEFAULT_SHELL def convert_to_ansible(self, asset, run_as_admin=False): info = { @@ -33,7 +35,7 @@ class JMSBaseInventory(BaseInventory): if asset.is_windows(): info["vars"].update({ "ansible_connection": "ssh", - "ansible_shell_type": "cmd", + "ansible_shell_type": self.windows_ssh_default_ssh, }) for label in asset.labels.all(): info["vars"].update({ From 5464c884db5c83e84fc0144d9478e67ea885faba Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 18 Sep 2019 22:06:46 +0800 Subject: [PATCH 3/7] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9swagger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/__init__.py | 1 + apps/assets/api/admin_user.py | 2 +- apps/assets/api/asset.py | 33 +- apps/assets/api/asset_user.py | 4 +- apps/assets/api/gathered_user.py | 22 + apps/assets/filters.py | 119 +++ .../migrations/0039_authbook_is_active.py | 18 + .../migrations/0040_auto_20190917_2056.py | 36 + apps/assets/migrations/0041_gathereduser.py | 28 + apps/assets/models/__init__.py | 1 + apps/assets/models/authbook.py | 21 +- apps/assets/models/base.py | 2 +- apps/assets/models/gathered_user.py | 39 + apps/assets/models/node.py | 12 +- apps/assets/serializers/__init__.py | 1 + apps/assets/serializers/asset_user.py | 1 + apps/assets/serializers/gathered_user.py | 16 + apps/assets/signals_handler.py | 9 +- apps/assets/tasks.py | 0 .../tasks/gather_asset_hardware_info.py | 2 +- apps/assets/tasks/gather_asset_users.py | 96 ++- apps/assets/templates/assets/asset_list.html | 12 +- apps/assets/urls/api_urls.py | 1 + apps/assets/utils.py | 31 - apps/common/filters.py | 53 +- apps/common/mixins/api.py | 96 +-- apps/common/utils/common.py | 16 +- apps/jumpserver/swagger.py | 15 + apps/locale/zh/LC_MESSAGES/django.mo | Bin 79372 -> 79735 bytes apps/locale/zh/LC_MESSAGES/django.po | 746 ++++++++++-------- apps/ops/celery/utils.py | 7 +- apps/ops/inventory.py | 2 +- apps/ops/tasks.py | 20 + apps/orgs/mixins/api.py | 8 +- apps/orgs/mixins/serializers.py | 7 +- apps/orgs/utils.py | 2 + apps/perms/api/mixin.py | 104 +-- apps/perms/api/user_permission/mixin.py | 12 +- apps/perms/utils/asset_permission.py | 6 +- apps/settings/api.py | 2 + apps/static/js/jumpserver.js | 1 + apps/templates/_nav.html | 2 - apps/terminal/serializers_v2/terminal.py | 2 +- apps/users/api/user.py | 4 +- 44 files changed, 979 insertions(+), 633 deletions(-) create mode 100644 apps/assets/api/gathered_user.py create mode 100644 apps/assets/filters.py create mode 100644 apps/assets/migrations/0039_authbook_is_active.py create mode 100644 apps/assets/migrations/0040_auto_20190917_2056.py create mode 100644 apps/assets/migrations/0041_gathereduser.py create mode 100644 apps/assets/models/gathered_user.py create mode 100644 apps/assets/serializers/gathered_user.py delete mode 100644 apps/assets/tasks.py diff --git a/apps/assets/api/__init__.py b/apps/assets/api/__init__.py index b0efb6af8..a4404e290 100644 --- a/apps/assets/api/__init__.py +++ b/apps/assets/api/__init__.py @@ -6,3 +6,4 @@ from .node import * from .domain import * from .cmd_filter import * from .asset_user import * +from .gathered_user import * diff --git a/apps/assets/api/admin_user.py b/apps/assets/api/admin_user.py index 9db193643..fd10e6129 100644 --- a/apps/assets/api/admin_user.py +++ b/apps/assets/api/admin_user.py @@ -19,7 +19,7 @@ from rest_framework import generics from rest_framework.response import Response from orgs.mixins.api import OrgBulkModelViewSet -from common.mixins import IDInCacheFilterMixin +from common.mixins import CommonApiMixin from common.utils import get_logger from ..hands import IsOrgAdmin from ..models import AdminUser, Asset diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index a6280ff56..86e3d8fd2 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -5,9 +5,7 @@ import random from rest_framework import generics from rest_framework.response import Response -from django.utils.translation import ugettext_lazy as _ from django.shortcuts import get_object_or_404 -from django.db.models import Q from common.utils import get_logger, get_object_or_none from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser @@ -16,7 +14,7 @@ from ..models import Asset, AdminUser, Node from .. import serializers from ..tasks import update_asset_hardware_info_manual, \ test_asset_connectivity_manual -from ..utils import LabelFilter +from ..filters import AssetByNodeFilterBackend, LabelFilterBackend logger = get_logger(__file__) @@ -27,7 +25,7 @@ __all__ = [ ] -class AssetViewSet(LabelFilter, OrgBulkModelViewSet): +class AssetViewSet(OrgBulkModelViewSet): """ API endpoint that allows Asset to be viewed or edited. """ @@ -37,7 +35,7 @@ class AssetViewSet(LabelFilter, OrgBulkModelViewSet): queryset = Asset.objects.all() serializer_class = serializers.AssetSerializer permission_classes = (IsOrgAdminOrAppUser,) - success_message = _("%(hostname)s was %(action)s successfully") + extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend] def set_assets_node(self, assets): if not isinstance(assets, list): @@ -54,30 +52,6 @@ class AssetViewSet(LabelFilter, OrgBulkModelViewSet): assets = serializer.save() self.set_assets_node(assets) - def filter_node(self, queryset): - node_id = self.request.query_params.get("node_id") - if not node_id: - 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_org_root() and show_current_asset: - queryset = queryset.filter( - Q(nodes=node_id) | Q(nodes__isnull=True) - ).distinct() - # 当前节点是顶层节点,显示所有资产 - elif node.is_org_root() and not show_current_asset: - return queryset - # 当前节点不是鼎城节点,只显示直接资产 - elif not node.is_org_root() and show_current_asset: - queryset = queryset.filter(nodes=node) - else: - children = node.get_all_children(with_self=True) - queryset = queryset.filter(nodes__in=children).distinct() - return queryset - def filter_admin_user_id(self, queryset): admin_user_id = self.request.query_params.get('admin_user_id') if not admin_user_id: @@ -88,7 +62,6 @@ class AssetViewSet(LabelFilter, OrgBulkModelViewSet): def filter_queryset(self, queryset): queryset = super().filter_queryset(queryset) - queryset = self.filter_node(queryset) queryset = self.filter_admin_user_id(queryset) return queryset diff --git a/apps/assets/api/asset_user.py b/apps/assets/api/asset_user.py index ec71e87f2..7ec1485b7 100644 --- a/apps/assets/api/asset_user.py +++ b/apps/assets/api/asset_user.py @@ -10,7 +10,7 @@ from django.http import Http404 from common.permissions import IsOrgAdminOrAppUser, NeedMFAVerify from common.utils import get_object_or_none, get_logger -from common.mixins import IDInCacheFilterMixin +from common.mixins import CommonApiMixin from ..backends import AssetUserManager from ..models import Asset, Node, SystemUser, AdminUser from .. import serializers @@ -52,7 +52,7 @@ class AssetUserSearchBackend(filters.BaseFilterBackend): return _queryset -class AssetUserViewSet(IDInCacheFilterMixin, BulkModelViewSet): +class AssetUserViewSet(CommonApiMixin, BulkModelViewSet): serializer_class = serializers.AssetUserSerializer permission_classes = [IsOrgAdminOrAppUser] http_method_names = ['get', 'post'] diff --git a/apps/assets/api/gathered_user.py b/apps/assets/api/gathered_user.py new file mode 100644 index 000000000..2f844d9e0 --- /dev/null +++ b/apps/assets/api/gathered_user.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# + +from orgs.mixins.api import OrgModelViewSet +from assets.models import GatheredUser +from common.permissions import IsOrgAdmin + +from ..serializers import GatheredUserSerializer + + +__all__ = ['GatheredUserViewSet'] + + +class GatheredUserViewSet(OrgModelViewSet): + queryset = GatheredUser.objects.all() + serializer_class = GatheredUserSerializer + permission_classes = [IsOrgAdmin] + + filter_fields = ['asset', 'username', 'present'] + search_fields = ['username', 'asset__ip', 'asset__hostname'] + + diff --git a/apps/assets/filters.py b/apps/assets/filters.py new file mode 100644 index 000000000..3636f76e2 --- /dev/null +++ b/apps/assets/filters.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# + +import coreapi +from rest_framework import filters +from django.db.models import Q + +from common.utils import dict_get_any, is_uuid, get_object_or_none +from .models import Node, Label + + +class AssetByNodeFilterBackend(filters.BaseFilterBackend): + fields = ['node', 'all'] + + # def filter_node(self, queryset): + # node_id = self.request.query_params.get("node_id") + # if not node_id: + # 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_org_root() and show_current_asset: + # queryset = queryset.filter( + # Q(nodes=node_id) | Q(nodes__isnull=True) + # ).distinct() + # # 当前节点是顶层节点,显示所有资产 + # elif node.is_org_root() and not show_current_asset: + # return queryset + # # 当前节点不是鼎城节点,只显示直接资产 + # elif not node.is_org_root() and show_current_asset: + # queryset = queryset.filter(nodes=node) + # else: + # children = node.get_all_children(with_self=True) + # queryset = queryset.filter(nodes__in=children).distinct() + # return queryset + + def get_schema_fields(self, view): + return [ + coreapi.Field( + name=field, location='query', required=False, + type='string', example='', description='' + ) + for field in self.fields + ] + + def filter_queryset(self, request, queryset, view): + node_id = dict_get_any(request.query_params, ['node', 'node_id']) + if not node_id: + return queryset + query_all_arg = request.query_params.get('all') + show_current_asset_arg = request.query_params.get('show_current_asset') + + query_all = query_all_arg == '1' + if show_current_asset_arg is not None: + query_all = show_current_asset_arg != '1' + + if is_uuid(node_id): + node = get_object_or_none(Node, id=node_id) + else: + node = get_object_or_none(Node, key=node_id) + + if not node: + return queryset.none() + + if query_all: + pattern = node.get_all_children_pattern(with_self=True) + else: + pattern = node.get_children_key_pattern(with_self=True) + return queryset.filter(nodes__key__regex=pattern) + + +class LabelFilterBackend(filters.BaseFilterBackend): + sep = '#' + query_arg = 'label' + + def get_schema_fields(self, view): + example = self.sep.join(['os', 'linux']) + return [ + coreapi.Field( + name=self.query_arg, location='query', required=False, + type='string', example=example, description='' + ) + ] + + def get_query_labels(self, request): + labels_query = request.query_params.getlist(self.query_arg) + if not labels_query: + return None + + q = None + for kv in labels_query: + if self.sep not in kv: + continue + key, value = kv.strip().split(self.sep)[:2] + if not all([key, value]): + continue + if q: + q |= Q(name=key, value=value) + else: + q = Q(name=key, value=value) + if not q: + return [] + labels = Label.objects.filter(q, is_active=True)\ + .values_list('id', flat=True) + return labels + + def filter_queryset(self, request, queryset, view): + labels = self.get_query_labels(request) + if labels is None: + return queryset + if len(labels) == 0: + return queryset.none() + for label in labels: + queryset = queryset.filter(labels=label) + return queryset + + diff --git a/apps/assets/migrations/0039_authbook_is_active.py b/apps/assets/migrations/0039_authbook_is_active.py new file mode 100644 index 000000000..3f600cfac --- /dev/null +++ b/apps/assets/migrations/0039_authbook_is_active.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.7 on 2019-09-17 12:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0038_auto_20190911_1634'), + ] + + operations = [ + migrations.AddField( + model_name='authbook', + name='is_active', + field=models.BooleanField(default=True, verbose_name='Is active'), + ), + ] diff --git a/apps/assets/migrations/0040_auto_20190917_2056.py b/apps/assets/migrations/0040_auto_20190917_2056.py new file mode 100644 index 000000000..2957a1f31 --- /dev/null +++ b/apps/assets/migrations/0040_auto_20190917_2056.py @@ -0,0 +1,36 @@ +# Generated by Django 2.1.7 on 2019-09-17 12:56 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0039_authbook_is_active'), + ] + + operations = [ + migrations.AlterField( + model_name='adminuser', + name='username', + field=models.CharField(blank=True, db_index=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'), + ), + migrations.AlterField( + model_name='authbook', + name='username', + field=models.CharField(blank=True, db_index=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'), + ), + migrations.AlterField( + model_name='gateway', + name='username', + field=models.CharField(blank=True, db_index=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'), + ), + migrations.AlterField( + model_name='systemuser', + name='username', + field=models.CharField(blank=True, db_index=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'), + ), + ] diff --git a/apps/assets/migrations/0041_gathereduser.py b/apps/assets/migrations/0041_gathereduser.py new file mode 100644 index 000000000..9accee746 --- /dev/null +++ b/apps/assets/migrations/0041_gathereduser.py @@ -0,0 +1,28 @@ +# Generated by Django 2.1.7 on 2019-09-18 04:10 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0040_auto_20190917_2056'), + ] + + operations = [ + migrations.CreateModel( + name='GatheredUser', + fields=[ + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('username', models.CharField(blank=True, db_index=True, max_length=32, verbose_name='Username')), + ('present', models.BooleanField(default=True)), + ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Asset')), + ], + options={'ordering': ['asset'], 'verbose_name': 'GatherUser'}, + ), + ] diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index 4b97a5929..c69f19bf6 100644 --- a/apps/assets/models/__init__.py +++ b/apps/assets/models/__init__.py @@ -9,3 +9,4 @@ from .cmd_filter import * from .authbook import * from .utils import * from .authbook import * +from .gathered_user import * diff --git a/apps/assets/models/authbook.py b/apps/assets/models/authbook.py index 01c8d4630..991729250 100644 --- a/apps/assets/models/authbook.py +++ b/apps/assets/models/authbook.py @@ -13,7 +13,7 @@ __all__ = ['AuthBook'] class AuthBookQuerySet(models.QuerySet): def latest_version(self): - return self.filter(is_latest=True) + return self.filter(is_latest=True).filter(is_active=True) class AuthBookManager(OrgManager): @@ -24,6 +24,7 @@ class AuthBook(AssetUser): asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset')) is_latest = models.BooleanField(default=False, verbose_name=_('Latest version')) version = models.IntegerField(default=1, verbose_name=_('Version')) + is_active = models.BooleanField(default=True, verbose_name=_("Is active")) objects = AuthBookManager.from_queryset(AuthBookQuerySet)() backend = "db" @@ -34,25 +35,25 @@ class AuthBook(AssetUser): class Meta: verbose_name = _('AuthBook') - def _set_latest(self): - self._remove_pre_obj_latest() + def set_to_latest(self): + self.remove_pre_latest() self.is_latest = True self.save() - def _get_pre_obj(self): + def get_pre_latest(self): pre_obj = self.__class__.objects.filter( username=self.username, asset=self.asset ).latest_version().first() return pre_obj - def _remove_pre_obj_latest(self): - pre_obj = self._get_pre_obj() + def remove_pre_latest(self): + pre_obj = self.get_pre_latest() if pre_obj: pre_obj.is_latest = False pre_obj.save() - def _set_version(self): - pre_obj = self._get_pre_obj() + def set_version(self): + pre_obj = self.get_pre_latest() if pre_obj: self.version = pre_obj.version + 1 else: @@ -60,8 +61,8 @@ class AuthBook(AssetUser): self.save() def set_version_and_latest(self): - self._set_version() - self._set_latest() + self.set_version() + self.set_to_latest() def get_related_assets(self): return [self.asset] diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 759285b80..2e591c940 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -26,7 +26,7 @@ logger = get_logger(__file__) class AssetUser(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, verbose_name=_('Name')) - username = models.CharField(max_length=32, blank=True, verbose_name=_('Username'), validators=[alphanumeric]) + username = models.CharField(max_length=32, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True) password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password')) private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key')) public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key')) diff --git a/apps/assets/models/gathered_user.py b/apps/assets/models/gathered_user.py new file mode 100644 index 000000000..305bced37 --- /dev/null +++ b/apps/assets/models/gathered_user.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# +import uuid +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from orgs.mixins.models import OrgModelMixin + +__all__ = ['GatheredUser'] + + +class GatheredUser(OrgModelMixin): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE) + username = models.CharField(max_length=32, blank=True, db_index=True, + verbose_name=_('Username')) + present = models.BooleanField(default=True) + date_created = models.DateTimeField(auto_now_add=True, + verbose_name=_("Date created")) + date_updated = models.DateTimeField(auto_now=True, + verbose_name=_("Date updated")) + + @property + def hostname(self): + return self.asset.hostname + + @property + def ip(self): + return self.asset.ip + + class Meta: + verbose_name = _('GatherUser') + ordering = ['asset'] + + def __str__(self): + return '{}: {}'.format(self.asset.hostname, self.username) + + + diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index 243c352a7..c294ff2ed 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -116,16 +116,24 @@ class FamilyMixin: def all_children(self): return self.get_all_children(with_self=False) - def get_children(self, with_self=False): + def get_children_key_pattern(self, with_self=False): pattern = r'^{0}:[0-9]+$'.format(self.key) if with_self: pattern += r'|^{0}$'.format(self.key) + return pattern + + def get_children(self, with_self=False): + pattern = self.get_children_key_pattern(with_self=with_self) return Node.objects.filter(key__regex=pattern) - def get_all_children(self, with_self=False): + def get_all_children_pattern(self, with_self=False): pattern = r'^{0}:'.format(self.key) if with_self: pattern += r'|^{0}$'.format(self.key) + return pattern + + def get_all_children(self, with_self=False): + pattern = self.get_all_children_pattern(with_self=with_self) children = Node.objects.filter(key__regex=pattern) return children diff --git a/apps/assets/serializers/__init__.py b/apps/assets/serializers/__init__.py index f9866688d..9c86ef407 100644 --- a/apps/assets/serializers/__init__.py +++ b/apps/assets/serializers/__init__.py @@ -9,3 +9,4 @@ from .node import * from .domain import * from .cmd_filter import * from .asset_user import * +from .gathered_user import * diff --git a/apps/assets/serializers/asset_user.py b/apps/assets/serializers/asset_user.py index 0e342e8b2..a93d2b2c3 100644 --- a/apps/assets/serializers/asset_user.py +++ b/apps/assets/serializers/asset_user.py @@ -53,6 +53,7 @@ class AssetUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): if not validated_data.get("name") and validated_data.get("username"): validated_data["name"] = validated_data["username"] instance = AssetUserManager.create(**validated_data) + instance.set_version_and_latest() return instance diff --git a/apps/assets/serializers/gathered_user.py b/apps/assets/serializers/gathered_user.py new file mode 100644 index 000000000..517b6a596 --- /dev/null +++ b/apps/assets/serializers/gathered_user.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# + +from ..models import GatheredUser + +from orgs.mixins.serializers import OrgResourceModelSerializerMixin + + +class GatheredUserSerializer(OrgResourceModelSerializerMixin): + class Meta: + model = GatheredUser + fields = [ + 'id', 'asset', 'hostname', 'ip', 'username', + 'present', 'date_created', 'date_updated' + ] + read_only_fields = fields diff --git a/apps/assets/signals_handler.py b/apps/assets/signals_handler.py index f4ff2ce04..2c980d22c 100644 --- a/apps/assets/signals_handler.py +++ b/apps/assets/signals_handler.py @@ -9,7 +9,7 @@ from django.dispatch import receiver from common.utils import get_logger from common.decorator import on_transaction_commit -from .models import Asset, SystemUser, Node, AuthBook +from .models import Asset, SystemUser, Node from .tasks import ( update_assets_hardware_info_util, test_asset_connectivity_util, @@ -190,10 +190,3 @@ def on_asset_nodes_remove(sender, instance=None, action='', model=None, def on_node_update_or_created(sender, **kwargs): # 刷新节点 Node.refresh_nodes() - - -@receiver(post_save, sender=AuthBook) -def on_auth_book_created(sender, instance=None, created=False, **kwargs): - if created: - logger.debug('Receive create auth book object signal.') - instance.set_version_and_latest() diff --git a/apps/assets/tasks.py b/apps/assets/tasks.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/assets/tasks/gather_asset_hardware_info.py b/apps/assets/tasks/gather_asset_hardware_info.py index 31262f699..2c0d75e99 100644 --- a/apps/assets/tasks/gather_asset_hardware_info.py +++ b/apps/assets/tasks/gather_asset_hardware_info.py @@ -103,7 +103,7 @@ def update_assets_hardware_info_util(assets, task_name=None): ) result = task.run() set_assets_hardware_info(assets, result) - return result + return True @shared_task(queue="ansible") diff --git a/apps/assets/tasks/gather_asset_users.py b/apps/assets/tasks/gather_asset_users.py index a48049d5a..2662e5c76 100644 --- a/apps/assets/tasks/gather_asset_users.py +++ b/apps/assets/tasks/gather_asset_users.py @@ -1,17 +1,102 @@ # ~*~ coding: utf-8 ~*~ +import re from collections import defaultdict from celery import shared_task + from django.utils.translation import ugettext as _ +from orgs.utils import tmp_to_org +from common.utils import get_logger +from ..models import GatheredUser, Node +from .utils import clean_hosts from . import const +__all__ = ['gather_asset_users', 'gather_nodes_asset_users'] +logger = get_logger(__name__) +space = re.compile('\s+') +ignore_login_shell = re.compile(r'nologin$|sync$|shutdown$|halt$') + + +def parse_linux_result_to_users(result): + task_result = {} + for task_name, raw in result.items(): + res = raw.get('ansible_facts', {}).get('getent_passwd') + if res: + task_result = res + break + if not task_result or not isinstance(task_result, dict): + return [] + users = [] + for username, attr in task_result.items(): + if ignore_login_shell.search(attr[-1]): + continue + users.append(username) + return users + + +def parse_windows_result_to_users(result): + task_result = [] + for task_name, raw in result.items(): + res = raw.get('stdout_lines', {}) + if res: + task_result = res + break + if not task_result: + return [] + + users = [] + + for i in range(4): + task_result.pop(0) + for i in range(2): + task_result.pop() + + for line in task_result: + user = space.split(line) + if user[0]: + users.append(user[0]) + return users + + +def add_asset_users(assets, results): + assets_map = {a.hostname: a for a in assets} + parser_map = { + 'linux': parse_linux_result_to_users, + 'windows': parse_windows_result_to_users + } + + assets_users_map = {} + + for platform, platform_results in results.items(): + for hostname, res in platform_results.items(): + parse = parser_map.get(platform) + users = parse(res) + logger.debug('Gathered host users: {} {}'.format(hostname, users)) + asset = assets_map.get(hostname) + if not asset: + continue + assets_users_map[asset] = users + + for asset, users in assets_users_map.items(): + with tmp_to_org(asset.org_id): + GatheredUser.objects.filter(asset=asset, present=True)\ + .update(present=False) + for username in users: + defaults = {'asset': asset, 'username': username, 'present': True} + GatheredUser.objects.update_or_create( + defaults=defaults, asset=asset, username=username, + ) + @shared_task(queue="ansible") -def gather_asset_all_users(assets, task_name=None): +def gather_asset_users(assets, task_name=None): from ops.utils import update_or_create_ansible_task if task_name is None: task_name = _("Gather assets users") + assets = clean_hosts(assets) + if not assets: + return hosts_category = { 'linux': { 'hosts': [], @@ -38,5 +123,12 @@ def gather_asset_all_users(assets, task_name=None): ) raw, summary = task.run() results[k].update(raw['ok']) - return results + add_asset_users(assets, results) + +@shared_task(queue="ansible") +def gather_nodes_asset_users(nodes_key): + assets = Node.get_nodes_all_assets(nodes_key) + assets_groups_by_100 = [assets[i:i+100] for i in range(0, len(assets), 100)] + for _assets in assets_groups_by_100: + gather_asset_users(_assets) diff --git a/apps/assets/templates/assets/asset_list.html b/apps/assets/templates/assets/asset_list.html index fcd0f79ff..3c8f93822 100644 --- a/apps/assets/templates/assets/asset_list.html +++ b/apps/assets/templates/assets/asset_list.html @@ -85,7 +85,7 @@ @@ -171,9 +171,13 @@ function initTable() { ], ajax_url: '{% url "api-assets:asset-list" %}', columns: [ - {data: "id"}, {data: "hostname" }, {data: "ip" }, + {data: "id"}, {data: "hostname"}, {data: "ip"}, {data: "cpu_cores", orderable: false}, - {data: "connectivity", orderable: false}, {data: "id", orderable: false } + { + data: "connectivity", + orderable: false, + width: '60px' + }, {data: "id", orderable: false} ], op_html: $('#actions').html() }; @@ -271,7 +275,7 @@ $(document).ready(function(){ setAssetModalOptions(modalOption); }) .on('click', '.labels li', function () { - var val = $(this).text(); + var val = 'label:' + $(this).text(); $("#asset_list_table_filter input").val(val); asset_table.search(val).draw(); }) diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index 47ff4dc14..46ea16483 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -21,6 +21,7 @@ router.register(r'gateways', api.GatewayViewSet, 'gateway') router.register(r'cmd-filters', api.CommandFilterViewSet, 'cmd-filter') router.register(r'asset-users', api.AssetUserViewSet, 'asset-user') router.register(r'asset-users-info', api.AssetUserExportViewSet, 'asset-user-info') +router.register(r'gathered-users', api.GatheredUserViewSet, 'gathered-user') cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filters', lookup='filter') cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule') diff --git a/apps/assets/utils.py b/apps/assets/utils.py index 01dae25be..0e4583dc8 100644 --- a/apps/assets/utils.py +++ b/apps/assets/utils.py @@ -24,37 +24,6 @@ def get_system_user_by_id(id): return system_user -class LabelFilterMixin: - def get_filter_labels_ids(self): - query_params = self.request.query_params - query_keys = query_params.keys() - all_label_keys = Label.objects.values_list('name', flat=True) - valid_keys = set(all_label_keys) & set(query_keys) - - if not valid_keys: - return [] - - labels_query = [ - {"name": key, "value": query_params[key]} - for key in valid_keys - ] - args = [Q(**kwargs) for kwargs in labels_query] - args = reduce(lambda x, y: x | y, args) - labels_id = Label.objects.filter(args).values_list('id', flat=True) - return labels_id - - -class LabelFilter(LabelFilterMixin): - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - labels_ids = self.get_filter_labels_ids() - if not labels_ids: - return queryset - for labels_id in labels_ids: - queryset = queryset.filter(labels=labels_id) - return queryset - - class TreeService(Tree): tag_sep = ' / ' cache_key = '_NODE_FULL_TREE' diff --git a/apps/common/filters.py b/apps/common/filters.py index 701f9c730..0dc275ce0 100644 --- a/apps/common/filters.py +++ b/apps/common/filters.py @@ -1,11 +1,15 @@ # -*- coding: utf-8 -*- # +import coreapi from rest_framework import filters from rest_framework.fields import DateTimeField from rest_framework.serializers import ValidationError +from django.core.cache import cache import logging -__all__ = ["DatetimeRangeFilter"] +from . import const + +__all__ = ["DatetimeRangeFilter", "IDSpmFilter", "CustomFilter"] class DatetimeRangeFilter(filters.BaseFilterBackend): @@ -40,3 +44,50 @@ class DatetimeRangeFilter(filters.BaseFilterBackend): if kwargs: queryset = queryset.filter(**kwargs) return queryset + + +class IDSpmFilter(filters.BaseFilterBackend): + def get_schema_fields(self, view): + return [ + coreapi.Field( + name='spm', location='query', required=False, + type='string', example='', + description='Pre post objects id get spm id, then using filter' + ) + ] + + def filter_queryset(self, request, queryset, view): + spm = request.query_params.get('spm') + if not spm: + return queryset + cache_key = const.KEY_CACHE_RESOURCES_ID.format(spm) + resources_id = cache.get(cache_key) + if not resources_id or not isinstance(resources_id, list): + queryset = queryset.none() + return queryset + queryset = queryset.filter(id__in=resources_id) + return queryset + + +class CustomFilter(filters.BaseFilterBackend): + custom_filter_fields = [] # ["node", "asset"] + + def get_schema_fields(self, view): + fields = [] + defaults = dict( + location='query', required=False, + type='string', example='', + description='' + ) + for field in self.custom_filter_fields: + if isinstance(field, str): + defaults['name'] = field + elif isinstance(field, dict): + defaults.update(field) + else: + continue + fields.append(coreapi.Field(**defaults)) + return fields + + def filter_queryset(self, request, queryset, view): + return queryset diff --git a/apps/common/mixins/api.py b/apps/common/mixins/api.py index 21f0394ce..6b1e8a893 100644 --- a/apps/common/mixins/api.py +++ b/apps/common/mixins/api.py @@ -1,15 +1,13 @@ # -*- coding: utf-8 -*- # from django.http import JsonResponse -from django.core.cache import cache -from django.utils.translation import ugettext_lazy as _ -from django.contrib import messages +from rest_framework.settings import api_settings -from ..const import KEY_CACHE_RESOURCES_ID +from ..filters import IDSpmFilter, CustomFilter __all__ = [ - "JSONResponseMixin", "IDInCacheFilterMixin", "IDExportFilterMixin", - "IDInFilterMixin", "ApiMessageMixin" + "JSONResponseMixin", "CommonApiMixin", + "IDSpmFilterMixin", "CommonApiMixin", ] @@ -20,69 +18,31 @@ class JSONResponseMixin(object): return JsonResponse(context) -class IDInFilterMixin(object): +class IDSpmFilterMixin: + def get_filter_backends(self): + backends = super().get_filter_backends() + backends.append(IDSpmFilter) + return backends + + +class ExtraFilterFieldsMixin: + default_added_filters = [CustomFilter, IDSpmFilter] + filter_backends = api_settings.DEFAULT_FILTER_BACKENDS + extra_filter_fields = [] + extra_filter_backends = [] + + def get_filter_backends(self): + if self.filter_backends != self.__class__.filter_backends: + return self.filter_backends + return list(self.filter_backends) + \ + self.default_added_filters + \ + list(self.extra_filter_backends) + def filter_queryset(self, queryset): - queryset = super(IDInFilterMixin, self).filter_queryset(queryset) - id_list = self.request.query_params.get('id__in') - if id_list: - import json - try: - ids = json.loads(id_list) - except Exception as e: - return queryset - if isinstance(ids, list): - queryset = queryset.filter(id__in=ids) + for backend in self.get_filter_backends(): + queryset = backend().filter_queryset(self.request, queryset, self) return queryset -class IDInCacheFilterMixin(object): - - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - spm = self.request.query_params.get('spm') - if not spm: - return queryset - cache_key = KEY_CACHE_RESOURCES_ID.format(spm) - resources_id = cache.get(cache_key) - if not resources_id or not isinstance(resources_id, list): - queryset = queryset.none() - return queryset - queryset = queryset.filter(id__in=resources_id) - return queryset - - -class IDExportFilterMixin(object): - def filter_queryset(self, queryset): - # 下载导入模版 - if self.request.query_params.get('template') == 'import': - return [] - else: - return super(IDExportFilterMixin, self).filter_queryset(queryset) - - -class ApiMessageMixin: - success_message = _("%(name)s was %(action)s successfully") - _action_map = {"create": _("create"), "update": _("update")} - - def get_success_message(self, cleaned_data): - if not isinstance(cleaned_data, dict): - return '' - data = {k: v for k, v in cleaned_data.items()} - action = getattr(self, "action", "create") - data["action"] = self._action_map.get(action) - try: - message = self.success_message % data - except: - message = '' - return message - - def dispatch(self, request, *args, **kwargs): - resp = super().dispatch(request, *args, **kwargs) - if request.method.lower() in ("get", "delete", "patch"): - return resp - if resp.status_code >= 400: - return resp - message = self.get_success_message(resp.data) - if message: - messages.success(request, message) - return resp +class CommonApiMixin(ExtraFilterFieldsMixin): + pass diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index a92af5f06..c73e9611d 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -8,7 +8,6 @@ import datetime import uuid from functools import wraps import time -import copy import ipaddress @@ -199,3 +198,18 @@ def timeit(func): logger.debug(msg) return result return wrapper + + +def group_obj_by_count(objs, count=50): + objs_grouped = [ + objs[i:i + count] for i in range(0, len(objs), count) + ] + return objs_grouped + + +def dict_get_any(d, keys): + for key in keys: + value = d.get(key) + if value: + return value + return None diff --git a/apps/jumpserver/swagger.py b/apps/jumpserver/swagger.py index eb8d89bf7..b68733b63 100644 --- a/apps/jumpserver/swagger.py +++ b/apps/jumpserver/swagger.py @@ -33,6 +33,21 @@ class CustomSwaggerAutoSchema(SwaggerAutoSchema): operation.summary = operation.operation_id return operation + def get_filter_parameters(self): + if not self.should_filter(): + return [] + + fields = [] + if hasattr(self.view, 'get_filter_backends'): + backends = self.view.get_filter_backends() + elif hasattr(self.view, 'filter_backends'): + backends = self.view.filter_backends + else: + backends = [] + for filter_backend in backends: + fields += self.probe_inspectors(self.filter_inspectors, 'get_filter_parameters', filter_backend()) or [] + return fields + def get_swagger_view(version='v1'): from .urls import api_v1, api_v2 diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 7e3bfc87f6b6c56fb5c97d2256b74ad4a93f5c5b..428c67c3e287794a8808e8fd6b10333195c7cf44 100644 GIT binary patch delta 23621 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±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 delta 23270 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\n" "Language-Team: Jumpserver team\n" @@ -71,13 +71,13 @@ msgstr "目标地址" msgid "Operating parameter" msgstr "运行参数" -#: applications/forms/remote_app.py:104 applications/models/remote_app.py:23 +#: applications/forms/remote_app.py:100 applications/models/remote_app.py:23 #: applications/templates/applications/remote_app_detail.html:57 #: applications/templates/applications/remote_app_list.html:22 #: applications/templates/applications/user_remote_app_list.html:18 #: assets/forms/domain.py:15 assets/forms/label.py:13 #: assets/models/asset.py:295 assets/models/authbook.py:24 -#: assets/serializers/admin_user.py:32 assets/serializers/asset_user.py:81 +#: assets/serializers/admin_user.py:32 assets/serializers/asset_user.py:82 #: assets/serializers/system_user.py:31 #: assets/templates/assets/admin_user_list.html:46 #: assets/templates/assets/domain_detail.html:60 @@ -96,40 +96,20 @@ msgstr "运行参数" #: terminal/templates/terminal/session_list.html:28 #: terminal/templates/terminal/session_list.html:72 #: xpack/plugins/change_auth_plan/forms.py:121 -#: xpack/plugins/change_auth_plan/models.py:413 +#: xpack/plugins/change_auth_plan/models.py:412 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:46 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:54 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:13 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:14 #: xpack/plugins/cloud/models.py:310 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:63 +#: xpack/plugins/gathered_user/forms.py:13 +#: xpack/plugins/gathered_user/forms.py:15 #: xpack/plugins/orgs/templates/orgs/org_list.html:16 #: xpack/plugins/vault/forms.py:13 xpack/plugins/vault/forms.py:15 msgid "Asset" msgstr "资产" -#: applications/forms/remote_app.py:107 applications/models/remote_app.py:27 -#: applications/templates/applications/remote_app_detail.html:61 -#: applications/templates/applications/remote_app_list.html:23 -#: applications/templates/applications/user_remote_app_list.html:19 -#: assets/models/user.py:168 assets/templates/assets/user_asset_list.html:52 -#: audits/models.py:20 audits/templates/audits/ftp_log_list.html:52 -#: audits/templates/audits/ftp_log_list.html:75 -#: perms/forms/asset_permission.py:85 perms/models/asset_permission.py:80 -#: perms/templates/perms/asset_permission_detail.html:140 -#: perms/templates/perms/asset_permission_list.html:54 -#: perms/templates/perms/asset_permission_list.html:75 -#: perms/templates/perms/asset_permission_list.html:127 templates/_nav.html:25 -#: terminal/backends/command/models.py:14 terminal/models.py:156 -#: terminal/templates/terminal/command_list.html:31 -#: terminal/templates/terminal/command_list.html:67 -#: terminal/templates/terminal/session_list.html:29 -#: terminal/templates/terminal/session_list.html:73 -#: users/templates/users/_granted_assets.html:27 -#: xpack/plugins/orgs/templates/orgs/org_list.html:19 -msgid "System user" -msgstr "系统用户" - #: applications/models/remote_app.py:21 #: applications/templates/applications/remote_app_detail.html:53 #: applications/templates/applications/remote_app_list.html:20 @@ -166,7 +146,7 @@ msgstr "系统用户" #: settings/templates/settings/terminal_setting.html:105 terminal/models.py:22 #: terminal/models.py:258 terminal/templates/terminal/terminal_detail.html:43 #: terminal/templates/terminal/terminal_list.html:29 users/models/group.py:14 -#: users/models/user.py:330 users/templates/users/_select_user_modal.html:13 +#: users/models/user.py:373 users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_detail.html:63 #: users/templates/users/user_group_detail.html:55 #: users/templates/users/user_group_list.html:35 @@ -174,7 +154,7 @@ msgstr "系统用户" #: users/templates/users/user_profile.html:51 #: users/templates/users/user_pubkey_update.html:57 #: xpack/plugins/change_auth_plan/forms.py:104 -#: xpack/plugins/change_auth_plan/models.py:61 +#: xpack/plugins/change_auth_plan/models.py:60 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:61 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:12 #: xpack/plugins/cloud/models.py:59 xpack/plugins/cloud/models.py:144 @@ -187,24 +167,24 @@ msgstr "系统用户" msgid "Name" msgstr "名称" -#: applications/models/remote_app.py:32 -#: applications/templates/applications/remote_app_detail.html:65 +#: applications/models/remote_app.py:28 +#: applications/templates/applications/remote_app_detail.html:61 #: applications/templates/applications/remote_app_list.html:21 #: applications/templates/applications/user_remote_app_list.html:17 msgid "App type" msgstr "应用类型" -#: applications/models/remote_app.py:36 -#: applications/templates/applications/remote_app_detail.html:69 +#: applications/models/remote_app.py:32 +#: applications/templates/applications/remote_app_detail.html:65 msgid "App path" msgstr "应用路径" -#: applications/models/remote_app.py:40 +#: applications/models/remote_app.py:36 msgid "Parameters" msgstr "参数" -#: applications/models/remote_app.py:43 -#: applications/templates/applications/remote_app_detail.html:77 +#: applications/models/remote_app.py:39 +#: applications/templates/applications/remote_app_detail.html:73 #: assets/models/asset.py:174 assets/models/base.py:36 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:25 #: assets/models/cmd_filter.py:58 assets/models/group.py:21 @@ -213,13 +193,13 @@ msgstr "参数" #: assets/templates/assets/cmd_filter_detail.html:77 #: assets/templates/assets/domain_detail.html:72 #: assets/templates/assets/system_user_detail.html:100 -#: ops/templates/ops/adhoc_detail.html:86 orgs/models.py:14 +#: ops/templates/ops/adhoc_detail.html:86 orgs/models.py:15 #: perms/models/base.py:54 #: perms/templates/perms/asset_permission_detail.html:98 #: perms/templates/perms/remote_app_permission_detail.html:90 -#: users/models/user.py:371 users/serializers/v1.py:119 +#: users/models/user.py:414 users/serializers/v1.py:141 #: users/templates/users/user_detail.html:111 -#: xpack/plugins/change_auth_plan/models.py:106 +#: xpack/plugins/change_auth_plan/models.py:105 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:113 #: xpack/plugins/cloud/models.py:80 xpack/plugins/cloud/models.py:179 msgid "Created by" @@ -227,17 +207,17 @@ msgstr "创建者" # msgid "Created by" # msgstr "创建者" -#: applications/models/remote_app.py:46 -#: applications/templates/applications/remote_app_detail.html:73 +#: applications/models/remote_app.py:42 +#: applications/templates/applications/remote_app_detail.html:69 #: assets/models/asset.py:175 assets/models/base.py:34 #: assets/models/cluster.py:26 assets/models/domain.py:23 -#: assets/models/group.py:22 assets/models/label.py:25 -#: assets/templates/assets/admin_user_detail.html:64 +#: assets/models/gathered_user.py:19 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 #: ops/templates/ops/adhoc_detail.html:90 ops/templates/ops/task_detail.html:64 -#: orgs/models.py:15 perms/models/base.py:55 +#: orgs/models.py:16 perms/models/base.py:55 #: perms/templates/perms/asset_permission_detail.html:94 #: perms/templates/perms/remote_app_permission_detail.html:86 #: terminal/templates/terminal/terminal_detail.html:59 users/models/group.py:17 @@ -252,10 +232,10 @@ msgstr "创建日期" # msgid "Date created" # msgstr "创建日期" -#: applications/models/remote_app.py:49 -#: applications/templates/applications/remote_app_detail.html:81 -#: applications/templates/applications/remote_app_list.html:24 -#: applications/templates/applications/user_remote_app_list.html:20 +#: applications/models/remote_app.py:45 +#: applications/templates/applications/remote_app_detail.html:77 +#: applications/templates/applications/remote_app_list.html:23 +#: applications/templates/applications/user_remote_app_list.html:19 #: assets/models/asset.py:176 assets/models/base.py:33 #: assets/models/cluster.py:29 assets/models/cmd_filter.py:22 #: assets/models/cmd_filter.py:55 assets/models/domain.py:21 @@ -271,16 +251,16 @@ msgstr "创建日期" #: assets/templates/assets/domain_list.html:28 #: assets/templates/assets/system_user_detail.html:104 #: assets/templates/assets/system_user_list.html:59 ops/models/adhoc.py:43 -#: orgs/models.py:16 perms/models/base.py:56 +#: orgs/models.py:17 perms/models/base.py:56 #: perms/templates/perms/asset_permission_detail.html:102 #: perms/templates/perms/remote_app_permission_detail.html:94 #: settings/models.py:34 terminal/models.py:32 #: terminal/templates/terminal/terminal_detail.html:63 users/models/group.py:15 -#: users/models/user.py:363 users/templates/users/user_detail.html:129 +#: users/models/user.py:406 users/templates/users/user_detail.html:129 #: users/templates/users/user_group_detail.html:67 #: users/templates/users/user_group_list.html:37 #: users/templates/users/user_profile.html:138 -#: xpack/plugins/change_auth_plan/models.py:102 +#: xpack/plugins/change_auth_plan/models.py:101 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:117 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:19 #: xpack/plugins/cloud/models.py:77 xpack/plugins/cloud/models.py:173 @@ -293,18 +273,18 @@ msgstr "创建日期" msgid "Comment" msgstr "备注" -#: applications/models/remote_app.py:53 perms/forms/remote_app_permission.py:37 +#: applications/models/remote_app.py:49 perms/forms/remote_app_permission.py:37 #: perms/models/remote_app_permission.py:15 #: perms/templates/perms/remote_app_permission_create_update.html:48 #: perms/templates/perms/remote_app_permission_detail.html:27 #: perms/templates/perms/remote_app_permission_list.html:17 #: perms/templates/perms/remote_app_permission_remote_app.html:26 #: perms/templates/perms/remote_app_permission_user.html:26 -#: templates/_nav.html:36 templates/_nav.html:48 templates/_nav_user.html:16 +#: templates/_nav.html:60 templates/_nav.html:76 templates/_nav_user.html:16 msgid "RemoteApp" msgstr "远程应用" -#: applications/templates/applications/remote_app_create_update.html:56 +#: applications/templates/applications/remote_app_create_update.html:55 #: assets/templates/assets/_system_user.html:75 #: assets/templates/assets/admin_user_create_update.html:45 #: assets/templates/assets/asset_bulk_update.html:23 @@ -315,7 +295,7 @@ msgstr "远程应用" #: assets/templates/assets/gateway_create_update.html:58 #: assets/templates/assets/label_create_update.html:18 #: perms/templates/perms/asset_permission_create_update.html:83 -#: perms/templates/perms/remote_app_permission_create_update.html:83 +#: perms/templates/perms/remote_app_permission_create_update.html:84 #: settings/templates/settings/basic_setting.html:64 #: settings/templates/settings/command_storage_create.html:79 #: settings/templates/settings/email_content_setting.html:54 @@ -336,12 +316,13 @@ msgstr "远程应用" #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:71 #: xpack/plugins/cloud/templates/cloud/account_create_update.html:33 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:53 +#: xpack/plugins/gathered_user/templates/gathered_user/vault_create.html:45 #: xpack/plugins/interface/templates/interface/interface.html:72 #: xpack/plugins/vault/templates/vault/vault_create.html:45 msgid "Reset" msgstr "重置" -#: applications/templates/applications/remote_app_create_update.html:58 +#: applications/templates/applications/remote_app_create_update.html:57 #: assets/templates/assets/_system_user.html:76 #: assets/templates/assets/admin_user_create_update.html:46 #: assets/templates/assets/asset_bulk_update.html:24 @@ -354,7 +335,7 @@ msgstr "重置" #: assets/templates/assets/label_create_update.html:19 #: audits/templates/audits/login_log_list.html:95 #: perms/templates/perms/asset_permission_create_update.html:84 -#: perms/templates/perms/remote_app_permission_create_update.html:84 +#: perms/templates/perms/remote_app_permission_create_update.html:85 #: settings/templates/settings/basic_setting.html:65 #: settings/templates/settings/command_storage_create.html:80 #: settings/templates/settings/email_content_setting.html:55 @@ -374,6 +355,7 @@ msgstr "重置" #: users/templates/users/user_profile_update.html:68 #: users/templates/users/user_pubkey_update.html:81 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:72 +#: xpack/plugins/gathered_user/templates/gathered_user/vault_create.html:46 #: xpack/plugins/interface/templates/interface/interface.html:74 #: xpack/plugins/vault/templates/vault/vault_create.html:46 msgid "Submit" @@ -406,7 +388,7 @@ msgid "Detail" msgstr "详情" #: applications/templates/applications/remote_app_detail.html:21 -#: applications/templates/applications/remote_app_list.html:56 +#: applications/templates/applications/remote_app_list.html:54 #: assets/templates/assets/_asset_user_list.html:69 #: assets/templates/assets/admin_user_detail.html:24 #: assets/templates/assets/admin_user_list.html:26 @@ -453,7 +435,7 @@ msgid "Update" msgstr "更新" #: applications/templates/applications/remote_app_detail.html:25 -#: applications/templates/applications/remote_app_list.html:57 +#: applications/templates/applications/remote_app_list.html:55 #: assets/templates/assets/admin_user_detail.html:28 #: assets/templates/assets/admin_user_list.html:112 #: assets/templates/assets/asset_detail.html:31 @@ -511,8 +493,8 @@ msgstr "下载应用加载器" msgid "Create RemoteApp" msgstr "创建远程应用" -#: applications/templates/applications/remote_app_list.html:25 -#: applications/templates/applications/user_remote_app_list.html:21 +#: applications/templates/applications/remote_app_list.html:24 +#: applications/templates/applications/user_remote_app_list.html:20 #: assets/models/cmd_filter.py:54 #: assets/templates/assets/_asset_user_list.html:20 #: assets/templates/assets/admin_user_list.html:51 @@ -550,7 +532,7 @@ msgstr "创建远程应用" msgid "Action" msgstr "动作" -#: applications/templates/applications/user_remote_app_list.html:57 +#: applications/templates/applications/user_remote_app_list.html:52 #: assets/templates/assets/user_asset_list.html:32 #: perms/models/asset_permission.py:30 msgid "Connect" @@ -558,7 +540,7 @@ msgstr "连接" #: applications/views/remote_app.py:31 applications/views/remote_app.py:47 #: applications/views/remote_app.py:70 applications/views/remote_app.py:89 -#: templates/_nav.html:33 +#: templates/_nav.html:57 msgid "Applications" msgstr "应用管理" @@ -595,20 +577,6 @@ msgstr "更新节点资产硬件信息: {}" msgid "Test if the assets under the node are connectable: {}" msgstr "测试节点下资产是否可连接: {}" -#: assets/const.py:77 assets/models/utils.py:43 -msgid "Unreachable" -msgstr "不可达" - -#: assets/const.py:78 assets/models/utils.py:44 -#: assets/templates/assets/asset_list.html:99 -msgid "Reachable" -msgstr "可连接" - -#: assets/const.py:79 assets/models/utils.py:45 authentication/utils.py:13 -#: xpack/plugins/license/models.py:78 -msgid "Unknown" -msgstr "未知" - #: assets/forms/asset.py:24 assets/models/asset.py:140 #: assets/models/domain.py:50 #: assets/templates/assets/domain_gateway_list.html:69 @@ -621,14 +589,14 @@ msgstr "端口" #: assets/templates/assets/asset_detail.html:198 #: assets/templates/assets/system_user_assets.html:83 #: perms/models/asset_permission.py:79 -#: xpack/plugins/change_auth_plan/models.py:72 +#: xpack/plugins/change_auth_plan/models.py:71 msgid "Nodes" msgstr "节点" #: assets/forms/asset.py:58 assets/forms/asset.py:104 #: assets/models/asset.py:149 assets/models/cluster.py:19 #: assets/models/user.py:68 assets/templates/assets/asset_detail.html:76 -#: templates/_nav.html:24 xpack/plugins/cloud/models.py:161 +#: templates/_nav.html:44 xpack/plugins/cloud/models.py:161 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:68 #: xpack/plugins/orgs/templates/orgs/org_list.html:18 msgid "Admin user" @@ -704,7 +672,7 @@ msgid "SSH gateway support proxy SSH,RDP,VNC" msgstr "SSH网关,支持代理SSH,RDP和VNC" #: assets/forms/domain.py:74 assets/forms/user.py:75 assets/forms/user.py:95 -#: assets/models/base.py:29 +#: assets/models/base.py:29 assets/models/gathered_user.py:16 #: assets/templates/assets/_asset_user_auth_update_modal.html:15 #: assets/templates/assets/_asset_user_auth_view_modal.html:21 #: assets/templates/assets/_asset_user_list.html:16 @@ -720,17 +688,18 @@ msgstr "SSH网关,支持代理SSH,RDP和VNC" #: perms/templates/perms/asset_permission_user.html:55 #: perms/templates/perms/remote_app_permission_user.html:54 #: settings/templates/settings/_ldap_list_users_modal.html:30 users/forms.py:14 -#: users/models/user.py:328 users/templates/users/_select_user_modal.html:14 +#: users/models/user.py:371 users/templates/users/_select_user_modal.html:14 #: users/templates/users/user_detail.html:67 #: users/templates/users/user_list.html:36 #: users/templates/users/user_profile.html:47 #: xpack/plugins/change_auth_plan/forms.py:106 -#: xpack/plugins/change_auth_plan/models.py:63 -#: xpack/plugins/change_auth_plan/models.py:409 +#: xpack/plugins/change_auth_plan/models.py:62 +#: xpack/plugins/change_auth_plan/models.py:408 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:65 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:53 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:12 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:13 +#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:74 msgid "Username" msgstr "用户名" @@ -739,7 +708,7 @@ msgid "Password or private key passphrase" msgstr "密码或密钥密码" #: assets/forms/user.py:26 assets/models/base.py:30 -#: assets/serializers/asset_user.py:62 +#: assets/serializers/asset_user.py:63 #: assets/templates/assets/_asset_user_auth_update_modal.html:21 #: assets/templates/assets/_asset_user_auth_view_modal.html:27 #: authentication/forms.py:15 @@ -752,14 +721,14 @@ msgstr "密码或密钥密码" #: users/templates/users/user_profile_update.html:41 #: users/templates/users/user_pubkey_update.html:41 #: users/templates/users/user_update.html:20 -#: xpack/plugins/change_auth_plan/models.py:93 -#: xpack/plugins/change_auth_plan/models.py:264 +#: xpack/plugins/change_auth_plan/models.py:92 +#: xpack/plugins/change_auth_plan/models.py:263 msgid "Password" msgstr "密码" -#: assets/forms/user.py:29 assets/serializers/asset_user.py:70 +#: assets/forms/user.py:29 assets/serializers/asset_user.py:71 #: assets/templates/assets/_asset_user_auth_update_modal.html:27 -#: users/models/user.py:357 +#: users/models/user.py:400 msgid "Private key" msgstr "ssh私钥" @@ -811,6 +780,7 @@ msgstr "使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig" #: perms/templates/perms/asset_permission_asset.html:58 settings/forms.py:140 #: users/templates/users/_granted_assets.html:26 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:51 +#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:73 msgid "IP" msgstr "IP" @@ -826,6 +796,7 @@ msgstr "IP" #: perms/templates/perms/asset_permission_list.html:73 settings/forms.py:139 #: users/templates/users/_granted_assets.html:25 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:50 +#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:72 msgid "Hostname" msgstr "主机名" @@ -841,7 +812,7 @@ msgstr "协议" #: assets/models/asset.py:142 assets/serializers/asset.py:63 #: assets/templates/assets/asset_create.html:24 #: assets/templates/assets/user_asset_list.html:50 -#: perms/serializers/user_permission.py:38 +#: perms/serializers/user_permission.py:48 msgid "Protocols" msgstr "协议组" @@ -850,9 +821,9 @@ msgstr "协议组" msgid "Platform" msgstr "系统平台" -#: assets/models/asset.py:146 assets/models/cmd_filter.py:21 -#: assets/models/domain.py:54 assets/models/label.py:22 -#: assets/templates/assets/asset_detail.html:112 +#: assets/models/asset.py:146 assets/models/authbook.py:27 +#: assets/models/cmd_filter.py:21 assets/models/domain.py:54 +#: assets/models/label.py:22 assets/templates/assets/asset_detail.html:112 msgid "Is active" msgstr "激活" @@ -922,7 +893,7 @@ msgid "Hostname raw" msgstr "主机名原始" #: assets/models/asset.py:173 assets/templates/assets/asset_create.html:46 -#: assets/templates/assets/asset_detail.html:224 templates/_nav.html:26 +#: assets/templates/assets/asset_detail.html:224 templates/_nav.html:46 msgid "Labels" msgstr "标签管理" @@ -938,22 +909,24 @@ msgstr "最新版本" msgid "Version" msgstr "版本" -#: assets/models/authbook.py:35 +#: assets/models/authbook.py:36 msgid "AuthBook" msgstr "" -#: assets/models/base.py:31 xpack/plugins/change_auth_plan/models.py:97 -#: xpack/plugins/change_auth_plan/models.py:271 +#: assets/models/base.py:31 xpack/plugins/change_auth_plan/models.py:96 +#: xpack/plugins/change_auth_plan/models.py:270 msgid "SSH private key" msgstr "ssh密钥" -#: assets/models/base.py:32 xpack/plugins/change_auth_plan/models.py:100 -#: xpack/plugins/change_auth_plan/models.py:267 +#: assets/models/base.py:32 xpack/plugins/change_auth_plan/models.py:99 +#: xpack/plugins/change_auth_plan/models.py:266 msgid "SSH public key" msgstr "ssh公钥" -#: assets/models/base.py:35 assets/templates/assets/cmd_filter_detail.html:73 +#: assets/models/base.py:35 assets/models/gathered_user.py:21 +#: assets/templates/assets/cmd_filter_detail.html:73 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:109 +#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:76 msgid "Date updated" msgstr "更新日期" @@ -965,7 +938,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:349 +#: assets/models/cluster.py:22 users/models/user.py:392 #: users/templates/users/user_detail.html:76 msgid "Phone" msgstr "手机" @@ -991,7 +964,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:469 +#: users/models/user.py:512 msgid "System" msgstr "系统" @@ -1081,6 +1054,10 @@ msgstr "命令过滤规则" msgid "Gateway" msgstr "网关" +#: assets/models/gathered_user.py:32 +msgid "GatherUser" +msgstr "收集用户" + #: assets/models/group.py:30 msgid "Asset group" msgstr "资产组" @@ -1110,9 +1087,9 @@ msgstr "默认资产组" #: terminal/templates/terminal/command_list.html:65 #: terminal/templates/terminal/session_list.html:27 #: terminal/templates/terminal/session_list.html:71 users/forms.py:316 -#: users/models/user.py:127 users/models/user.py:457 -#: users/serializers/v1.py:108 users/templates/users/user_group_detail.html:78 -#: users/templates/users/user_group_list.html:36 users/views/user.py:243 +#: users/models/user.py:127 users/models/user.py:143 users/models/user.py:500 +#: users/serializers/v1.py:130 users/templates/users/user_group_detail.html:78 +#: users/templates/users/user_group_list.html:36 users/views/user.py:250 #: xpack/plugins/orgs/forms.py:26 #: xpack/plugins/orgs/templates/orgs/org_detail.html:113 #: xpack/plugins/orgs/templates/orgs/org_list.html:14 @@ -1132,11 +1109,11 @@ msgstr "分类" msgid "New node" msgstr "新节点" -#: assets/models/node.py:308 perms/api/mixin.py:146 +#: assets/models/node.py:308 msgid "ungrouped" msgstr "未分组" -#: assets/models/node.py:310 perms/api/mixin.py:151 +#: assets/models/node.py:310 msgid "empty" msgstr "空" @@ -1171,7 +1148,7 @@ msgstr "手动登录" #: assets/views/label.py:27 assets/views/label.py:45 assets/views/label.py:73 #: assets/views/system_user.py:29 assets/views/system_user.py:46 #: assets/views/system_user.py:63 assets/views/system_user.py:79 -#: templates/_nav.html:19 xpack/plugins/change_auth_plan/models.py:68 +#: templates/_nav.html:39 xpack/plugins/change_auth_plan/models.py:67 msgid "Assets" msgstr "资产管理" @@ -1194,11 +1171,45 @@ msgstr "Shell" msgid "Login mode" msgstr "登录模式" +#: assets/models/user.py:168 assets/templates/assets/user_asset_list.html:52 +#: audits/models.py:20 audits/templates/audits/ftp_log_list.html:52 +#: audits/templates/audits/ftp_log_list.html:75 +#: perms/forms/asset_permission.py:85 perms/forms/remote_app_permission.py:40 +#: perms/models/asset_permission.py:80 perms/models/remote_app_permission.py:16 +#: perms/templates/perms/asset_permission_detail.html:140 +#: perms/templates/perms/asset_permission_list.html:54 +#: perms/templates/perms/asset_permission_list.html:75 +#: perms/templates/perms/asset_permission_list.html:127 +#: perms/templates/perms/remote_app_permission_detail.html:131 +#: templates/_nav.html:45 terminal/backends/command/models.py:14 +#: terminal/models.py:156 terminal/templates/terminal/command_list.html:31 +#: terminal/templates/terminal/command_list.html:67 +#: terminal/templates/terminal/session_list.html:29 +#: terminal/templates/terminal/session_list.html:73 +#: users/templates/users/_granted_assets.html:27 +#: xpack/plugins/orgs/templates/orgs/org_list.html:19 +msgid "System user" +msgstr "系统用户" + #: assets/models/utils.py:35 #, python-format msgid "%(value)s is not an even number" msgstr "%(value)s is not an even number" +#: assets/models/utils.py:43 assets/tasks/const.py:81 +msgid "Unreachable" +msgstr "不可达" + +#: assets/models/utils.py:44 assets/tasks/const.py:82 +#: assets/templates/assets/asset_list.html:99 +msgid "Reachable" +msgstr "可连接" + +#: assets/models/utils.py:45 assets/tasks/const.py:83 +#: authentication/utils.py:13 xpack/plugins/license/models.py:78 +msgid "Unknown" +msgstr "未知" + #: assets/serializers/asset.py:21 msgid "Protocol format should {}/{}" msgstr "协议格式 {}/{}" @@ -1216,7 +1227,7 @@ msgstr "连接" msgid "Hardware info" msgstr "硬件信息" -#: assets/serializers/asset.py:91 orgs/mixins/serializers.py:26 +#: assets/serializers/asset.py:91 orgs/mixins/serializers.py:27 msgid "Org name" msgstr "组织名称" @@ -1224,8 +1235,8 @@ msgstr "组织名称" msgid "Backend" msgstr "后端" -#: assets/serializers/asset_user.py:66 users/forms.py:263 -#: users/models/user.py:360 users/templates/users/first_login.html:42 +#: assets/serializers/asset_user.py:67 users/forms.py:263 +#: users/models/user.py:403 users/templates/users/first_login.html:42 #: users/templates/users/user_password_update.html:49 #: users/templates/users/user_profile.html:69 #: users/templates/users/user_profile_update.html:46 @@ -1262,88 +1273,93 @@ msgstr "自动登录模式,必须填写用户名" msgid "Password or private key required" msgstr "密码或密钥密码需要一个" -#: assets/tasks.py:33 -msgid "Asset has been disabled, skipped: {}" -msgstr "资产或许不支持ansible, 跳过: {}" - -#: assets/tasks.py:37 -msgid "Asset may not be support ansible, skipped: {}" -msgstr "资产或许不支持ansible, 跳过: {}" - -#: assets/tasks.py:50 -msgid "No assets matched, stop task" -msgstr "没有匹配到资产,结束任务" - -#: assets/tasks.py:60 -msgid "No assets matched related system user protocol, stop task" -msgstr "没有匹配到与系统用户协议相关的资产,结束任务" - -#: assets/tasks.py:86 -msgid "Get asset info failed: {}" -msgstr "获取资产信息失败:{}" - -#: assets/tasks.py:136 -msgid "Update some assets hardware info" -msgstr "更新资产硬件信息" - -#: assets/tasks.py:153 -msgid "Update asset hardware info: {}" -msgstr "更新资产硬件信息: {}" - -#: assets/tasks.py:178 -msgid "Test assets connectivity" -msgstr "测试资产可连接性" - -#: assets/tasks.py:232 -msgid "Test assets connectivity: {}" -msgstr "测试资产可连接性: {}" - -#: assets/tasks.py:274 +#: assets/tasks/admin_user_connectivity.py:56 msgid "Test admin user connectivity period: {}" msgstr "定期测试管理账号可连接性: {}" -#: assets/tasks.py:281 +#: assets/tasks/admin_user_connectivity.py:63 msgid "Test admin user connectivity: {}" msgstr "测试管理行号可连接性: {}" -#: assets/tasks.py:349 -msgid "Test system user connectivity: {}" -msgstr "测试系统用户可连接性: {}" +#: assets/tasks/asset_connectivity.py:21 +msgid "Test assets connectivity" +msgstr "测试资产可连接性" -#: assets/tasks.py:356 -msgid "Test system user connectivity: {} => {}" -msgstr "测试系统用户可连接性: {} => {}" +#: assets/tasks/asset_connectivity.py:75 +msgid "Test assets connectivity: {}" +msgstr "测试资产可连接性: {}" -#: assets/tasks.py:369 -msgid "Test system user connectivity period: {}" -msgstr "定期测试系统用户可连接性: {}" - -#: assets/tasks.py:487 assets/tasks.py:573 -#: xpack/plugins/change_auth_plan/models.py:522 +#: assets/tasks/asset_user_connectivity.py:27 +#: assets/tasks/push_system_user.py:130 +#: xpack/plugins/change_auth_plan/models.py:521 msgid "The asset {} system platform {} does not support run Ansible tasks" msgstr "资产 {} 系统平台 {} 不支持运行 Ansible 任务" -#: assets/tasks.py:499 +#: assets/tasks/asset_user_connectivity.py:74 +msgid "Test asset user connectivity: {}" +msgstr "测试资产用户可连接性: {}" + +#: assets/tasks/gather_asset_hardware_info.py:44 +msgid "Get asset info failed: {}" +msgstr "获取资产信息失败:{}" + +#: assets/tasks/gather_asset_hardware_info.py:94 +msgid "Update some assets hardware info" +msgstr "更新资产硬件信息" + +#: assets/tasks/gather_asset_hardware_info.py:111 +msgid "Update asset hardware info: {}" +msgstr "更新资产硬件信息: {}" + +#: assets/tasks/gather_asset_users.py:95 +msgid "Gather assets users" +msgstr "收集资产上的用户" + +#: assets/tasks/push_system_user.py:142 msgid "" "Push system user task skip, auto push not enable or protocol is not ssh or " "rdp: {}" msgstr "推送系统用户任务跳过,自动推送没有打开,或协议不是ssh或rdp: {}" -#: assets/tasks.py:506 +#: assets/tasks/push_system_user.py:149 msgid "For security, do not push user {}" msgstr "为了安全,禁止推送用户 {}" -#: assets/tasks.py:534 assets/tasks.py:548 +#: assets/tasks/push_system_user.py:177 assets/tasks/push_system_user.py:191 msgid "Push system users to assets: {}" msgstr "推送系统用户到入资产: {}" -#: assets/tasks.py:540 +#: assets/tasks/push_system_user.py:183 msgid "Push system users to asset: {} => {}" msgstr "推送系统用户到入资产: {} => {}" -#: assets/tasks.py:620 -msgid "Test asset user connectivity: {}" -msgstr "测试资产用户可连接性: {}" +#: assets/tasks/system_user_connectivity.py:79 +msgid "Test system user connectivity: {}" +msgstr "测试系统用户可连接性: {}" + +#: assets/tasks/system_user_connectivity.py:86 +msgid "Test system user connectivity: {} => {}" +msgstr "测试系统用户可连接性: {} => {}" + +#: assets/tasks/system_user_connectivity.py:99 +msgid "Test system user connectivity period: {}" +msgstr "定期测试系统用户可连接性: {}" + +#: assets/tasks/utils.py:16 +msgid "Asset has been disabled, skipped: {}" +msgstr "资产或许不支持ansible, 跳过: {}" + +#: assets/tasks/utils.py:20 +msgid "Asset may not be support ansible, skipped: {}" +msgstr "资产或许不支持ansible, 跳过: {}" + +#: assets/tasks/utils.py:33 +msgid "No assets matched, stop task" +msgstr "没有匹配到资产,结束任务" + +#: assets/tasks/utils.py:43 +msgid "No assets matched related system user protocol, stop task" +msgstr "没有匹配到与系统用户协议相关的资产,结束任务" #: assets/templates/assets/_admin_user_import_modal.html:4 msgid "Import admin user" @@ -1385,12 +1401,13 @@ msgid "Import assets" msgstr "导入资产" #: assets/templates/assets/_asset_list_modal.html:7 assets/views/asset.py:38 -#: templates/_nav.html:22 xpack/plugins/change_auth_plan/views.py:115 +#: templates/_nav.html:42 xpack/plugins/change_auth_plan/views.py:115 msgid "Asset list" msgstr "资产列表" #: assets/templates/assets/_asset_list_modal.html:33 #: assets/templates/assets/_node_tree.html:40 +#: ops/templates/ops/command_execution_create.html:49 #: users/templates/users/_granted_assets.html:7 #: users/templates/users/_granted_assets.html:83 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:66 @@ -1412,8 +1429,8 @@ msgstr "请输入密码" #: assets/templates/assets/_asset_user_auth_update_modal.html:68 #: assets/templates/assets/asset_detail.html:306 -#: users/templates/users/user_detail.html:311 -#: users/templates/users/user_detail.html:338 +#: users/templates/users/user_detail.html:313 +#: users/templates/users/user_detail.html:340 #: xpack/plugins/interface/views.py:35 msgid "Update successfully!" msgstr "更新成功" @@ -1537,7 +1554,7 @@ msgstr "自动生成密钥" #: assets/templates/assets/asset_create.html:74 #: assets/templates/assets/gateway_create_update.html:53 #: perms/templates/perms/asset_permission_create_update.html:53 -#: perms/templates/perms/remote_app_permission_create_update.html:52 +#: perms/templates/perms/remote_app_permission_create_update.html:53 #: terminal/templates/terminal/terminal_update.html:40 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:67 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:48 @@ -1605,10 +1622,10 @@ msgstr "选择节点" #: authentication/templates/authentication/_mfa_confirm_modal.html:20 #: settings/templates/settings/terminal_setting.html:168 #: templates/_modal.html:23 terminal/templates/terminal/session_detail.html:108 -#: users/templates/users/user_detail.html:392 -#: users/templates/users/user_detail.html:418 -#: users/templates/users/user_detail.html:441 -#: users/templates/users/user_detail.html:486 +#: users/templates/users/user_detail.html:394 +#: users/templates/users/user_detail.html:420 +#: users/templates/users/user_detail.html:443 +#: users/templates/users/user_detail.html:488 #: users/templates/users/user_group_create_update.html:32 #: users/templates/users/user_group_list.html:120 #: users/templates/users/user_list.html:256 @@ -1638,6 +1655,7 @@ msgstr "Jumpserver 使用该用户来 `推送系统用户`、`获取资产硬件 #: audits/templates/audits/login_log_list.html:91 #: users/templates/users/user_group_list.html:10 #: users/templates/users/user_list.html:10 +#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:59 #: xpack/plugins/vault/templates/vault/vault.html:55 msgid "Export" msgstr "导出" @@ -1721,7 +1739,7 @@ msgstr "创建日期" #: perms/models/base.py:51 #: perms/templates/perms/asset_permission_create_update.html:55 #: perms/templates/perms/asset_permission_detail.html:120 -#: perms/templates/perms/remote_app_permission_create_update.html:54 +#: perms/templates/perms/remote_app_permission_create_update.html:55 #: perms/templates/perms/remote_app_permission_detail.html:112 #: terminal/templates/terminal/terminal_list.html:34 #: users/templates/users/_select_user_modal.html:18 @@ -1805,9 +1823,9 @@ msgstr "显示所有子节点资产" #: assets/templates/assets/asset_list.html:412 #: assets/templates/assets/system_user_list.html:133 -#: users/templates/users/user_detail.html:386 -#: users/templates/users/user_detail.html:412 -#: users/templates/users/user_detail.html:480 +#: users/templates/users/user_detail.html:388 +#: users/templates/users/user_detail.html:414 +#: users/templates/users/user_detail.html:482 #: users/templates/users/user_group_list.html:114 #: users/templates/users/user_list.html:250 #: xpack/plugins/interface/templates/interface/interface.html:97 @@ -1821,9 +1839,9 @@ msgstr "删除选择资产" #: assets/templates/assets/asset_list.html:416 #: assets/templates/assets/system_user_list.html:137 #: settings/templates/settings/terminal_setting.html:166 -#: users/templates/users/user_detail.html:390 -#: users/templates/users/user_detail.html:416 -#: users/templates/users/user_detail.html:484 +#: users/templates/users/user_detail.html:392 +#: users/templates/users/user_detail.html:418 +#: users/templates/users/user_detail.html:486 #: users/templates/users/user_group_create_update.html:31 #: users/templates/users/user_group_list.html:118 #: users/templates/users/user_list.html:254 @@ -2082,7 +2100,7 @@ msgstr "创建命令过滤器规则" msgid "Update command filter rule" msgstr "更新命令过滤器规则" -#: assets/views/domain.py:31 templates/_nav.html:23 +#: assets/views/domain.py:31 templates/_nav.html:43 msgid "Domain list" msgstr "网域列表" @@ -2153,7 +2171,7 @@ msgstr "文件名" #: audits/templates/audits/ftp_log_list.html:79 #: ops/templates/ops/command_execution_list.html:68 #: ops/templates/ops/task_list.html:31 -#: users/templates/users/user_detail.html:462 +#: users/templates/users/user_detail.html:464 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:14 #: xpack/plugins/cloud/api.py:61 msgid "Success" @@ -2161,6 +2179,7 @@ msgstr "成功" #: audits/models.py:32 #: authentication/templates/authentication/_access_key_modal.html:38 +#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:48 #: xpack/plugins/vault/templates/vault/vault.html:46 msgid "Create" msgstr "创建" @@ -2214,13 +2233,13 @@ msgstr "Agent" #: audits/models.py:85 audits/templates/audits/login_log_list.html:62 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 -#: users/forms.py:175 users/models/user.py:352 +#: users/forms.py:175 users/models/user.py:395 #: users/templates/users/first_login.html:45 msgid "MFA" msgstr "MFA" #: audits/models.py:86 audits/templates/audits/login_log_list.html:63 -#: xpack/plugins/change_auth_plan/models.py:417 +#: xpack/plugins/change_auth_plan/models.py:416 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:15 #: xpack/plugins/cloud/models.py:281 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:69 @@ -2246,8 +2265,8 @@ msgstr "登录日期" #: perms/templates/perms/asset_permission_detail.html:86 #: perms/templates/perms/remote_app_permission_detail.html:78 #: terminal/models.py:165 terminal/templates/terminal/session_list.html:34 -#: xpack/plugins/change_auth_plan/models.py:250 -#: xpack/plugins/change_auth_plan/models.py:420 +#: xpack/plugins/change_auth_plan/models.py:249 +#: xpack/plugins/change_auth_plan/models.py:419 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:59 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:17 msgid "Date start" @@ -2297,47 +2316,46 @@ msgstr "城市" msgid "Date" msgstr "日期" -#: audits/views.py:85 audits/views.py:129 audits/views.py:166 -#: audits/views.py:211 audits/views.py:243 templates/_nav.html:87 -#: templates/_nav_audits.html:22 +#: audits/views.py:86 audits/views.py:130 audits/views.py:167 +#: audits/views.py:212 audits/views.py:244 templates/_nav.html:126 msgid "Audits" msgstr "日志审计" -#: audits/views.py:86 templates/_nav.html:91 templates/_nav_audits.html:26 +#: audits/views.py:87 templates/_nav.html:130 msgid "FTP log" msgstr "FTP日志" -#: audits/views.py:130 templates/_nav.html:92 templates/_nav_audits.html:27 +#: audits/views.py:131 templates/_nav.html:131 msgid "Operate log" msgstr "操作日志" -#: audits/views.py:167 templates/_nav.html:93 templates/_nav_audits.html:28 +#: audits/views.py:168 templates/_nav.html:132 msgid "Password change log" msgstr "改密日志" -#: audits/views.py:212 templates/_nav.html:90 templates/_nav_audits.html:25 +#: audits/views.py:213 templates/_nav.html:129 msgid "Login log" msgstr "登录日志" -#: audits/views.py:244 +#: audits/views.py:245 msgid "Command execution log" msgstr "命令执行" -#: authentication/api/auth.py:51 authentication/api/token.py:45 +#: authentication/api/auth.py:61 authentication/api/token.py:45 #: authentication/templates/authentication/login.html:52 #: authentication/templates/authentication/new_login.html:77 msgid "Log in frequently and try again later" msgstr "登录频繁, 稍后重试" -#: authentication/api/auth.py:76 +#: authentication/api/auth.py:86 msgid "Please carry seed value and conduct MFA secondary certification" msgstr "请携带seed值, 进行MFA二次认证" -#: authentication/api/auth.py:156 +#: authentication/api/auth.py:176 msgid "Please verify the user name and password first" msgstr "请先进行用户名和密码验证" -#: authentication/api/auth.py:161 +#: authentication/api/auth.py:181 msgid "MFA certification failed" msgstr "MFA认证失败" @@ -2467,14 +2485,14 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:287 users/templates/users/user_profile.html:94 +#: users/models/user.py:330 users/templates/users/user_profile.html:94 #: users/templates/users/user_profile.html:163 #: users/templates/users/user_profile.html:166 msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:288 users/templates/users/user_profile.html:92 +#: users/models/user.py:331 users/templates/users/user_profile.html:92 #: users/templates/users/user_profile.html:170 msgid "Enable" msgstr "启用" @@ -2611,8 +2629,8 @@ msgstr "欢迎回来,请输入用户名和密码登录" msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:174 users/views/user.py:386 -#: users/views/user.py:411 +#: authentication/views/login.py:174 users/views/user.py:393 +#: users/views/user.py:418 msgid "MFA code invalid, or ntp sync server time" msgstr "MFA验证码不正确,或者服务器端时间不对" @@ -2670,16 +2688,16 @@ msgstr "" msgid "Encrypt field using Secret Key" msgstr "" -#: common/mixins/api.py:63 +#: common/mixins/api.py:64 #, python-format msgid "%(name)s was %(action)s successfully" msgstr "%(name)s %(action)s成功" -#: common/mixins/api.py:64 +#: common/mixins/api.py:65 msgid "create" msgstr "创建" -#: common/mixins/api.py:64 +#: common/mixins/api.py:65 msgid "update" msgstr "更新" @@ -2699,11 +2717,11 @@ msgstr "不能包含特殊字符" msgid "This field must be unique." msgstr "字段必须唯一" -#: jumpserver/views.py:188 templates/_nav.html:4 templates/_nav_audits.html:4 +#: jumpserver/views.py:184 templates/_nav.html:7 msgid "Dashboard" msgstr "仪表盘" -#: jumpserver/views.py:197 +#: jumpserver/views.py:193 msgid "" "
Luna is a separately deployed program, you need to deploy Luna, coco, " "configure nginx for url distribution,
If you see this page, " @@ -2800,8 +2818,8 @@ msgstr "完成时间" #: ops/models/adhoc.py:327 ops/templates/ops/adhoc_history.html:57 #: ops/templates/ops/task_history.html:63 ops/templates/ops/task_list.html:33 -#: xpack/plugins/change_auth_plan/models.py:253 -#: xpack/plugins/change_auth_plan/models.py:423 +#: xpack/plugins/change_auth_plan/models.py:252 +#: xpack/plugins/change_auth_plan/models.py:422 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:58 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:16 msgid "Time" @@ -2950,39 +2968,39 @@ msgstr "成功资产" msgid "Task log" msgstr "任务列表" -#: ops/templates/ops/command_execution_create.html:71 +#: ops/templates/ops/command_execution_create.html:90 #: terminal/templates/terminal/session_detail.html:91 #: terminal/templates/terminal/session_detail.html:100 msgid "Go" msgstr "" -#: ops/templates/ops/command_execution_create.html:197 +#: ops/templates/ops/command_execution_create.html:215 msgid "Selected assets" msgstr "已选择资产" -#: ops/templates/ops/command_execution_create.html:200 +#: ops/templates/ops/command_execution_create.html:218 msgid "In total" msgstr "总共" -#: ops/templates/ops/command_execution_create.html:236 +#: ops/templates/ops/command_execution_create.html:254 msgid "" "Select the left asset, select the running system user, execute command in " "batch" msgstr "选择左侧资产, 选择运行的系统用户,批量执行命令" -#: ops/templates/ops/command_execution_create.html:257 +#: ops/templates/ops/command_execution_create.html:275 msgid "Unselected assets" msgstr "没有选中资产" -#: ops/templates/ops/command_execution_create.html:261 +#: ops/templates/ops/command_execution_create.html:279 msgid "No input command" msgstr "没有输入命令" -#: ops/templates/ops/command_execution_create.html:265 +#: ops/templates/ops/command_execution_create.html:283 msgid "No system user was selected" msgstr "没有选择系统用户" -#: ops/templates/ops/command_execution_create.html:310 +#: ops/templates/ops/command_execution_create.html:328 msgid "Pending" msgstr "等待" @@ -3044,11 +3062,11 @@ msgstr "更新任务内容: {}" #: ops/views/adhoc.py:45 ops/views/adhoc.py:71 ops/views/adhoc.py:85 #: ops/views/adhoc.py:99 ops/views/adhoc.py:113 ops/views/adhoc.py:127 -#: ops/views/adhoc.py:141 ops/views/command.py:47 ops/views/command.py:77 +#: ops/views/adhoc.py:141 ops/views/command.py:48 ops/views/command.py:79 msgid "Ops" msgstr "作业中心" -#: ops/views/adhoc.py:46 templates/_nav.html:81 +#: ops/views/adhoc.py:46 templates/_nav.html:115 msgid "Task list" msgstr "任务列表" @@ -3056,15 +3074,15 @@ msgstr "任务列表" msgid "Task run history" msgstr "执行历史" -#: ops/views/command.py:48 +#: ops/views/command.py:49 msgid "Command execution list" msgstr "命令执行列表" -#: ops/views/command.py:78 templates/_nav_user.html:26 +#: ops/views/command.py:80 templates/_nav_user.html:26 msgid "Command execution" msgstr "命令执行" -#: orgs/mixins/models.py:61 orgs/mixins/serializers.py:25 orgs/models.py:29 +#: orgs/mixins/models.py:61 orgs/mixins/serializers.py:26 orgs/models.py:30 msgid "Organization" msgstr "组织" @@ -3081,9 +3099,9 @@ msgstr "空" #: perms/templates/perms/asset_permission_list.html:71 #: perms/templates/perms/asset_permission_list.html:118 #: perms/templates/perms/remote_app_permission_list.html:16 -#: templates/_nav.html:14 users/forms.py:286 users/models/group.py:26 -#: users/models/user.py:336 users/templates/users/_select_user_modal.html:16 -#: users/templates/users/user_detail.html:217 +#: templates/_nav.html:21 users/forms.py:286 users/models/group.py:26 +#: users/models/user.py:379 users/templates/users/_select_user_modal.html:16 +#: users/templates/users/user_detail.html:218 #: users/templates/users/user_list.html:38 #: xpack/plugins/orgs/templates/orgs/org_list.html:15 msgid "User group" @@ -3095,7 +3113,7 @@ msgid "" "downloading files" msgstr "提示:RDP 协议不支持单独控制上传或下载文件" -#: perms/forms/asset_permission.py:102 perms/forms/remote_app_permission.py:47 +#: perms/forms/asset_permission.py:102 perms/forms/remote_app_permission.py:50 msgid "User or group at least one required" msgstr "用户和用户组至少选一个" @@ -3123,19 +3141,19 @@ msgstr "上传下载" msgid "Actions" msgstr "动作" -#: perms/models/asset_permission.py:85 templates/_nav.html:44 +#: perms/models/asset_permission.py:85 templates/_nav.html:72 msgid "Asset permission" msgstr "资产授权" #: perms/models/base.py:53 #: perms/templates/perms/asset_permission_detail.html:90 #: perms/templates/perms/remote_app_permission_detail.html:82 -#: users/models/user.py:368 users/templates/users/user_detail.html:107 +#: users/models/user.py:411 users/templates/users/user_detail.html:107 #: users/templates/users/user_profile.html:120 msgid "Date expired" msgstr "失效日期" -#: perms/models/remote_app_permission.py:19 +#: perms/models/remote_app_permission.py:20 msgid "RemoteApp permission" msgstr "远程应用授权" @@ -3165,6 +3183,7 @@ msgstr "添加资产" #: perms/templates/perms/asset_permission_detail.html:157 #: perms/templates/perms/asset_permission_user.html:97 #: perms/templates/perms/asset_permission_user.html:125 +#: perms/templates/perms/remote_app_permission_detail.html:148 #: perms/templates/perms/remote_app_permission_remote_app.html:96 #: perms/templates/perms/remote_app_permission_user.html:96 #: perms/templates/perms/remote_app_permission_user.html:124 @@ -3182,13 +3201,13 @@ msgid "Add node to this permission" msgstr "添加节点" #: perms/templates/perms/asset_permission_asset.html:109 -#: users/templates/users/user_detail.html:234 +#: users/templates/users/user_detail.html:235 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:115 msgid "Join" msgstr "加入" #: perms/templates/perms/asset_permission_create_update.html:61 -#: perms/templates/perms/remote_app_permission_create_update.html:60 +#: perms/templates/perms/remote_app_permission_create_update.html:61 msgid "Validity period" msgstr "有效期" @@ -3217,6 +3236,7 @@ msgid "System user count" msgstr "系统用户数量" #: perms/templates/perms/asset_permission_detail.html:148 +#: perms/templates/perms/remote_app_permission_detail.html:139 msgid "Select system users" msgstr "选择系统用户" @@ -3286,13 +3306,13 @@ msgstr "添加用户组" #: perms/views/asset_permission.py:33 perms/views/asset_permission.py:65 #: perms/views/asset_permission.py:82 perms/views/asset_permission.py:99 -#: perms/views/asset_permission.py:136 perms/views/asset_permission.py:171 +#: perms/views/asset_permission.py:139 perms/views/asset_permission.py:170 #: perms/views/remote_app_permission.py:33 #: perms/views/remote_app_permission.py:49 #: perms/views/remote_app_permission.py:66 #: perms/views/remote_app_permission.py:81 -#: perms/views/remote_app_permission.py:108 -#: perms/views/remote_app_permission.py:145 templates/_nav.html:41 +#: perms/views/remote_app_permission.py:115 +#: perms/views/remote_app_permission.py:148 templates/_nav.html:69 #: xpack/plugins/orgs/templates/orgs/org_list.html:21 msgid "Perms" msgstr "权限管理" @@ -3313,11 +3333,11 @@ msgstr "更新资产授权" msgid "Asset permission detail" msgstr "资产授权详情" -#: perms/views/asset_permission.py:137 +#: perms/views/asset_permission.py:140 msgid "Asset permission user list" msgstr "资产授权用户列表" -#: perms/views/asset_permission.py:172 +#: perms/views/asset_permission.py:171 msgid "Asset permission asset list" msgstr "资产授权资产列表" @@ -3337,11 +3357,11 @@ msgstr "更新远程应用授权规则" msgid "RemoteApp permission detail" msgstr "远程应用授权详情" -#: perms/views/remote_app_permission.py:109 +#: perms/views/remote_app_permission.py:116 msgid "RemoteApp permission user list" msgstr "远程应用授权用户列表" -#: perms/views/remote_app_permission.py:146 +#: perms/views/remote_app_permission.py:149 msgid "RemoteApp permission RemoteApp list" msgstr "远程应用授权远程应用列表" @@ -3684,7 +3704,7 @@ msgid "Please submit the LDAP configuration before import" msgstr "请先提交LDAP配置再进行导入" #: settings/templates/settings/_ldap_list_users_modal.html:32 -#: users/models/user.py:332 users/templates/users/user_detail.html:71 +#: users/models/user.py:375 users/templates/users/user_detail.html:71 #: users/templates/users/user_profile.html:59 msgid "Email" msgstr "邮件" @@ -3888,7 +3908,7 @@ msgstr "用户来源不是LDAP" #: settings/views.py:19 settings/views.py:46 settings/views.py:73 #: settings/views.py:103 settings/views.py:131 settings/views.py:144 -#: settings/views.py:158 settings/views.py:185 templates/_nav.html:122 +#: settings/views.py:158 settings/views.py:185 templates/_nav.html:167 msgid "Settings" msgstr "系统设置" @@ -3917,14 +3937,15 @@ msgstr "文档" msgid "Commercial support" msgstr "商业支持" -#: templates/_header_bar.html:70 templates/_nav_user.html:32 users/forms.py:154 +#: templates/_header_bar.html:70 templates/_nav.html:30 +#: templates/_nav_user.html:32 users/forms.py:154 #: 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:61 -#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:224 +#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:231 msgid "Profile" msgstr "个人信息" @@ -4029,72 +4050,70 @@ msgstr "" "\"%(user_pubkey_update)s\"> 链接 更新\n" " " -#: templates/_nav.html:10 users/views/group.py:28 users/views/group.py:45 -#: users/views/group.py:63 users/views/group.py:81 users/views/group.py:98 -#: users/views/login.py:154 users/views/user.py:60 users/views/user.py:77 -#: users/views/user.py:121 users/views/user.py:188 users/views/user.py:210 -#: users/views/user.py:263 users/views/user.py:298 +#: templates/_nav.html:17 users/views/group.py:28 users/views/group.py:45 +#: users/views/group.py:63 users/views/group.py:82 users/views/group.py:99 +#: users/views/login.py:154 users/views/user.py:61 users/views/user.py:78 +#: users/views/user.py:122 users/views/user.py:189 users/views/user.py:217 +#: users/views/user.py:270 users/views/user.py:305 msgid "Users" msgstr "用户管理" -#: templates/_nav.html:13 users/views/user.py:61 +#: templates/_nav.html:20 users/views/user.py:62 msgid "User list" msgstr "用户列表" -#: templates/_nav.html:27 +#: templates/_nav.html:47 msgid "Command filters" msgstr "命令过滤" -#: templates/_nav.html:55 templates/_nav_audits.html:11 -#: terminal/views/command.py:21 terminal/views/session.py:43 -#: terminal/views/session.py:54 terminal/views/session.py:78 -#: terminal/views/terminal.py:32 terminal/views/terminal.py:48 -#: terminal/views/terminal.py:61 +#: templates/_nav.html:88 terminal/views/command.py:21 +#: terminal/views/session.py:43 terminal/views/session.py:54 +#: terminal/views/session.py:78 terminal/views/terminal.py:32 +#: terminal/views/terminal.py:48 terminal/views/terminal.py:61 msgid "Sessions" msgstr "会话管理" -#: templates/_nav.html:58 templates/_nav_audits.html:14 +#: templates/_nav.html:91 msgid "Session online" msgstr "在线会话" -#: templates/_nav.html:59 templates/_nav_audits.html:15 -#: terminal/views/session.py:55 +#: templates/_nav.html:92 terminal/views/session.py:55 msgid "Session offline" msgstr "历史会话" -#: templates/_nav.html:60 templates/_nav_audits.html:16 +#: templates/_nav.html:93 msgid "Commands" msgstr "命令记录" -#: templates/_nav.html:63 templates/_nav_user.html:37 +#: templates/_nav.html:96 templates/_nav_user.html:37 msgid "Web terminal" msgstr "Web终端" -#: templates/_nav.html:68 templates/_nav_user.html:42 +#: templates/_nav.html:97 templates/_nav_user.html:42 msgid "File manager" msgstr "文件管理" -#: templates/_nav.html:72 +#: templates/_nav.html:101 msgid "Terminal" msgstr "终端管理" -#: templates/_nav.html:78 +#: templates/_nav.html:112 msgid "Job Center" msgstr "作业中心" -#: templates/_nav.html:82 templates/_nav.html:94 templates/_nav_audits.html:29 +#: templates/_nav.html:116 templates/_nav.html:133 msgid "Batch command" msgstr "批量命令" -#: templates/_nav.html:100 +#: templates/_nav.html:143 msgid "XPack" msgstr "" -#: templates/_nav.html:108 xpack/plugins/cloud/views.py:28 +#: templates/_nav.html:151 xpack/plugins/cloud/views.py:28 msgid "Account list" msgstr "账户列表" -#: templates/_nav.html:109 +#: templates/_nav.html:152 msgid "Sync instance" msgstr "同步实例" @@ -4476,11 +4495,11 @@ msgid "" "You should use your ssh client tools connect terminal: {}

{}" msgstr "你可以使用ssh客户端工具连接终端" -#: users/api/user.py:187 +#: users/api/user.py:173 msgid "Could not reset self otp, use profile reset instead" msgstr "不能再该页面重置MFA, 请去个人信息页面重置" -#: users/forms.py:33 users/models/user.py:340 +#: users/forms.py:33 users/models/user.py:383 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:87 #: users/templates/users/user_list.html:37 @@ -4501,7 +4520,7 @@ msgstr "" msgid "Paste user id_rsa.pub here." msgstr "复制用户公钥到这里" -#: users/forms.py:52 users/templates/users/user_detail.html:225 +#: users/forms.py:52 users/templates/users/user_detail.html:226 msgid "Join user groups" msgstr "添加到用户组" @@ -4509,11 +4528,11 @@ msgstr "添加到用户组" msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" -#: users/forms.py:91 users/forms.py:252 users/serializers/v1.py:94 +#: users/forms.py:91 users/forms.py:252 users/serializers/v1.py:116 msgid "Not a valid ssh public key" msgstr "ssh密钥不合法" -#: users/forms.py:104 users/views/login.py:114 users/views/user.py:280 +#: users/forms.py:104 users/views/login.py:114 users/views/user.py:287 msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" @@ -4525,7 +4544,7 @@ msgstr "生成重置密码链接,通过邮件发送给用户" msgid "Set password" msgstr "设置密码" -#: users/forms.py:133 xpack/plugins/change_auth_plan/models.py:86 +#: users/forms.py:133 xpack/plugins/change_auth_plan/models.py:85 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:51 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:69 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:57 @@ -4599,7 +4618,7 @@ msgstr "选择用户" msgid "User auth from {}, go there change password" msgstr "用户认证源来自 {}, 请去相应系统修改密码" -#: users/models/user.py:126 users/models/user.py:465 +#: users/models/user.py:126 users/models/user.py:508 msgid "Administrator" msgstr "管理员" @@ -4611,68 +4630,80 @@ msgstr "应用程序" msgid "Auditor" msgstr "审计员" -#: users/models/user.py:289 users/templates/users/user_profile.html:90 +#: users/models/user.py:139 +msgid "Org admin" +msgstr "组织管理员" + +#: users/models/user.py:141 +msgid "Org auditor" +msgstr "组织审计员" + +#: users/models/user.py:332 users/templates/users/user_profile.html:90 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:343 +#: users/models/user.py:386 msgid "Avatar" msgstr "头像" -#: users/models/user.py:346 users/templates/users/user_detail.html:82 +#: users/models/user.py:389 users/templates/users/user_detail.html:82 msgid "Wechat" msgstr "微信" -#: users/models/user.py:375 users/templates/users/user_detail.html:103 +#: users/models/user.py:418 users/templates/users/user_detail.html:103 #: users/templates/users/user_list.html:39 #: users/templates/users/user_profile.html:102 msgid "Source" msgstr "用户来源" -#: users/models/user.py:379 +#: users/models/user.py:422 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:468 +#: users/models/user.py:511 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/serializers/v1.py:39 +#: users/serializers/v1.py:45 msgid "Groups name" msgstr "用户组名" -#: users/serializers/v1.py:40 +#: users/serializers/v1.py:46 msgid "Source name" msgstr "用户来源名" -#: users/serializers/v1.py:41 +#: users/serializers/v1.py:47 msgid "Is first login" msgstr "首次登录" -#: users/serializers/v1.py:42 +#: users/serializers/v1.py:48 msgid "Role name" msgstr "角色名" -#: users/serializers/v1.py:43 +#: users/serializers/v1.py:49 msgid "Is valid" msgstr "账户是否有效" -#: users/serializers/v1.py:44 +#: users/serializers/v1.py:50 msgid "Is expired" msgstr " 是否过期" -#: users/serializers/v1.py:45 +#: users/serializers/v1.py:51 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/v1.py:54 +#: users/serializers/v1.py:72 msgid "Role limit to {}" msgstr "角色只能为 {}" -#: users/serializers/v1.py:66 +#: users/serializers/v1.py:84 msgid "Password does not match security rules" msgstr "密码不满足安全规则" +#: users/serializers/v1.py:147 +msgid "Auditors cannot be join in the group" +msgstr "" + #: users/serializers_v2/user.py:36 msgid "name not unique" msgstr "名称重复" @@ -4723,7 +4754,7 @@ msgid "Import users" msgstr "导入用户" #: users/templates/users/_user_update_modal.html:4 -#: users/templates/users/user_update.html:4 users/views/user.py:122 +#: users/templates/users/user_update.html:4 users/views/user.py:123 msgid "Update user" msgstr "更新用户" @@ -4801,7 +4832,7 @@ msgid "Always young, always with tears in my eyes. Stay foolish Stay hungry" msgstr "永远年轻,永远热泪盈眶 stay foolish stay hungry" #: users/templates/users/reset_password.html:46 -#: users/templates/users/user_detail.html:377 users/utils.py:84 +#: users/templates/users/user_detail.html:379 users/utils.py:84 msgid "Reset password" msgstr "重置密码" @@ -4866,12 +4897,12 @@ msgid "Very strong" msgstr "很强" #: users/templates/users/user_create.html:4 -#: users/templates/users/user_list.html:28 users/views/user.py:78 +#: users/templates/users/user_list.html:28 users/views/user.py:79 msgid "Create user" msgstr "创建用户" #: users/templates/users/user_detail.html:19 -#: users/templates/users/user_granted_asset.html:18 users/views/user.py:189 +#: users/templates/users/user_granted_asset.html:18 users/views/user.py:190 msgid "User detail" msgstr "用户详情" @@ -4918,7 +4949,7 @@ msgid "Send reset ssh key mail" msgstr "发送重置密钥邮件" #: users/templates/users/user_detail.html:203 -#: users/templates/users/user_detail.html:465 +#: users/templates/users/user_detail.html:467 msgid "Unblock user" msgstr "解除登录限制" @@ -4926,52 +4957,52 @@ msgstr "解除登录限制" msgid "Unblock" msgstr "解除" -#: users/templates/users/user_detail.html:320 +#: users/templates/users/user_detail.html:322 msgid "Goto profile page enable MFA" msgstr "请去个人信息页面启用自己的MFA" -#: users/templates/users/user_detail.html:376 +#: users/templates/users/user_detail.html:378 msgid "An e-mail has been sent to the user`s mailbox." msgstr "已发送邮件到用户邮箱" -#: users/templates/users/user_detail.html:387 +#: users/templates/users/user_detail.html:389 msgid "This will reset the user password and send a reset mail" msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱" -#: users/templates/users/user_detail.html:402 +#: users/templates/users/user_detail.html:404 msgid "" "The reset-ssh-public-key E-mail has been sent successfully. Please inform " "the user to update his new ssh public key." msgstr "重设密钥邮件将会发送到用户邮箱" -#: users/templates/users/user_detail.html:403 +#: users/templates/users/user_detail.html:405 msgid "Reset SSH public key" msgstr "重置SSH密钥" -#: users/templates/users/user_detail.html:413 +#: users/templates/users/user_detail.html:415 msgid "This will reset the user public key and send a reset mail" msgstr "将会失效用户当前密钥,并发送重置邮件到用户邮箱" -#: users/templates/users/user_detail.html:431 +#: users/templates/users/user_detail.html:433 msgid "Successfully updated the SSH public key." msgstr "更新ssh密钥成功" -#: users/templates/users/user_detail.html:432 -#: users/templates/users/user_detail.html:436 +#: users/templates/users/user_detail.html:434 +#: users/templates/users/user_detail.html:438 msgid "User SSH public key update" msgstr "ssh密钥" -#: users/templates/users/user_detail.html:481 +#: users/templates/users/user_detail.html:483 msgid "After unlocking the user, the user can log in normally." msgstr "解除用户登录限制后,此用户即可正常登录" -#: users/templates/users/user_detail.html:495 +#: users/templates/users/user_detail.html:497 msgid "Reset user MFA success" msgstr "重置用户MFA成功" #: users/templates/users/user_group_detail.html:22 #: users/templates/users/user_group_granted_asset.html:18 -#: users/views/group.py:82 +#: users/views/group.py:83 msgid "User group detail" msgstr "用户组详情" @@ -5005,24 +5036,24 @@ msgstr "用户组删除失败" msgid "This will delete the selected users !!!" msgstr "删除选中用户 !!!" -#: users/templates/users/user_list.html:267 +#: users/templates/users/user_list.html:262 msgid "User Deleted." msgstr "已被删除" -#: users/templates/users/user_list.html:268 -#: users/templates/users/user_list.html:272 +#: users/templates/users/user_list.html:263 +#: users/templates/users/user_list.html:267 msgid "User Delete" msgstr "删除" -#: users/templates/users/user_list.html:271 +#: users/templates/users/user_list.html:266 msgid "User Deleting failed." msgstr "用户删除失败" -#: users/templates/users/user_list.html:324 +#: users/templates/users/user_list.html:327 msgid "User is expired" msgstr "用户已失效" -#: users/templates/users/user_list.html:327 +#: users/templates/users/user_list.html:330 msgid "User is inactive" msgstr "用户已禁用" @@ -5327,7 +5358,7 @@ msgstr "" msgid "User group list" msgstr "用户组列表" -#: users/views/group.py:99 +#: users/views/group.py:100 msgid "User group granted asset" msgstr "用户组授权资产" @@ -5364,47 +5395,47 @@ msgstr "密码不一致" msgid "First login" msgstr "首次登录" -#: users/views/user.py:140 +#: users/views/user.py:141 msgid "Bulk update user success" msgstr "批量更新用户成功" -#: users/views/user.py:168 +#: users/views/user.py:169 msgid "Bulk update user" msgstr "批量更新用户" -#: users/views/user.py:211 +#: users/views/user.py:218 msgid "User granted assets" msgstr "用户授权资产" -#: users/views/user.py:244 +#: users/views/user.py:251 msgid "Profile setting" msgstr "个人信息设置" -#: users/views/user.py:264 +#: users/views/user.py:271 msgid "Password update" msgstr "密码更新" -#: users/views/user.py:299 +#: users/views/user.py:306 msgid "Public key update" msgstr "密钥更新" -#: users/views/user.py:341 +#: users/views/user.py:348 msgid "Password invalid" msgstr "用户名或密码无效" -#: users/views/user.py:441 +#: users/views/user.py:448 msgid "MFA enable success" msgstr "MFA 绑定成功" -#: users/views/user.py:442 +#: users/views/user.py:449 msgid "MFA enable success, return login page" msgstr "MFA 绑定成功,返回到登录页面" -#: users/views/user.py:444 +#: users/views/user.py:451 msgid "MFA disable success" msgstr "MFA 解绑成功" -#: users/views/user.py:445 +#: users/views/user.py:452 msgid "MFA disable success, return login page" msgstr "MFA 解绑成功,返回登录页面" @@ -5413,7 +5444,7 @@ msgid "Password length" msgstr "密码长度" #: xpack/plugins/change_auth_plan/forms.py:51 -#: xpack/plugins/change_auth_plan/models.py:213 +#: xpack/plugins/change_auth_plan/models.py:212 msgid "* For security, do not change {} user's password" msgstr "* 为了安全,禁止更改 {} 用户的密码" @@ -5461,8 +5492,8 @@ msgstr "" "具)
注意: 如果同时设置了定期执行和周期执行,优先使用定期执行" #: xpack/plugins/change_auth_plan/meta.py:9 -#: xpack/plugins/change_auth_plan/models.py:114 -#: xpack/plugins/change_auth_plan/models.py:257 +#: xpack/plugins/change_auth_plan/models.py:113 +#: xpack/plugins/change_auth_plan/models.py:256 #: xpack/plugins/change_auth_plan/views.py:33 #: xpack/plugins/change_auth_plan/views.py:50 #: xpack/plugins/change_auth_plan/views.py:72 @@ -5473,61 +5504,61 @@ msgstr "" msgid "Change auth plan" msgstr "改密计划" -#: xpack/plugins/change_auth_plan/models.py:55 +#: xpack/plugins/change_auth_plan/models.py:54 msgid "Custom password" msgstr "自定义密码" -#: xpack/plugins/change_auth_plan/models.py:56 +#: xpack/plugins/change_auth_plan/models.py:55 msgid "All assets use the same random password" msgstr "所有资产使用相同的随机密码" -#: xpack/plugins/change_auth_plan/models.py:57 +#: xpack/plugins/change_auth_plan/models.py:56 msgid "All assets use different random password" msgstr "所有资产使用不同的随机密码" -#: xpack/plugins/change_auth_plan/models.py:76 -#: xpack/plugins/change_auth_plan/models.py:145 +#: xpack/plugins/change_auth_plan/models.py:75 +#: xpack/plugins/change_auth_plan/models.py:144 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:100 #: xpack/plugins/cloud/models.py:165 xpack/plugins/cloud/models.py:219 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:91 msgid "Cycle perform" msgstr "周期执行" -#: xpack/plugins/change_auth_plan/models.py:81 -#: xpack/plugins/change_auth_plan/models.py:143 +#: xpack/plugins/change_auth_plan/models.py:80 +#: xpack/plugins/change_auth_plan/models.py:142 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:92 #: xpack/plugins/cloud/models.py:170 xpack/plugins/cloud/models.py:217 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:83 msgid "Regularly perform" msgstr "定期执行" -#: xpack/plugins/change_auth_plan/models.py:90 +#: xpack/plugins/change_auth_plan/models.py:89 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:74 msgid "Password rules" msgstr "密码规则" -#: xpack/plugins/change_auth_plan/models.py:217 +#: xpack/plugins/change_auth_plan/models.py:216 msgid "Assets is empty, please add the asset" msgstr "资产为空,请添加资产" -#: xpack/plugins/change_auth_plan/models.py:261 +#: xpack/plugins/change_auth_plan/models.py:260 msgid "Change auth plan snapshot" msgstr "改密计划快照" -#: xpack/plugins/change_auth_plan/models.py:276 -#: xpack/plugins/change_auth_plan/models.py:427 +#: xpack/plugins/change_auth_plan/models.py:275 +#: xpack/plugins/change_auth_plan/models.py:426 msgid "Change auth plan execution" msgstr "改密计划执行" -#: xpack/plugins/change_auth_plan/models.py:436 +#: xpack/plugins/change_auth_plan/models.py:435 msgid "Change auth plan execution subtask" msgstr "改密计划执行子任务" -#: xpack/plugins/change_auth_plan/models.py:454 +#: xpack/plugins/change_auth_plan/models.py:453 msgid "Authentication failed" msgstr "认证失败" -#: xpack/plugins/change_auth_plan/models.py:456 +#: xpack/plugins/change_auth_plan/models.py:455 msgid "Connection timeout" msgstr "连接超时" @@ -5841,6 +5872,28 @@ msgstr "创建同步实例任务" msgid "Update sync Instance task" msgstr "更新同步实例任务" +#: xpack/plugins/gathered_user/meta.py:11 +#: xpack/plugins/gathered_user/views.py:22 +msgid "Gathered user" +msgstr "收集用户" + +#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:75 +msgid "Present" +msgstr "存在" + +#: xpack/plugins/gathered_user/views.py:23 +msgid "Gathered user list" +msgstr "收集用户列表" + +#: xpack/plugins/gathered_user/views.py:37 xpack/plugins/vault/meta.py:11 +#: xpack/plugins/vault/views.py:23 xpack/plugins/vault/views.py:38 +msgid "Vault" +msgstr "密码匣子" + +#: xpack/plugins/gathered_user/views.py:38 xpack/plugins/vault/views.py:39 +msgid "vault create" +msgstr "创建" + #: xpack/plugins/interface/forms.py:17 xpack/plugins/interface/models.py:15 msgid "Title of login page" msgstr "登录页面标题" @@ -6051,11 +6104,6 @@ msgstr "创建组织" msgid "Update org" msgstr "更新组织" -#: xpack/plugins/vault/meta.py:11 xpack/plugins/vault/views.py:23 -#: xpack/plugins/vault/views.py:38 -msgid "Vault" -msgstr "密码匣子" - #: xpack/plugins/vault/templates/vault/_xpack_import_modal.html:4 msgid "Import vault" msgstr "导入密码" @@ -6064,10 +6112,6 @@ msgstr "导入密码" msgid "vault list" msgstr "密码匣子" -#: xpack/plugins/vault/views.py:39 -msgid "vault create" -msgstr "创建" - #~ msgid "* For security, do not change {}'s password" #~ msgstr "* 为了安全,禁止更改 {} 的密码" diff --git a/apps/ops/celery/utils.py b/apps/ops/celery/utils.py index ea975af78..f9a80e794 100644 --- a/apps/ops/celery/utils.py +++ b/apps/ops/celery/utils.py @@ -6,7 +6,9 @@ import os from django.conf import settings from django.utils.timezone import get_current_timezone from django.db.utils import ProgrammingError, OperationalError -from django_celery_beat.models import PeriodicTask, IntervalSchedule, CrontabSchedule +from django_celery_beat.models import ( + PeriodicTask, IntervalSchedule, CrontabSchedule, PeriodicTasks +) def create_or_update_celery_periodic_tasks(tasks): @@ -75,17 +77,20 @@ def create_or_update_celery_periodic_tasks(tasks): task = PeriodicTask.objects.update_or_create( defaults=defaults, name=name, ) + PeriodicTasks.update_changed() return task def disable_celery_periodic_task(task_name): from django_celery_beat.models import PeriodicTask PeriodicTask.objects.filter(name=task_name).update(enabled=False) + PeriodicTasks.update_changed() def delete_celery_periodic_task(task_name): from django_celery_beat.models import PeriodicTask PeriodicTask.objects.filter(name=task_name).delete() + PeriodicTasks.update_changed() def get_celery_task_log_path(task_id): diff --git a/apps/ops/inventory.py b/apps/ops/inventory.py index b6c302e41..694957b14 100644 --- a/apps/ops/inventory.py +++ b/apps/ops/inventory.py @@ -51,7 +51,7 @@ class JMSBaseInventory(BaseInventory): def make_proxy_command(asset): gateway = asset.domain.random_gateway() proxy_command_list = [ - "ssh", "-p", str(gateway.port), + "ssh", "-o", "Port={}".format(gateway.port), "-o", "StrictHostKeyChecking=no", "{}@{}".format(gateway.username, gateway.ip), "-W", "%h:%p", "-q", diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py index d2f55f623..f5e190780 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -2,6 +2,7 @@ import os import subprocess import datetime +import time from django.conf import settings from celery import shared_task, subtask @@ -122,3 +123,22 @@ def hello123(): def hello_callback(result): print(result) print("Hello callback") + + +@shared_task +def add(a, b): + time.sleep(5) + return max(b) + + +@shared_task +def add_m(x): + from celery import chain + a = range(x) + b = [a[i:i + 10] for i in range(0, len(a), 10)] + s = list() + s.append(add.s(b[0], b[1])) + for i in b[1:]: + s.append(add.s(i)) + res = chain(*tuple(s))() + return res diff --git a/apps/orgs/mixins/api.py b/apps/orgs/mixins/api.py index d36360f51..fb72d78e5 100644 --- a/apps/orgs/mixins/api.py +++ b/apps/orgs/mixins/api.py @@ -3,7 +3,7 @@ from django.shortcuts import get_object_or_404 from rest_framework.viewsets import ModelViewSet from rest_framework_bulk import BulkModelViewSet -from common.mixins import IDInCacheFilterMixin +from common.mixins import CommonApiMixin from ..utils import set_to_root_org from ..models import Organization @@ -20,14 +20,16 @@ class RootOrgViewMixin: return super().dispatch(request, *args, **kwargs) -class OrgModelViewSet(IDInCacheFilterMixin, ModelViewSet): +class OrgModelViewSet(CommonApiMixin, ModelViewSet): def get_queryset(self): return super().get_queryset().all() -class OrgBulkModelViewSet(IDInCacheFilterMixin, BulkModelViewSet): +class OrgBulkModelViewSet(CommonApiMixin, BulkModelViewSet): def get_queryset(self): queryset = super().get_queryset().all() + if hasattr(self, 'swagger_fake_view'): + return queryset[:1] if hasattr(self, 'action') and self.action == 'list' and \ hasattr(self, 'serializer_class') and \ hasattr(self.serializer_class, 'setup_eager_loading'): diff --git a/apps/orgs/mixins/serializers.py b/apps/orgs/mixins/serializers.py index 3c039d56f..a34f8d9b1 100644 --- a/apps/orgs/mixins/serializers.py +++ b/apps/orgs/mixins/serializers.py @@ -11,7 +11,8 @@ from ..utils import get_current_org_id_for_serializer __all__ = [ "OrgResourceSerializerMixin", "BulkOrgResourceSerializerMixin", - "BulkOrgResourceModelSerializer", "OrgMembershipSerializerMixin" + "BulkOrgResourceModelSerializer", "OrgMembershipSerializerMixin", + "OrgResourceModelSerializerMixin", ] @@ -42,6 +43,10 @@ class OrgResourceSerializerMixin(serializers.Serializer): return fields +class OrgResourceModelSerializerMixin(OrgResourceSerializerMixin, serializers.ModelSerializer): + pass + + class BulkOrgResourceSerializerMixin(BulkSerializerMixin, OrgResourceSerializerMixin): pass diff --git a/apps/orgs/utils.py b/apps/orgs/utils.py index 90dde34d4..0d040d1d2 100644 --- a/apps/orgs/utils.py +++ b/apps/orgs/utils.py @@ -18,6 +18,8 @@ def get_org_from_request(request): def set_current_org(org): + if isinstance(org, str): + org = Organization.get_instance(org) setattr(thread_local, 'current_org_id', org.id) diff --git a/apps/perms/api/mixin.py b/apps/perms/api/mixin.py index fba8155ae..85db529a0 100644 --- a/apps/perms/api/mixin.py +++ b/apps/perms/api/mixin.py @@ -1,15 +1,11 @@ # -*- coding: utf-8 -*- # -import uuid -from django.db.models import Q from rest_framework.generics import get_object_or_404 -from assets.utils import LabelFilterMixin from common.permissions import IsValidUser, IsOrgAdminOrAppUser from common.utils import get_logger from orgs.utils import set_to_root_org -from ..hands import User, UserGroup, Asset, SystemUser -from .. import serializers +from ..hands import User, UserGroup logger = get_logger(__name__) @@ -54,101 +50,3 @@ class UserGroupPermissionMixin: return user_group -class GrantAssetsMixin(LabelFilterMixin): - serializer_class = serializers.AssetGrantedSerializer - - def get_serializer_queryset(self, queryset): - assets_ids = [] - system_users_ids = set() - for asset in queryset: - assets_ids.append(asset["id"]) - system_users_ids.update(set(asset["system_users"])) - assets = Asset.objects.filter(id__in=assets_ids).only( - *self.serializer_class.Meta.only_fields - ) - assets_map = {asset.id: asset for asset in assets} - system_users = SystemUser.objects.filter(id__in=system_users_ids).only( - *self.serializer_class.system_users_only_fields - ) - system_users_map = {s.id: s for s in system_users} - data = [] - for item in queryset: - i = item["id"] - asset = assets_map.get(i) - if not asset: - continue - - _system_users = item["system_users"] - system_users_granted = [] - for sid, action in _system_users.items(): - system_user = system_users_map.get(sid) - if not system_user: - continue - if not asset.has_protocol(system_user.protocol): - continue - system_user.actions = action - system_users_granted.append(system_user) - asset.system_users_granted = system_users_granted - data.append(asset) - return data - - def get_serializer(self, assets_items=None, many=True): - if assets_items is None: - assets_items = [] - assets_items = self.get_serializer_queryset(assets_items) - return super().get_serializer(assets_items, many=many) - - def filter_queryset_by_id(self, assets_items): - i = self.request.query_params.get("id") - if not i: - return assets_items - try: - pk = uuid.UUID(i) - except ValueError: - return assets_items - assets_map = {asset['id']: asset for asset in assets_items} - if pk in assets_map: - return [assets_map.get(pk)] - else: - return [] - - def search_queryset(self, assets_items): - search = self.request.query_params.get("search") - if not search: - return assets_items - assets_map = {asset['id']: asset for asset in assets_items} - assets_ids = set(assets_map.keys()) - assets_ids_search = Asset.objects.filter(id__in=assets_ids).filter( - Q(hostname__icontains=search) | Q(ip__icontains=search) - ).values_list('id', flat=True) - return [assets_map.get(asset_id) for asset_id in assets_ids_search] - - def filter_queryset_by_label(self, assets_items): - labels_id = self.get_filter_labels_ids() - if not labels_id: - return assets_items - - assets_map = {asset['id']: asset for asset in assets_items} - assets_matched = Asset.objects.filter(id__in=assets_map.keys()) - for label_id in labels_id: - assets_matched = assets_matched.filter(labels=label_id) - assets_ids_matched = assets_matched.values_list('id', flat=True) - return [assets_map.get(asset_id) for asset_id in assets_ids_matched] - - def sort_queryset(self, assets_items): - order_by = self.request.query_params.get('order', 'hostname') - - if order_by not in ['hostname', '-hostname', 'ip', '-ip']: - order_by = 'hostname' - assets_map = {asset['id']: asset for asset in assets_items} - assets_ids_search = Asset.objects.filter(id__in=assets_map.keys())\ - .order_by(order_by)\ - .values_list('id', flat=True) - return [assets_map.get(asset_id) for asset_id in assets_ids_search] - - def filter_queryset(self, assets_items): - assets_items = self.filter_queryset_by_id(assets_items) - assets_items = self.search_queryset(assets_items) - assets_items = self.filter_queryset_by_label(assets_items) - assets_items = self.sort_queryset(assets_items) - return assets_items diff --git a/apps/perms/api/user_permission/mixin.py b/apps/perms/api/user_permission/mixin.py index 3fb1e99ad..a5c6ffca0 100644 --- a/apps/perms/api/user_permission/mixin.py +++ b/apps/perms/api/user_permission/mixin.py @@ -2,12 +2,12 @@ # from ..mixin import UserPermissionMixin from ...utils import AssetPermissionUtilV2, ParserNode -from ...hands import Node +from ...hands import Node, Asset from common.tree import TreeNodeSerializer class UserAssetPermissionMixin(UserPermissionMixin): - util = None + util = AssetPermissionUtilV2(None) tree = None def initial(self, *args, **kwargs): @@ -41,7 +41,9 @@ class UserNodeTreeMixin: queryset = self.parse_nodes_to_queryset(queryset) return queryset - def get_serializer(self, queryset, many=True, **kwargs): + def get_serializer(self, queryset=None, many=True, **kwargs): + if queryset is None: + queryset = Node.objects.none() queryset = self.get_serializer_queryset(queryset) queryset.sort() return super().get_serializer(queryset, many=many, **kwargs) @@ -64,7 +66,9 @@ class UserAssetTreeMixin: _queryset = self.parse_assets_to_queryset(queryset, None) return _queryset - def get_serializer(self, queryset, many=True, **kwargs): + def get_serializer(self, queryset=None, many=True, **kwargs): + if queryset is None: + queryset = Asset.objects.none() queryset = self.get_serializer_queryset(queryset) queryset.sort() return super().get_serializer(queryset, many=many, **kwargs) diff --git a/apps/perms/utils/asset_permission.py b/apps/perms/utils/asset_permission.py index bb0ede53d..ed3616933 100644 --- a/apps/perms/utils/asset_permission.py +++ b/apps/perms/utils/asset_permission.py @@ -126,10 +126,10 @@ class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): 'comment', 'is_active', 'os', 'org_id' ) - def __init__(self, obj, cache_policy='0'): + def __init__(self, obj=None, cache_policy='0'): self.object = obj self.cache_policy = cache_policy - self.obj_id = str(obj.id) + self.obj_id = str(obj.id) if obj else None self._permissions = None self._permissions_id = None # 标记_permission的唯一值 self._filter_id = 'None' # 当通过filter更改 permission是标记 @@ -147,6 +147,8 @@ class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): def permissions(self): if self._permissions: return self._permissions + if self.object is None: + return AssetPermission.objects.none() object_cls = self.object.__class__.__name__ func = self.get_permissions_map[object_cls] permissions = func(self.object) diff --git a/apps/settings/api.py b/apps/settings/api.py index eed1aa8df..af5742921 100644 --- a/apps/settings/api.py +++ b/apps/settings/api.py @@ -97,6 +97,8 @@ class LDAPUserListApi(generics.ListAPIView): serializer_class = LDAPUserSerializer def get_queryset(self): + if hasattr(self, 'swagger_fake_view'): + return [] util = LDAPUtil() try: users = util.search_user_items() diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js index 127529a73..03c804da8 100644 --- a/apps/static/js/jumpserver.js +++ b/apps/static/js/jumpserver.js @@ -153,6 +153,7 @@ function activeNav() { } else { $("#" + app).addClass('active'); $('#' + app + ' #' + resource).addClass('active'); + $('#' + app + ' #' + resource.replace('-', '_')).addClass('active'); } } diff --git a/apps/templates/_nav.html b/apps/templates/_nav.html index 67f6140d8..8b76e9b5d 100644 --- a/apps/templates/_nav.html +++ b/apps/templates/_nav.html @@ -171,7 +171,5 @@ \ No newline at end of file diff --git a/apps/terminal/serializers_v2/terminal.py b/apps/terminal/serializers_v2/terminal.py index c7ebe682c..021519564 100644 --- a/apps/terminal/serializers_v2/terminal.py +++ b/apps/terminal/serializers_v2/terminal.py @@ -58,5 +58,5 @@ class TerminalSerializer(serializers.ModelSerializer): class TerminalRegistrationSerializer(serializers.Serializer): name = serializers.CharField(max_length=128) - comment = serializers.CharField(max_length=128) + comment = serializers.CharField(max_length=128, ) service_account = ServiceAccountSerializer(read_only=True) diff --git a/apps/users/api/user.py b/apps/users/api/user.py index 1864f72f4..b8a215c47 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -14,7 +14,7 @@ from common.permissions import ( IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser, CanUpdateDeleteUser, ) -from common.mixins import IDInCacheFilterMixin +from common.mixins import CommonApiMixin from common.utils import get_logger from orgs.utils import current_org from .. import serializers @@ -30,7 +30,7 @@ __all__ = [ ] -class UserViewSet(IDInCacheFilterMixin, BulkModelViewSet): +class UserViewSet(CommonApiMixin, BulkModelViewSet): filter_fields = ('username', 'email', 'name', 'id') search_fields = filter_fields queryset = User.objects.exclude(role=User.ROLE_APP) From b7021b5ecb74238f4cc127bbed61a92562e28d1b Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 19 Sep 2019 14:48:25 +0800 Subject: [PATCH 4/7] =?UTF-8?q?[Update]=20=E6=94=AF=E6=8C=81=E6=94=B6?= =?UTF-8?q?=E9=9B=86=E8=B5=84=E4=BA=A7=E4=B8=8A=E7=9A=84=E7=94=A8=E6=88=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/asset.py | 1 + apps/assets/api/gathered_user.py | 2 + apps/assets/filters.py | 60 +++-- apps/assets/migrations/0041_gathereduser.py | 4 +- apps/assets/models/gathered_user.py | 4 +- apps/assets/serializers/gathered_user.py | 7 +- apps/assets/tasks/gather_asset_users.py | 3 +- apps/common/filters.py | 6 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 79735 -> 79863 bytes apps/locale/zh/LC_MESSAGES/django.po | 266 ++++++++++++-------- apps/ops/models/adhoc.py | 36 +-- apps/orgs/mixins/api.py | 13 +- apps/perms/api/user_permission/mixin.py | 2 +- 13 files changed, 236 insertions(+), 168 deletions(-) diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index 86e3d8fd2..371ee1fdc 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -36,6 +36,7 @@ class AssetViewSet(OrgBulkModelViewSet): serializer_class = serializers.AssetSerializer permission_classes = (IsOrgAdminOrAppUser,) extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend] + custom_filter_fields = ['admin_user_id'] def set_assets_node(self, assets): if not isinstance(assets, list): diff --git a/apps/assets/api/gathered_user.py b/apps/assets/api/gathered_user.py index 2f844d9e0..038d47208 100644 --- a/apps/assets/api/gathered_user.py +++ b/apps/assets/api/gathered_user.py @@ -6,6 +6,7 @@ from assets.models import GatheredUser from common.permissions import IsOrgAdmin from ..serializers import GatheredUserSerializer +from ..filters import AssetRelatedByNodeFilterBackend __all__ = ['GatheredUserViewSet'] @@ -15,6 +16,7 @@ class GatheredUserViewSet(OrgModelViewSet): queryset = GatheredUser.objects.all() serializer_class = GatheredUserSerializer permission_classes = [IsOrgAdmin] + extra_filter_backends = [AssetRelatedByNodeFilterBackend] filter_fields = ['asset', 'username', 'present'] search_fields = ['username', 'asset__ip', 'asset__hostname'] diff --git a/apps/assets/filters.py b/apps/assets/filters.py index 3636f76e2..0c6008172 100644 --- a/apps/assets/filters.py +++ b/apps/assets/filters.py @@ -12,63 +12,54 @@ from .models import Node, Label class AssetByNodeFilterBackend(filters.BaseFilterBackend): fields = ['node', 'all'] - # def filter_node(self, queryset): - # node_id = self.request.query_params.get("node_id") - # if not node_id: - # 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_org_root() and show_current_asset: - # queryset = queryset.filter( - # Q(nodes=node_id) | Q(nodes__isnull=True) - # ).distinct() - # # 当前节点是顶层节点,显示所有资产 - # elif node.is_org_root() and not show_current_asset: - # return queryset - # # 当前节点不是鼎城节点,只显示直接资产 - # elif not node.is_org_root() and show_current_asset: - # queryset = queryset.filter(nodes=node) - # else: - # children = node.get_all_children(with_self=True) - # queryset = queryset.filter(nodes__in=children).distinct() - # return queryset - def get_schema_fields(self, view): return [ coreapi.Field( name=field, location='query', required=False, - type='string', example='', description='' + type='string', example='', description='', schema=None, ) for field in self.fields ] - def filter_queryset(self, request, queryset, view): - node_id = dict_get_any(request.query_params, ['node', 'node_id']) - if not node_id: - return queryset + @staticmethod + def is_query_all(request): query_all_arg = request.query_params.get('all') show_current_asset_arg = request.query_params.get('show_current_asset') query_all = query_all_arg == '1' if show_current_asset_arg is not None: query_all = show_current_asset_arg != '1' + return query_all + + @staticmethod + def get_query_node(request): + node_id = dict_get_any(request.query_params, ['node', 'node_id']) + if not node_id: + return None, False if is_uuid(node_id): node = get_object_or_none(Node, id=node_id) else: node = get_object_or_none(Node, key=node_id) + return node, True - if not node: + @staticmethod + def perform_query(pattern, queryset): + return queryset.filter(nodes__key__regex=pattern) + + def filter_queryset(self, request, queryset, view): + node, has_query_arg = self.get_query_node(request) + if not has_query_arg: + return queryset + + if node is None: return queryset.none() - + query_all = self.is_query_all(request) if query_all: pattern = node.get_all_children_pattern(with_self=True) else: pattern = node.get_children_key_pattern(with_self=True) - return queryset.filter(nodes__key__regex=pattern) + return self.perform_query(pattern, queryset) class LabelFilterBackend(filters.BaseFilterBackend): @@ -117,3 +108,8 @@ class LabelFilterBackend(filters.BaseFilterBackend): return queryset +class AssetRelatedByNodeFilterBackend(AssetByNodeFilterBackend): + @staticmethod + def perform_query(pattern, queryset): + return queryset.filter(asset__nodes__key__regex=pattern).distinct() + diff --git a/apps/assets/migrations/0041_gathereduser.py b/apps/assets/migrations/0041_gathereduser.py index 9accee746..f1b464e74 100644 --- a/apps/assets/migrations/0041_gathereduser.py +++ b/apps/assets/migrations/0041_gathereduser.py @@ -18,10 +18,10 @@ class Migration(migrations.Migration): ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('username', models.CharField(blank=True, db_index=True, max_length=32, verbose_name='Username')), - ('present', models.BooleanField(default=True)), + ('present', models.BooleanField(default=True, verbose_name='Present')), ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Asset')), + ('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Asset', verbose_name='Asset')), ], options={'ordering': ['asset'], 'verbose_name': 'GatherUser'}, ), diff --git a/apps/assets/models/gathered_user.py b/apps/assets/models/gathered_user.py index 305bced37..282f9293a 100644 --- a/apps/assets/models/gathered_user.py +++ b/apps/assets/models/gathered_user.py @@ -11,10 +11,10 @@ __all__ = ['GatheredUser'] class GatheredUser(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) - asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE) + asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_("Asset")) username = models.CharField(max_length=32, blank=True, db_index=True, verbose_name=_('Username')) - present = models.BooleanField(default=True) + present = models.BooleanField(default=True, verbose_name=_("Present")) date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created")) date_updated = models.DateTimeField(auto_now=True, diff --git a/apps/assets/serializers/gathered_user.py b/apps/assets/serializers/gathered_user.py index 517b6a596..956c19c6b 100644 --- a/apps/assets/serializers/gathered_user.py +++ b/apps/assets/serializers/gathered_user.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- # -from ..models import GatheredUser +from django.utils.translation import ugettext_lazy as _ from orgs.mixins.serializers import OrgResourceModelSerializerMixin +from ..models import GatheredUser class GatheredUserSerializer(OrgResourceModelSerializerMixin): @@ -14,3 +15,7 @@ class GatheredUserSerializer(OrgResourceModelSerializerMixin): 'present', 'date_created', 'date_updated' ] read_only_fields = fields + labels = { + 'hostname': _("Hostname"), + 'ip': "IP" + } diff --git a/apps/assets/tasks/gather_asset_users.py b/apps/assets/tasks/gather_asset_users.py index 2662e5c76..efeecc25e 100644 --- a/apps/assets/tasks/gather_asset_users.py +++ b/apps/assets/tasks/gather_asset_users.py @@ -116,8 +116,9 @@ def gather_asset_users(assets, task_name=None): for k, value in hosts_category.items(): if not value['hosts']: continue + _task_name = '{}: {}'.format(task_name, k) task, created = update_or_create_ansible_task( - task_name=task_name, hosts=value['hosts'], tasks=value['tasks'], + task_name=_task_name, hosts=value['hosts'], tasks=value['tasks'], pattern='all', options=const.TASK_OPTIONS, run_as_admin=True, created_by=value['hosts'][0].org_id, ) diff --git a/apps/common/filters.py b/apps/common/filters.py index 0dc275ce0..7792ff088 100644 --- a/apps/common/filters.py +++ b/apps/common/filters.py @@ -70,7 +70,6 @@ class IDSpmFilter(filters.BaseFilterBackend): class CustomFilter(filters.BaseFilterBackend): - custom_filter_fields = [] # ["node", "asset"] def get_schema_fields(self, view): fields = [] @@ -79,7 +78,10 @@ class CustomFilter(filters.BaseFilterBackend): type='string', example='', description='' ) - for field in self.custom_filter_fields: + if not hasattr(view, 'custom_filter_fields'): + return [] + + for field in view.custom_filter_fields: if isinstance(field, str): defaults['name'] = field elif isinstance(field, dict): diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 428c67c3e287794a8808e8fd6b10333195c7cf44..d927e9014bb3e4bb0bacf96b2dce82843b5b6927 100644 GIT binary patch delta 23667 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\n" "Language-Team: Jumpserver team\n" @@ -77,8 +77,8 @@ msgstr "运行参数" #: applications/templates/applications/user_remote_app_list.html:18 #: assets/forms/domain.py:15 assets/forms/label.py:13 #: assets/models/asset.py:295 assets/models/authbook.py:24 -#: assets/serializers/admin_user.py:32 assets/serializers/asset_user.py:82 -#: assets/serializers/system_user.py:31 +#: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:32 +#: assets/serializers/asset_user.py:82 assets/serializers/system_user.py:31 #: assets/templates/assets/admin_user_list.html:46 #: assets/templates/assets/domain_detail.html:60 #: assets/templates/assets/domain_list.html:26 @@ -96,15 +96,13 @@ msgstr "运行参数" #: terminal/templates/terminal/session_list.html:28 #: terminal/templates/terminal/session_list.html:72 #: xpack/plugins/change_auth_plan/forms.py:121 -#: xpack/plugins/change_auth_plan/models.py:412 +#: xpack/plugins/change_auth_plan/models.py:409 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:46 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:54 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:13 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:14 -#: xpack/plugins/cloud/models.py:310 +#: xpack/plugins/cloud/models.py:307 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:63 -#: xpack/plugins/gathered_user/forms.py:13 -#: xpack/plugins/gathered_user/forms.py:15 #: xpack/plugins/orgs/templates/orgs/org_list.html:16 #: xpack/plugins/vault/forms.py:13 xpack/plugins/vault/forms.py:15 msgid "Asset" @@ -162,6 +160,8 @@ msgstr "资产" #: xpack/plugins/cloud/templates/cloud/account_list.html:12 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:56 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:12 +#: xpack/plugins/gathered_user/models.py:28 +#: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:16 #: xpack/plugins/orgs/templates/orgs/org_detail.html:52 #: xpack/plugins/orgs/templates/orgs/org_list.html:12 msgid "Name" @@ -202,6 +202,7 @@ msgstr "参数" #: xpack/plugins/change_auth_plan/models.py:105 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:113 #: xpack/plugins/cloud/models.py:80 xpack/plugins/cloud/models.py:179 +#: xpack/plugins/gathered_user/models.py:46 msgid "Created by" msgstr "创建者" @@ -268,6 +269,7 @@ msgstr "创建日期" #: xpack/plugins/cloud/templates/cloud/account_list.html:15 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:105 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:18 +#: xpack/plugins/gathered_user/models.py:42 #: xpack/plugins/orgs/templates/orgs/org_detail.html:64 #: xpack/plugins/orgs/templates/orgs/org_list.html:22 msgid "Comment" @@ -316,7 +318,7 @@ msgstr "远程应用" #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:71 #: xpack/plugins/cloud/templates/cloud/account_create_update.html:33 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:53 -#: xpack/plugins/gathered_user/templates/gathered_user/vault_create.html:45 +#: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:44 #: xpack/plugins/interface/templates/interface/interface.html:72 #: xpack/plugins/vault/templates/vault/vault_create.html:45 msgid "Reset" @@ -355,7 +357,6 @@ msgstr "重置" #: users/templates/users/user_profile_update.html:68 #: users/templates/users/user_pubkey_update.html:81 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:72 -#: xpack/plugins/gathered_user/templates/gathered_user/vault_create.html:46 #: xpack/plugins/interface/templates/interface/interface.html:74 #: xpack/plugins/vault/templates/vault/vault_create.html:46 msgid "Submit" @@ -429,6 +430,7 @@ msgstr "详情" #: xpack/plugins/cloud/templates/cloud/account_list.html:40 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:29 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:57 +#: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:51 #: xpack/plugins/orgs/templates/orgs/org_detail.html:25 #: xpack/plugins/orgs/templates/orgs/org_list.html:88 msgid "Update" @@ -470,6 +472,7 @@ msgstr "更新" #: xpack/plugins/cloud/templates/cloud/account_list.html:42 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:33 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:58 +#: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:52 #: xpack/plugins/orgs/templates/orgs/org_detail.html:29 #: xpack/plugins/orgs/templates/orgs/org_list.html:90 msgid "Delete" @@ -528,6 +531,7 @@ msgstr "创建远程应用" #: xpack/plugins/cloud/templates/cloud/account_list.html:16 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:72 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:19 +#: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:20 #: xpack/plugins/orgs/templates/orgs/org_list.html:23 msgid "Action" msgstr "动作" @@ -560,11 +564,6 @@ msgstr "远程应用详情" msgid "My RemoteApp" msgstr "我的远程应用" -#: assets/api/asset.py:40 -#, python-format -msgid "%(hostname)s was %(action)s successfully" -msgstr "%(hostname)s %(action)s成功" - #: assets/api/node.py:58 msgid "You can't update the root node name" msgstr "不能修改根节点名称" @@ -590,6 +589,8 @@ msgstr "端口" #: assets/templates/assets/system_user_assets.html:83 #: perms/models/asset_permission.py:79 #: xpack/plugins/change_auth_plan/models.py:71 +#: xpack/plugins/gathered_user/models.py:31 +#: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:17 msgid "Nodes" msgstr "节点" @@ -619,7 +620,7 @@ msgid "Domain" msgstr "网域" #: assets/forms/asset.py:68 assets/forms/asset.py:101 assets/forms/asset.py:114 -#: assets/forms/asset.py:149 assets/models/node.py:393 +#: assets/forms/asset.py:149 assets/models/node.py:401 #: assets/templates/assets/asset_create.html:42 #: perms/forms/asset_permission.py:82 perms/forms/asset_permission.py:89 #: perms/templates/perms/asset_permission_list.html:53 @@ -694,7 +695,7 @@ msgstr "SSH网关,支持代理SSH,RDP和VNC" #: users/templates/users/user_profile.html:47 #: xpack/plugins/change_auth_plan/forms.py:106 #: xpack/plugins/change_auth_plan/models.py:62 -#: xpack/plugins/change_auth_plan/models.py:408 +#: xpack/plugins/change_auth_plan/models.py:405 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:65 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:53 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:12 @@ -722,7 +723,7 @@ msgstr "密码或密钥密码" #: users/templates/users/user_pubkey_update.html:41 #: users/templates/users/user_update.html:20 #: xpack/plugins/change_auth_plan/models.py:92 -#: xpack/plugins/change_auth_plan/models.py:263 +#: xpack/plugins/change_auth_plan/models.py:260 msgid "Password" msgstr "密码" @@ -785,6 +786,7 @@ msgid "IP" msgstr "IP" #: assets/models/asset.py:136 assets/serializers/asset_user.py:27 +#: assets/serializers/gathered_user.py:19 #: assets/templates/assets/_asset_list_modal.html:46 #: assets/templates/assets/_asset_user_auth_update_modal.html:9 #: assets/templates/assets/_asset_user_auth_view_modal.html:15 @@ -914,12 +916,12 @@ msgid "AuthBook" msgstr "" #: assets/models/base.py:31 xpack/plugins/change_auth_plan/models.py:96 -#: xpack/plugins/change_auth_plan/models.py:270 +#: xpack/plugins/change_auth_plan/models.py:267 msgid "SSH private key" msgstr "ssh密钥" #: assets/models/base.py:32 xpack/plugins/change_auth_plan/models.py:99 -#: xpack/plugins/change_auth_plan/models.py:266 +#: xpack/plugins/change_auth_plan/models.py:263 msgid "SSH public key" msgstr "ssh公钥" @@ -1054,6 +1056,11 @@ msgstr "命令过滤规则" msgid "Gateway" msgstr "网关" +#: assets/models/gathered_user.py:17 +#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:75 +msgid "Present" +msgstr "存在" + #: assets/models/gathered_user.py:32 msgid "GatherUser" msgstr "收集用户" @@ -1096,7 +1103,7 @@ msgstr "默认资产组" msgid "User" msgstr "用户" -#: assets/models/label.py:19 assets/models/node.py:384 +#: assets/models/label.py:19 assets/models/node.py:392 #: assets/templates/assets/label_list.html:15 settings/models.py:30 msgid "Value" msgstr "值" @@ -1105,19 +1112,19 @@ msgstr "值" msgid "Category" msgstr "分类" -#: assets/models/node.py:222 +#: assets/models/node.py:230 msgid "New node" msgstr "新节点" -#: assets/models/node.py:308 +#: assets/models/node.py:316 msgid "ungrouped" msgstr "未分组" -#: assets/models/node.py:310 +#: assets/models/node.py:318 msgid "empty" msgstr "空" -#: assets/models/node.py:383 +#: assets/models/node.py:391 msgid "Key" msgstr "键" @@ -1291,7 +1298,7 @@ msgstr "测试资产可连接性: {}" #: assets/tasks/asset_user_connectivity.py:27 #: assets/tasks/push_system_user.py:130 -#: xpack/plugins/change_auth_plan/models.py:521 +#: xpack/plugins/change_auth_plan/models.py:518 msgid "The asset {} system platform {} does not support run Ansible tasks" msgstr "资产 {} 系统平台 {} 不支持运行 Ansible 任务" @@ -1311,7 +1318,7 @@ msgstr "更新资产硬件信息" msgid "Update asset hardware info: {}" msgstr "更新资产硬件信息: {}" -#: assets/tasks/gather_asset_users.py:95 +#: assets/tasks/gather_asset_users.py:96 msgid "Gather assets users" msgstr "收集资产上的用户" @@ -1536,6 +1543,7 @@ msgstr "重命名成功" #: perms/templates/perms/remote_app_permission_create_update.html:39 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:43 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:27 +#: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:27 msgid "Basic" msgstr "基本" @@ -1558,6 +1566,7 @@ msgstr "自动生成密钥" #: terminal/templates/terminal/terminal_update.html:40 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:67 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:48 +#: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:39 msgid "Other" msgstr "其它" @@ -1609,12 +1618,13 @@ msgstr "替换资产的管理员" #: perms/templates/perms/asset_permission_asset.html:103 #: xpack/plugins/change_auth_plan/forms.py:116 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:109 +#: xpack/plugins/gathered_user/forms.py:36 msgid "Select nodes" msgstr "选择节点" #: assets/templates/assets/admin_user_detail.html:100 #: assets/templates/assets/asset_detail.html:204 -#: assets/templates/assets/asset_list.html:418 +#: assets/templates/assets/asset_list.html:422 #: assets/templates/assets/cmd_filter_detail.html:106 #: assets/templates/assets/system_user_assets.html:97 #: assets/templates/assets/system_user_detail.html:182 @@ -1631,6 +1641,7 @@ msgstr "选择节点" #: users/templates/users/user_list.html:256 #: xpack/plugins/cloud/templates/cloud/account_create_update.html:34 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:54 +#: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:45 #: xpack/plugins/interface/templates/interface/interface.html:103 #: xpack/plugins/orgs/templates/orgs/org_create_update.html:33 msgid "Confirm" @@ -1678,8 +1689,8 @@ msgstr "创建管理用户" #: assets/templates/assets/admin_user_list.html:162 #: assets/templates/assets/admin_user_list.html:193 -#: assets/templates/assets/asset_list.html:299 -#: assets/templates/assets/asset_list.html:336 +#: assets/templates/assets/asset_list.html:303 +#: assets/templates/assets/asset_list.html:340 #: assets/templates/assets/system_user_list.html:192 #: assets/templates/assets/system_user_list.html:223 #: users/templates/users/user_group_list.html:164 @@ -1797,31 +1808,31 @@ msgstr "禁用所选" msgid "Active selected" msgstr "激活所选" -#: assets/templates/assets/asset_list.html:189 +#: assets/templates/assets/asset_list.html:193 msgid "Add assets to node" msgstr "添加资产到节点" -#: assets/templates/assets/asset_list.html:190 +#: assets/templates/assets/asset_list.html:194 msgid "Move assets to node" msgstr "移动资产到节点" -#: assets/templates/assets/asset_list.html:192 +#: assets/templates/assets/asset_list.html:196 msgid "Refresh node hardware info" msgstr "更新节点资产硬件信息" -#: assets/templates/assets/asset_list.html:193 +#: assets/templates/assets/asset_list.html:197 msgid "Test node connective" msgstr "测试节点资产可连接性" -#: assets/templates/assets/asset_list.html:195 +#: assets/templates/assets/asset_list.html:199 msgid "Display only current node assets" msgstr "仅显示当前节点资产" -#: assets/templates/assets/asset_list.html:196 +#: assets/templates/assets/asset_list.html:200 msgid "Displays all child node assets" msgstr "显示所有子节点资产" -#: assets/templates/assets/asset_list.html:412 +#: assets/templates/assets/asset_list.html:416 #: assets/templates/assets/system_user_list.html:133 #: users/templates/users/user_detail.html:388 #: users/templates/users/user_detail.html:414 @@ -1832,11 +1843,11 @@ msgstr "显示所有子节点资产" msgid "Are you sure?" msgstr "你确认吗?" -#: assets/templates/assets/asset_list.html:413 +#: assets/templates/assets/asset_list.html:417 msgid "This will delete the selected assets !!!" msgstr "删除选择资产" -#: assets/templates/assets/asset_list.html:416 +#: assets/templates/assets/asset_list.html:420 #: assets/templates/assets/system_user_list.html:137 #: settings/templates/settings/terminal_setting.html:166 #: users/templates/users/user_detail.html:392 @@ -1850,16 +1861,16 @@ msgstr "删除选择资产" msgid "Cancel" msgstr "取消" -#: assets/templates/assets/asset_list.html:429 +#: assets/templates/assets/asset_list.html:433 msgid "Asset Deleted." msgstr "已被删除" -#: assets/templates/assets/asset_list.html:430 #: assets/templates/assets/asset_list.html:434 +#: assets/templates/assets/asset_list.html:438 msgid "Asset Delete" msgstr "删除" -#: assets/templates/assets/asset_list.html:433 +#: assets/templates/assets/asset_list.html:437 msgid "Asset Deleting failed." msgstr "删除失败" @@ -2179,7 +2190,6 @@ msgstr "成功" #: audits/models.py:32 #: authentication/templates/authentication/_access_key_modal.html:38 -#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:48 #: xpack/plugins/vault/templates/vault/vault.html:46 msgid "Create" msgstr "创建" @@ -2210,8 +2220,8 @@ msgstr "启用" msgid "-" msgstr "" -#: audits/models.py:77 xpack/plugins/cloud/models.py:267 -#: xpack/plugins/cloud/models.py:290 +#: audits/models.py:77 xpack/plugins/cloud/models.py:264 +#: xpack/plugins/cloud/models.py:287 msgid "Failed" msgstr "失败" @@ -2239,15 +2249,15 @@ msgid "MFA" msgstr "MFA" #: audits/models.py:86 audits/templates/audits/login_log_list.html:63 -#: xpack/plugins/change_auth_plan/models.py:416 +#: xpack/plugins/change_auth_plan/models.py:413 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:15 -#: xpack/plugins/cloud/models.py:281 +#: xpack/plugins/cloud/models.py:278 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:69 msgid "Reason" msgstr "原因" #: audits/models.py:87 audits/templates/audits/login_log_list.html:64 -#: xpack/plugins/cloud/models.py:278 xpack/plugins/cloud/models.py:313 +#: xpack/plugins/cloud/models.py:275 xpack/plugins/cloud/models.py:310 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:70 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:65 msgid "Status" @@ -2265,10 +2275,11 @@ msgstr "登录日期" #: perms/templates/perms/asset_permission_detail.html:86 #: perms/templates/perms/remote_app_permission_detail.html:78 #: terminal/models.py:165 terminal/templates/terminal/session_list.html:34 -#: xpack/plugins/change_auth_plan/models.py:249 -#: xpack/plugins/change_auth_plan/models.py:419 +#: xpack/plugins/change_auth_plan/models.py:246 +#: xpack/plugins/change_auth_plan/models.py:416 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:59 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:17 +#: xpack/plugins/gathered_user/models.py:143 msgid "Date start" msgstr "开始日期" @@ -2688,19 +2699,6 @@ msgstr "" msgid "Encrypt field using Secret Key" msgstr "" -#: common/mixins/api.py:64 -#, python-format -msgid "%(name)s was %(action)s successfully" -msgstr "%(name)s %(action)s成功" - -#: common/mixins/api.py:65 -msgid "create" -msgstr "创建" - -#: common/mixins/api.py:65 -msgid "update" -msgstr "更新" - #: common/mixins/models.py:31 msgid "is discard" msgstr "" @@ -2800,54 +2798,55 @@ msgstr "Become" msgid "Create by" msgstr "创建者" -#: ops/models/adhoc.py:224 +#: ops/models/adhoc.py:226 msgid "{} Start task: {}" msgstr "{} 任务开始: {}" -#: ops/models/adhoc.py:227 +#: ops/models/adhoc.py:238 msgid "{} Task finish" msgstr "{} 任务结束" -#: ops/models/adhoc.py:325 +#: ops/models/adhoc.py:329 msgid "Start time" msgstr "开始时间" -#: ops/models/adhoc.py:326 +#: ops/models/adhoc.py:330 msgid "End time" msgstr "完成时间" -#: ops/models/adhoc.py:327 ops/templates/ops/adhoc_history.html:57 +#: ops/models/adhoc.py:331 ops/templates/ops/adhoc_history.html:57 #: ops/templates/ops/task_history.html:63 ops/templates/ops/task_list.html:33 -#: xpack/plugins/change_auth_plan/models.py:252 -#: xpack/plugins/change_auth_plan/models.py:422 +#: xpack/plugins/change_auth_plan/models.py:249 +#: xpack/plugins/change_auth_plan/models.py:419 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:58 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:16 +#: xpack/plugins/gathered_user/models.py:146 msgid "Time" msgstr "时间" -#: ops/models/adhoc.py:328 ops/templates/ops/adhoc_detail.html:106 +#: ops/models/adhoc.py:332 ops/templates/ops/adhoc_detail.html:106 #: ops/templates/ops/adhoc_history.html:55 #: ops/templates/ops/adhoc_history_detail.html:69 #: ops/templates/ops/task_detail.html:84 ops/templates/ops/task_history.html:61 msgid "Is finished" msgstr "是否完成" -#: ops/models/adhoc.py:329 ops/templates/ops/adhoc_history.html:56 +#: ops/models/adhoc.py:333 ops/templates/ops/adhoc_history.html:56 #: ops/templates/ops/task_history.html:62 msgid "Is success" msgstr "是否成功" -#: ops/models/adhoc.py:330 +#: ops/models/adhoc.py:334 msgid "Adhoc raw result" msgstr "结果" -#: ops/models/adhoc.py:331 +#: ops/models/adhoc.py:335 msgid "Adhoc result summary" msgstr "汇总" #: ops/models/command.py:22 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:56 -#: xpack/plugins/cloud/models.py:276 +#: xpack/plugins/cloud/models.py:273 msgid "Result" msgstr "结果" @@ -2882,6 +2881,7 @@ msgstr "运行用户" #: ops/templates/ops/adhoc_detail.html:94 ops/templates/ops/task_list.html:28 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:18 +#: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:19 msgid "Run times" msgstr "执行次数" @@ -3049,6 +3049,7 @@ msgstr "版本" #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:54 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:141 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:55 +#: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:49 msgid "Run" msgstr "执行" @@ -3067,6 +3068,7 @@ msgid "Ops" msgstr "作业中心" #: ops/views/adhoc.py:46 templates/_nav.html:115 +#: xpack/plugins/gathered_user/views.py:35 msgid "Task list" msgstr "任务列表" @@ -3377,21 +3379,21 @@ msgstr "连接LDAP成功" msgid "Match {} s users" msgstr "匹配 {} 个用户" -#: settings/api.py:159 +#: settings/api.py:161 msgid "succeed: {} failed: {} total: {}" msgstr "成功:{} 失败:{} 总数:{}" -#: settings/api.py:181 settings/api.py:217 +#: settings/api.py:183 settings/api.py:219 msgid "" "Error: Account invalid (Please make sure the information such as Access key " "or Secret key is correct)" msgstr "错误:账户无效 (请确保 Access key 或 Secret key 等信息正确)" -#: settings/api.py:187 settings/api.py:223 +#: settings/api.py:189 settings/api.py:225 msgid "Create succeed" msgstr "创建成功" -#: settings/api.py:205 settings/api.py:243 +#: settings/api.py:207 settings/api.py:245 #: settings/templates/settings/terminal_setting.html:154 msgid "Delete succeed" msgstr "删除成功" @@ -3850,7 +3852,7 @@ msgid "Endpoint suffix" msgstr "端点后缀" #: settings/templates/settings/replay_storage_create.html:136 -#: xpack/plugins/cloud/models.py:307 +#: xpack/plugins/cloud/models.py:304 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:109 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:62 msgid "Region" @@ -5444,7 +5446,7 @@ msgid "Password length" msgstr "密码长度" #: xpack/plugins/change_auth_plan/forms.py:51 -#: xpack/plugins/change_auth_plan/models.py:212 +#: xpack/plugins/change_auth_plan/models.py:209 msgid "* For security, do not change {} user's password" msgstr "* 为了安全,禁止更改 {} 用户的密码" @@ -5465,6 +5467,9 @@ msgstr "* 请输入有效的 crontab 表达式" #: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:41 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:72 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:16 +#: xpack/plugins/gathered_user/forms.py:13 +#: xpack/plugins/gathered_user/forms.py:41 +#: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:32 msgid "Periodic perform" msgstr "定时执行" @@ -5477,10 +5482,12 @@ msgstr "" "用户不存在,则创建用户。" #: xpack/plugins/change_auth_plan/forms.py:131 xpack/plugins/cloud/forms.py:90 +#: xpack/plugins/gathered_user/forms.py:44 msgid "Tips: (Units: hour)" msgstr "提示:(单位: 时)" #: xpack/plugins/change_auth_plan/forms.py:132 xpack/plugins/cloud/forms.py:91 +#: xpack/plugins/gathered_user/forms.py:45 msgid "" "eg: Every Sunday 03:05 run <5 3 * * 0>
Tips: Using 5 digits linux " "crontab expressions ( Date: Thu, 19 Sep 2019 17:27:49 +0800 Subject: [PATCH 5/7] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9=E4=B8=80?= =?UTF-8?q?=E4=BA=9Bbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/models/node.py | 11 ++-- apps/assets/signals_handler.py | 11 ++-- apps/assets/tasks/__init__.py | 1 + apps/assets/tasks/common.py | 14 ++++++ .../templates/assets/user_asset_list.html | 2 +- apps/common/utils/common.py | 13 +++++ apps/perms/api/asset_permission.py | 4 ++ apps/perms/api/user_permission/common.py | 6 +-- apps/perms/api/user_permission/mixin.py | 18 ++++--- apps/perms/signals_handler.py | 12 ----- apps/perms/utils/asset_permission.py | 50 +++++++++---------- .../templates/users/_granted_assets.html | 30 +++++++---- .../templates/users/user_granted_asset.html | 4 +- .../users/user_group_granted_asset.html | 4 +- 14 files changed, 105 insertions(+), 75 deletions(-) create mode 100644 apps/assets/tasks/common.py diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index c294ff2ed..259405104 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext from django.core.cache import cache -from common.utils import get_logger +from common.utils import get_logger, timeit from orgs.mixins.models import OrgModelMixin, OrgManager from orgs.utils import set_current_org, get_current_org, tmp_to_org from orgs.models import Organization @@ -298,14 +298,15 @@ class NodeAssetsMixin: return self.get_all_assets().valid() @classmethod + @timeit def get_nodes_all_assets(cls, nodes_keys): from .asset import Asset nodes_keys = cls.clean_children_keys(nodes_keys) - pattern = set() + assets_ids = set() for key in nodes_keys: - pattern.add(r'^{0}$|^{0}:'.format(key)) - pattern = '|'.join(list(pattern)) - return Asset.objects.filter(nodes__key__regex=pattern) + node_assets_ids = cls.tree().all_assets(key) + assets_ids.update(set(node_assets_ids)) + return Asset.objects.filter(id__in=assets_ids) class SomeNodesMixin: diff --git a/apps/assets/signals_handler.py b/apps/assets/signals_handler.py index 2c980d22c..ea5f8f4ad 100644 --- a/apps/assets/signals_handler.py +++ b/apps/assets/signals_handler.py @@ -7,13 +7,14 @@ from django.db.models.signals import ( from django.db.models.aggregates import Count from django.dispatch import receiver -from common.utils import get_logger +from common.utils import get_logger, timeit from common.decorator import on_transaction_commit from .models import Asset, SystemUser, Node from .tasks import ( update_assets_hardware_info_util, test_asset_connectivity_util, - push_system_user_to_assets + push_system_user_to_assets, + add_nodes_assets_to_system_users ) @@ -99,7 +100,7 @@ def on_system_user_nodes_change(sender, instance=None, action=None, model=None, """ if action != "post_add": return - logger.info("System user `{}` nodes update signal recv".format(instance)) + logger.info("System user nodes update signal recv: {}".format(instance)) queryset = model.objects.filter(pk__in=pk_set) if model == Node: @@ -108,9 +109,7 @@ def on_system_user_nodes_change(sender, instance=None, action=None, model=None, else: nodes_keys = [instance.key] system_users = queryset - assets = Node.get_nodes_all_assets(nodes_keys).values_list('id', flat=True) - for system_user in system_users: - system_user.assets.add(*tuple(assets)) + add_nodes_assets_to_system_users.delay(nodes_keys, system_users) @receiver(m2m_changed, sender=Asset.nodes.through) diff --git a/apps/assets/tasks/__init__.py b/apps/assets/tasks/__init__.py index eb8c5a7b9..6f53c9fa2 100644 --- a/apps/assets/tasks/__init__.py +++ b/apps/assets/tasks/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # from .utils import * +from .common import * from .admin_user_connectivity import * from .asset_connectivity import * from .asset_user_connectivity import * diff --git a/apps/assets/tasks/common.py b/apps/assets/tasks/common.py new file mode 100644 index 000000000..2c352b64e --- /dev/null +++ b/apps/assets/tasks/common.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# + +from celery import shared_task + +__all__ = ['add_nodes_assets_to_system_users'] + + +@shared_task +def add_nodes_assets_to_system_users(nodes_keys, system_users): + from ..models import Node + assets = Node.get_nodes_all_assets(nodes_keys).values_list('id', flat=True) + for system_user in system_users: + system_user.assets.add(*tuple(assets)) diff --git a/apps/assets/templates/assets/user_asset_list.html b/apps/assets/templates/assets/user_asset_list.html index cd268a699..a158244a5 100644 --- a/apps/assets/templates/assets/user_asset_list.html +++ b/apps/assets/templates/assets/user_asset_list.html @@ -24,7 +24,7 @@ var treeUrl = "{% url 'api-perms:my-nodes-children-as-tree' %}?&cache_policy=1"; var assetTableUrl = "{% url 'api-perms:my-assets' %}?cache_policy=1"; var selectUrl = '{% url "api-perms:my-node-assets" node_id=DEFAULT_PK %}?cache_policy=1&all=1'; -var systemUsersUrl = "{% url 'api-perms:my-asset-system-users' asset_id=DEFAULT_PK %}"; +var systemUsersUrl = "{% url 'api-perms:my-asset-system-users' asset_id=DEFAULT_PK %}?cache_policy=1"; var showAssetHref = false; // Need input default true var actions = { targets: 4, createdCell: function (td, cellData) { diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index c73e9611d..667f166b3 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -213,3 +213,16 @@ def dict_get_any(d, keys): if value: return value return None + + +class lazyproperty: + def __init__(self, func): + self.func = func + + def __get__(self, instance, cls): + if instance is None: + return self + else: + value = self.func(instance) + setattr(instance, self.func.__name__, value) + return value \ No newline at end of file diff --git a/apps/perms/api/asset_permission.py b/apps/perms/api/asset_permission.py index 3e7f4a078..c5745b84a 100644 --- a/apps/perms/api/asset_permission.py +++ b/apps/perms/api/asset_permission.py @@ -180,6 +180,7 @@ class AssetPermissionRemoveUserApi(RetrieveUpdateAPIView): users = serializer.validated_data.get('users') if users: perm.users.remove(*tuple(users)) + perm.save() return Response({"msg": "ok"}) else: return Response({"error": serializer.errors}) @@ -197,6 +198,7 @@ class AssetPermissionAddUserApi(RetrieveUpdateAPIView): users = serializer.validated_data.get('users') if users: perm.users.add(*tuple(users)) + perm.save() return Response({"msg": "ok"}) else: return Response({"error": serializer.errors}) @@ -217,6 +219,7 @@ class AssetPermissionRemoveAssetApi(RetrieveUpdateAPIView): assets = serializer.validated_data.get('assets') if assets: perm.assets.remove(*tuple(assets)) + perm.save() return Response({"msg": "ok"}) else: return Response({"error": serializer.errors}) @@ -234,6 +237,7 @@ class AssetPermissionAddAssetApi(RetrieveUpdateAPIView): assets = serializer.validated_data.get('assets') if assets: perm.assets.add(*tuple(assets)) + perm.save() return Response({"msg": "ok"}) else: return Response({"error": serializer.errors}) diff --git a/apps/perms/api/user_permission/common.py b/apps/perms/api/user_permission/common.py index 6a2f8adf7..7e4271af8 100644 --- a/apps/perms/api/user_permission/common.py +++ b/apps/perms/api/user_permission/common.py @@ -51,8 +51,7 @@ class GetUserAssetPermissionActionsApi(UserAssetPermissionMixin, asset = get_object_or_404(Asset, id=asset_id) system_user = get_object_or_404(SystemUser, id=system_id) - system_users_actions = self.util.get_asset_system_users_with_actions( - asset) + system_users_actions = self.util.get_asset_system_users_with_actions(asset) actions = system_users_actions.get(system_user) return {"actions": actions} @@ -103,8 +102,7 @@ class UserGrantedAssetSystemUsersApi(UserAssetPermissionMixin, ListAPIView): def get_queryset(self): asset_id = self.kwargs.get('asset_id') asset = get_object_or_404(Asset, id=asset_id) - system_users_with_actions = self.util.get_asset_system_users_with_actions( - asset) + system_users_with_actions = self.util.get_asset_system_users_with_actions(asset) system_users = [] for system_user, actions in system_users_with_actions.items(): system_user.actions = actions diff --git a/apps/perms/api/user_permission/mixin.py b/apps/perms/api/user_permission/mixin.py index 9ca8c5e7d..a25e49251 100644 --- a/apps/perms/api/user_permission/mixin.py +++ b/apps/perms/api/user_permission/mixin.py @@ -1,23 +1,27 @@ # -*- coding: utf-8 -*- # +from common.utils import lazyproperty +from common.tree import TreeNodeSerializer from ..mixin import UserPermissionMixin from ...utils import AssetPermissionUtilV2, ParserNode from ...hands import Node, Asset -from common.tree import TreeNodeSerializer class UserAssetPermissionMixin(UserPermissionMixin): util = None - tree = None - def initial(self, *args, **kwargs): - super().initial(*args, *kwargs) + @lazyproperty + def util(self): cache_policy = self.request.query_params.get('cache_policy', '0') system_user_id = self.request.query_params.get("system_user") - self.util = AssetPermissionUtilV2(self.obj, cache_policy=cache_policy) + util = AssetPermissionUtilV2(self.obj, cache_policy=cache_policy) if system_user_id: - self.util.filter_permissions(system_users=system_user_id) - self.tree = self.util.get_user_tree() + util.filter_permissions(system_users=system_user_id) + return util + + @lazyproperty + def tree(self): + return self.util.get_user_tree() class UserNodeTreeMixin: diff --git a/apps/perms/signals_handler.py b/apps/perms/signals_handler.py index 8066ce5ca..eecd101e8 100644 --- a/apps/perms/signals_handler.py +++ b/apps/perms/signals_handler.py @@ -19,18 +19,6 @@ permission_m2m_senders = ( ) -@on_transaction_commit -def on_permission_m2m_change(sender, action='', **kwargs): - if not action.startswith('post'): - return - logger.debug('Asset permission m2m changed, refresh user tree cache') - AssetPermissionUtilV2.expire_all_user_tree_cache() - - -for sender in permission_m2m_senders: - m2m_changed.connect(on_permission_m2m_change, sender=sender) - - @receiver([post_save, post_delete], sender=AssetPermission) @on_transaction_commit def on_permission_change(sender, action='', **kwargs): diff --git a/apps/perms/utils/asset_permission.py b/apps/perms/utils/asset_permission.py index ed3616933..f472a162a 100644 --- a/apps/perms/utils/asset_permission.py +++ b/apps/perms/utils/asset_permission.py @@ -1,7 +1,7 @@ # coding: utf-8 +import time import pickle -import threading from collections import defaultdict from functools import reduce from hashlib import md5 @@ -12,7 +12,7 @@ from django.db.models import Q from django.conf import settings from orgs.utils import set_to_root_org -from common.utils import get_logger, timeit +from common.utils import get_logger, timeit, lazyproperty from common.tree import TreeNode from assets.utils import TreeService from ..models import AssetPermission @@ -131,18 +131,19 @@ class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): self.cache_policy = cache_policy self.obj_id = str(obj.id) if obj else None self._permissions = None - self._permissions_id = None # 标记_permission的唯一值 self._filter_id = 'None' # 当通过filter更改 permission是标记 self.change_org_if_need() self._user_tree = None self._user_tree_filter_id = 'None' - self.full_tree = Node.tree() - self.mutex = threading.Lock() @staticmethod def change_org_if_need(): set_to_root_org() + @lazyproperty + def full_tree(self): + return Node.tree() + @property def permissions(self): if self._permissions: @@ -161,7 +162,7 @@ class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): self._permissions = self.permissions.filter(**filters) self._filter_id = md5(filters_json.encode()).hexdigest() - @property + @lazyproperty def user_tree(self): return self.get_user_tree() @@ -305,27 +306,26 @@ class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): @timeit def get_user_tree(self): # 使用锁,保证多次获取tree的时候顺序执行,可以使用缓存 - with self.mutex: - user_tree = self.get_user_tree_from_local() - if user_tree: - return user_tree - user_tree = self.get_user_tree_from_cache_if_need() - if user_tree: - self.set_user_tree_to_local(user_tree) - return user_tree - user_tree = TreeService() - full_tree_root = self.full_tree.root_node() - user_tree.create_node( - tag=full_tree_root.tag, - identifier=full_tree_root.identifier - ) - self.add_direct_nodes_to_user_tree(user_tree) - self.add_single_assets_node_to_user_tree(user_tree) - self.parse_user_tree_to_full_tree(user_tree) - self.add_empty_node_if_need(user_tree) - self.set_user_tree_to_cache_if_need(user_tree) + user_tree = self.get_user_tree_from_local() + if user_tree: + return user_tree + user_tree = self.get_user_tree_from_cache_if_need() + if user_tree: self.set_user_tree_to_local(user_tree) return user_tree + user_tree = TreeService() + full_tree_root = self.full_tree.root_node() + user_tree.create_node( + tag=full_tree_root.tag, + identifier=full_tree_root.identifier + ) + self.add_direct_nodes_to_user_tree(user_tree) + self.add_single_assets_node_to_user_tree(user_tree) + self.parse_user_tree_to_full_tree(user_tree) + self.add_empty_node_if_need(user_tree) + self.set_user_tree_to_cache_if_need(user_tree) + self.set_user_tree_to_local(user_tree) + return user_tree # Todo: 是否可以获取多个资产的系统用户 def get_asset_system_users_with_actions(self, asset): diff --git a/apps/users/templates/users/_granted_assets.html b/apps/users/templates/users/_granted_assets.html index 173aec607..eabff1487 100644 --- a/apps/users/templates/users/_granted_assets.html +++ b/apps/users/templates/users/_granted_assets.html @@ -72,16 +72,10 @@ function initTable() { $(td).html(cellData); }}, {targets: 3, createdCell: function (td, cellData) { - function success(systemUsers) { - var users = []; - $.each(systemUsers, function (id, data) { - var name = htmlEscape(data.name); - users.push(name); - }); - $(td).html(users.join(',')) - } - $(td).html("{% trans 'Loading' %}"); - getGrantedAssetSystemUsers(cellData, success) + var innerHtml = ' {% trans "Show" %} ' + .replace('99999999', cellData); + $(td).html(innerHtml); + }}, ], ajax_url: assetTableUrl, @@ -185,5 +179,19 @@ $(document).ready(function () { var val = $(this).text(); $("#user_assets_table_filter input").val(val); assetTable.search(val).draw(); - }) +}).on('click', '.btn-show-system-users', function () { + var $this = $(this); + var assetId = $this.data('aid'); + + function success(systemUsers) { + var users = []; + $.each(systemUsers, function (id, data) { + var name = htmlEscape(data.name); + users.push(name); + }); + $this.parent().html(users.join(',')) + } + + getGrantedAssetSystemUsers(assetId, success) +}) diff --git a/apps/users/templates/users/user_granted_asset.html b/apps/users/templates/users/user_granted_asset.html index d70418081..184537cd4 100644 --- a/apps/users/templates/users/user_granted_asset.html +++ b/apps/users/templates/users/user_granted_asset.html @@ -33,9 +33,9 @@ {% block custom_foot_js %} {% endblock %} diff --git a/apps/ops/views/adhoc.py b/apps/ops/views/adhoc.py index 06a9f1932..4df75305b 100644 --- a/apps/ops/views/adhoc.py +++ b/apps/ops/views/adhoc.py @@ -2,7 +2,7 @@ from django.utils.translation import ugettext as _ from django.conf import settings -from django.views.generic import ListView, DetailView +from django.views.generic import ListView, DetailView, TemplateView from common.mixins import DatetimeSearchMixin from common.permissions import PermissionsMixin, IsOrgAdmin @@ -17,7 +17,7 @@ __all__ = [ ] -class TaskListView(PermissionsMixin, DatetimeSearchMixin, ListView): +class TaskListView(PermissionsMixin, TemplateView): paginate_by = settings.DISPLAY_PER_PAGE model = Task ordering = ('-date_created',) @@ -26,27 +26,10 @@ class TaskListView(PermissionsMixin, DatetimeSearchMixin, ListView): keyword = '' permission_classes = [IsOrgAdmin] - def get_queryset(self): - queryset = super().get_queryset() - if current_org.is_real(): - queryset = queryset.filter(created_by=current_org.id) - else: - queryset = queryset.filter(created_by='') - - self.keyword = self.request.GET.get('keyword', '') - if self.keyword: - queryset = queryset.filter( - name__icontains=self.keyword, - ) - return queryset - def get_context_data(self, **kwargs): context = { 'app': _('Ops'), 'action': _('Task list'), - 'date_from': self.date_from, - 'date_to': self.date_to, - 'keyword': self.keyword, } kwargs.update(context) return super().get_context_data(**kwargs) diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js index 03c804da8..96f7a27dd 100644 --- a/apps/static/js/jumpserver.js +++ b/apps/static/js/jumpserver.js @@ -1139,7 +1139,10 @@ function timeOffset(a, b) { var start = safeDate(a); var end = safeDate(b); var offset = (end - start) / 1000; + return readableSecond(offset) +} +function readableSecond(offset) { var days = offset / 3600 / 24; var hours = offset / 3600; var minutes = offset / 60; diff --git a/apps/templates/_nav.html b/apps/templates/_nav.html index 8b76e9b5d..5690c961d 100644 --- a/apps/templates/_nav.html +++ b/apps/templates/_nav.html @@ -114,6 +114,9 @@ {% endif %} diff --git a/jms b/jms index 17c0d8b47..805faac50 100755 --- a/jms +++ b/jms @@ -200,9 +200,13 @@ def is_running(s, unlink=True): def parse_service(s): - all_services = ['gunicorn', 'celery_ansible', 'celery_default', 'beat'] + all_services = [ + 'gunicorn', 'celery_ansible', 'celery_default', 'beat', 'flower' + ] if s == 'all': return all_services + elif s == 'gunicorn': + return ['gunicorn', 'flower'] elif s == "celery": return ["celery_ansible", "celery_default"] elif "," in s: @@ -240,17 +244,19 @@ def get_start_gunicorn_kwargs(): def get_start_celery_ansible_kwargs(): - print("\n- Start Celery as Distributed Task Queue") + print("\n- Start Celery as Distributed Task Queue: Ansible") return get_start_worker_kwargs('ansible', 4) def get_start_celery_default_kwargs(): + print("\n- Start Celery as Distributed Task Queue: Celery") return get_start_worker_kwargs('celery', 2) def get_start_worker_kwargs(queue, num): # Todo: Must set this environment, otherwise not no ansible result return os.environ.setdefault('PYTHONOPTIMIZE', '1') + os.environ.setdefault('ANSIBLE_FORCE_COLOR', 'True') if os.getuid() == 0: os.environ.setdefault('C_FORCE_ROOT', '1') @@ -261,6 +267,24 @@ def get_start_worker_kwargs(queue, num): '-l', 'INFO', '-c', str(num), '-Q', queue, + '-n', '{}@%h'.format(queue) + ] + return {"cmd": cmd, "cwd": APPS_DIR} + + +def get_start_flower_kwargs(): + print("\n- Start Flower as Task Monitor") + if os.getuid() == 0: + os.environ.setdefault('C_FORCE_ROOT', '1') + + cmd = [ + 'celery', 'flower', + '-A', 'ops', + '-l', 'INFO', + '--url_prefix=flower', + '--auto_refresh=False', + '--max_tasks=1000', + '--tasks_columns=uuid,name,args,state,received,started,runtime,worker' ] return {"cmd": cmd, "cwd": APPS_DIR} @@ -333,6 +357,7 @@ def start_service(s): "celery_ansible": get_start_celery_ansible_kwargs, "celery_default": get_start_celery_default_kwargs, "beat": get_start_beat_kwargs, + "flower": get_start_flower_kwargs, } kwargs = services_kwargs.get(s)() @@ -449,7 +474,7 @@ if __name__ == '__main__': ) parser.add_argument( "service", type=str, default="all", nargs="?", - choices=("all", "gunicorn", "celery", "beat", "celery,beat"), + choices=("all", "gunicorn", "celery", "beat", "celery,beat", "flower"), help="The service to start", ) parser.add_argument('-d', '--daemon', nargs="?", const=1) From 6fc666e60dc084979fb63df174e37ddbd7a10e79 Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 19 Sep 2019 22:12:53 +0800 Subject: [PATCH 7/7] =?UTF-8?q?[Update]=20=E4=BC=98=E5=8C=96=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E7=B3=BB=E7=BB=9F=E7=94=A8=E6=88=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/models/node.py | 5 ++- .../user_permission/user_permission_assets.py | 2 +- apps/perms/utils/asset_permission.py | 38 +++---------------- 3 files changed, 10 insertions(+), 35 deletions(-) diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index 259405104..eb860e9ea 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -298,14 +298,15 @@ class NodeAssetsMixin: return self.get_all_assets().valid() @classmethod - @timeit - def get_nodes_all_assets(cls, nodes_keys): + def get_nodes_all_assets(cls, nodes_keys, extra_assets_ids=None): from .asset import Asset nodes_keys = cls.clean_children_keys(nodes_keys) assets_ids = set() for key in nodes_keys: node_assets_ids = cls.tree().all_assets(key) assets_ids.update(set(node_assets_ids)) + if extra_assets_ids: + assets_ids.update(set(extra_assets_ids)) return Asset.objects.filter(id__in=assets_ids) diff --git a/apps/perms/api/user_permission/user_permission_assets.py b/apps/perms/api/user_permission/user_permission_assets.py index c97f59cce..b2ed1fe65 100644 --- a/apps/perms/api/user_permission/user_permission_assets.py +++ b/apps/perms/api/user_permission/user_permission_assets.py @@ -7,7 +7,7 @@ from rest_framework.generics import ( ) from common.permissions import IsOrgAdminOrAppUser -from common.utils import get_logger +from common.utils import get_logger, timeit from ...hands import Node from ... import serializers from .mixin import UserAssetPermissionMixin, UserAssetTreeMixin diff --git a/apps/perms/utils/asset_permission.py b/apps/perms/utils/asset_permission.py index f472a162a..b23e31e8b 100644 --- a/apps/perms/utils/asset_permission.py +++ b/apps/perms/utils/asset_permission.py @@ -334,17 +334,13 @@ class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): for node in nodes: ancestor_keys = node.get_ancestor_keys(with_self=True) nodes_keys_related.update(set(ancestor_keys)) - pattern = [] - for key in nodes_keys_related: - pattern.append(r'^{0}$|^{0}:'.format(key)) - pattern = '|'.join(list(pattern)) kwargs = {"assets": asset} - if pattern: - kwargs["nodes__key__regex"] = pattern + if nodes_keys_related: + kwargs["nodes__key__in"] = nodes_keys_related queryset = self.permissions - if len(kwargs) == 1: + if kwargs == 1: queryset = queryset.filter(**kwargs) elif len(kwargs) > 1: kwargs = [{k: v} for k, v in kwargs.items()] @@ -378,33 +374,11 @@ class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): nodes_keys = Node.clean_children_keys(nodes_keys) return nodes_keys, assets_ids - @staticmethod - def filter_assets_by_or_kwargs(kwargs): - if len(kwargs) == 1: - queryset = Asset.objects.filter(**kwargs) - elif len(kwargs) > 1: - kwargs = [{k: v} for k, v in kwargs.items()] - args = [Q(**kw) for kw in kwargs] - args = reduce(lambda x, y: x | y, args) - queryset = Asset.objects.filter(args) - else: - queryset = Asset.objects.none() - return queryset - @timeit def get_assets(self): nodes_keys, assets_ids = self.get_permissions_nodes_and_assets() - pattern = set() - for key in nodes_keys: - pattern.add(r'^{0}$|^{0}:'.format(key)) - pattern = '|'.join(list(pattern)) - kwargs = {} - if assets_ids: - kwargs["id__in"] = assets_ids - if pattern: - kwargs["nodes__key__regex"] = pattern - queryset = self.filter_assets_by_or_kwargs(kwargs) - return queryset.valid().distinct() + queryset = Node.get_nodes_all_assets(nodes_keys, extra_assets_ids=assets_ids) + return queryset.valid() def get_nodes_assets(self, node, deep=False): if deep: @@ -412,7 +386,7 @@ class AssetPermissionUtilV2(AssetPermissionUtilCacheMixin): else: assets_ids = self.user_tree.assets(node.key) queryset = Asset.objects.filter(id__in=assets_ids) - return queryset.valid().distinct() + return queryset.valid() def get_nodes(self): return [n.identifier for n in self.user_tree.all_nodes_itr()]