diff --git a/apps/applications/api/remote_app.py b/apps/applications/api/remote_app.py index c41c5d100..83b1d490a 100644 --- a/apps/applications/api/remote_app.py +++ b/apps/applications/api/remote_app.py @@ -3,9 +3,8 @@ from rest_framework import generics -from rest_framework.pagination import LimitOffsetPagination -from rest_framework_bulk import BulkModelViewSet +from orgs.mixins.api import OrgBulkModelViewSet from ..hands import IsOrgAdmin, IsAppUser from ..models import RemoteApp from ..serializers import RemoteAppSerializer, RemoteAppConnectionInfoSerializer @@ -16,13 +15,12 @@ __all__ = [ ] -class RemoteAppViewSet(BulkModelViewSet): +class RemoteAppViewSet(OrgBulkModelViewSet): filter_fields = ('name',) search_fields = filter_fields permission_classes = (IsOrgAdmin,) queryset = RemoteApp.objects.all() serializer_class = RemoteAppSerializer - pagination_class = LimitOffsetPagination class RemoteAppConnectionInfoApi(generics.RetrieveAPIView): diff --git a/apps/applications/forms/remote_app.py b/apps/applications/forms/remote_app.py index ba7661acd..81fc20b2b 100644 --- a/apps/applications/forms/remote_app.py +++ b/apps/applications/forms/remote_app.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext as _ from django import forms -from orgs.mixins import OrgModelForm +from orgs.mixins.forms import OrgModelForm from assets.models import SystemUser from ..models import RemoteApp diff --git a/apps/applications/models/remote_app.py b/apps/applications/models/remote_app.py index 772d39834..636eb1f66 100644 --- a/apps/applications/models/remote_app.py +++ b/apps/applications/models/remote_app.py @@ -5,7 +5,7 @@ import uuid from django.db import models from django.utils.translation import ugettext_lazy as _ -from orgs.mixins import OrgModelMixin +from orgs.mixins.models import OrgModelMixin from common.fields.model import EncryptJsonDictTextField from .. import const diff --git a/apps/applications/serializers/remote_app.py b/apps/applications/serializers/remote_app.py index 9b5c56315..80faff539 100644 --- a/apps/applications/serializers/remote_app.py +++ b/apps/applications/serializers/remote_app.py @@ -5,7 +5,7 @@ from rest_framework import serializers from common.serializers import AdaptedBulkListSerializer -from orgs.mixins import BulkOrgResourceModelSerializer +from orgs.mixins.serializers import BulkOrgResourceModelSerializer from .. import const from ..models import RemoteApp diff --git a/apps/applications/urls/api_urls.py b/apps/applications/urls/api_urls.py index 97487b5a1..137cea733 100644 --- a/apps/applications/urls/api_urls.py +++ b/apps/applications/urls/api_urls.py @@ -1,20 +1,24 @@ # coding:utf-8 # -from django.urls import path +from django.urls import path, re_path from rest_framework_bulk.routes import BulkRouter +from common import api as capi from .. import api app_name = 'applications' router = BulkRouter() -router.register(r'remote-app', api.RemoteAppViewSet, 'remote-app') +router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app') urlpatterns = [ path('remote-apps//connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info') ] +old_version_urlpatterns = [ + re_path('(?Premote-app)/.*', capi.redirect_plural_name_api) +] -urlpatterns += router.urls +urlpatterns += router.urls + old_version_urlpatterns diff --git a/apps/assets/api/admin_user.py b/apps/assets/api/admin_user.py index e84d9731a..9db193643 100644 --- a/apps/assets/api/admin_user.py +++ b/apps/assets/api/admin_user.py @@ -17,8 +17,7 @@ from django.db import transaction from django.shortcuts import get_object_or_404 from rest_framework import generics from rest_framework.response import Response -from rest_framework_bulk import BulkModelViewSet -from rest_framework.pagination import LimitOffsetPagination +from orgs.mixins.api import OrgBulkModelViewSet from common.mixins import IDInCacheFilterMixin from common.utils import get_logger @@ -36,7 +35,7 @@ __all__ = [ ] -class AdminUserViewSet(IDInCacheFilterMixin, BulkModelViewSet): +class AdminUserViewSet(OrgBulkModelViewSet): """ Admin user api set, for add,delete,update,list,retrieve resource """ @@ -46,11 +45,6 @@ class AdminUserViewSet(IDInCacheFilterMixin, BulkModelViewSet): queryset = AdminUser.objects.all() serializer_class = serializers.AdminUserSerializer permission_classes = (IsOrgAdmin,) - pagination_class = LimitOffsetPagination - - def get_queryset(self): - queryset = super().get_queryset().all() - return queryset class AdminUserAuthApi(generics.UpdateAPIView): @@ -98,7 +92,6 @@ class AdminUserTestConnectiveApi(generics.RetrieveAPIView): class AdminUserAssetsListView(generics.ListAPIView): permission_classes = (IsOrgAdmin,) serializer_class = serializers.AssetSimpleSerializer - pagination_class = LimitOffsetPagination filter_fields = ("hostname", "ip") http_method_names = ['get'] search_fields = filter_fields diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index bdf5b8d89..f24768217 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -1,27 +1,17 @@ # -*- coding: utf-8 -*- # -import uuid import random from rest_framework import generics -from rest_framework.views import APIView from rest_framework.response import Response -from rest_framework_bulk import BulkModelViewSet -from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView -from rest_framework.pagination import LimitOffsetPagination from django.utils.translation import ugettext_lazy as _ from django.shortcuts import get_object_or_404 -from django.urls import reverse_lazy -from django.core.cache import cache from django.db.models import Q -from common.mixins import IDInCacheFilterMixin, ApiMessageMixin - from common.utils import get_logger, get_object_or_none from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser -from orgs.mixins import OrgBulkModelViewSet -from ..const import CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX +from orgs.mixins.api import OrgBulkModelViewSet from ..models import Asset, AdminUser, Node from .. import serializers from ..tasks import update_asset_hardware_info_manual, \ @@ -31,9 +21,9 @@ from ..utils import LabelFilter logger = get_logger(__file__) __all__ = [ - 'AssetViewSet', 'AssetListUpdateApi', + 'AssetViewSet', 'AssetRefreshHardwareApi', 'AssetAdminUserTestApi', - 'AssetGatewayApi', 'AssetBulkUpdateSelectAPI' + 'AssetGatewayApi', ] @@ -46,7 +36,6 @@ class AssetViewSet(LabelFilter, OrgBulkModelViewSet): ordering_fields = ("hostname", "ip", "port", "cpu_cores") queryset = Asset.objects.all() serializer_class = serializers.AssetSerializer - pagination_class = LimitOffsetPagination permission_classes = (IsOrgAdminOrAppUser,) success_message = _("%(hostname)s was %(action)s successfully") @@ -73,19 +62,21 @@ class AssetViewSet(LabelFilter, OrgBulkModelViewSet): node = get_object_or_404(Node, id=node_id) show_current_asset = self.request.query_params.get("show_current_asset") in ('1', 'true') + # 当前节点是顶层节点, 并且仅显示直接资产 if node.is_root() and show_current_asset: queryset = queryset.filter( Q(nodes=node_id) | Q(nodes__isnull=True) - ) + ).distinct() + # 当前节点是顶层节点,显示所有资产 elif node.is_root() and not show_current_asset: - pass + return queryset + # 当前节点不是鼎城节点,只显示直接资产 elif not node.is_root() and show_current_asset: queryset = queryset.filter(nodes=node) else: - queryset = queryset.filter( - nodes__key__regex='^{}(:[0-9]+)*$'.format(node.key), - ) - return queryset.distinct() + 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') @@ -102,30 +93,6 @@ class AssetViewSet(LabelFilter, OrgBulkModelViewSet): return queryset -class AssetListUpdateApi(IDInCacheFilterMixin, ListBulkCreateUpdateDestroyAPIView): - """ - Asset bulk update api - """ - queryset = Asset.objects.all() - serializer_class = serializers.AssetSerializer - permission_classes = (IsOrgAdmin,) - - -class AssetBulkUpdateSelectAPI(APIView): - permission_classes = (IsOrgAdmin,) - - def post(self, request, *args, **kwargs): - assets_id = request.data.get('assets_id', '') - if assets_id: - spm = uuid.uuid4().hex - key = CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX.format(spm) - cache.set(key, assets_id, 300) - url = reverse_lazy('assets:asset-bulk-update') + '?spm=%s' % spm - return Response({'url': url}) - error = _('Please select assets that need to be updated') - return Response({'error': error}, status=400) - - class AssetRefreshHardwareApi(generics.RetrieveAPIView): """ Refresh asset hardware info diff --git a/apps/assets/api/asset_user.py b/apps/assets/api/asset_user.py index b916e43ec..ec71e87f2 100644 --- a/apps/assets/api/asset_user.py +++ b/apps/assets/api/asset_user.py @@ -2,11 +2,11 @@ # from rest_framework.response import Response -from rest_framework import viewsets, status, generics -from rest_framework.pagination import LimitOffsetPagination +from rest_framework import generics from rest_framework import filters from rest_framework_bulk import BulkModelViewSet from django.shortcuts import get_object_or_404 +from django.http import Http404 from common.permissions import IsOrgAdminOrAppUser, NeedMFAVerify from common.utils import get_object_or_none, get_logger @@ -53,7 +53,6 @@ class AssetUserSearchBackend(filters.BaseFilterBackend): class AssetUserViewSet(IDInCacheFilterMixin, BulkModelViewSet): - pagination_class = LimitOffsetPagination serializer_class = serializers.AssetUserSerializer permission_classes = [IsOrgAdminOrAppUser] http_method_names = ['get', 'post'] @@ -67,6 +66,9 @@ class AssetUserViewSet(IDInCacheFilterMixin, BulkModelViewSet): AssetUserFilterBackend, AssetUserSearchBackend, ) + def allow_bulk_destroy(self, qs, filtered): + return False + def get_queryset(self): # 尽可能先返回更少的数据 username = self.request.GET.get('username') @@ -115,14 +117,6 @@ class AssetUserAuthInfoApi(generics.RetrieveAPIView): serializer_class = serializers.AssetUserAuthInfoSerializer permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify] - def retrieve(self, request, *args, **kwargs): - instance = self.get_object() - serializer = self.get_serializer(instance) - status_code = status.HTTP_200_OK - if not instance: - status_code = status.HTTP_400_BAD_REQUEST - return Response(serializer.data, status=status_code) - def get_object(self): query_params = self.request.query_params username = query_params.get('username') @@ -133,8 +127,7 @@ class AssetUserAuthInfoApi(generics.RetrieveAPIView): manger = AssetUserManager() instance = manger.get(username, asset, prefer=prefer) except Exception as e: - logger.error(e, exc_info=True) - return None + raise Http404("Not found") else: return instance diff --git a/apps/assets/api/cmd_filter.py b/apps/assets/api/cmd_filter.py index cecdf2432..846172548 100644 --- a/apps/assets/api/cmd_filter.py +++ b/apps/assets/api/cmd_filter.py @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- # -from rest_framework_bulk import BulkModelViewSet -from rest_framework.pagination import LimitOffsetPagination from django.shortcuts import get_object_or_404 +from orgs.mixins.api import OrgBulkModelViewSet from ..hands import IsOrgAdmin from ..models import CommandFilter, CommandFilterRule from .. import serializers @@ -13,21 +12,19 @@ from .. import serializers __all__ = ['CommandFilterViewSet', 'CommandFilterRuleViewSet'] -class CommandFilterViewSet(BulkModelViewSet): +class CommandFilterViewSet(OrgBulkModelViewSet): filter_fields = ("name",) search_fields = filter_fields permission_classes = (IsOrgAdmin,) queryset = CommandFilter.objects.all() serializer_class = serializers.CommandFilterSerializer - pagination_class = LimitOffsetPagination -class CommandFilterRuleViewSet(BulkModelViewSet): +class CommandFilterRuleViewSet(OrgBulkModelViewSet): filter_fields = ("content",) search_fields = filter_fields permission_classes = (IsOrgAdmin,) serializer_class = serializers.CommandFilterRuleSerializer - pagination_class = LimitOffsetPagination def get_queryset(self): fpk = self.kwargs.get('filter_pk') diff --git a/apps/assets/api/domain.py b/apps/assets/api/domain.py index 27ea40a47..c82c75788 100644 --- a/apps/assets/api/domain.py +++ b/apps/assets/api/domain.py @@ -1,13 +1,11 @@ # ~*~ coding: utf-8 ~*~ -from rest_framework_bulk import BulkModelViewSet from rest_framework.views import APIView, Response -from rest_framework.pagination import LimitOffsetPagination - from django.views.generic.detail import SingleObjectMixin from common.utils import get_logger -from common.permissions import IsOrgAdmin, IsAppUser, IsOrgAdminOrAppUser +from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser +from orgs.mixins.api import OrgBulkModelViewSet from ..models import Domain, Gateway from .. import serializers @@ -16,11 +14,10 @@ logger = get_logger(__file__) __all__ = ['DomainViewSet', 'GatewayViewSet', "GatewayTestConnectionApi"] -class DomainViewSet(BulkModelViewSet): +class DomainViewSet(OrgBulkModelViewSet): queryset = Domain.objects.all() permission_classes = (IsOrgAdmin,) serializer_class = serializers.DomainSerializer - pagination_class = LimitOffsetPagination def get_queryset(self): queryset = super().get_queryset().all() @@ -37,13 +34,12 @@ class DomainViewSet(BulkModelViewSet): return super().get_permissions() -class GatewayViewSet(BulkModelViewSet): +class GatewayViewSet(OrgBulkModelViewSet): filter_fields = ("domain__name", "name", "username", "ip", "domain") search_fields = filter_fields queryset = Gateway.objects.all() permission_classes = (IsOrgAdmin,) serializer_class = serializers.GatewaySerializer - pagination_class = LimitOffsetPagination class GatewayTestConnectionApi(SingleObjectMixin, APIView): diff --git a/apps/assets/api/label.py b/apps/assets/api/label.py index d3537b20c..c3b798959 100644 --- a/apps/assets/api/label.py +++ b/apps/assets/api/label.py @@ -13,11 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from rest_framework.pagination import LimitOffsetPagination from django.db.models import Count from common.utils import get_logger -from orgs.mixins import OrgBulkModelViewSet +from orgs.mixins.api import OrgBulkModelViewSet from ..hands import IsOrgAdmin from ..models import Label from .. import serializers @@ -32,7 +31,6 @@ class LabelViewSet(OrgBulkModelViewSet): search_fields = filter_fields permission_classes = (IsOrgAdmin,) serializer_class = serializers.LabelSerializer - pagination_class = LimitOffsetPagination def list(self, request, *args, **kwargs): if request.query_params.get("distinct"): diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index 2478303d0..83b7ec2da 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -13,7 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from rest_framework import generics, mixins, viewsets +import time + +from rest_framework import generics from rest_framework.serializers import ValidationError from rest_framework.views import APIView from rest_framework.response import Response @@ -22,11 +24,11 @@ from django.shortcuts import get_object_or_404 from common.utils import get_logger, get_object_or_none from common.tree import TreeNodeSerializer +from orgs.mixins.api import OrgModelViewSet from ..hands import IsOrgAdmin from ..models import Node from ..tasks import update_assets_hardware_info_util, test_asset_connectivity_util from .. import serializers -from ..utils import NodeUtil logger = get_logger(__file__) @@ -39,29 +41,25 @@ __all__ = [ ] -class NodeViewSet(viewsets.ModelViewSet): - filter_fields = ('value', 'key', ) - search_fields = filter_fields +class NodeViewSet(OrgModelViewSet): + filter_fields = ('value', 'key', 'id') + search_fields = ('value', ) queryset = Node.objects.all() permission_classes = (IsOrgAdmin,) serializer_class = serializers.NodeSerializer + # 仅支持根节点指直接创建,子节点下的节点需要通过children接口创建 def perform_create(self, serializer): child_key = Node.root().get_next_child_key() serializer.validated_data["key"] = child_key serializer.save() - def update(self, request, *args, **kwargs): + def perform_update(self, serializer): node = self.get_object() - if node.is_root(): - node_value = node.value - post_value = request.data.get('value') - if node_value != post_value: - return Response( - {"msg": _("You can't update the root node name")}, - status=400 - ) - return super().update(request, *args, **kwargs) + if node.is_root() and node.value != serializer.validated_data['value']: + msg = _("You can't update the root node name") + raise ValidationError({"error": msg}) + return super().perform_update(serializer) class NodeListAsTreeApi(generics.ListAPIView): @@ -79,21 +77,72 @@ class NodeListAsTreeApi(generics.ListAPIView): permission_classes = (IsOrgAdmin,) serializer_class = TreeNodeSerializer + @staticmethod + def to_tree_queryset(queryset): + queryset = [node.as_tree_node() for node in queryset] + return queryset + def get_queryset(self): queryset = Node.objects.all() - util = NodeUtil() - nodes = util.get_nodes_by_queryset(queryset) - queryset = [node.as_tree_node() for node in nodes] return queryset - @staticmethod - def refresh_nodes(queryset): - Node.expire_nodes_assets_amount() - Node.expire_nodes_full_value() + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + queryset = self.to_tree_queryset(queryset) return queryset -class NodeChildrenAsTreeApi(generics.ListAPIView): +class NodeChildrenApi(generics.ListCreateAPIView): + queryset = Node.objects.all() + permission_classes = (IsOrgAdmin,) + serializer_class = serializers.NodeSerializer + instance = None + + def initial(self, request, *args, **kwargs): + self.instance = self.get_object() + return super().initial(request, *args, **kwargs) + + def perform_create(self, serializer): + data = serializer.validated_data + _id = data.get("id") + value = data.get("value") + if not value: + value = self.instance.get_next_child_preset_name() + node = self.instance.create_child(value=value, _id=_id) + # 避免查询 full value + node._full_value = node.value + serializer.instance = node + + def get_object(self): + pk = self.kwargs.get('pk') or self.request.query_params.get('id') + key = self.request.query_params.get("key") + if not pk and not key: + node = Node.root() + return node + if pk: + node = get_object_or_404(Node, pk=pk) + else: + node = get_object_or_404(Node, key=key) + return node + + def get_queryset(self): + query_all = self.request.query_params.get("all", "0") == "all" + if not self.instance: + return Node.objects.none() + + if self.instance.is_root(): + with_self = True + else: + with_self = False + + if query_all: + queryset = self.instance.get_all_children(with_self=with_self) + else: + queryset = self.instance.get_children(with_self=with_self) + return queryset + + +class NodeChildrenAsTreeApi(NodeChildrenApi): """ 节点子节点作为树返回, [ @@ -106,39 +155,26 @@ class NodeChildrenAsTreeApi(generics.ListAPIView): ] """ - permission_classes = (IsOrgAdmin,) serializer_class = TreeNodeSerializer - node = None - is_root = False + http_method_names = ['get'] def get_queryset(self): - self.check_need_refresh_nodes() - node_key = self.request.query_params.get('key') - util = NodeUtil() - # 是否包含自己 - with_self = False - if not node_key: - node_key = Node.root().key - with_self = True - self.node = util.get_node_by_key(node_key) - queryset = self.node.get_children(with_self=with_self) + queryset = super().get_queryset() queryset = [node.as_tree_node() for node in queryset] + queryset = self.add_assets_if_need(queryset) queryset = sorted(queryset) return queryset - def filter_assets(self, queryset): + def add_assets_if_need(self, queryset): include_assets = self.request.query_params.get('assets', '0') == '1' if not include_assets: return queryset - assets = self.node.get_assets().only( - "id", "hostname", "ip", 'platform', "os", "org_id", "protocols", + assets = self.instance.get_assets().only( + "id", "hostname", "ip", 'platform', "os", + "org_id", "protocols", ) for asset in assets: - queryset.append(asset.as_tree_node(self.node)) - return queryset - - def filter_queryset(self, queryset): - queryset = self.filter_assets(queryset) + queryset.append(asset.as_tree_node(self.instance)) return queryset def check_need_refresh_nodes(self): @@ -146,59 +182,6 @@ class NodeChildrenAsTreeApi(generics.ListAPIView): Node.refresh_nodes() -class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView): - queryset = Node.objects.all() - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.NodeSerializer - instance = None - - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - - def post(self, request, *args, **kwargs): - instance = self.get_object() - if not request.data.get("value"): - request.data["value"] = instance.get_next_child_preset_name() - return super().post(request, *args, **kwargs) - - def create(self, request, *args, **kwargs): - instance = self.get_object() - value = request.data.get("value") - _id = request.data.get('id') or None - values = [child.value for child in instance.get_children()] - if value in values: - raise ValidationError( - 'The same level node name cannot be the same' - ) - node = instance.create_child(value=value, _id=_id) - return Response(self.serializer_class(instance=node).data, status=201) - - def get_object(self): - pk = self.kwargs.get('pk') or self.request.query_params.get('id') - if not pk: - node = Node.root() - else: - node = get_object_or_404(Node, pk=pk) - return node - - def get_queryset(self): - queryset = [] - query_all = self.request.query_params.get("all") - node = self.get_object() - - if node is None: - node = Node.root() - node.assets__count = node.get_all_assets().count() - queryset.append(node) - - if query_all: - children = node.get_all_children() - else: - children = node.get_children() - queryset.extend(list(children)) - return queryset - - class NodeAssetsApi(generics.ListAPIView): permission_classes = (IsOrgAdmin,) serializer_class = serializers.AssetSerializer diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index 8baacd4f8..9b87ed9aa 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -16,18 +16,17 @@ from django.shortcuts import get_object_or_404 from rest_framework import generics from rest_framework.response import Response -from rest_framework_bulk import BulkModelViewSet -from rest_framework.pagination import LimitOffsetPagination +from common.serializers import CeleryTaskSerializer from common.utils import get_logger from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser -from common.mixins import IDInCacheFilterMixin -from orgs.mixins import OrgBulkModelViewSet +from orgs.mixins.api import OrgBulkModelViewSet from ..models import SystemUser, Asset from .. import serializers -from ..tasks import push_system_user_to_assets_manual, \ - test_system_user_connectivity_manual, push_system_user_a_asset_manual, \ - test_system_user_connectivity_a_asset +from ..tasks import ( + push_system_user_to_assets_manual, test_system_user_connectivity_manual, + push_system_user_a_asset_manual, test_system_user_connectivity_a_asset, +) logger = get_logger(__file__) @@ -49,7 +48,6 @@ class SystemUserViewSet(OrgBulkModelViewSet): queryset = SystemUser.objects.all() serializer_class = serializers.SystemUserSerializer permission_classes = (IsOrgAdminOrAppUser,) - pagination_class = LimitOffsetPagination def get_queryset(self): queryset = super().get_queryset().all() @@ -92,6 +90,7 @@ class SystemUserPushApi(generics.RetrieveAPIView): """ queryset = SystemUser.objects.all() permission_classes = (IsOrgAdmin,) + serializer_class = CeleryTaskSerializer def retrieve(self, request, *args, **kwargs): system_user = self.get_object() @@ -108,6 +107,7 @@ class SystemUserTestConnectiveApi(generics.RetrieveAPIView): """ queryset = SystemUser.objects.all() permission_classes = (IsOrgAdmin,) + serializer_class = CeleryTaskSerializer def retrieve(self, request, *args, **kwargs): system_user = self.get_object() @@ -118,7 +118,6 @@ class SystemUserTestConnectiveApi(generics.RetrieveAPIView): class SystemUserAssetsListView(generics.ListAPIView): permission_classes = (IsOrgAdmin,) serializer_class = serializers.AssetSimpleSerializer - pagination_class = LimitOffsetPagination filter_fields = ("hostname", "ip") http_method_names = ['get'] search_fields = filter_fields diff --git a/apps/assets/forms/asset.py b/apps/assets/forms/asset.py index a044988ab..3500862e5 100644 --- a/apps/assets/forms/asset.py +++ b/apps/assets/forms/asset.py @@ -4,7 +4,7 @@ from django import forms from django.utils.translation import gettext_lazy as _ from common.utils import get_logger -from orgs.mixins import OrgModelForm +from orgs.mixins.forms import OrgModelForm from ..models import Asset, Node @@ -29,9 +29,14 @@ class ProtocolForm(forms.Form): class AssetCreateForm(OrgModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + if self.data: + return nodes_field = self.fields['nodes'] - nodes_field.choices = ((n.id, n.full_value) for n in - Node.get_queryset()) + if self.instance: + nodes_field.choices = ((n.id, n.full_value) for n in + self.instance.nodes.all()) + else: + nodes_field.choices = [] class Meta: model = Asset @@ -42,7 +47,7 @@ class AssetCreateForm(OrgModelForm): ] widgets = { 'nodes': forms.SelectMultiple(attrs={ - 'class': 'select2', 'data-placeholder': _('Nodes') + 'class': 'nodes-select2', 'data-placeholder': _('Nodes') }), 'admin_user': forms.Select(attrs={ 'class': 'select2', 'data-placeholder': _('Admin user') @@ -68,6 +73,17 @@ class AssetCreateForm(OrgModelForm): class AssetUpdateForm(OrgModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.data: + return + nodes_field = self.fields['nodes'] + if self.instance: + nodes_field.choices = ((n.id, n.full_value) for n in + self.instance.nodes.all()) + else: + nodes_field.choices = [] + class Meta: model = Asset fields = [ @@ -77,7 +93,7 @@ class AssetUpdateForm(OrgModelForm): ] widgets = { 'nodes': forms.SelectMultiple(attrs={ - 'class': 'select2', 'data-placeholder': _('Node') + 'class': 'nodes-select2', 'data-placeholder': _('Node') }), 'admin_user': forms.Select(attrs={ 'class': 'select2', 'data-placeholder': _('Admin user') diff --git a/apps/assets/forms/cmd_filter.py b/apps/assets/forms/cmd_filter.py index cd7df89e8..46f8fc244 100644 --- a/apps/assets/forms/cmd_filter.py +++ b/apps/assets/forms/cmd_filter.py @@ -5,7 +5,7 @@ from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ import re -from orgs.mixins import OrgModelForm +from orgs.mixins.forms import OrgModelForm from ..models import CommandFilter, CommandFilterRule __all__ = ['CommandFilterForm', 'CommandFilterRuleForm'] diff --git a/apps/assets/forms/domain.py b/apps/assets/forms/domain.py index 90db16fd0..496147a99 100644 --- a/apps/assets/forms/domain.py +++ b/apps/assets/forms/domain.py @@ -3,7 +3,7 @@ from django import forms from django.utils.translation import gettext_lazy as _ -from orgs.mixins import OrgModelForm +from orgs.mixins.forms import OrgModelForm from ..models import Domain, Asset, Gateway from .user import PasswordAndKeyAuthForm diff --git a/apps/assets/forms/user.py b/apps/assets/forms/user.py index b0096c2f0..3d67b434b 100644 --- a/apps/assets/forms/user.py +++ b/apps/assets/forms/user.py @@ -4,7 +4,7 @@ from django import forms from django.utils.translation import gettext_lazy as _ from common.utils import validate_ssh_private_key, ssh_pubkey_gen, get_logger -from orgs.mixins import OrgModelForm +from orgs.mixins.forms import OrgModelForm from ..models import AdminUser, SystemUser logger = get_logger(__file__) diff --git a/apps/assets/migrations/0037_auto_20190724_2002.py b/apps/assets/migrations/0037_auto_20190724_2002.py new file mode 100644 index 000000000..49c5490c5 --- /dev/null +++ b/apps/assets/migrations/0037_auto_20190724_2002.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.7 on 2019-07-24 12:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0036_auto_20190716_1535'), + ] + + operations = [ + migrations.AlterField( + model_name='adminuser', + name='_become_pass', + field=models.CharField(blank=True, default='', max_length=128), + ), + ] diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index 4a0816419..1c398e49d 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -13,7 +13,7 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from .utils import Connectivity -from orgs.mixins import OrgModelMixin, OrgManager +from orgs.mixins.models import OrgModelMixin, OrgManager __all__ = ['Asset', 'ProtocolsMixin'] logger = logging.getLogger(__name__) @@ -345,7 +345,6 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): else: _nodes = [Node.default_node()] asset.nodes.set(_nodes) - asset.system_users = [choice(SystemUser.objects.all()) for i in range(3)] logger.debug('Generate fake asset : %s' % asset.ip) except IntegrityError: print('Error continue') diff --git a/apps/assets/models/authbook.py b/apps/assets/models/authbook.py index 85cb22606..01c8d4630 100644 --- a/apps/assets/models/authbook.py +++ b/apps/assets/models/authbook.py @@ -4,7 +4,7 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -from orgs.mixins import OrgManager +from orgs.mixins.models import OrgManager from .base import AssetUser __all__ = ['AuthBook'] diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 2c36b9d14..759285b80 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -15,7 +15,7 @@ from common.utils import ( ) from common.validators import alphanumeric from common import fields -from orgs.mixins import OrgModelMixin +from orgs.mixins.models import OrgModelMixin from .utils import private_key_validator, Connectivity signer = get_signer() diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py index 92135894c..38776ff38 100644 --- a/apps/assets/models/cmd_filter.py +++ b/apps/assets/models/cmd_filter.py @@ -7,7 +7,7 @@ from django.db import models from django.core.validators import MinValueValidator, MaxValueValidator from django.utils.translation import ugettext_lazy as _ -from orgs.mixins import OrgModelMixin +from orgs.mixins.models import OrgModelMixin __all__ = [ diff --git a/apps/assets/models/domain.py b/apps/assets/models/domain.py index 9bee7f77e..2051181fe 100644 --- a/apps/assets/models/domain.py +++ b/apps/assets/models/domain.py @@ -9,7 +9,7 @@ import paramiko from django.db import models from django.utils.translation import ugettext_lazy as _ -from orgs.mixins import OrgModelMixin +from orgs.mixins.models import OrgModelMixin from .base import AssetUser __all__ = ['Domain', 'Gateway'] diff --git a/apps/assets/models/label.py b/apps/assets/models/label.py index 458f3077d..c81726425 100644 --- a/apps/assets/models/label.py +++ b/apps/assets/models/label.py @@ -4,7 +4,7 @@ import uuid from django.db import models from django.utils.translation import ugettext_lazy as _ -from orgs.mixins import OrgModelMixin +from orgs.mixins.models import OrgModelMixin class Label(OrgModelMixin): diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index aab5ebaf4..3694f9f7d 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -2,6 +2,7 @@ # import uuid import re +import time from django.db import models, transaction from django.db.models import Q @@ -9,10 +10,11 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext from django.core.cache import cache -from orgs.mixins import OrgModelMixin, OrgManager +from orgs.mixins.models import OrgModelMixin, OrgManager from orgs.utils import set_current_org, get_current_org from orgs.models import Organization + __all__ = ['Node'] @@ -21,58 +23,81 @@ class NodeQuerySet(models.QuerySet): raise PermissionError("Bulk delete node deny") +class TreeMixin: + time_tree_updated = None + time_tree_updated_cache_key = 'NODE_TREE_CREATED_AT' + tree_cache_time = 3600 + _tree_service = None + + @classmethod + def tree(cls): + # Todo: 有待优化, 因为每次刷新都会导致其他节点的tree失效 + # Todo: ungroup node + # TOdo: 游离的资产,在树上显示的数量不对 + # Todo: api key页面有bug + from ..utils import TreeService + cache_updated_time = cls.get_cache_time() + if not cls.time_tree_updated or \ + cache_updated_time != cls.time_tree_updated: + t = TreeService.new() + cls.update_cache_tree(t) + return t + return cls._tree_service + + @classmethod + def get_cache_time(cls): + return cache.get(cls.time_tree_updated_cache_key) + + @classmethod + def update_cache_tree(cls, t): + cls._tree_service = t + now = time.time() + cls.time_tree_updated = now + cache.set(cls.time_tree_updated_cache_key, now, cls.tree_cache_time) + + @classmethod + def expire_cache_tree(cls): + cache.delete(cls.time_tree_updated_cache_key) + + @classmethod + def refresh_tree(cls): + cls.expire_cache_tree() + + @property + def _tree(self): + return self.__class__.tree() + + class FamilyMixin: - _parents = None - _children = None - _all_children = None + __parents = None + __children = None + __all_children = None is_node = True @property def children(self): - if self._children: - return self._children - pattern = r'^{0}:[0-9]+$'.format(self.key) - return Node.objects.filter(key__regex=pattern) - - @children.setter - def children(self, value): - self._children = value + return self.get_children(with_self=False) @property def all_children(self): - if self._all_children: - return self._all_children - pattern = r'^{0}:'.format(self.key) - return Node.objects.filter( - key__regex=pattern - ) + return self.get_all_children(with_self=False) def get_children(self, with_self=False): - children = list(self.children) + pattern = r'^{0}:[0-9]+$'.format(self.key) if with_self: - children.append(self) - return children + pattern += r'|^{0}$'.format(self.key) + return Node.objects.filter(key__regex=pattern) def get_all_children(self, with_self=False): - children = self.all_children + pattern = r'^{0}:'.format(self.key) if with_self: - children = list(children) - children.append(self) + pattern += r'|^{0}$'.format(self.key) + children = Node.objects.filter(key__regex=pattern) return children @property def parents(self): - if self._parents: - return self._parents - ancestor_keys = self.get_ancestor_keys() - ancestor = Node.objects.filter( - key__in=ancestor_keys - ).order_by('key') - return ancestor - - @parents.setter - def parents(self, value): - self._parents = value + return self.get_ancestor(with_self=False) def get_ancestor(self, with_self=False): parents = self.parents @@ -83,15 +108,10 @@ class FamilyMixin: @property def parent(self): - if self._parents: - return self._parents[0] if self.is_root(): return self - try: - parent = Node.objects.get(key=self.parent_key) - return parent - except Node.DoesNotExist: - return Node.root() + parent_key = self.parent_key + return Node.objects.get(key=parent_key) @parent.setter def parent(self, parent): @@ -107,7 +127,7 @@ class FamilyMixin: child.save() self.save() - def get_sibling(self, with_self=False): + def get_siblings(self, with_self=False): key = ':'.join(self.key.split(':')[:-1]) pattern = r'^{}:[0-9]+$'.format(key) sibling = Node.objects.filter( @@ -133,12 +153,11 @@ class FamilyMixin: return parent_keys def is_children(self, other): - pattern = re.compile(r'^{0}:[0-9]+$'.format(self.key)) - return pattern.match(other.key) + pattern = r'^{0}:[0-9]+$'.format(self.key) + return re.match(pattern, other.key) def is_parent(self, other): - pattern = re.compile(r'^{0}:[0-9]+$'.format(other.key)) - return pattern.match(self.key) + return other.is_children(self) @property def parent_key(self): @@ -158,46 +177,27 @@ class FamilyMixin: class FullValueMixin: - _full_value_cache_key = '_NODE_VALUE_{}' - _full_value = '' + _full_value = None key = '' @property def full_value(self): - if self._full_value: - return self._full_value - key = self._full_value_cache_key.format(self.key) - cached = cache.get(key) - if cached: - return cached if self.is_root(): return self.value - parent_full_value = self.parent.full_value - value = parent_full_value + ' / ' + self.value - self.full_value = value + if self._full_value is not None: + return self._full_value + print("Get full value") + value = self._tree.get_node_full_tag(self.key) return value - @full_value.setter - def full_value(self, value): - self._full_value = value - key = self._full_value_cache_key.format(self.key) - cache.set(key, value, 3600*24) - def expire_full_value(self): - key = self._full_value_cache_key.format(self.key) - cache.delete_pattern(key+'*') - - @classmethod - def expire_nodes_full_value(cls, nodes=None): - key = cls._full_value_cache_key.format('*') - cache.delete_pattern(key+'*') - - -class AssetsAmountMixin: +class NodeAssetsMixin: _assets_amount_cache_key = '_NODE_ASSETS_AMOUNT_{}' + _assets_cache_key = '_NODE_ASSETS_{}' _assets_amount = None key = '' cache_time = 3600 * 24 * 7 + id = None @property def assets_amount(self): @@ -207,40 +207,37 @@ class AssetsAmountMixin: """ if self._assets_amount is not None: return self._assets_amount - cache_key = self._assets_amount_cache_key.format(self.key) - cached = cache.get(cache_key) - if cached is not None: - return cached - assets_amount = self.get_all_assets().count() - self.assets_amount = assets_amount - return assets_amount + amount = self._tree.assets_amount(self.key) + return amount - @assets_amount.setter - def assets_amount(self, value): - self._assets_amount = value - cache_key = self._assets_amount_cache_key.format(self.key) - cache.set(cache_key, value, self.cache_time) + # TOdo: 是否依赖tree + def get_all_assets(self): + from .asset import Asset + if self.is_root(): + return Asset.objects.filter(org_id=self.org_id) + assets_ids = self._tree.all_assets(self.key) + return Asset.objects.filter(id__in=assets_ids) - def expire_assets_amount(self): - ancestor_keys = self.get_ancestor_keys(with_self=True) - cache_keys = [self._assets_amount_cache_key.format(k) for k in - ancestor_keys] - cache.delete_many(cache_keys) + def assets_ids(self): + assets_ids = self._tree.assets(self.key) + return assets_ids - @classmethod - def expire_nodes_assets_amount(cls, nodes=None): - key = cls._assets_amount_cache_key.format('*') - cache.delete_pattern(key) + def get_assets(self): + from .asset import Asset + if self.is_default_node(): + assets = Asset.objects.filter(Q(nodes__id=self.id) | Q(nodes__isnull=True)) + else: + assets = Asset.objects.filter(id=self.assets_ids()) + return assets.distinct() - @classmethod - def refresh_nodes(cls): - from ..utils import NodeUtil - util = NodeUtil(with_assets_amount=True) - util.set_assets_amount() - util.set_full_value() + def get_valid_assets(self): + return self.get_assets().valid() + + def get_all_valid_assets(self): + return self.get_all_assets().valid() -class Node(OrgModelMixin, FamilyMixin, FullValueMixin, AssetsAmountMixin): +class Node(OrgModelMixin, TreeMixin, FamilyMixin, FullValueMixin, NodeAssetsMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) key = models.CharField(unique=True, max_length=64, verbose_name=_("Key")) # '1:1:1:1' value = models.CharField(max_length=128, verbose_name=_("Value")) @@ -256,7 +253,7 @@ class Node(OrgModelMixin, FamilyMixin, FullValueMixin, AssetsAmountMixin): ordering = ['key'] def __str__(self): - return self.full_value + return self.value def __eq__(self, other): if not other: @@ -316,31 +313,9 @@ class Node(OrgModelMixin, FamilyMixin, FullValueMixin, AssetsAmountMixin): child = self.__class__.objects.create(id=_id, key=child_key, value=value) return child - def get_assets(self): - from .asset import Asset - if self.is_default_node(): - assets = Asset.objects.filter(Q(nodes__id=self.id) | Q(nodes__isnull=True)) - else: - assets = Asset.objects.filter(nodes__id=self.id) - return assets.distinct() - - def get_valid_assets(self): - return self.get_assets().valid() - - def get_all_assets(self): - from .asset import Asset - pattern = r'^{0}$|^{0}:'.format(self.key) - args = [] - kwargs = {} - if self.is_root(): - args.append(Q(nodes__key__regex=pattern) | Q(nodes=None)) - else: - kwargs['nodes__key__regex'] = pattern - assets = Asset.objects.filter(*args, **kwargs).distinct() - return assets - - def get_all_valid_assets(self): - return self.get_all_assets().valid() + @classmethod + def refresh_nodes(cls): + cls.refresh_tree() def is_default_node(self): return self.is_root() and self.key == '1' @@ -410,19 +385,20 @@ class Node(OrgModelMixin, FamilyMixin, FullValueMixin, AssetsAmountMixin): return return super().delete(using=using, keep_parents=keep_parents) - @classmethod - def get_queryset(cls): - from ..utils import NodeUtil - util = NodeUtil() - return sorted(util.nodes) - @classmethod def generate_fake(cls, count=100): import random org = get_current_org() if not org or not org.is_real(): Organization.default().change_to() + i = 0 + while i < count: + nodes = list(cls.objects.all()) + if count > 100: + length = 100 + else: + length = count - for i in range(count): - node = random.choice(cls.objects.all()) - node.create_child('Node {}'.format(i)) + for i in range(length): + node = random.choice(nodes) + node.create_child('Node {}'.format(i)) diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 0f6278f30..61a353e67 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -31,7 +31,7 @@ class AdminUser(AssetUser): become = models.BooleanField(default=True) become_method = models.CharField(choices=BECOME_METHOD_CHOICES, default='sudo', max_length=4) become_user = models.CharField(default='root', max_length=64) - _become_pass = models.CharField(default='', max_length=128) + _become_pass = models.CharField(default='', blank=True, max_length=128) CONNECTIVITY_CACHE_KEY = '_ADMIN_USER_CONNECTIVE_{}' _prefer = "admin_user" diff --git a/apps/assets/serializers/admin_user.py b/apps/assets/serializers/admin_user.py index 0efc09801..918f1d9bf 100644 --- a/apps/assets/serializers/admin_user.py +++ b/apps/assets/serializers/admin_user.py @@ -6,7 +6,7 @@ from rest_framework import serializers from common.serializers import AdaptedBulkListSerializer from ..models import Node, AdminUser -from orgs.mixins import BulkOrgResourceModelSerializer +from orgs.mixins.serializers import BulkOrgResourceModelSerializer from .base import AuthSerializer, AuthSerializerMixin diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index 0207f4e7d..f67521c3d 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -4,7 +4,7 @@ from rest_framework import serializers from django.db.models import Prefetch from django.utils.translation import ugettext_lazy as _ -from orgs.mixins import BulkOrgResourceModelSerializer +from orgs.mixins.serializers import BulkOrgResourceModelSerializer from common.serializers import AdaptedBulkListSerializer from ..models import Asset, Node, Label from .base import ConnectivitySerializer diff --git a/apps/assets/serializers/asset_user.py b/apps/assets/serializers/asset_user.py index 99497c593..0e342e8b2 100644 --- a/apps/assets/serializers/asset_user.py +++ b/apps/assets/serializers/asset_user.py @@ -5,7 +5,7 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers from common.serializers import AdaptedBulkListSerializer -from orgs.mixins import BulkOrgResourceModelSerializer +from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import AuthBook, Asset from ..backends import AssetUserManager from .base import ConnectivitySerializer, AuthSerializerMixin diff --git a/apps/assets/serializers/cmd_filter.py b/apps/assets/serializers/cmd_filter.py index 1b90ce439..6559a8189 100644 --- a/apps/assets/serializers/cmd_filter.py +++ b/apps/assets/serializers/cmd_filter.py @@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _ from common.fields import ChoiceDisplayField from common.serializers import AdaptedBulkListSerializer from ..models import CommandFilter, CommandFilterRule, SystemUser -from orgs.mixins import BulkOrgResourceModelSerializer +from orgs.mixins.serializers import BulkOrgResourceModelSerializer class CommandFilterSerializer(BulkOrgResourceModelSerializer): diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index 68feccb41..630941860 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -3,7 +3,7 @@ from rest_framework import serializers from common.serializers import AdaptedBulkListSerializer -from orgs.mixins import BulkOrgResourceModelSerializer +from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import Domain, Gateway from .base import AuthSerializerMixin diff --git a/apps/assets/serializers/label.py b/apps/assets/serializers/label.py index a20c43a11..de5ab7ea2 100644 --- a/apps/assets/serializers/label.py +++ b/apps/assets/serializers/label.py @@ -3,7 +3,7 @@ from rest_framework import serializers from common.serializers import AdaptedBulkListSerializer -from orgs.mixins import BulkOrgResourceModelSerializer +from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import Label diff --git a/apps/assets/serializers/node.py b/apps/assets/serializers/node.py index 29c2d7f55..7be8b9d47 100644 --- a/apps/assets/serializers/node.py +++ b/apps/assets/serializers/node.py @@ -2,7 +2,7 @@ from rest_framework import serializers from django.utils.translation import ugettext as _ -from orgs.mixins import BulkOrgResourceModelSerializer +from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import Asset, Node @@ -13,22 +13,21 @@ __all__ = [ class NodeSerializer(BulkOrgResourceModelSerializer): - assets_amount = serializers.IntegerField(read_only=True) name = serializers.ReadOnlyField(source='value') + value = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("value")) class Meta: model = Node only_fields = ['id', 'key', 'value', 'org_id'] - fields = only_fields + ['name', 'assets_amount'] - read_only_fields = [ - 'key', 'name', 'assets_amount', 'org_id', - ] + fields = only_fields + ['name', 'full_value'] + read_only_fields = ['key', 'org_id'] def validate_value(self, data): - instance = self.instance if self.instance else Node.root() - children = instance.parent.get_children() - children_values = [node.value for node in children if node != instance] - if data in children_values: + if not self.instance and not data: + return data + instance = self.instance + siblings = instance.get_siblings() + if siblings.filter(value=data): raise serializers.ValidationError( _('The same level node name cannot be the same') ) diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index e29e79f2c..eea93360b 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _ from common.serializers import AdaptedBulkListSerializer from common.utils import ssh_pubkey_gen -from orgs.mixins import BulkOrgResourceModelSerializer +from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import SystemUser from .base import AuthSerializer, AuthSerializerMixin diff --git a/apps/assets/signals_handler.py b/apps/assets/signals_handler.py index 70eecbd10..85b24e416 100644 --- a/apps/assets/signals_handler.py +++ b/apps/assets/signals_handler.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # from collections import defaultdict -from django.db.models.signals import post_save, m2m_changed, post_delete +from django.db.models.signals import post_save, m2m_changed, pre_delete from django.dispatch import receiver from common.utils import get_logger @@ -38,15 +38,13 @@ def on_asset_created_or_update(sender, instance=None, created=False, **kwargs): test_asset_conn_on_created(instance) # 过期节点资产数量 - nodes = instance.nodes.all() - Node.expire_nodes_assets_amount(nodes) + Node.refresh_nodes() -@receiver(post_delete, sender=Asset, dispatch_uid="my_unique_identifier") +@receiver(pre_delete, sender=Asset, dispatch_uid="my_unique_identifier") def on_asset_delete(sender, instance=None, **kwargs): # 过期节点资产数量 - nodes = instance.nodes.all() - Node.expire_nodes_assets_amount(nodes) + Node.refresh_nodes() @receiver(post_save, sender=SystemUser, dispatch_uid="my_unique_identifier") @@ -80,19 +78,18 @@ def on_asset_node_changed(sender, instance=None, **kwargs): logger.debug("Asset nodes change signal received") Asset.expire_all_nodes_keys_cache() if isinstance(instance, Asset): - if kwargs['action'] == 'pre_remove': - nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) - Node.expire_nodes_assets_amount(nodes) + # nodes = [] + # if kwargs['action'] == 'pre_remove': + # nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) if kwargs['action'] == 'post_add': nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) - Node.expire_nodes_assets_amount(nodes) system_users_assets = defaultdict(set) system_users = SystemUser.objects.filter(nodes__in=nodes) - # 清理节点缓存 for system_user in system_users: system_users_assets[system_user].update({instance}) for system_user, assets in system_users_assets.items(): system_user.assets.add(*tuple(assets)) + Node.refresh_nodes() @receiver(m2m_changed, sender=Asset.nodes.through) @@ -100,7 +97,6 @@ def on_node_assets_changed(sender, instance=None, **kwargs): if isinstance(instance, Node): logger.debug("Node assets change signal {} received".format(instance)) # 当节点和资产关系发生改变时,过期资产数量缓存 - instance.expire_assets_amount() assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) if kwargs['action'] == 'post_add': # 重新关联系统用户和资产的关系 @@ -112,7 +108,7 @@ def on_node_assets_changed(sender, instance=None, **kwargs): @receiver(post_save, sender=Node) def on_node_update_or_created(sender, instance=None, created=False, **kwargs): if instance and not created: - instance.expire_full_value() + Node.refresh_nodes() @receiver(post_save, sender=AuthBook) diff --git a/apps/assets/tasks.py b/apps/assets/tasks.py index 645b40c02..5fa7bc452 100644 --- a/apps/assets/tasks.py +++ b/apps/assets/tasks.py @@ -24,7 +24,7 @@ FORKS = 10 TIMEOUT = 60 logger = get_logger(__file__) CACHE_MAX_TIME = 60*60*2 -disk_pattern = re.compile(r'^hd|sd|xvd|vd') +disk_pattern = re.compile(r'^hd|sd|xvd|vd|nv') PERIOD_TASK = os.environ.get("PERIOD_TASK", "on") @@ -62,7 +62,7 @@ def clean_hosts_by_protocol(system_user, assets): return hosts -@shared_task +@shared_task(queue="ansible") def set_assets_hardware_info(assets, result, **kwargs): """ Using ops task run result, to update asset info @@ -106,7 +106,7 @@ def set_assets_hardware_info(assets, result, **kwargs): for dev, dev_info in info.get('ansible_devices', {}).items(): if disk_pattern.match(dev) and dev_info['removable'] == '0': disk_info[dev] = dev_info['size'] - ___disk_total = '%s %s' % sum_capacity(disk_info.values()) + ___disk_total = '%.1f %s' % sum_capacity(disk_info.values()) ___disk_info = json.dumps(disk_info) # ___platform = info.get('ansible_system', 'Unknown') @@ -148,7 +148,7 @@ def update_assets_hardware_info_util(assets, task_name=None): return result -@shared_task +@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( @@ -156,7 +156,7 @@ def update_asset_hardware_info_manual(asset): ) -@shared_task +@shared_task(queue="ansible") def update_assets_hardware_info_period(): """ Update asset hardware period task @@ -170,7 +170,7 @@ def update_assets_hardware_info_period(): ## ADMIN USER CONNECTIVE ## -@shared_task +@shared_task(queue="ansible") def test_asset_connectivity_util(assets, task_name=None): from ops.utils import update_or_create_ansible_task @@ -227,7 +227,7 @@ def test_asset_connectivity_util(assets, task_name=None): return results_summary -@shared_task +@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) @@ -238,7 +238,7 @@ def test_asset_connectivity_manual(asset): return True, "" -@shared_task +@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 @@ -254,7 +254,7 @@ def test_admin_user_connectivity_util(admin_user, task_name): return summary -@shared_task +@shared_task(queue="ansible") @register_as_period_task(interval=3600) def test_admin_user_connectivity_period(): """ @@ -276,7 +276,7 @@ def test_admin_user_connectivity_period(): cache.set(key, 1, 60*40) -@shared_task +@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) @@ -286,7 +286,7 @@ def test_admin_user_connectivity_manual(admin_user): ## System user connective ## -@shared_task +@shared_task(queue="ansible") def test_system_user_connectivity_util(system_user, assets, task_name): """ Test system cant connect his assets or not. @@ -344,14 +344,14 @@ def test_system_user_connectivity_util(system_user, assets, task_name): return results_summary -@shared_task +@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 +@shared_task(queue="ansible") def test_system_user_connectivity_a_asset(system_user, asset): task_name = _("Test system user connectivity: {} => {}").format( system_user, asset @@ -359,7 +359,7 @@ def test_system_user_connectivity_a_asset(system_user, asset): return test_system_user_connectivity_util(system_user, [asset], task_name) -@shared_task +@shared_task(queue="ansible") def test_system_user_connectivity_period(): if PERIOD_TASK != "on": logger.debug("Period task disabled, test system user connectivity pass") @@ -483,7 +483,7 @@ def get_push_system_user_tasks(host, system_user): return tasks -@shared_task +@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(): @@ -519,14 +519,14 @@ def push_system_user_util(system_user, assets, task_name): task.run() -@shared_task +@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 +@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 @@ -534,7 +534,7 @@ def push_system_user_a_asset_manual(system_user, asset): return push_system_user_util(system_user, [asset], task_name=task_name) -@shared_task +@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) @@ -569,7 +569,7 @@ def get_test_asset_user_connectivity_tasks(asset): return tasks -@shared_task +@shared_task(queue="ansible") def test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=False): """ :param asset_user: 对象 @@ -602,7 +602,7 @@ def test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=False) asset_user.set_connectivity(summary) -@shared_task +@shared_task(queue="ansible") def test_asset_users_connectivity_manual(asset_users, run_as_admin=False): """ :param asset_users: 对象 diff --git a/apps/assets/templates/assets/_node_tree.html b/apps/assets/templates/assets/_node_tree.html index 65e5b8f1e..cd64e5b85 100644 --- a/apps/assets/templates/assets/_node_tree.html +++ b/apps/assets/templates/assets/_node_tree.html @@ -236,7 +236,8 @@ function onBodyMouseDown(event){ } function onRename(event, treeId, treeNode, isCancel){ - var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id); + var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}" + .replace("{{ DEFAULT_PK }}", current_node_id); var data = {"value": treeNode.name}; if (isCancel){ return @@ -247,10 +248,13 @@ function onRename(event, treeId, treeNode, isCancel){ method: "PATCH", success_message: "{% trans 'Rename success' %}", success: function () { - treeNode.name = treeNode.name + ' (' + treeNode.meta.node.assets_amount + ')'; + var assets_amount = treeNode.meta.node.assets_amount; + if (!assets_amount) { + assets_amount = 0; + } + treeNode.name = treeNode.name + ' (' + assets_amount + ')'; zTree.updateNode(treeNode); - console.log("Success: " + treeNode.name) - } + }, }) } diff --git a/apps/assets/templates/assets/admin_user_detail.html b/apps/assets/templates/assets/admin_user_detail.html index 9e3365509..a44b3cbb5 100644 --- a/apps/assets/templates/assets/admin_user_detail.html +++ b/apps/assets/templates/assets/admin_user_detail.html @@ -88,9 +88,9 @@
- {% for node in nodes %} - + {% endfor %} @@ -140,7 +140,8 @@ function replaceNodeAssetsAdminUser(nodes) { jumpserver.nodes_selected = {}; $(document).ready(function () { - $('.select2').select2() + var url = "{% url 'api-assets:node-list' %}"; + nodesSelect2Init(".nodes-select2", url) .on('select2:select', function(evt) { var data = evt.params.data; jumpserver.nodes_selected[data.id] = data.text; diff --git a/apps/assets/templates/assets/asset_create.html b/apps/assets/templates/assets/asset_create.html index 6df7ff862..3a2f9929f 100644 --- a/apps/assets/templates/assets/asset_create.html +++ b/apps/assets/templates/assets/asset_create.html @@ -110,6 +110,8 @@ $(document).ready(function () { $('.select2').select2({ allowClear: true }); + var url = "{% url 'api-assets:node-list' %}"; + nodesSelect2Init(".nodes-select2", url); $(".labels").select2({ allowClear: true, templateSelection: format diff --git a/apps/assets/templates/assets/asset_detail.html b/apps/assets/templates/assets/asset_detail.html index 760cc7e6d..dea949bce 100644 --- a/apps/assets/templates/assets/asset_detail.html +++ b/apps/assets/templates/assets/asset_detail.html @@ -195,10 +195,7 @@ - @@ -211,7 +208,7 @@ {% for node in asset.nodes.all %} - {{ node }} + {{ node.full_value }} @@ -291,7 +288,9 @@ function refreshAssetHardware() { $(document).ready(function () { - $('.select2.groups').select2().on('select2:select', function(evt) { + var url = "{% url 'api-assets:node-list' %}"; + nodesSelect2Init(".nodes-select2", url) + .on('select2:select', function(evt) { var data = evt.params.data; jumpserver.nodes_selected[data.id] = data.text; }).on('select2:unselect', function(evt) { diff --git a/apps/assets/templates/assets/asset_list.html b/apps/assets/templates/assets/asset_list.html index 0f67a7709..9e994eba8 100644 --- a/apps/assets/templates/assets/asset_list.html +++ b/apps/assets/templates/assets/asset_list.html @@ -442,9 +442,10 @@ $(document).ready(function(){ var success = function () { asset_table.ajax.reload() }; + var url = "{% url 'api-assets:node-remove-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id); requestApi({ - 'url': '/api/assets/v1/nodes/' + current_node_id + '/assets/remove/', + 'url': url, 'method': 'PUT', 'body': JSON.stringify(data), 'success': success diff --git a/apps/assets/templates/assets/system_user_assets.html b/apps/assets/templates/assets/system_user_assets.html index 5818e4ce6..4cad3fd4a 100644 --- a/apps/assets/templates/assets/system_user_assets.html +++ b/apps/assets/templates/assets/system_user_assets.html @@ -88,10 +88,7 @@ - @@ -104,7 +101,7 @@ {% for node in system_user.nodes.all|sort %} - {{ node }} + {{ node.full_value }} @@ -156,6 +153,8 @@ jumpserver.nodes_selected = {}; $(document).ready(function () { $('.select2').select2() + var url = "{% url 'api-assets:node-list' %}"; + nodesSelect2Init(".nodes-select2", url) .on('select2:select', function(evt) { var data = evt.params.data; jumpserver.nodes_selected[data.id] = data.text; diff --git a/apps/assets/templates/assets/user_asset_list.html b/apps/assets/templates/assets/user_asset_list.html index 4f0b6e671..1a134cb18 100644 --- a/apps/assets/templates/assets/user_asset_list.html +++ b/apps/assets/templates/assets/user_asset_list.html @@ -21,9 +21,10 @@ {% block custom_foot_js %} +{% endblock %} +{% block modal_button %} + +{% endblock %} diff --git a/apps/authentication/templates/authentication/new_login.html b/apps/authentication/templates/authentication/new_login.html index 4a62dd6d0..ff61b981c 100644 --- a/apps/authentication/templates/authentication/new_login.html +++ b/apps/authentication/templates/authentication/new_login.html @@ -18,7 +18,7 @@ - + diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index 85f3aa522..68dd8eeaa 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -4,18 +4,27 @@ from __future__ import absolute_import from django.urls import path +from rest_framework.routers import DefaultRouter from .. import api +router = DefaultRouter() +router.register('access-keys', api.AccessKeyViewSet, 'access-key') + + app_name = 'authentication' urlpatterns = [ # path('token/', api.UserToken.as_view(), name='user-token'), path('auth/', api.UserAuthApi.as_view(), name='user-auth'), + path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'), + path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'), path('connection-token/', api.UserConnectionTokenApi.as_view(), name='connection-token'), path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'), path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'), ] +urlpatterns += router.urls + diff --git a/apps/authentication/utils.py b/apps/authentication/utils.py index d070c594c..70c7e52fa 100644 --- a/apps/authentication/utils.py +++ b/apps/authentication/utils.py @@ -1,7 +1,11 @@ # -*- coding: utf-8 -*- # from django.utils.translation import ugettext as _ -from common.utils import get_ip_city, validate_ip +from django.contrib.auth import authenticate + +from common.utils import get_ip_city, get_object_or_none, validate_ip +from users.models import User +from . import const def write_login_log(*args, **kwargs): @@ -16,3 +20,36 @@ def write_login_log(*args, **kwargs): kwargs.update({'ip': ip, 'city': city}) UserLoginLog.objects.create(**kwargs) + +def check_user_valid(**kwargs): + password = kwargs.pop('password', None) + public_key = kwargs.pop('public_key', None) + email = kwargs.pop('email', None) + username = kwargs.pop('username', None) + + if username: + user = get_object_or_none(User, username=username) + elif email: + user = get_object_or_none(User, email=email) + else: + user = None + + if user is None: + return None, const.user_not_exist + elif not user.is_valid: + return None, const.user_invalid + elif user.password_has_expired: + return None, const.password_expired + + if password and authenticate(username=username, password=password): + return user, '' + + if public_key and user.public_key: + public_key_saved = user.public_key.split() + if len(public_key_saved) == 1: + if public_key == public_key_saved[0]: + return user, '' + elif len(public_key_saved) > 1: + if public_key == public_key_saved[1]: + return user, '' + return None, const.password_failed diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index 0939370f1..2a7098ae7 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -26,6 +26,7 @@ from users.utils import ( ) from ..signals import post_auth_success, post_auth_failed from .. import forms +from .. import const __all__ = [ @@ -81,7 +82,7 @@ class UserLoginView(FormView): user = form.get_user() # user password expired if user.password_has_expired: - reason = LoginLog.REASON_PASSWORD_EXPIRED + reason = const.password_expired self.send_auth_signal(success=False, username=user.username, reason=reason) return self.render_to_response(self.get_context_data(password_expired=True)) @@ -96,7 +97,7 @@ class UserLoginView(FormView): # write login failed log username = form.cleaned_data.get('username') exist = User.objects.filter(username=username).first() - reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST + reason = const.password_failed if exist else const.user_not_exist # limit user login failed count ip = get_request_ip(self.request) increase_login_failed_count(username, ip) @@ -167,7 +168,7 @@ class UserLoginOtpView(FormView): else: self.send_auth_signal( success=False, username=user.username, - reason=LoginLog.REASON_MFA + reason=const.mfa_failed ) form.add_error( 'otp_code', _('MFA code invalid, or ntp sync server time') diff --git a/apps/common/auth/__init__.py b/apps/common/auth/__init__.py new file mode 100644 index 000000000..ec51c5a2b --- /dev/null +++ b/apps/common/auth/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# diff --git a/apps/common/auth/signature.py b/apps/common/auth/signature.py new file mode 100644 index 000000000..793d2a639 --- /dev/null +++ b/apps/common/auth/signature.py @@ -0,0 +1,112 @@ +from rest_framework import authentication +from rest_framework import exceptions + +from httpsig import HeaderVerifier, utils + +""" +Reusing failure exceptions serves several purposes: + + 1. Lack of useful information regarding the failure inhibits attackers + from learning about valid keyIDs or other forms of information leakage. + Using the same actual object for any failure makes preventing such + leakage through mistakenly-distinct error messages less likely. + + 2. In an API scenario, the object is created once and raised many times + rather than generated on every failure, which could lead to higher loads + or memory usage in high-volume attack scenarios. + +""" +FAILED = exceptions.AuthenticationFailed('Invalid signature.') + + +class SignatureAuthentication(authentication.BaseAuthentication): + """ + DRF authentication class for HTTP Signature support. + + You must subclass this class in your own project and implement the + `fetch_user_data(self, keyId, algorithm)` method, returning a tuple of + the User object and a bytes object containing the user's secret. Note + that key_id and algorithm are DIRTY as they are supplied by the client + and so must be verified in your subclass! + + You may set the following class properties in your subclass to configure + authentication for your particular use case: + + :param www_authenticate_realm: Default: "api" + :param required_headers: Default: ["(request-target)", "date"] + """ + + www_authenticate_realm = "api" + required_headers = ["(request-target)", "date"] + + def fetch_user_data(self, key_id, algorithm=None): + """Retuns a tuple (User, secret) or (None, None).""" + raise NotImplementedError() + + def authenticate_header(self, request): + """ + DRF sends this for unauthenticated responses if we're the primary + authenticator. + """ + h = " ".join(self.required_headers) + return 'Signature realm="%s",headers="%s"' % ( + self.www_authenticate_realm, h) + + def authenticate(self, request): + """ + Perform the actual authentication. + + Note that the exception raised is always the same. This is so that we + don't leak information about in/valid keyIds and other such useful + things. + """ + auth_header = authentication.get_authorization_header(request) + if not auth_header or len(auth_header) == 0: + return None + + method, fields = utils.parse_authorization_header(auth_header) + + # Ignore foreign Authorization headers. + if method.lower() != 'signature': + return None + + # Verify basic header structure. + if len(fields) == 0: + raise FAILED + + # Ensure all required fields were included. + if len({"keyid", "algorithm", "signature"} - set(fields.keys())) > 0: + raise FAILED + + # Fetch the secret associated with the keyid + user, secret = self.fetch_user_data( + fields["keyid"], + algorithm=fields["algorithm"] + ) + + if not (user and secret): + raise FAILED + + # Gather all request headers and translate them as stated in the Django docs: + # https://docs.djangoproject.com/en/1.6/ref/request-response/#django.http.HttpRequest.META + headers = {} + for key in request.META.keys(): + if key.startswith("HTTP_") or \ + key in ("CONTENT_TYPE", "CONTENT_LENGTH"): + header = key[5:].lower().replace('_', '-') + headers[header] = request.META[key] + + # Verify headers + hs = HeaderVerifier( + headers, + secret, + required_headers=self.required_headers, + method=request.method.lower(), + path=request.get_full_path() + ) + + # All of that just to get to this. + if not hs.verify(): + raise FAILED + + return user, fields["keyid"] diff --git a/apps/common/serializers.py b/apps/common/serializers.py index 7f2abce92..eb657b4cc 100644 --- a/apps/common/serializers.py +++ b/apps/common/serializers.py @@ -1,9 +1,14 @@ # -*- coding: utf-8 -*- # -from .mixins import BulkListSerializerMixin from rest_framework_bulk.serializers import BulkListSerializer +from rest_framework import serializers +from .mixins import BulkListSerializerMixin class AdaptedBulkListSerializer(BulkListSerializerMixin, BulkListSerializer): pass + + +class CeleryTaskSerializer(serializers.Serializer): + task = serializers.CharField(read_only=True) diff --git a/apps/common/tasks.py b/apps/common/tasks.py index 465f46eb1..912ef28c3 100644 --- a/apps/common/tasks.py +++ b/apps/common/tasks.py @@ -29,6 +29,6 @@ def send_mail_async(*args, **kwargs): args = tuple(args) try: - send_mail(*args, **kwargs) + return send_mail(*args, **kwargs) except Exception as e: logger.error("Sending mail error: {}".format(e)) diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index 0000d35b4..a92af5f06 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -31,6 +31,10 @@ def get_logger(name=None): return logging.getLogger('jumpserver.%s' % name) +def get_syslogger(name=None): + return logging.getLogger('jms.%s' % name) + + def timesince(dt, since='', default="just now"): """ Returns string representing "time since" e.g. diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 4f3c3992a..a7fb5fcb5 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -379,6 +379,8 @@ defaults = { 'ASSETS_PERM_CACHE_TIME': 3600*24, 'SECURITY_MFA_VERIFY_TTL': 3600, 'ASSETS_PERM_CACHE_ENABLE': False, + 'SYSLOG_ADDR': '', # '192.168.0.1:514' + 'SYSLOG_FACILITY': 'user', 'PERM_SINGLE_ASSET_TO_UNGROUP_NODE': False, } diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index a4d8e0796..803c62b83 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -217,6 +217,9 @@ LOGGING = { 'simple': { 'format': '%(levelname)s %(message)s' }, + 'syslog': { + 'format': 'jumpserver: %(message)s' + }, 'msg': { 'format': '%(message)s' } @@ -249,19 +252,10 @@ LOGGING = { 'backupCount': 7, 'filename': ANSIBLE_LOG_FILE, }, - 'gunicorn_file': { - 'encoding': 'utf8', - 'level': 'DEBUG', - 'class': 'logging.handlers.RotatingFileHandler', - 'formatter': 'msg', - 'maxBytes': 1024*1024*100, - 'backupCount': 2, - 'filename': GUNICORN_LOG_FILE, - }, - 'gunicorn_console': { - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'formatter': 'msg' + 'syslog': { + 'level': 'INFO', + 'class': 'logging.NullHandler', + 'formatter': 'syslog' }, }, 'loggers': { @@ -271,25 +265,17 @@ LOGGING = { 'level': LOG_LEVEL, }, 'django.request': { - 'handlers': ['console', 'file'], + 'handlers': ['console', 'file', 'syslog'], 'level': LOG_LEVEL, 'propagate': False, }, 'django.server': { - 'handlers': ['console', 'file'], + 'handlers': ['console', 'file', 'syslog'], 'level': LOG_LEVEL, 'propagate': False, }, 'jumpserver': { - 'handlers': ['console', 'file'], - 'level': LOG_LEVEL, - }, - 'jumpserver.users.api': { - 'handlers': ['console', 'file'], - 'level': LOG_LEVEL, - }, - 'jumpserver.users.view': { - 'handlers': ['console', 'file'], + 'handlers': ['console', 'file', 'syslog'], 'level': LOG_LEVEL, }, 'ops.ansible_api': { @@ -300,17 +286,28 @@ LOGGING = { 'handlers': ['console', 'file'], 'level': "INFO", }, - 'gunicorn': { - 'handlers': ['gunicorn_console', 'gunicorn_file'], - 'level': 'INFO', + 'jms_audits': { + 'handlers': ['syslog'], + 'level': 'INFO' }, - # 'django.db': { - # 'handlers': ['console', 'file'], - # 'level': 'DEBUG' - # } + 'django.db': { + 'handlers': ['console', 'file'], + 'level': 'DEBUG' + } } } +SYSLOG_ENABLE = False + +if CONFIG.SYSLOG_ADDR != '' and len(CONFIG.SYSLOG_ADDR.split(':')) == 2: + host, port = CONFIG.SYSLOG_ADDR.split(':') + SYSLOG_ENABLE = True + LOGGING['handlers']['syslog'].update({ + 'class': 'logging.handlers.SysLogHandler', + 'facility': CONFIG.SYSLOG_FACILITY, + 'address': (host, int(port)), + }) + # Internationalization # https://docs.djangoproject.com/en/1.10/topics/i18n/ # LANGUAGE_CODE = 'en' @@ -391,6 +388,7 @@ REST_FRAMEWORK = { 'authentication.backends.api.AccessKeyAuthentication', 'authentication.backends.api.AccessTokenAuthentication', 'authentication.backends.api.PrivateTokenAuthentication', + 'authentication.backends.api.SignatureAuthentication', 'authentication.backends.api.SessionAuthentication', ), 'DEFAULT_FILTER_BACKENDS': ( @@ -403,7 +401,7 @@ REST_FRAMEWORK = { 'SEARCH_PARAM': "search", 'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S %z', 'DATETIME_INPUT_FORMATS': ['iso-8601', '%Y-%m-%d %H:%M:%S %z'], - # 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', # 'PAGE_SIZE': 15 } @@ -601,9 +599,12 @@ USER_GUIDE_URL = "" SWAGGER_SETTINGS = { 'DEFAULT_AUTO_SCHEMA_CLASS': 'jumpserver.swagger.CustomSwaggerAutoSchema', + 'USE_SESSION_AUTH': True, 'SECURITY_DEFINITIONS': { - 'basic': { - 'type': 'basic' + 'Bearer': { + 'type': 'apiKey', + 'name': 'Authorization', + 'in': 'header' } }, } diff --git a/apps/jumpserver/swagger.py b/apps/jumpserver/swagger.py index bd7662b46..eb8d89bf7 100644 --- a/apps/jumpserver/swagger.py +++ b/apps/jumpserver/swagger.py @@ -7,13 +7,44 @@ from drf_yasg import openapi class CustomSwaggerAutoSchema(SwaggerAutoSchema): def get_tags(self, operation_keys): - if len(operation_keys) > 2 and operation_keys[1].startswith('v'): - return [operation_keys[2]] + if len(operation_keys) > 2: + return [operation_keys[0] + '_' + operation_keys[1]] return super().get_tags(operation_keys) + def get_operation_id(self, operation_keys): + action = '' + dump_keys = [k for k in operation_keys] + if hasattr(self.view, 'action'): + action = self.view.action + if action == "bulk_destroy": + action = "bulk_delete" + if dump_keys[-2] == "children": + if self.path.find('id') < 0: + dump_keys.insert(-2, "root") + if dump_keys[0] == "perms" and dump_keys[1] == "users": + if self.path.find('{id}') < 0: + dump_keys.insert(2, "my") + if action.replace('bulk_', '') == dump_keys[-1]: + dump_keys[-1] = action + return super().get_operation_id(tuple(dump_keys)) + + def get_operation(self, operation_keys): + operation = super().get_operation(operation_keys) + operation.summary = operation.operation_id + return operation + def get_swagger_view(version='v1'): - from .urls import api_v1_patterns, api_v2_patterns + from .urls import api_v1, api_v2 + from django.urls import path, include + api_v1_patterns = [ + path('api/v1/', include(api_v1)) + ] + + api_v2_patterns = [ + path('api/v2/', include(api_v2)) + ] + if version == "v2": patterns = api_v2_patterns else: diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index f13bb667a..29168ab6c 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -7,26 +7,26 @@ from django.conf.urls.static import static from django.conf.urls.i18n import i18n_patterns from django.views.i18n import JavaScriptCatalog -from .views import IndexView, LunaView, I18NView, HealthCheckView +from .views import IndexView, LunaView, I18NView, HealthCheckView, redirect_format_api from .swagger import get_swagger_view api_v1 = [ - path('users/v1/', include('users.urls.api_urls', namespace='api-users')), - path('assets/v1/', include('assets.urls.api_urls', namespace='api-assets')), - path('perms/v1/', include('perms.urls.api_urls', namespace='api-perms')), - path('terminal/v1/', include('terminal.urls.api_urls', namespace='api-terminal')), - path('ops/v1/', include('ops.urls.api_urls', namespace='api-ops')), - path('audits/v1/', include('audits.urls.api_urls', namespace='api-audits')), - path('orgs/v1/', include('orgs.urls.api_urls', namespace='api-orgs')), - path('settings/v1/', include('settings.urls.api_urls', namespace='api-settings')), - path('authentication/v1/', include('authentication.urls.api_urls', namespace='api-auth')), - path('common/v1/', include('common.urls.api_urls', namespace='api-common')), - path('applications/v1/', include('applications.urls.api_urls', namespace='api-applications')), + path('users/', include('users.urls.api_urls', namespace='api-users')), + path('assets/', include('assets.urls.api_urls', namespace='api-assets')), + path('perms/', include('perms.urls.api_urls', namespace='api-perms')), + path('terminal/', include('terminal.urls.api_urls', namespace='api-terminal')), + path('ops/', include('ops.urls.api_urls', namespace='api-ops')), + path('audits/', include('audits.urls.api_urls', namespace='api-audits')), + path('orgs/', include('orgs.urls.api_urls', namespace='api-orgs')), + path('settings/', include('settings.urls.api_urls', namespace='api-settings')), + path('authentication/', include('authentication.urls.api_urls', namespace='api-auth')), + path('common/', include('common.urls.api_urls', namespace='api-common')), + path('applications/', include('applications.urls.api_urls', namespace='api-applications')), ] api_v2 = [ - path('terminal/v2/', include('terminal.urls.api_urls_v2', namespace='api-terminal-v2')), - path('users/v2/', include('users.urls.api_urls_v2', namespace='api-users-v2')), + path('terminal/', include('terminal.urls.api_urls_v2', namespace='api-terminal-v2')), + path('users/', include('users.urls.api_urls_v2', namespace='api-users-v2')), ] @@ -48,30 +48,23 @@ if settings.XPACK_ENABLED: path('xpack/', include('xpack.urls.view_urls', namespace='xpack')) ) api_v1.append( - path('xpack/v1/', include('xpack.urls.api_urls', namespace='api-xpack')) + path('xpack/', include('xpack.urls.api_urls', namespace='api-xpack')) ) js_i18n_patterns = i18n_patterns( path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'), ) -api_v1_patterns = [ - path('api/', include(api_v1)) -] - -api_v2_patterns = [ - path('api/', include(api_v2)) -] urlpatterns = [ path('', IndexView.as_view(), name='index'), - path('', include(api_v2_patterns)), - path('', include(api_v1_patterns)), + path('api/v1/', include(api_v1)), + path('api/v2/', include(api_v2)), + re_path('api/(?P\w+)/(?Pv\d)/.*', redirect_format_api), path('api/health/', HealthCheckView.as_view(), name="health"), path('luna/', LunaView.as_view(), name='luna-view'), path('i18n//', I18NView.as_view(), name='i18n-switch'), path('settings/', include('settings.urls.view_urls', namespace='settings')), - # path('api/v2/', include(api_v2_patterns)), # External apps url path('captcha/', include('captcha.urls')), diff --git a/apps/jumpserver/views.py b/apps/jumpserver/views.py index f9d692f31..2afd4d420 100644 --- a/apps/jumpserver/views.py +++ b/apps/jumpserver/views.py @@ -2,14 +2,13 @@ import datetime import re import time -from django.http import HttpResponseRedirect +from django.http import HttpResponseRedirect, JsonResponse from django.conf import settings from django.views.generic import TemplateView, View from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from django.db.models import Count from django.shortcuts import redirect -from rest_framework.response import Response from rest_framework.views import APIView from django.views.decorators.csrf import csrf_exempt from django.http import HttpResponse @@ -208,7 +207,7 @@ class I18NView(View): return response -api_url_pattern = re.compile(r'^/api/(?P\w+)/(?P\w+)/(?P.*)$') +api_url_pattern = re.compile(r'^/api/(?P\w+)/(?Pv\d)/(?P.*)$') @csrf_exempt @@ -216,18 +215,16 @@ def redirect_format_api(request, *args, **kwargs): _path, query = request.path, request.GET.urlencode() matched = api_url_pattern.match(_path) if matched: - version, app, extra = matched.groups() - _path = '/api/{app}/{version}/{extra}?{query}'.format(**{ - "app": app, "version": version, "extra": extra, - "query": query - }) + kwargs = matched.groupdict() + kwargs["query"] = query + _path = '/api/{version}/{app}/{extra}?{query}'.format(**kwargs).rstrip("?") return HttpResponseTemporaryRedirect(_path) else: - return Response({"msg": "Redirect url failed: {}".format(_path)}, status=404) + return JsonResponse({"msg": "Redirect url failed: {}".format(_path)}, status=404) class HealthCheckView(APIView): permission_classes = () def get(self, request): - return Response({"status": 1, "time": int(time.time())}) + return JsonResponse({"status": 1, "time": int(time.time())}) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 2247e8f62..231f4766d 100644 Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index cc7e6b362..1ab0df5d2 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Jumpserver 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-07-31 16:35+0800\n" +"POT-Creation-Date: 2019-08-07 16:56+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: Jumpserver team\n" @@ -78,7 +78,7 @@ msgstr "运行参数" #: assets/forms/domain.py:15 assets/forms/label.py:13 #: assets/models/asset.py:318 assets/models/authbook.py:24 #: assets/serializers/admin_user.py:32 assets/serializers/asset_user.py:81 -#: assets/serializers/system_user.py:30 +#: 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 @@ -218,7 +218,7 @@ msgstr "参数" #: perms/models/asset_permission.py:117 perms/models/base.py:41 #: perms/templates/perms/asset_permission_detail.html:98 #: perms/templates/perms/remote_app_permission_detail.html:90 -#: users/models/user.py:372 users/serializers/v1.py:120 +#: users/models/user.py:372 users/serializers/v1.py:119 #: users/templates/users/user_detail.html:111 #: xpack/plugins/change_auth_plan/models.py:106 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:113 @@ -471,6 +471,7 @@ msgstr "更新" #: assets/templates/assets/label_list.html:40 #: assets/templates/assets/system_user_detail.html:30 #: assets/templates/assets/system_user_list.html:86 audits/models.py:34 +#: authentication/templates/authentication/_access_key_modal.html:65 #: ops/templates/ops/task_list.html:64 #: perms/templates/perms/asset_permission_detail.html:34 #: perms/templates/perms/asset_permission_list.html:179 @@ -527,6 +528,7 @@ msgstr "创建远程应用" #: assets/templates/assets/system_user_list.html:60 audits/models.py:38 #: audits/templates/audits/operate_log_list.html:41 #: audits/templates/audits/operate_log_list.html:67 +#: authentication/templates/authentication/_access_key_modal.html:30 #: ops/templates/ops/adhoc_history.html:59 ops/templates/ops/task_adhoc.html:64 #: ops/templates/ops/task_history.html:65 ops/templates/ops/task_list.html:34 #: perms/forms/asset_permission.py:21 @@ -579,15 +581,11 @@ msgstr "远程应用详情" msgid "My RemoteApp" msgstr "我的远程应用" -#: assets/api/asset.py:51 +#: assets/api/asset.py:42 #, python-format msgid "%(hostname)s was %(action)s successfully" msgstr "%(hostname)s %(action)s成功" -#: assets/api/asset.py:125 -msgid "Please select assets that need to be updated" -msgstr "请选择需要更新的资产" - #: assets/api/node.py:61 msgid "You can't update the root node name" msgstr "不能修改根节点名称" @@ -609,7 +607,7 @@ msgstr "不可达" msgid "Reachable" msgstr "可连接" -#: assets/const.py:79 assets/models/utils.py:45 authentication/utils.py:9 +#: assets/const.py:79 assets/models/utils.py:45 authentication/utils.py:13 #: xpack/plugins/license/models.py:78 msgid "Unknown" msgstr "未知" @@ -656,7 +654,7 @@ msgid "Domain" msgstr "网域" #: assets/forms/asset.py:58 assets/forms/asset.py:80 assets/forms/asset.py:93 -#: assets/forms/asset.py:128 assets/models/node.py:253 +#: assets/forms/asset.py:128 assets/models/node.py:254 #: assets/templates/assets/asset_create.html:42 #: perms/forms/asset_permission.py:72 perms/forms/asset_permission.py:79 #: perms/models/asset_permission.py:112 @@ -718,7 +716,7 @@ msgstr "SSH网关,支持代理SSH,RDP和VNC" #: assets/templates/assets/admin_user_list.html:45 #: assets/templates/assets/domain_gateway_list.html:71 #: assets/templates/assets/system_user_detail.html:62 -#: assets/templates/assets/system_user_list.html:52 audits/models.py:94 +#: assets/templates/assets/system_user_list.html:52 audits/models.py:80 #: audits/templates/audits/login_log_list.html:51 authentication/forms.py:13 #: authentication/templates/authentication/login.html:65 #: authentication/templates/authentication/new_login.html:92 @@ -1116,8 +1114,8 @@ 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:128 users/models/user.py:458 -#: users/serializers/v1.py:109 users/templates/users/user_group_detail.html:78 +#: users/models/user.py:127 users/models/user.py:458 +#: 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 #: xpack/plugins/orgs/forms.py:26 #: xpack/plugins/orgs/templates/orgs/org_detail.html:113 @@ -1125,7 +1123,7 @@ msgstr "默认资产组" msgid "User" msgstr "用户" -#: assets/models/label.py:19 assets/models/node.py:244 +#: assets/models/label.py:19 assets/models/node.py:245 #: assets/templates/assets/label_list.html:15 settings/models.py:30 msgid "Value" msgstr "值" @@ -1134,11 +1132,11 @@ msgstr "值" msgid "Category" msgstr "分类" -#: assets/models/node.py:243 +#: assets/models/node.py:244 msgid "Key" msgstr "键" -#: assets/models/node.py:301 +#: assets/models/node.py:302 msgid "New node" msgstr "新节点" @@ -1244,98 +1242,98 @@ msgstr "密钥不合法" msgid "The same level node name cannot be the same" msgstr "同级别节点名字不能重复" -#: assets/serializers/system_user.py:31 +#: assets/serializers/system_user.py:32 msgid "Login mode display" msgstr "登录模式显示" -#: assets/serializers/system_user.py:66 +#: assets/serializers/system_user.py:67 msgid "* Automatic login mode must fill in the username." msgstr "自动登录模式,必须填写用户名" -#: assets/serializers/system_user.py:75 +#: assets/serializers/system_user.py:78 msgid "Password or private key required" msgstr "密码或密钥密码需要一个" -#: assets/tasks.py:34 +#: assets/tasks.py:33 msgid "Asset has been disabled, skipped: {}" msgstr "资产或许不支持ansible, 跳过: {}" -#: assets/tasks.py:38 +#: assets/tasks.py:37 msgid "Asset may not be support ansible, skipped: {}" msgstr "资产或许不支持ansible, 跳过: {}" -#: assets/tasks.py:51 +#: assets/tasks.py:50 msgid "No assets matched, stop task" msgstr "没有匹配到资产,结束任务" -#: assets/tasks.py:61 +#: assets/tasks.py:60 msgid "No assets matched related system user protocol, stop task" msgstr "没有匹配到与系统用户协议相关的资产,结束任务" -#: assets/tasks.py:87 +#: assets/tasks.py:86 msgid "Get asset info failed: {}" msgstr "获取资产信息失败:{}" -#: assets/tasks.py:137 +#: assets/tasks.py:136 msgid "Update some assets hardware info" msgstr "更新资产硬件信息" -#: assets/tasks.py:154 +#: assets/tasks.py:153 msgid "Update asset hardware info: {}" msgstr "更新资产硬件信息: {}" -#: assets/tasks.py:179 +#: assets/tasks.py:178 msgid "Test assets connectivity" msgstr "测试资产可连接性" -#: assets/tasks.py:233 +#: assets/tasks.py:232 msgid "Test assets connectivity: {}" msgstr "测试资产可连接性: {}" -#: assets/tasks.py:275 +#: assets/tasks.py:274 msgid "Test admin user connectivity period: {}" msgstr "定期测试管理账号可连接性: {}" -#: assets/tasks.py:282 +#: assets/tasks.py:281 msgid "Test admin user connectivity: {}" msgstr "测试管理行号可连接性: {}" -#: assets/tasks.py:350 +#: assets/tasks.py:349 msgid "Test system user connectivity: {}" msgstr "测试系统用户可连接性: {}" -#: assets/tasks.py:357 +#: assets/tasks.py:356 msgid "Test system user connectivity: {} => {}" msgstr "测试系统用户可连接性: {} => {}" -#: assets/tasks.py:370 +#: assets/tasks.py:369 msgid "Test system user connectivity period: {}" msgstr "定期测试系统用户可连接性: {}" -#: assets/tasks.py:479 assets/tasks.py:565 +#: assets/tasks.py:478 assets/tasks.py:564 #: xpack/plugins/change_auth_plan/models.py:522 msgid "The asset {} system platform {} does not support run Ansible tasks" msgstr "资产 {} 系统平台 {} 不支持运行 Ansible 任务" -#: assets/tasks.py:491 +#: assets/tasks.py:490 msgid "" "Push system user task skip, auto push not enable or protocol is not ssh or " "rdp: {}" msgstr "推送系统用户任务跳过,自动推送没有打开,或协议不是ssh或rdp: {}" -#: assets/tasks.py:498 +#: assets/tasks.py:497 msgid "For security, do not push user {}" msgstr "为了安全,禁止推送用户 {}" -#: assets/tasks.py:526 assets/tasks.py:540 +#: assets/tasks.py:525 assets/tasks.py:539 msgid "Push system users to assets: {}" msgstr "推送系统用户到入资产: {}" -#: assets/tasks.py:532 +#: assets/tasks.py:531 msgid "Push system users to asset: {} => {}" msgstr "推送系统用户到入资产: {} => {}" -#: assets/tasks.py:612 +#: assets/tasks.py:611 msgid "Test asset user connectivity: {}" msgstr "测试资产用户可连接性: {}" @@ -1418,6 +1416,7 @@ msgstr "获取认证信息错误" #: assets/templates/assets/_asset_user_auth_view_modal.html:97 #: assets/templates/assets/_user_asset_detail_modal.html:23 +#: authentication/templates/authentication/_access_key_modal.html:143 #: authentication/templates/authentication/_mfa_confirm_modal.html:53 #: settings/templates/settings/_ldap_list_users_modal.html:99 #: templates/_modal.html:22 @@ -1701,7 +1700,8 @@ msgstr "硬盘" msgid "Date joined" msgstr "创建日期" -#: assets/templates/assets/asset_detail.html:150 +#: assets/templates/assets/asset_detail.html:150 authentication/models.py:15 +#: authentication/templates/authentication/_access_key_modal.html:28 #: perms/models/asset_permission.py:115 perms/models/base.py:38 #: perms/templates/perms/asset_permission_create_update.html:55 #: perms/templates/perms/asset_permission_detail.html:120 @@ -2133,7 +2133,7 @@ msgstr "操作" msgid "Filename" msgstr "文件名" -#: audits/models.py:23 audits/models.py:90 +#: audits/models.py:23 audits/models.py:76 #: audits/templates/audits/ftp_log_list.html:76 #: ops/templates/ops/command_execution_list.html:65 #: ops/templates/ops/task_list.html:31 @@ -2143,7 +2143,9 @@ msgstr "文件名" msgid "Success" msgstr "成功" -#: audits/models.py:32 xpack/plugins/vault/templates/vault/vault.html:46 +#: audits/models.py:32 +#: authentication/templates/authentication/_access_key_modal.html:38 +#: xpack/plugins/vault/templates/vault/vault.html:46 msgid "Create" msgstr "创建" @@ -2169,55 +2171,39 @@ msgstr "禁用" msgid "Enabled" msgstr "启用" -#: audits/models.py:72 audits/models.py:82 +#: audits/models.py:72 msgid "-" msgstr "" -#: audits/models.py:83 -msgid "Username/password check failed" -msgstr "用户名/密码 校验失败" - -#: audits/models.py:84 -msgid "MFA authentication failed" -msgstr "MFA 认证失败" - -#: audits/models.py:85 -msgid "Username does not exist" -msgstr "用户名不存在" - -#: audits/models.py:86 -msgid "Password expired" -msgstr "密码过期" - -#: audits/models.py:91 xpack/plugins/cloud/models.py:267 +#: audits/models.py:77 xpack/plugins/cloud/models.py:267 #: xpack/plugins/cloud/models.py:290 msgid "Failed" msgstr "失败" -#: audits/models.py:95 +#: audits/models.py:81 msgid "Login type" msgstr "登录方式" -#: audits/models.py:96 +#: audits/models.py:82 msgid "Login ip" msgstr "登录IP" -#: audits/models.py:97 +#: audits/models.py:83 msgid "Login city" msgstr "登录城市" -#: audits/models.py:98 +#: audits/models.py:84 msgid "User agent" msgstr "Agent" -#: audits/models.py:99 audits/templates/audits/login_log_list.html:56 +#: audits/models.py:85 audits/templates/audits/login_log_list.html:56 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: users/forms.py:175 users/models/user.py:353 #: users/templates/users/first_login.html:45 msgid "MFA" msgstr "MFA" -#: audits/models.py:100 audits/templates/audits/login_log_list.html:57 +#: audits/models.py:86 audits/templates/audits/login_log_list.html:57 #: xpack/plugins/change_auth_plan/models.py:417 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:15 #: xpack/plugins/cloud/models.py:281 @@ -2225,14 +2211,14 @@ msgstr "MFA" msgid "Reason" msgstr "原因" -#: audits/models.py:101 audits/templates/audits/login_log_list.html:58 +#: audits/models.py:87 audits/templates/audits/login_log_list.html:58 #: xpack/plugins/cloud/models.py:278 xpack/plugins/cloud/models.py:313 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:70 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:65 msgid "Status" msgstr "状态" -#: audits/models.py:102 +#: audits/models.py:88 msgid "Date login" msgstr "登录日期" @@ -2264,13 +2250,14 @@ msgstr "选择用户" #: ops/templates/ops/command_execution_list.html:43 #: ops/templates/ops/command_execution_list.html:48 #: ops/templates/ops/task_list.html:13 ops/templates/ops/task_list.html:18 -#: templates/_base_list.html:41 templates/_header_bar.html:8 +#: templates/_base_list.html:41 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:52 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:48 msgid "Search" msgstr "搜索" #: audits/templates/audits/login_log_list.html:50 +#: authentication/templates/authentication/_access_key_modal.html:26 #: ops/templates/ops/adhoc_detail.html:49 #: ops/templates/ops/adhoc_history_detail.html:49 #: ops/templates/ops/task_detail.html:56 @@ -2289,6 +2276,7 @@ msgid "City" msgstr "城市" #: audits/templates/audits/login_log_list.html:59 +#: authentication/templates/authentication/_access_key_modal.html:29 #: ops/templates/ops/task_list.html:32 msgid "Date" msgstr "日期" @@ -2319,79 +2307,99 @@ msgstr "登录日志" msgid "Command execution log" msgstr "命令执行" -#: authentication/api/auth.py:49 +#: authentication/api/auth.py:51 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:67 -msgid "The user {} password has expired, please update." -msgstr "用户 {} 密码已经过期,请更新。" - -#: authentication/api/auth.py:86 +#: authentication/api/auth.py:76 msgid "Please carry seed value and conduct MFA secondary certification" msgstr "请携带seed值, 进行MFA二次认证" -#: authentication/api/auth.py:166 +#: authentication/api/auth.py:156 msgid "Please verify the user name and password first" msgstr "请先进行用户名和密码验证" -#: authentication/api/auth.py:171 +#: authentication/api/auth.py:161 msgid "MFA certification failed" msgstr "MFA认证失败" -#: authentication/backends/api.py:52 +#: authentication/api/token.py:80 +msgid "MFA required" +msgstr "" + +#: authentication/backends/api.py:53 msgid "Invalid signature header. No credentials provided." msgstr "" -#: authentication/backends/api.py:55 +#: authentication/backends/api.py:56 msgid "Invalid signature header. Signature string should not contain spaces." msgstr "" -#: authentication/backends/api.py:62 +#: authentication/backends/api.py:63 msgid "Invalid signature header. Format like AccessKeyId:Signature" msgstr "" -#: authentication/backends/api.py:66 +#: authentication/backends/api.py:67 msgid "" "Invalid signature header. Signature string should not contain invalid " "characters." msgstr "" -#: authentication/backends/api.py:86 authentication/backends/api.py:102 +#: authentication/backends/api.py:87 authentication/backends/api.py:103 msgid "Invalid signature." msgstr "" -#: authentication/backends/api.py:93 +#: authentication/backends/api.py:94 msgid "HTTP header: Date not provide or not %a, %d %b %Y %H:%M:%S GMT" msgstr "" -#: authentication/backends/api.py:98 +#: authentication/backends/api.py:99 msgid "Expired, more than 15 minutes" msgstr "" -#: authentication/backends/api.py:105 +#: authentication/backends/api.py:106 msgid "User disabled." msgstr "用户已禁用" -#: authentication/backends/api.py:120 +#: authentication/backends/api.py:121 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication/backends/api.py:123 +#: authentication/backends/api.py:124 msgid "Invalid token header. Sign string should not contain spaces." msgstr "" -#: authentication/backends/api.py:130 +#: authentication/backends/api.py:131 msgid "" "Invalid token header. Sign string should not contain invalid characters." msgstr "" -#: authentication/backends/api.py:140 +#: authentication/backends/api.py:141 msgid "Invalid token or cache refreshed." msgstr "" +#: authentication/const.py:6 +msgid "Username/password check failed" +msgstr "用户名/密码 校验失败" + +#: authentication/const.py:7 +msgid "MFA authentication failed" +msgstr "MFA 认证失败" + +#: authentication/const.py:8 +msgid "Username does not exist" +msgstr "用户名不存在" + +#: authentication/const.py:9 +msgid "Password expired" +msgstr "密码过期" + +#: authentication/const.py:10 +msgid "Disabled or expired" +msgstr "禁用或失效" + #: authentication/forms.py:21 msgid "" "The username or password you entered is incorrect, please enter it again." @@ -2418,10 +2426,43 @@ msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)" msgid "MFA code" msgstr "MFA 验证码" -#: authentication/models.py:33 +#: authentication/models.py:35 msgid "Private Token" msgstr "ssh密钥" +#: authentication/templates/authentication/_access_key_modal.html:6 +msgid "API key list" +msgstr "API Key列表" + +#: authentication/templates/authentication/_access_key_modal.html:18 +msgid "Using api key sign api header, every requests header difference" +msgstr "使用api key签名请求头,每个请求的头部是不一样的" + +#: authentication/templates/authentication/_access_key_modal.html:18 +msgid "docs" +msgstr "文档" + +#: authentication/templates/authentication/_access_key_modal.html:27 +msgid "Secret" +msgstr "密文" + +#: authentication/templates/authentication/_access_key_modal.html:48 +msgid "Show" +msgstr "显示" + +#: authentication/templates/authentication/_access_key_modal.html:66 +#: users/models/user.py:288 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:289 users/templates/users/user_profile.html:92 +#: users/templates/users/user_profile.html:170 +msgid "Enable" +msgstr "启用" + #: authentication/templates/authentication/_mfa_confirm_modal.html:5 msgid "MFA confirm" msgstr "MFA确认" @@ -2480,7 +2521,7 @@ msgstr "改变世界,从一点点开始。" #: authentication/templates/authentication/login.html:46 #: authentication/templates/authentication/login.html:73 #: authentication/templates/authentication/new_login.html:101 -#: templates/_header_bar.html:101 +#: templates/_header_bar.html:83 msgid "Login" msgstr "登录" @@ -2550,20 +2591,20 @@ msgstr "如果不能提供MFA验证码,请联系管理员!" msgid "Welcome back, please enter username and password to login" msgstr "欢迎回来,请输入用户名和密码登录" -#: authentication/views/login.py:80 +#: authentication/views/login.py:81 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:173 users/views/user.py:386 +#: authentication/views/login.py:174 users/views/user.py:386 #: users/views/user.py:411 msgid "MFA code invalid, or ntp sync server time" msgstr "MFA验证码不正确,或者服务器端时间不对" -#: authentication/views/login.py:204 +#: authentication/views/login.py:205 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:205 +#: authentication/views/login.py:206 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" @@ -2642,11 +2683,11 @@ msgstr "不能包含特殊字符" msgid "This field must be unique." msgstr "字段必须唯一" -#: jumpserver/views.py:189 templates/_nav.html:4 templates/_nav_audits.html:4 +#: jumpserver/views.py:188 templates/_nav.html:4 templates/_nav_audits.html:4 msgid "Dashboard" msgstr "仪表盘" -#: jumpserver/views.py:198 +#: jumpserver/views.py:197 msgid "" "
Luna is a separately deployed program, you need to deploy Luna, coco, " "configure nginx for url distribution,
If you see this page, " @@ -3298,21 +3339,21 @@ msgstr "连接LDAP成功" msgid "Match {} s users" msgstr "匹配 {} 个用户" -#: settings/api.py:160 +#: settings/api.py:161 msgid "succeed: {} failed: {} total: {}" msgstr "成功:{} 失败:{} 总数:{}" -#: settings/api.py:182 settings/api.py:218 +#: 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:188 settings/api.py:224 +#: settings/api.py:189 settings/api.py:225 msgid "Create succeed" msgstr "创建成功" -#: settings/api.py:206 settings/api.py:244 +#: settings/api.py:207 settings/api.py:245 #: settings/templates/settings/terminal_setting.html:154 msgid "Delete succeed" msgstr "删除成功" @@ -3846,19 +3887,19 @@ msgstr "创建录像存储" msgid "Create command storage" msgstr "创建命令存储" -#: templates/_header_bar.html:31 +#: templates/_header_bar.html:12 msgid "Help" msgstr "帮助" -#: templates/_header_bar.html:38 users/templates/users/_base_otp.html:29 +#: templates/_header_bar.html:19 users/templates/users/_base_otp.html:29 msgid "Docs" msgstr "文档" -#: templates/_header_bar.html:44 +#: templates/_header_bar.html:25 msgid "Commercial support" msgstr "商业支持" -#: templates/_header_bar.html:89 templates/_nav_user.html:32 users/forms.py:154 +#: templates/_header_bar.html:70 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 @@ -3869,15 +3910,19 @@ msgstr "商业支持" msgid "Profile" msgstr "个人信息" -#: templates/_header_bar.html:92 +#: templates/_header_bar.html:73 msgid "Admin page" msgstr "管理页面" -#: templates/_header_bar.html:94 +#: templates/_header_bar.html:75 msgid "User page" msgstr "用户页面" -#: templates/_header_bar.html:97 +#: templates/_header_bar.html:78 +msgid "API Key" +msgstr "" + +#: templates/_header_bar.html:79 msgid "Logout" msgstr "注销登录" @@ -4413,7 +4458,7 @@ msgid "" "You should use your ssh client tools connect terminal: {}

{}" msgstr "你可以使用ssh客户端工具连接终端" -#: users/api/user.py:97 +#: users/api/user.py:96 msgid "You do not have permission." msgstr "你没有权限" @@ -4450,7 +4495,7 @@ 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:95 +#: users/forms.py:91 users/forms.py:252 users/serializers/v1.py:94 msgid "Not a valid ssh public key" msgstr "ssh密钥不合法" @@ -4540,29 +4585,28 @@ msgstr "选择用户" msgid "User auth from {}, go there change password" msgstr "用户认证源来自 {}, 请去相应系统修改密码" -#: users/models/user.py:127 users/models/user.py:466 +#: users/models/user.py:126 users/models/user.py:466 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:129 +#: users/models/user.py:128 msgid "Application" msgstr "应用程序" -#: users/models/user.py:130 +#: users/models/user.py:129 msgid "Auditor" msgstr "审计员" -#: users/models/user.py:288 users/templates/users/user_profile.html:94 -#: users/templates/users/user_profile.html:163 -#: users/templates/users/user_profile.html:166 -msgid "Disable" -msgstr "禁用" - -#: users/models/user.py:289 users/templates/users/user_profile.html:92 -#: users/templates/users/user_profile.html:170 -msgid "Enable" -msgstr "启用" - +# #: users/models/user.py:288 users/templates/users/user_profile.html:94 +# #: users/templates/users/user_profile.html:163 +# #: users/templates/users/user_profile.html:166 +# msgid "Disable" +# msgstr "禁用" +# +# #: users/models/user.py:289 users/templates/users/user_profile.html:92 +# #: users/templates/users/user_profile.html:170 +# msgid "Enable" +# msgstr "启用" #: users/models/user.py:290 users/templates/users/user_profile.html:90 msgid "Force enable" msgstr "强制启用" @@ -4589,39 +4633,39 @@ msgstr "最后更新密码日期" msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/serializers/v1.py:41 +#: users/serializers/v1.py:39 msgid "Groups name" msgstr "用户组名" -#: users/serializers/v1.py:42 +#: users/serializers/v1.py:40 msgid "Source name" msgstr "用户来源名" -#: users/serializers/v1.py:43 +#: users/serializers/v1.py:41 msgid "Is first login" msgstr "首次登录" -#: users/serializers/v1.py:44 +#: users/serializers/v1.py:42 msgid "Role name" msgstr "角色名" -#: users/serializers/v1.py:45 +#: users/serializers/v1.py:43 msgid "Is valid" msgstr "账户是否有效" -#: users/serializers/v1.py:46 +#: users/serializers/v1.py:44 msgid "Is expired" msgstr " 是否过期" -#: users/serializers/v1.py:47 +#: users/serializers/v1.py:45 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/v1.py:55 +#: users/serializers/v1.py:54 msgid "Role limit to {}" msgstr "角色只能为 {}" -#: users/serializers/v1.py:67 +#: users/serializers/v1.py:66 msgid "Password does not match security rules" msgstr "密码不满足安全规则" @@ -4753,7 +4797,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:88 +#: users/templates/users/user_detail.html:377 users/utils.py:84 msgid "Reset password" msgstr "重置密码" @@ -5069,7 +5113,7 @@ msgid "" "corresponding private key." msgstr "新的公钥已设置成功,请下载对应的私钥" -#: users/utils.py:28 +#: users/utils.py:24 #, python-format msgid "" "\n" @@ -5114,16 +5158,16 @@ msgstr "" "

\n" " " -#: users/utils.py:63 +#: users/utils.py:59 msgid "Create account successfully" msgstr "创建账户成功" -#: users/utils.py:67 +#: users/utils.py:63 #, python-format msgid "Hello %(name)s" msgstr "您好 %(name)s" -#: users/utils.py:90 +#: users/utils.py:86 #, python-format msgid "" "\n" @@ -5167,11 +5211,11 @@ msgstr "" "
\n" " " -#: users/utils.py:121 +#: users/utils.py:117 msgid "Security notice" msgstr "安全通知" -#: users/utils.py:123 +#: users/utils.py:119 #, python-format msgid "" "\n" @@ -5220,11 +5264,11 @@ msgstr "" "
\n" " " -#: users/utils.py:159 +#: users/utils.py:155 msgid "Expiration notice" msgstr "过期通知" -#: users/utils.py:161 +#: users/utils.py:157 #, python-format msgid "" "\n" @@ -5246,11 +5290,11 @@ msgstr "" "
\n" " " -#: users/utils.py:180 +#: users/utils.py:176 msgid "SSH Key Reset" msgstr "重置ssh密钥" -#: users/utils.py:182 +#: users/utils.py:178 #, python-format msgid "" "\n" @@ -5275,18 +5319,6 @@ msgstr "" "
\n" " " -#: users/utils.py:215 -msgid "User not exist" -msgstr "用户不存在" - -#: users/utils.py:217 -msgid "Disabled or expired" -msgstr "禁用或失效" - -#: users/utils.py:230 -msgid "Password or SSH public key invalid" -msgstr "密码或密钥不合法" - #: users/views/group.py:29 msgid "User group list" msgstr "用户组列表" @@ -5718,12 +5750,16 @@ msgid "Create account" msgstr "创建账户" #: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:33 +#, fuzzy +#| msgid "Instance" msgid "Region & Instance" -msgstr "地域 & 实例" +msgstr "实例" #: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:37 +#, fuzzy +#| msgid "Admin user" msgid "Node & AdminUser" -msgstr "节点 & 管理用户" +msgstr "管理用户" #: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:66 msgid "Loading..." @@ -5791,6 +5827,8 @@ msgstr "执行次数" msgid "Instance count" msgstr "实例个数" +# msgid "Sync success" +# msgstr "同步成功" #: xpack/plugins/cloud/views.py:63 msgid "Update account" msgstr "更新账户" @@ -6034,15 +6072,23 @@ msgstr "密码匣子" msgid "vault create" msgstr "创建" +#~ msgid "User not exist" +#~ msgstr "用户不存在" + +# msgid "Disabled or expired" +# msgstr "禁用或失效" +#~ msgid "Password or SSH public key invalid" +#~ msgstr "密码或密钥不合法" + +#~ msgid "Please select assets that need to be updated" +#~ msgstr "请选择需要更新的资产" + #~ msgid "Interface" #~ msgstr "界面" #~ msgid "Orgs" #~ msgstr "组织管理" -#~ msgid "Org" -#~ msgstr "组织" - #~ msgid "already exists" #~ msgstr "已经存在" diff --git a/apps/ops/ansible/callback.py b/apps/ops/ansible/callback.py index 694bb9e27..c30e6428b 100644 --- a/apps/ops/ansible/callback.py +++ b/apps/ops/ansible/callback.py @@ -2,6 +2,7 @@ import datetime import json +import os from collections import defaultdict from ansible import constants as C @@ -41,7 +42,11 @@ class CallbackMixin: super().__init__() if display: self._display = display + + cols = os.environ.get("TERM_COLS", None) self._display.columns = 79 + if cols and cols.isdigit(): + self._display.columns = int(cols) - 1 def display(self, msg): self._display.display(msg) diff --git a/apps/ops/api/adhoc.py b/apps/ops/api/adhoc.py index ded0da2e6..e01e8ef16 100644 --- a/apps/ops/api/adhoc.py +++ b/apps/ops/api/adhoc.py @@ -6,6 +6,7 @@ from rest_framework import viewsets, generics from rest_framework.views import Response from common.permissions import IsOrgAdmin +from common.serializers import CeleryTaskSerializer from orgs.utils import current_org from ..models import Task, AdHoc, AdHocRunHistory from ..serializers import TaskSerializer, AdHocSerializer, \ @@ -33,7 +34,7 @@ class TaskViewSet(viewsets.ModelViewSet): class TaskRun(generics.RetrieveAPIView): queryset = Task.objects.all() - # serializer_class = TaskViewSet + serializer_class = CeleryTaskSerializer permission_classes = (IsOrgAdmin,) def retrieve(self, request, *args, **kwargs): diff --git a/apps/ops/api/command.py b/apps/ops/api/command.py index c63cbfdb8..6712e97c6 100644 --- a/apps/ops/api/command.py +++ b/apps/ops/api/command.py @@ -1,11 +1,14 @@ # -*- coding: utf-8 -*- # from rest_framework import viewsets +from rest_framework.exceptions import ValidationError from django.db import transaction +from django.utils.translation import ugettext as _ from django.conf import settings -from orgs.mixins import RootOrgViewMixin +from orgs.mixins.api import RootOrgViewMixin from common.permissions import IsValidUser +from perms.utils import AssetPermissionUtilV2 from ..models import CommandExecution from ..serializers import CommandExecutionSerializer from ..tasks import run_command_execution @@ -20,15 +23,33 @@ class CommandExecutionViewSet(RootOrgViewMixin, viewsets.ModelViewSet): user_id=str(self.request.user.id) ) + def check_hosts(self, serializer): + data = serializer.validated_data + assets = data["hosts"] + system_user = data["run_as"] + util = AssetPermissionUtilV2(self.request.user) + util.filter_permissions(system_users=system_user.id) + permed_assets = util.get_assets().filter(id__in=[a.id for a in assets]) + unpermed_assets = set(assets) - set(permed_assets) + if unpermed_assets: + msg = _("Not has host {} permission").format( + [str(a.id) for a in unpermed_assets] + ) + raise ValidationError({"hosts": msg}) + def check_permissions(self, request): if not settings.SECURITY_COMMAND_EXECUTION and request.user.is_common_user: return self.permission_denied(request, "Command execution disabled") return super().check_permissions(request) def perform_create(self, serializer): + self.check_hosts(serializer) instance = serializer.save() instance.user = self.request.user instance.save() + cols = self.request.query_params.get("cols", '80') + rows = self.request.query_params.get("rows", '24') transaction.on_commit(lambda: run_command_execution.apply_async( - args=(instance.id,), task_id=str(instance.id) + args=(instance.id,), kwargs={"cols": cols, "rows": rows}, + task_id=str(instance.id) )) diff --git a/apps/ops/celery/__init__.py b/apps/ops/celery/__init__.py index b64e20f01..a9866780b 100644 --- a/apps/ops/celery/__init__.py +++ b/apps/ops/celery/__init__.py @@ -2,6 +2,7 @@ import os +from kombu import Exchange, Queue from celery import Celery # set the default Django settings module for the 'celery' program. @@ -15,6 +16,14 @@ configs = {k: v for k, v in settings.__dict__.items() if k.startswith('CELERY')} # Using a string here means the worker will not have to # pickle the object when using Windows. # app.config_from_object('django.conf:settings', namespace='CELERY') +configs["CELERY_QUEUES"] = [ + Queue("celery", Exchange("celery"), routing_key="celery"), + Queue("ansible", Exchange("ansible"), routing_key="ansible"), +] +configs["CELERY_ROUTES"] = { + "ops.tasks.run_ansible_task": {'exchange': 'ansible', 'routing_key': 'ansible'}, +} + app.namespace = 'CELERY' app.conf.update(configs) app.autodiscover_tasks(lambda: [app_config.split('.')[0] for app_config in settings.INSTALLED_APPS]) diff --git a/apps/ops/inventory.py b/apps/ops/inventory.py index 9b6d3e183..9cb1027ef 100644 --- a/apps/ops/inventory.py +++ b/apps/ops/inventory.py @@ -30,8 +30,6 @@ class JMSBaseInventory(BaseInventory): info.update(asset.get_auth_info()) if asset.is_unixlike(): info["become"] = asset.admin_user.become_info - for node in asset.nodes.all(): - info["groups"].append(node.value) if asset.is_windows(): info["vars"].update({ "ansible_connection": "ssh", @@ -45,7 +43,6 @@ class JMSBaseInventory(BaseInventory): info["vars"].update({ "domain": asset.domain.name, }) - info["groups"].append("domain_"+asset.domain.name) return info @staticmethod diff --git a/apps/ops/migrations/0007_auto_20190724_2002.py b/apps/ops/migrations/0007_auto_20190724_2002.py new file mode 100644 index 000000000..d0303aaa0 --- /dev/null +++ b/apps/ops/migrations/0007_auto_20190724_2002.py @@ -0,0 +1,28 @@ +# Generated by Django 2.1.7 on 2019-07-24 12:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0006_auto_20190318_1023'), + ] + + operations = [ + migrations.AlterField( + model_name='adhoc', + name='_become', + field=models.CharField(blank=True, default='', max_length=1024, verbose_name='Become'), + ), + migrations.AlterField( + model_name='adhoc', + name='created_by', + field=models.CharField(blank=True, default='', max_length=64, null=True, verbose_name='Create by'), + ), + migrations.AlterField( + model_name='adhoc', + name='run_as', + field=models.CharField(blank=True, default='', max_length=64, null=True, verbose_name='Username'), + ), + ] diff --git a/apps/ops/models/adhoc.py b/apps/ops/models/adhoc.py index 91c9041a5..24d82bbf1 100644 --- a/apps/ops/models/adhoc.py +++ b/apps/ops/models/adhoc.py @@ -161,9 +161,9 @@ class AdHoc(models.Model): _hosts = models.TextField(blank=True, verbose_name=_('Hosts')) # ['hostname1', 'hostname2'] hosts = models.ManyToManyField('assets.Asset', verbose_name=_("Host")) run_as_admin = models.BooleanField(default=False, verbose_name=_('Run as admin')) - run_as = models.CharField(max_length=64, default='', null=True, verbose_name=_('Username')) - _become = models.CharField(max_length=1024, default='', verbose_name=_("Become")) - created_by = models.CharField(max_length=64, default='', null=True, verbose_name=_('Create by')) + run_as = models.CharField(max_length=64, default='', blank=True, null=True, verbose_name=_('Username')) + _become = models.CharField(max_length=1024, default='', blank=True, verbose_name=_("Become")) + created_by = models.CharField(max_length=64, default='', blank=True, null=True, verbose_name=_('Create by')) date_created = models.DateTimeField(auto_now_add=True, db_index=True) @property diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py index de7602f6b..64d979c25 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -23,7 +23,7 @@ def rerun_task(): pass -@shared_task +@shared_task(queue="ansible") def run_ansible_task(tid, callback=None, **kwargs): """ :param tid: is the tasks serialized data @@ -45,6 +45,10 @@ def run_command_execution(cid, **kwargs): execution = get_object_or_none(CommandExecution, id=cid) if execution: try: + os.environ.update({ + "TERM_ROWS": kwargs.get("rows", ""), + "TERM_COLS": kwargs.get("cols", ""), + }) execution.run() except SoftTimeLimitExceeded: logger.error("Run time out") @@ -98,7 +102,7 @@ def create_or_update_registered_periodic_tasks(): create_or_update_celery_periodic_tasks(task) -@shared_task +@shared_task(queue="ansible") def hello(name, callback=None): import time time.sleep(10) @@ -109,7 +113,9 @@ def hello(name, callback=None): # @after_app_shutdown_clean_periodic # @register_as_period_task(interval=30) def hello123(): + p = subprocess.Popen('ls /tmp', shell=True) print("{} Hello world".format(datetime.datetime.now().strftime("%H:%M:%S"))) + return None @shared_task diff --git a/apps/ops/templates/ops/celery_task_log.html b/apps/ops/templates/ops/celery_task_log.html index a182789fa..3cdcb008d 100644 --- a/apps/ops/templates/ops/celery_task_log.html +++ b/apps/ops/templates/ops/celery_task_log.html @@ -2,7 +2,7 @@ {% load i18n %} {% trans 'Task log' %} - +