Commit 2fde6cfe authored by ibuler's avatar ibuler

Merge branch 'dev' of github.com:jumpserver/jumpserver into dev

parents e5953e19 9ab3f044
...@@ -34,7 +34,7 @@ Jumpserver采纳分布式架构,支持多机房跨区域部署,中心节点 ...@@ -34,7 +34,7 @@ Jumpserver采纳分布式架构,支持多机房跨区域部署,中心节点
我们提供了DEMO和截图可以让你快速了解Jumpserver 我们提供了DEMO和截图可以让你快速了解Jumpserver
[DEMO](http://demo.jumpserver.org) [DEMO](https://demo.jumpserver.org)
[截图](http://docs.jumpserver.org/zh/docs/snapshot.html) [截图](http://docs.jumpserver.org/zh/docs/snapshot.html)
### SDK ### SDK
......
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
__version__ = "1.4.4" __version__ = "1.4.6"
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
# limitations under the License. # limitations under the License.
from django.db import transaction from django.db import transaction
from django.shortcuts import get_object_or_404
from rest_framework import generics from rest_framework import generics
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
...@@ -24,13 +25,14 @@ from common.utils import get_logger ...@@ -24,13 +25,14 @@ from common.utils import get_logger
from ..hands import IsOrgAdmin from ..hands import IsOrgAdmin
from ..models import AdminUser, Asset from ..models import AdminUser, Asset
from .. import serializers from .. import serializers
from ..tasks import test_admin_user_connectability_manual from ..tasks import test_admin_user_connectivity_manual
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = [
'AdminUserViewSet', 'ReplaceNodesAdminUserApi', 'AdminUserViewSet', 'ReplaceNodesAdminUserApi',
'AdminUserTestConnectiveApi', 'AdminUserAuthApi', 'AdminUserTestConnectiveApi', 'AdminUserAuthApi',
'AdminUserAssetsListView',
] ]
...@@ -81,12 +83,29 @@ class ReplaceNodesAdminUserApi(generics.UpdateAPIView): ...@@ -81,12 +83,29 @@ class ReplaceNodesAdminUserApi(generics.UpdateAPIView):
class AdminUserTestConnectiveApi(generics.RetrieveAPIView): class AdminUserTestConnectiveApi(generics.RetrieveAPIView):
""" """
Test asset admin user connectivity Test asset admin user assets_connectivity
""" """
queryset = AdminUser.objects.all() queryset = AdminUser.objects.all()
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
admin_user = self.get_object() admin_user = self.get_object()
task = test_admin_user_connectability_manual.delay(admin_user) task = test_admin_user_connectivity_manual.delay(admin_user)
return Response({"task": task.id}) return Response({"task": task.id})
class AdminUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSimpleSerializer
pagination_class = LimitOffsetPagination
filter_fields = ("hostname", "ip")
http_method_names = ['get']
search_fields = filter_fields
def get_object(self):
pk = self.kwargs.get('pk')
return get_object_or_404(AdminUser, pk=pk)
def get_queryset(self):
admin_user = self.get_object()
return admin_user.get_related_assets()
...@@ -17,7 +17,7 @@ from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser ...@@ -17,7 +17,7 @@ from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from ..models import Asset, AdminUser, Node from ..models import Asset, AdminUser, Node
from .. import serializers from .. import serializers
from ..tasks import update_asset_hardware_info_manual, \ from ..tasks import update_asset_hardware_info_manual, \
test_asset_connectability_manual test_asset_connectivity_manual
from ..utils import LabelFilter from ..utils import LabelFilter
...@@ -41,40 +41,46 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet): ...@@ -41,40 +41,46 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet):
pagination_class = LimitOffsetPagination pagination_class = LimitOffsetPagination
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
def filter_node(self): def filter_node(self, queryset):
node_id = self.request.query_params.get("node_id") node_id = self.request.query_params.get("node_id")
if not node_id: if not node_id:
return return queryset
node = get_object_or_404(Node, id=node_id) node = get_object_or_404(Node, id=node_id)
show_current_asset = self.request.query_params.get("show_current_asset") in ('1', 'true') show_current_asset = self.request.query_params.get("show_current_asset") in ('1', 'true')
if node.is_root(): if node.is_root() and show_current_asset:
if show_current_asset: queryset = queryset.filter(
self.queryset = self.queryset.filter( Q(nodes=node_id) | Q(nodes__isnull=True)
Q(nodes=node_id) | Q(nodes__isnull=True) )
) elif node.is_root() and not show_current_asset:
return pass
if show_current_asset: elif not node.is_root() and show_current_asset:
self.queryset = self.queryset.filter(nodes=node) queryset = queryset.filter(nodes=node)
else: else:
self.queryset = self.queryset.filter( queryset = queryset.filter(
nodes__key__regex='^{}(:[0-9]+)*$'.format(node.key), nodes__key__regex='^{}(:[0-9]+)*$'.format(node.key),
) )
return queryset
def filter_admin_user_id(self): def filter_admin_user_id(self, queryset):
admin_user_id = self.request.query_params.get('admin_user_id') admin_user_id = self.request.query_params.get('admin_user_id')
if admin_user_id: if not admin_user_id:
admin_user = get_object_or_404(AdminUser, id=admin_user_id) return queryset
self.queryset = self.queryset.filter(admin_user=admin_user) admin_user = get_object_or_404(AdminUser, id=admin_user_id)
queryset = queryset.filter(admin_user=admin_user)
return queryset
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
queryset = self.filter_node(queryset)
queryset = self.filter_admin_user_id(queryset)
return queryset
def get_queryset(self): def get_queryset(self):
self.queryset = super().get_queryset()\ queryset = super().get_queryset().distinct()
.prefetch_related('labels', 'nodes')\ queryset = self.get_serializer_class().setup_eager_loading(queryset)
.select_related('admin_user') return queryset
self.filter_admin_user_id()
self.filter_node()
return self.queryset.distinct()
class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView): class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView):
...@@ -103,7 +109,7 @@ class AssetRefreshHardwareApi(generics.RetrieveAPIView): ...@@ -103,7 +109,7 @@ class AssetRefreshHardwareApi(generics.RetrieveAPIView):
class AssetAdminUserTestApi(generics.RetrieveAPIView): class AssetAdminUserTestApi(generics.RetrieveAPIView):
""" """
Test asset admin user connectivity Test asset admin user assets_connectivity
""" """
queryset = Asset.objects.all() queryset = Asset.objects.all()
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
...@@ -111,7 +117,7 @@ class AssetAdminUserTestApi(generics.RetrieveAPIView): ...@@ -111,7 +117,7 @@ class AssetAdminUserTestApi(generics.RetrieveAPIView):
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
asset_id = kwargs.get('pk') asset_id = kwargs.get('pk')
asset = get_object_or_404(Asset, pk=asset_id) asset = get_object_or_404(Asset, pk=asset_id)
task = test_asset_connectability_manual.delay(asset) task = test_asset_connectivity_manual.delay(asset)
return Response({"task": task.id}) return Response({"task": task.id})
......
...@@ -9,7 +9,6 @@ from django.views.generic.detail import SingleObjectMixin ...@@ -9,7 +9,6 @@ from django.views.generic.detail import SingleObjectMixin
from common.utils import get_logger from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsAppUser, IsOrgAdminOrAppUser from common.permissions import IsOrgAdmin, IsAppUser, IsOrgAdminOrAppUser
from ..models import Domain, Gateway from ..models import Domain, Gateway
from ..utils import test_gateway_connectability
from .. import serializers from .. import serializers
...@@ -54,7 +53,7 @@ class GatewayTestConnectionApi(SingleObjectMixin, APIView): ...@@ -54,7 +53,7 @@ class GatewayTestConnectionApi(SingleObjectMixin, APIView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.object = self.get_object(Gateway.objects.all()) self.object = self.get_object(Gateway.objects.all())
ok, e = test_gateway_connectability(self.object) ok, e = self.object.test_connective()
if ok: if ok:
return Response("ok") return Response("ok")
else: else:
......
...@@ -17,15 +17,14 @@ from rest_framework import generics, mixins, viewsets ...@@ -17,15 +17,14 @@ from rest_framework import generics, mixins, viewsets
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.db.models import Count
from common.utils import get_logger, get_object_or_none from common.utils import get_logger, get_object_or_none
from common.tree import TreeNodeSerializer
from ..hands import IsOrgAdmin from ..hands import IsOrgAdmin
from ..models import Node from ..models import Node
from ..tasks import update_assets_hardware_info_util, test_asset_connectability_util from ..tasks import update_assets_hardware_info_util, test_asset_connectivity_util
from .. import serializers from .. import serializers
...@@ -34,7 +33,8 @@ __all__ = [ ...@@ -34,7 +33,8 @@ __all__ = [
'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi', 'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi',
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi', 'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi',
'NodeAddChildrenApi', 'RefreshNodeHardwareInfoApi', 'NodeAddChildrenApi', 'RefreshNodeHardwareInfoApi',
'TestNodeConnectiveApi' 'TestNodeConnectiveApi', 'NodeListAsTreeApi',
'NodeChildrenAsTreeApi', 'RefreshAssetsAmount',
] ]
...@@ -43,22 +43,89 @@ class NodeViewSet(viewsets.ModelViewSet): ...@@ -43,22 +43,89 @@ class NodeViewSet(viewsets.ModelViewSet):
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeSerializer serializer_class = serializers.NodeSerializer
def perform_create(self, serializer):
child_key = Node.root().get_next_child_key()
serializer.validated_data["key"] = child_key
serializer.save()
def update(self, request, *args, **kwargs): class NodeListAsTreeApi(generics.ListAPIView):
node = self.get_object() """
if node.is_root(): 获取节点列表树
node_value = node.value [
post_value = request.data.get('value') {
if node_value != post_value: "id": "",
return Response( "name": "",
{"msg": _("You can't update the root node name")}, "pId": "",
status=400 "meta": ""
) }
return super().update(request, *args, **kwargs) ]
"""
permission_classes = (IsOrgAdmin,)
serializer_class = TreeNodeSerializer
def get_queryset(self):
queryset = [node.as_tree_node() for node in Node.objects.all()]
return queryset
def filter_queryset(self, queryset):
if self.request.query_params.get('refresh', '0') == '1':
queryset = self.refresh_nodes(queryset)
return queryset
@staticmethod
def refresh_nodes(queryset):
Node.expire_nodes_assets_amount()
Node.expire_nodes_full_value()
return queryset
class NodeChildrenAsTreeApi(generics.ListAPIView):
"""
节点子节点作为树返回,
[
{
"id": "",
"name": "",
"pId": "",
"meta": ""
}
]
"""
permission_classes = (IsOrgAdmin,)
serializer_class = TreeNodeSerializer
node = None
is_root = False
def get_queryset(self):
node_key = self.request.query_params.get('key')
if node_key:
self.node = Node.objects.get(key=node_key)
queryset = self.node.get_children(with_self=False)
else:
self.is_root = True
self.node = Node.root()
queryset = list(self.node.get_children(with_self=True))
nodes_invalid = Node.objects.exclude(key__startswith=self.node.key)
queryset.extend(list(nodes_invalid))
queryset = [node.as_tree_node() for node in queryset]
return queryset
def filter_assets(self, queryset):
include_assets = self.request.query_params.get('assets', '0') == '1'
if not include_assets:
return queryset
assets = self.node.get_assets()
for asset in assets:
queryset.append(asset.as_tree_node(self.node))
return queryset
def filter_queryset(self, queryset):
queryset = self.filter_assets(queryset)
queryset = self.filter_refresh_nodes(queryset)
return queryset
def filter_refresh_nodes(self, queryset):
if self.request.query_params.get('refresh', '0') == '1':
Node.expire_nodes_assets_amount()
Node.expire_nodes_full_value()
return queryset
class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView): class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
...@@ -67,19 +134,13 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView): ...@@ -67,19 +134,13 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
serializer_class = serializers.NodeSerializer serializer_class = serializers.NodeSerializer
instance = None instance = None
def counter(self): def get(self, request, *args, **kwargs):
values = [ return self.list(request, *args, **kwargs)
child.value[child.value.rfind(' '):]
for child in self.get_object().get_children()
if child.value.startswith("新节点 ")
]
values = [int(value) for value in values if value.strip().isdigit()]
count = max(values)+1 if values else 1
return count
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
instance = self.get_object()
if not request.data.get("value"): if not request.data.get("value"):
request.data["value"] = _("New node {}").format(self.counter()) request.data["value"] = instance.get_next_child_preset_name()
return super().post(request, *args, **kwargs) return super().post(request, *args, **kwargs)
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
...@@ -91,15 +152,12 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView): ...@@ -91,15 +152,12 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
'The same level node name cannot be the same' 'The same level node name cannot be the same'
) )
node = instance.create_child(value=value) node = instance.create_child(value=value)
return Response( return Response(self.serializer_class(instance=node).data, status=201)
{"id": node.id, "key": node.key, "value": node.value},
status=201,
)
def get_object(self): def get_object(self):
pk = self.kwargs.get('pk') or self.request.query_params.get('id') pk = self.kwargs.get('pk') or self.request.query_params.get('id')
if not pk: if not pk:
node = None node = Node.root()
else: else:
node = get_object_or_404(Node, pk=pk) node = get_object_or_404(Node, pk=pk)
return node return node
...@@ -107,7 +165,6 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView): ...@@ -107,7 +165,6 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
def get_queryset(self): def get_queryset(self):
queryset = [] queryset = []
query_all = self.request.query_params.get("all") query_all = self.request.query_params.get("all")
query_assets = self.request.query_params.get('assets')
node = self.get_object() node = self.get_object()
if node is None: if node is None:
...@@ -120,23 +177,8 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView): ...@@ -120,23 +177,8 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
else: else:
children = node.get_children() children = node.get_children()
queryset.extend(list(children)) queryset.extend(list(children))
if query_assets:
assets = node.get_assets()
for asset in assets:
node_fake = Node()
node_fake.assets__count = 0
node_fake.id = asset.id
node_fake.is_node = False
node_fake.key = node.key + ':0'
node_fake.value = asset.hostname
queryset.append(node_fake)
queryset = sorted(queryset, key=lambda x: x.is_node, reverse=True)
return queryset return queryset
def get(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
class NodeAssetsApi(generics.ListAPIView): class NodeAssetsApi(generics.ListAPIView):
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
...@@ -234,5 +276,14 @@ class TestNodeConnectiveApi(APIView): ...@@ -234,5 +276,14 @@ class TestNodeConnectiveApi(APIView):
assets = node.assets.all() assets = node.assets.all()
# task_name = _("测试节点下资产是否可连接: {}".format(node.name)) # task_name = _("测试节点下资产是否可连接: {}".format(node.name))
task_name = _("Test if the assets under the node are connectable: {}".format(node.name)) task_name = _("Test if the assets under the node are connectable: {}".format(node.name))
task = test_asset_connectability_util.delay(assets, task_name=task_name) task = test_asset_connectivity_util.delay(assets, task_name=task_name)
return Response({"task": task.id}) return Response({"task": task.id})
class RefreshAssetsAmount(APIView):
permission_classes = (IsOrgAdmin,)
model = Node
def get(self, request, *args, **kwargs):
self.model.expire_nodes_assets_amount()
return Response("Ok")
...@@ -24,8 +24,8 @@ from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser ...@@ -24,8 +24,8 @@ from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from ..models import SystemUser, Asset from ..models import SystemUser, Asset
from .. import serializers from .. import serializers
from ..tasks import push_system_user_to_assets_manual, \ from ..tasks import push_system_user_to_assets_manual, \
test_system_user_connectability_manual, push_system_user_a_asset_manual, \ test_system_user_connectivity_manual, push_system_user_a_asset_manual, \
test_system_user_connectability_a_asset test_system_user_connectivity_a_asset
logger = get_logger(__file__) logger = get_logger(__file__)
...@@ -33,7 +33,7 @@ __all__ = [ ...@@ -33,7 +33,7 @@ __all__ = [
'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserViewSet', 'SystemUserAuthInfoApi',
'SystemUserPushApi', 'SystemUserTestConnectiveApi', 'SystemUserPushApi', 'SystemUserTestConnectiveApi',
'SystemUserAssetsListView', 'SystemUserPushToAssetApi', 'SystemUserAssetsListView', 'SystemUserPushToAssetApi',
'SystemUserTestAssetConnectabilityApi', 'SystemUserCommandFilterRuleListApi', 'SystemUserTestAssetConnectivityApi', 'SystemUserCommandFilterRuleListApi',
] ]
...@@ -93,15 +93,16 @@ class SystemUserTestConnectiveApi(generics.RetrieveAPIView): ...@@ -93,15 +93,16 @@ class SystemUserTestConnectiveApi(generics.RetrieveAPIView):
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
system_user = self.get_object() system_user = self.get_object()
task = test_system_user_connectability_manual.delay(system_user) task = test_system_user_connectivity_manual.delay(system_user)
return Response({"task": task.id}) return Response({"task": task.id})
class SystemUserAssetsListView(generics.ListAPIView): class SystemUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSerializer serializer_class = serializers.AssetSimpleSerializer
pagination_class = LimitOffsetPagination pagination_class = LimitOffsetPagination
filter_fields = ("hostname", "ip") filter_fields = ("hostname", "ip")
http_method_names = ['get']
search_fields = filter_fields search_fields = filter_fields
def get_object(self): def get_object(self):
...@@ -125,7 +126,7 @@ class SystemUserPushToAssetApi(generics.RetrieveAPIView): ...@@ -125,7 +126,7 @@ class SystemUserPushToAssetApi(generics.RetrieveAPIView):
return Response({"task": task.id}) return Response({"task": task.id})
class SystemUserTestAssetConnectabilityApi(generics.RetrieveAPIView): class SystemUserTestAssetConnectivityApi(generics.RetrieveAPIView):
queryset = SystemUser.objects.all() queryset = SystemUser.objects.all()
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
...@@ -133,7 +134,7 @@ class SystemUserTestAssetConnectabilityApi(generics.RetrieveAPIView): ...@@ -133,7 +134,7 @@ class SystemUserTestAssetConnectabilityApi(generics.RetrieveAPIView):
system_user = self.get_object() system_user = self.get_object()
asset_id = self.kwargs.get('aid') asset_id = self.kwargs.get('aid')
asset = get_object_or_404(Asset, id=asset_id) asset = get_object_or_404(Asset, id=asset_id)
task = test_system_user_connectability_a_asset.delay(system_user, asset) task = test_system_user_connectivity_a_asset.delay(system_user, asset)
return Response({"task": task.id}) return Response({"task": task.id})
......
...@@ -41,9 +41,6 @@ class AssetCreateForm(OrgModelForm): ...@@ -41,9 +41,6 @@ class AssetCreateForm(OrgModelForm):
'nodes': _("Node"), 'nodes': _("Node"),
} }
help_texts = { help_texts = {
'hostname': '* required',
'ip': '* required',
'port': '* required',
'admin_user': _( 'admin_user': _(
'root or other NOPASSWD sudo privilege user existed in asset,' 'root or other NOPASSWD sudo privilege user existed in asset,'
'If asset is windows or other set any one, more see admin user left menu' 'If asset is windows or other set any one, more see admin user left menu'
...@@ -80,10 +77,6 @@ class AssetUpdateForm(OrgModelForm): ...@@ -80,10 +77,6 @@ class AssetUpdateForm(OrgModelForm):
'nodes': _("Node"), 'nodes': _("Node"),
} }
help_texts = { help_texts = {
'hostname': '* required',
'ip': '* required',
'port': '* required',
'cluster': '* required',
'admin_user': _( 'admin_user': _(
'root or other NOPASSWD sudo privilege user existed in asset,' 'root or other NOPASSWD sudo privilege user existed in asset,'
'If asset is windows or other set any one, more see admin user left menu' 'If asset is windows or other set any one, more see admin user left menu'
...@@ -95,7 +88,7 @@ class AssetUpdateForm(OrgModelForm): ...@@ -95,7 +88,7 @@ class AssetUpdateForm(OrgModelForm):
class AssetBulkUpdateForm(OrgModelForm): class AssetBulkUpdateForm(OrgModelForm):
assets = forms.ModelMultipleChoiceField( assets = forms.ModelMultipleChoiceField(
required=True, help_text='* required', required=True,
label=_('Select assets'), queryset=Asset.objects.all(), label=_('Select assets'), queryset=Asset.objects.all(),
widget=forms.SelectMultiple( widget=forms.SelectMultiple(
attrs={ attrs={
...@@ -142,14 +135,14 @@ class AssetBulkUpdateForm(OrgModelForm): ...@@ -142,14 +135,14 @@ class AssetBulkUpdateForm(OrgModelForm):
if k in changed_fields} if k in changed_fields}
assets = cleaned_data.pop('assets') assets = cleaned_data.pop('assets')
labels = cleaned_data.pop('labels', []) labels = cleaned_data.pop('labels', [])
nodes = cleaned_data.pop('nodes') nodes = cleaned_data.pop('nodes', None)
assets = Asset.objects.filter(id__in=[asset.id for asset in assets]) assets = Asset.objects.filter(id__in=[asset.id for asset in assets])
assets.update(**cleaned_data) assets.update(**cleaned_data)
if labels: if labels:
for label in labels: for asset in assets:
label.assets.add(*tuple(assets)) asset.labels.set(labels)
if nodes: if nodes:
for node in nodes: for asset in assets:
node.assets.add(*tuple(assets)) asset.nodes.set(nodes)
return assets return assets
...@@ -28,6 +28,15 @@ class DomainForm(forms.ModelForm): ...@@ -28,6 +28,15 @@ class DomainForm(forms.ModelForm):
initial['assets'] = kwargs['instance'].assets.all() initial['assets'] = kwargs['instance'].assets.all()
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# 前端渲染优化, 防止过多资产
assets_field = self.fields.get('assets')
if not self.data:
instance = kwargs.get('instance')
if instance:
assets_field.queryset = instance.assets.all()
else:
assets_field.queryset = Asset.objects.none()
def save(self, commit=True): def save(self, commit=True):
instance = super().save(commit=commit) instance = super().save(commit=commit)
assets = self.cleaned_data['assets'] assets = self.cleaned_data['assets']
...@@ -40,6 +49,8 @@ class GatewayForm(PasswordAndKeyAuthForm, OrgModelForm): ...@@ -40,6 +49,8 @@ class GatewayForm(PasswordAndKeyAuthForm, OrgModelForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
password_field = self.fields.get('password') password_field = self.fields.get('password')
password_field.help_text = _('Password should not contain special characters') password_field.help_text = _('Password should not contain special characters')
protocol_field = self.fields.get('protocol')
protocol_field.choices = [Gateway.PROTOCOL_CHOICES[0]]
def save(self, commit=True): def save(self, commit=True):
# Because we define custom field, so we need rewrite :method: `save` # Because we define custom field, so we need rewrite :method: `save`
...@@ -59,7 +70,3 @@ class GatewayForm(PasswordAndKeyAuthForm, OrgModelForm): ...@@ -59,7 +70,3 @@ class GatewayForm(PasswordAndKeyAuthForm, OrgModelForm):
'name': forms.TextInput(attrs={'placeholder': _('Name')}), 'name': forms.TextInput(attrs={'placeholder': _('Name')}),
'username': forms.TextInput(attrs={'placeholder': _('Username')}), 'username': forms.TextInput(attrs={'placeholder': _('Username')}),
} }
help_texts = {
'name': '* required',
'username': '* required',
}
...@@ -26,6 +26,15 @@ class LabelForm(forms.ModelForm): ...@@ -26,6 +26,15 @@ class LabelForm(forms.ModelForm):
initial['assets'] = kwargs['instance'].assets.all() initial['assets'] = kwargs['instance'].assets.all()
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# 前端渲染优化, 防止过多资产
assets_field = self.fields.get('assets')
if not self.data:
instance = kwargs.get('instance')
if instance:
assets_field.queryset = instance.assets.all()
else:
assets_field.queryset = Asset.objects.none()
def save(self, commit=True): def save(self, commit=True):
label = super().save(commit=commit) label = super().save(commit=commit)
assets = self.cleaned_data['assets'] assets = self.cleaned_data['assets']
......
...@@ -80,10 +80,6 @@ class AdminUserForm(PasswordAndKeyAuthForm): ...@@ -80,10 +80,6 @@ class AdminUserForm(PasswordAndKeyAuthForm):
'name': forms.TextInput(attrs={'placeholder': _('Name')}), 'name': forms.TextInput(attrs={'placeholder': _('Name')}),
'username': forms.TextInput(attrs={'placeholder': _('Username')}), 'username': forms.TextInput(attrs={'placeholder': _('Username')}),
} }
help_texts = {
'name': '* required',
'username': '* required',
}
class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm): class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
...@@ -99,8 +95,10 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm): ...@@ -99,8 +95,10 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
auto_generate_key = self.cleaned_data.get('auto_generate_key', False) auto_generate_key = self.cleaned_data.get('auto_generate_key', False)
private_key, public_key = super().gen_keys() private_key, public_key = super().gen_keys()
if login_mode == SystemUser.MANUAL_LOGIN or \ if login_mode == SystemUser.LOGIN_MANUAL or \
protocol in [SystemUser.RDP_PROTOCOL, SystemUser.TELNET_PROTOCOL]: protocol in [SystemUser.PROTOCOL_RDP,
SystemUser.PROTOCOL_TELNET,
SystemUser.PROTOCOL_VNC]:
system_user.auto_push = 0 system_user.auto_push = 0
auto_generate_key = False auto_generate_key = False
system_user.save() system_user.save()
...@@ -120,17 +118,18 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm): ...@@ -120,17 +118,18 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
if not self.instance and not auto_generate: if not self.instance and not auto_generate:
super().validate_password_key() super().validate_password_key()
def is_valid(self): def clean_username(self):
validated = super().is_valid() username = self.data.get('username')
username = self.cleaned_data.get('username') login_mode = self.data.get('login_mode')
login_mode = self.cleaned_data.get('login_mode') protocol = self.data.get('protocol')
if login_mode == SystemUser.AUTO_LOGIN and not username:
self.add_error( if username:
"username", _('* Automatic login mode,' return username
' must fill in the username.') if login_mode == SystemUser.LOGIN_AUTO and \
) protocol != SystemUser.PROTOCOL_VNC:
return False msg = _('* Automatic login mode must fill in the username.')
return validated raise forms.ValidationError(msg)
return username
class Meta: class Meta:
model = SystemUser model = SystemUser
...@@ -147,8 +146,6 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm): ...@@ -147,8 +146,6 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
}), }),
} }
help_texts = { help_texts = {
'name': '* required',
'username': '* required',
'auto_push': _('Auto push system user to asset'), 'auto_push': _('Auto push system user to asset'),
'priority': _('1-100, High level will be using login asset as default, ' 'priority': _('1-100, High level will be using login asset as default, '
'if user was granted more than 2 system user'), 'if user was granted more than 2 system user'),
......
...@@ -13,36 +13,36 @@ class Migration(migrations.Migration): ...@@ -13,36 +13,36 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='adminuser', model_name='adminuser',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='asset', model_name='asset',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='domain', model_name='domain',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='gateway', model_name='gateway',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='label', model_name='label',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='node', model_name='node',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='systemuser', model_name='systemuser',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
] ]
...@@ -16,7 +16,7 @@ class Migration(migrations.Migration): ...@@ -16,7 +16,7 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='CommandFilter', name='CommandFilter',
fields=[ fields=[
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')), ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=64, verbose_name='Name')), ('name', models.CharField(max_length=64, verbose_name='Name')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')), ('is_active', models.BooleanField(default=True, verbose_name='Is active')),
...@@ -32,7 +32,7 @@ class Migration(migrations.Migration): ...@@ -32,7 +32,7 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='CommandFilterRule', name='CommandFilterRule',
fields=[ fields=[
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')), ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('type', models.CharField(choices=[('regex', 'Regex'), ('command', 'Command')], default='command', max_length=16, verbose_name='Type')), ('type', models.CharField(choices=[('regex', 'Regex'), ('command', 'Command')], default='command', max_length=16, verbose_name='Type')),
('priority', models.IntegerField(default=50, help_text='1-100, the lower will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')), ('priority', models.IntegerField(default=50, help_text='1-100, the lower will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')),
......
# Generated by Django 2.1.4 on 2018-12-19 08:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0023_auto_20181016_1650'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)'), ('vnc', 'vnc')], default='ssh', max_length=128, verbose_name='Protocol'),
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)'), ('vnc', 'vnc')], default='ssh', max_length=16, verbose_name='Protocol'),
),
]
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
from .user import * from .user import *
from .label import Label from .label import Label
from .cluster import * from .cluster import *
......
...@@ -13,7 +13,6 @@ from django.db.models import Q ...@@ -13,7 +13,6 @@ from django.db.models import Q
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.cache import cache from django.core.cache import cache
from ..const import ASSET_ADMIN_CONN_CACHE_KEY
from .user import AdminUser, SystemUser from .user import AdminUser, SystemUser
from orgs.mixins import OrgModelMixin, OrgManager from orgs.mixins import OrgModelMixin, OrgManager
...@@ -60,78 +59,65 @@ class Asset(OrgModelMixin): ...@@ -60,78 +59,65 @@ class Asset(OrgModelMixin):
('Other', 'Other'), ('Other', 'Other'),
) )
SSH_PROTOCOL = 'ssh' PROTOCOL_SSH = 'ssh'
RDP_PROTOCOL = 'rdp' PROTOCOL_RDP = 'rdp'
TELNET_PROTOCOL = 'telnet' PROTOCOL_TELNET = 'telnet'
PROTOCOL_VNC = 'vnc'
PROTOCOL_CHOICES = ( PROTOCOL_CHOICES = (
(SSH_PROTOCOL, 'ssh'), (PROTOCOL_SSH, 'ssh'),
(RDP_PROTOCOL, 'rdp'), (PROTOCOL_RDP, 'rdp'),
(TELNET_PROTOCOL, 'telnet (beta)'), (PROTOCOL_TELNET, 'telnet (beta)'),
(PROTOCOL_VNC, 'vnc'),
) )
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True) ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True)
hostname = models.CharField(max_length=128, verbose_name=_('Hostname')) hostname = models.CharField(max_length=128, verbose_name=_('Hostname'))
protocol = models.CharField(max_length=128, default=SSH_PROTOCOL, choices=PROTOCOL_CHOICES, verbose_name=_('Protocol')) protocol = models.CharField(max_length=128, default=PROTOCOL_SSH, choices=PROTOCOL_CHOICES, verbose_name=_('Protocol'))
port = models.IntegerField(default=22, verbose_name=_('Port')) port = models.IntegerField(default=22, verbose_name=_('Port'))
platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform')) platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform'))
domain = models.ForeignKey("assets.Domain", null=True, blank=True, domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), on_delete=models.SET_NULL)
related_name='assets', verbose_name=_("Domain"), nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes"))
on_delete=models.SET_NULL)
nodes = models.ManyToManyField('assets.Node', default=default_node,
related_name='assets',
verbose_name=_("Nodes"))
is_active = models.BooleanField(default=True, verbose_name=_('Is active')) is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
# Auth # Auth
admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, null=True, verbose_name=_("Admin user"))
null=True, verbose_name=_("Admin user"))
# Some information # Some information
public_ip = models.GenericIPAddressField(max_length=32, blank=True, null=True, verbose_name=_('Public IP')) public_ip = models.GenericIPAddressField(max_length=32, blank=True, null=True, verbose_name=_('Public IP'))
number = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Asset number')) number = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Asset number'))
# Collect # Collect
vendor = models.CharField(max_length=64, null=True, blank=True, vendor = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Vendor'))
verbose_name=_('Vendor')) model = models.CharField(max_length=54, null=True, blank=True, verbose_name=_('Model'))
model = models.CharField(max_length=54, null=True, blank=True, sn = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Serial number'))
verbose_name=_('Model'))
sn = models.CharField(max_length=128, null=True, blank=True, cpu_model = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('CPU model'))
verbose_name=_('Serial number'))
cpu_model = models.CharField(max_length=64, null=True, blank=True,
verbose_name=_('CPU model'))
cpu_count = models.IntegerField(null=True, verbose_name=_('CPU count')) cpu_count = models.IntegerField(null=True, verbose_name=_('CPU count'))
cpu_cores = models.IntegerField(null=True, verbose_name=_('CPU cores')) cpu_cores = models.IntegerField(null=True, verbose_name=_('CPU cores'))
cpu_vcpus = models.IntegerField(null=True, verbose_name=_('CPU vcpus')) cpu_vcpus = models.IntegerField(null=True, verbose_name=_('CPU vcpus'))
memory = models.CharField(max_length=64, null=True, blank=True, memory = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Memory'))
verbose_name=_('Memory')) disk_total = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk total'))
disk_total = models.CharField(max_length=1024, null=True, blank=True, disk_info = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk info'))
verbose_name=_('Disk total'))
disk_info = models.CharField(max_length=1024, null=True, blank=True, os = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('OS'))
verbose_name=_('Disk info')) os_version = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('OS version'))
os_arch = models.CharField(max_length=16, blank=True, null=True, verbose_name=_('OS arch'))
os = models.CharField(max_length=128, null=True, blank=True, hostname_raw = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Hostname raw'))
verbose_name=_('OS'))
os_version = models.CharField(max_length=16, null=True, blank=True, labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
verbose_name=_('OS version')) created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
os_arch = models.CharField(max_length=16, blank=True, null=True, date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created'))
verbose_name=_('OS arch')) comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment'))
hostname_raw = models.CharField(max_length=128, blank=True, null=True,
verbose_name=_('Hostname raw'))
labels = models.ManyToManyField('assets.Label', blank=True,
related_name='assets',
verbose_name=_("Labels"))
created_by = models.CharField(max_length=32, null=True, blank=True,
verbose_name=_('Created by'))
date_created = models.DateTimeField(auto_now_add=True, null=True,
blank=True,
verbose_name=_('Date created'))
comment = models.TextField(max_length=128, default='', blank=True,
verbose_name=_('Comment'))
objects = OrgManager.from_queryset(AssetQuerySet)() objects = OrgManager.from_queryset(AssetQuerySet)()
CONNECTIVITY_CACHE_KEY = '_JMS_ASSET_CONNECTIVITY_{}'
UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3)
CONNECTIVITY_CHOICES = (
(UNREACHABLE, _("Unreachable")),
(REACHABLE, _('Reachable')),
(UNKNOWN, _("Unknown")),
)
def __str__(self): def __str__(self):
return '{0.hostname}({0.ip})'.format(self) return '{0.hostname}({0.ip})'.format(self)
...@@ -145,6 +131,13 @@ class Asset(OrgModelMixin): ...@@ -145,6 +131,13 @@ class Asset(OrgModelMixin):
return True, '' return True, ''
return False, warning return False, warning
def support_ansible(self):
if self.platform in ("Windows", "Windows2016", "Other"):
return False
if self.protocol != 'ssh':
return False
return True
def is_unixlike(self): def is_unixlike(self):
if self.platform not in ("Windows", "Windows2016"): if self.platform not in ("Windows", "Windows2016"):
return True return True
...@@ -190,25 +183,17 @@ class Asset(OrgModelMixin): ...@@ -190,25 +183,17 @@ class Asset(OrgModelMixin):
return '' return ''
@property @property
def is_connective(self): def connectivity(self):
if not self.is_unixlike(): if not self.is_unixlike():
return True return self.UNKNOWN
val = cache.get(ASSET_ADMIN_CONN_CACHE_KEY.format(self.hostname)) key = self.CONNECTIVITY_CACHE_KEY.format(str(self.id))
if val == 1: cached = cache.get(key, None)
return True return cached if cached is not None else self.UNKNOWN
else:
return False
def to_json(self): @connectivity.setter
info = { def connectivity(self, value):
'id': self.id, key = self.CONNECTIVITY_CACHE_KEY.format(str(self.id))
'hostname': self.hostname, cache.set(key, value, 3600*2)
'ip': self.ip,
'port': self.port,
}
if self.domain and self.domain.gateway_set.all():
info["gateways"] = [d.id for d in self.domain.gateway_set.all()]
return info
def get_auth_info(self): def get_auth_info(self):
if self.admin_user: if self.admin_user:
...@@ -229,11 +214,20 @@ class Asset(OrgModelMixin): ...@@ -229,11 +214,20 @@ class Asset(OrgModelMixin):
fake_node.is_node = False fake_node.is_node = False
return fake_node return fake_node
def to_json(self):
info = {
'id': self.id,
'hostname': self.hostname,
'ip': self.ip,
'port': self.port,
}
if self.domain and self.domain.gateway_set.all():
info["gateways"] = [d.id for d in self.domain.gateway_set.all()]
return info
def _to_secret_json(self): def _to_secret_json(self):
""" """
Ansible use it create inventory, First using asset user, Ansible use it create inventory
otherwise using cluster admin user
Todo: May be move to ops implements it Todo: May be move to ops implements it
""" """
data = self.to_json() data = self.to_json()
...@@ -248,6 +242,36 @@ class Asset(OrgModelMixin): ...@@ -248,6 +242,36 @@ class Asset(OrgModelMixin):
}) })
return data return data
def as_tree_node(self, parent_node):
from common.tree import TreeNode
icon_skin = 'file'
if self.platform.lower() == 'windows':
icon_skin = 'windows'
elif self.platform.lower() == 'linux':
icon_skin = 'linux'
data = {
'id': str(self.id),
'name': self.hostname,
'title': self.ip,
'pId': parent_node.key,
'isParent': False,
'open': False,
'iconSkin': icon_skin,
'meta': {
'type': 'asset',
'asset': {
'id': self.id,
'hostname': self.hostname,
'ip': self.ip,
'port': self.port,
'platform': self.platform,
'protocol': self.protocol,
}
}
}
tree_node = TreeNode(**data)
return tree_node
class Meta: class Meta:
unique_together = [('org_id', 'hostname')] unique_together = [('org_id', 'hostname')]
verbose_name = _("Asset") verbose_name = _("Asset")
...@@ -257,7 +281,8 @@ class Asset(OrgModelMixin): ...@@ -257,7 +281,8 @@ class Asset(OrgModelMixin):
from random import seed, choice from random import seed, choice
import forgery_py import forgery_py
from django.db import IntegrityError from django.db import IntegrityError
from .node import Node
nodes = list(Node.objects.all())
seed() seed()
for i in range(count): for i in range(count):
ip = [str(i) for i in random.sample(range(255), 4)] ip = [str(i) for i in random.sample(range(255), 4)]
...@@ -268,6 +293,11 @@ class Asset(OrgModelMixin): ...@@ -268,6 +293,11 @@ class Asset(OrgModelMixin):
created_by='Fake') created_by='Fake')
try: try:
asset.save() asset.save()
if nodes and len(nodes) > 3:
_nodes = random.sample(nodes, 3)
else:
_nodes = [Node.default_node()]
asset.nodes.set(_nodes)
asset.system_users = [choice(SystemUser.objects.all()) for i in range(3)] asset.system_users = [choice(SystemUser.objects.all()) for i in range(3)]
logger.debug('Generate fake asset : %s' % asset.ip) logger.debug('Generate fake asset : %s' % asset.ip)
except IntegrityError: except IntegrityError:
......
...@@ -29,6 +29,13 @@ class AssetUser(OrgModelMixin): ...@@ -29,6 +29,13 @@ class AssetUser(OrgModelMixin):
date_updated = models.DateTimeField(auto_now=True) date_updated = models.DateTimeField(auto_now=True)
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by')) created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3)
CONNECTIVITY_CHOICES = (
(UNREACHABLE, _("Unreachable")),
(REACHABLE, _('Reachable')),
(UNKNOWN, _("Unknown")),
)
@property @property
def password(self): def password(self):
if self._password: if self._password:
...@@ -105,6 +112,9 @@ class AssetUser(OrgModelMixin): ...@@ -105,6 +112,9 @@ class AssetUser(OrgModelMixin):
if update_fields: if update_fields:
self.save(update_fields=update_fields) self.save(update_fields=update_fields)
def get_auth(self, asset=None):
pass
def clear_auth(self): def clear_auth(self):
self._password = '' self._password = ''
self._private_key = '' self._private_key = ''
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import uuid import uuid
import re
from django.db import models from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
...@@ -35,7 +36,7 @@ class CommandFilterRule(OrgModelMixin): ...@@ -35,7 +36,7 @@ class CommandFilterRule(OrgModelMixin):
(TYPE_COMMAND, _('Command')), (TYPE_COMMAND, _('Command')),
) )
ACTION_DENY, ACTION_ALLOW = range(2) ACTION_DENY, ACTION_ALLOW, ACTION_UNKNOWN = range(3)
ACTION_CHOICES = ( ACTION_CHOICES = (
(ACTION_DENY, _('Deny')), (ACTION_DENY, _('Deny')),
(ACTION_ALLOW, _('Allow')), (ACTION_ALLOW, _('Allow')),
...@@ -53,8 +54,34 @@ class CommandFilterRule(OrgModelMixin): ...@@ -53,8 +54,34 @@ class CommandFilterRule(OrgModelMixin):
date_updated = models.DateTimeField(auto_now=True) date_updated = models.DateTimeField(auto_now=True)
created_by = models.CharField(max_length=128, blank=True, default='', verbose_name=_('Created by')) created_by = models.CharField(max_length=128, blank=True, default='', verbose_name=_('Created by'))
__pattern = None
class Meta: class Meta:
ordering = ('-priority', 'action') ordering = ('-priority', 'action')
@property
def _pattern(self):
if self.__pattern:
return self.__pattern
if self.type == 'command':
regex = []
for cmd in self.content.split('\r\n'):
cmd = cmd.replace(' ', '\s+')
regex.append(r'\b{0}\b'.format(cmd))
self.__pattern = re.compile(r'{}'.format('|'.join(regex)))
else:
self.__pattern = re.compile(r'{0}'.format(self.content))
return self.__pattern
def match(self, data):
found = self._pattern.search(data)
if not found:
return self.ACTION_UNKNOWN, ''
if self.action == self.ACTION_ALLOW:
return self.ACTION_ALLOW, found.group()
else:
return self.ACTION_DENY, found.group()
def __str__(self): def __str__(self):
return '{} % {}'.format(self.type, self.content) return '{} % {}'.format(self.type, self.content)
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
import uuid import uuid
import random import random
import paramiko
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
...@@ -38,15 +40,15 @@ class Domain(OrgModelMixin): ...@@ -38,15 +40,15 @@ class Domain(OrgModelMixin):
class Gateway(AssetUser): class Gateway(AssetUser):
SSH_PROTOCOL = 'ssh' PROTOCOL_SSH = 'ssh'
RDP_PROTOCOL = 'rdp' PROTOCOL_RDP = 'rdp'
PROTOCOL_CHOICES = ( PROTOCOL_CHOICES = (
(SSH_PROTOCOL, 'ssh'), (PROTOCOL_SSH, 'ssh'),
(RDP_PROTOCOL, 'rdp'), (PROTOCOL_RDP, 'rdp'),
) )
ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True) ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True)
port = models.IntegerField(default=22, verbose_name=_('Port')) port = models.IntegerField(default=22, verbose_name=_('Port'))
protocol = models.CharField(choices=PROTOCOL_CHOICES, max_length=16, default=SSH_PROTOCOL, verbose_name=_("Protocol")) protocol = models.CharField(choices=PROTOCOL_CHOICES, max_length=16, default=PROTOCOL_SSH, verbose_name=_("Protocol"))
domain = models.ForeignKey(Domain, on_delete=models.CASCADE, verbose_name=_("Domain")) domain = models.ForeignKey(Domain, on_delete=models.CASCADE, verbose_name=_("Domain"))
comment = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Comment")) comment = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Comment"))
is_active = models.BooleanField(default=True, verbose_name=_("Is active")) is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
...@@ -57,3 +59,37 @@ class Gateway(AssetUser): ...@@ -57,3 +59,37 @@ class Gateway(AssetUser):
class Meta: class Meta:
unique_together = [('name', 'org_id')] unique_together = [('name', 'org_id')]
verbose_name = _("Gateway") verbose_name = _("Gateway")
def test_connective(self):
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
proxy = paramiko.SSHClient()
proxy.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
proxy.connect(self.ip, port=self.port,
username=self.username,
password=self.password,
pkey=self.private_key_obj)
except(paramiko.AuthenticationException,
paramiko.BadAuthenticationType,
paramiko.SSHException) as e:
return False, str(e)
sock = proxy.get_transport().open_channel(
'direct-tcpip', ('127.0.0.1', self.port), ('127.0.0.1', 0)
)
try:
client.connect("127.0.0.1", port=self.port,
username=self.username,
password=self.password,
key_filename=self.private_key_file,
sock=sock,
timeout=5)
except (paramiko.SSHException, paramiko.ssh_exception.SSHException,
paramiko.AuthenticationException, TimeoutError) as e:
return False, str(e)
finally:
client.close()
return True, None
...@@ -17,7 +17,8 @@ class Label(OrgModelMixin): ...@@ -17,7 +17,8 @@ class Label(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_("Name")) name = models.CharField(max_length=128, verbose_name=_("Name"))
value = models.CharField(max_length=128, verbose_name=_("Value")) value = models.CharField(max_length=128, verbose_name=_("Value"))
category = models.CharField(max_length=128, choices=CATEGORY_CHOICES, default=USER_CATEGORY, verbose_name=_("Category")) category = models.CharField(max_length=128, choices=CATEGORY_CHOICES,
default=USER_CATEGORY, verbose_name=_("Category"))
is_active = models.BooleanField(default=True, verbose_name=_("Is active")) is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
comment = models.TextField(blank=True, null=True, verbose_name=_("Comment")) comment = models.TextField(blank=True, null=True, verbose_name=_("Comment"))
date_created = models.DateTimeField( date_created = models.DateTimeField(
......
...@@ -5,6 +5,7 @@ import uuid ...@@ -5,6 +5,7 @@ import uuid
from django.db import models, transaction from django.db import models, transaction
from django.db.models import Q from django.db.models import Q
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.core.cache import cache from django.core.cache import cache
from orgs.mixins import OrgModelMixin from orgs.mixins import OrgModelMixin
...@@ -22,7 +23,9 @@ class Node(OrgModelMixin): ...@@ -22,7 +23,9 @@ class Node(OrgModelMixin):
date_create = models.DateTimeField(auto_now_add=True) date_create = models.DateTimeField(auto_now_add=True)
is_node = True is_node = True
_full_value_cache_key_prefix = '_NODE_VALUE_{}' _assets_amount = None
_full_value_cache_key = '_NODE_VALUE_{}'
_assets_amount_cache_key = '_NODE_ASSETS_AMOUNT_{}'
class Meta: class Meta:
verbose_name = _("Node") verbose_name = _("Node")
...@@ -49,30 +52,65 @@ class Node(OrgModelMixin): ...@@ -49,30 +52,65 @@ class Node(OrgModelMixin):
def name(self): def name(self):
return self.value return self.value
@property
def assets_amount(self):
"""
获取节点下所有资产数量速度太慢,所以需要重写,使用cache等方案
:return:
"""
if self._assets_amount is not None:
return self._assets_amount
cache_key = self._assets_amount_cache_key.format(self.key)
cached = cache.get(cache_key)
if cached is not None:
return cached
assets_amount = self.get_all_assets().count()
cache.set(cache_key, assets_amount, 3600)
return assets_amount
@assets_amount.setter
def assets_amount(self, value):
self._assets_amount = value
def expire_assets_amount(self):
ancestor_keys = self.get_ancestor_keys(with_self=True)
cache_keys = [self._assets_amount_cache_key.format(k) for k in ancestor_keys]
cache.delete_many(cache_keys)
@classmethod
def expire_nodes_assets_amount(cls, nodes=None):
if nodes:
for node in nodes:
node.expire_assets_amount()
return
key = cls._assets_amount_cache_key.format('*')
cache.delete_pattern(key)
@property @property
def full_value(self): def full_value(self):
key = self._full_value_cache_key_prefix.format(self.key) key = self._full_value_cache_key.format(self.key)
cached = cache.get(key) cached = cache.get(key)
if cached: if cached:
return cached return cached
value = self.get_full_value()
self.cache_full_value(value)
return value
def get_full_value(self):
# ancestor = [a.value for a in self.get_ancestor(with_self=True)]
if self.is_root(): if self.is_root():
return self.value return self.value
parent_full_value = self.parent.full_value parent_full_value = self.parent.full_value
value = parent_full_value + ' / ' + self.value value = parent_full_value + ' / ' + self.value
return value key = self._full_value_cache_key.format(self.key)
def cache_full_value(self, value):
key = self._full_value_cache_key_prefix.format(self.key)
cache.set(key, value, 3600) cache.set(key, value, 3600)
return value
def expire_full_value(self): def expire_full_value(self):
key = self._full_value_cache_key_prefix.format(self.key) key = self._full_value_cache_key.format(self.key)
cache.delete_pattern(key+'*')
@classmethod
def expire_nodes_full_value(cls, nodes=None):
if nodes:
for node in nodes:
node.expire_full_value()
return
key = cls._full_value_cache_key.format('*')
cache.delete_pattern(key+'*') cache.delete_pattern(key+'*')
@property @property
...@@ -85,6 +123,17 @@ class Node(OrgModelMixin): ...@@ -85,6 +123,17 @@ class Node(OrgModelMixin):
self.save() self.save()
return "{}:{}".format(self.key, mark) return "{}:{}".format(self.key, mark)
def get_next_child_preset_name(self):
name = ugettext("New node")
values = [
child.value[child.value.rfind(' '):]
for child in self.get_children()
if child.value.startswith(name)
]
values = [int(value) for value in values if value.strip().isdigit()]
count = max(values) + 1 if values else 1
return '{} {}'.format(name, count)
def create_child(self, value): def create_child(self, value):
with transaction.atomic(): with transaction.atomic():
child_key = self.get_next_child_key() child_key = self.get_next_child_key()
...@@ -134,7 +183,7 @@ class Node(OrgModelMixin): ...@@ -134,7 +183,7 @@ class Node(OrgModelMixin):
pattern = r'^{0}$|^{0}:'.format(self.key) pattern = r'^{0}$|^{0}:'.format(self.key)
args = [] args = []
kwargs = {} kwargs = {}
if self.is_default_node(): if self.is_root():
args.append(Q(nodes__key__regex=pattern) | Q(nodes=None)) args.append(Q(nodes__key__regex=pattern) | Q(nodes=None))
else: else:
kwargs['nodes__key__regex'] = pattern kwargs['nodes__key__regex'] = pattern
...@@ -182,17 +231,18 @@ class Node(OrgModelMixin): ...@@ -182,17 +231,18 @@ class Node(OrgModelMixin):
child.save() child.save()
self.save() self.save()
def get_ancestor(self, with_self=False): def get_ancestor_keys(self, with_self=False):
if self.is_root(): parent_keys = []
root = self.__class__.root() key_list = self.key.split(":")
return [root]
_key = self.key.split(':')
if not with_self: if not with_self:
_key.pop() key_list.pop()
ancestor_keys = [] for i in range(len(key_list)):
for i in range(len(_key)): parent_keys.append(":".join(key_list))
ancestor_keys.append(':'.join(_key)) key_list.pop()
_key.pop() return parent_keys
def get_ancestor(self, with_self=False):
ancestor_keys = self.get_ancestor_keys(with_self=with_self)
ancestor = self.__class__.objects.filter( ancestor = self.__class__.objects.filter(
key__in=ancestor_keys key__in=ancestor_keys
).order_by('key') ).order_by('key')
...@@ -203,17 +253,14 @@ class Node(OrgModelMixin): ...@@ -203,17 +253,14 @@ class Node(OrgModelMixin):
# 如果使用current_org 在set_current_org时会死循环 # 如果使用current_org 在set_current_org时会死循环
_current_org = get_current_org() _current_org = get_current_org()
with transaction.atomic(): with transaction.atomic():
if _current_org.is_root(): if not _current_org.is_real():
key = '0' return cls.default_node()
elif _current_org.is_default(): set_current_org(Organization.root())
key = '1' org_nodes_roots = cls.objects.filter(key__regex=r'^[0-9]+$')
else: org_nodes_roots_keys = org_nodes_roots.values_list('key', flat=True) or ['1']
set_current_org(Organization.root()) key = max([int(k) for k in org_nodes_roots_keys])
org_nodes_roots = cls.objects.filter(key__regex=r'^[0-9]+$') key = str(key + 1) if key != 0 else '2'
org_nodes_roots_keys = org_nodes_roots.values_list('key', flat=True) or ['1'] set_current_org(_current_org)
key = max([int(k) for k in org_nodes_roots_keys])
key = str(key + 1) if key != 0 else '2'
set_current_org(_current_org)
root = cls.objects.create(key=key, value=_current_org.name) root = cls.objects.create(key=key, value=_current_org.name)
return root return root
...@@ -230,9 +277,25 @@ class Node(OrgModelMixin): ...@@ -230,9 +277,25 @@ class Node(OrgModelMixin):
defaults = {'value': 'Default'} defaults = {'value': 'Default'}
return cls.objects.get_or_create(defaults=defaults, key='1') return cls.objects.get_or_create(defaults=defaults, key='1')
@classmethod def as_tree_node(self):
def get_tree_name_ref(cls): from common.tree import TreeNode
pass from ..serializers import NodeSerializer
name = '{} ({})'.format(self.value, self.assets_amount)
node_serializer = NodeSerializer(instance=self)
data = {
'id': self.key,
'name': name,
'title': name,
'pId': self.parent_key,
'isParent': True,
'open': self.is_root(),
'meta': {
'node': node_serializer.data,
'type': 'node'
}
}
tree_node = TreeNode(**data)
return tree_node
@classmethod @classmethod
def generate_fake(cls, count=100): def generate_fake(cls, count=100):
......
...@@ -14,7 +14,7 @@ from ..const import SYSTEM_USER_CONN_CACHE_KEY ...@@ -14,7 +14,7 @@ from ..const import SYSTEM_USER_CONN_CACHE_KEY
from .base import AssetUser from .base import AssetUser
__all__ = ['AdminUser', 'SystemUser',] __all__ = ['AdminUser', 'SystemUser']
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
signer = get_signer() signer = get_signer()
...@@ -31,6 +31,7 @@ class AdminUser(AssetUser): ...@@ -31,6 +31,7 @@ class AdminUser(AssetUser):
become_method = models.CharField(choices=BECOME_METHOD_CHOICES, default='sudo', max_length=4) become_method = models.CharField(choices=BECOME_METHOD_CHOICES, default='sudo', max_length=4)
become_user = models.CharField(default='root', max_length=64) become_user = models.CharField(default='root', max_length=64)
_become_pass = models.CharField(default='', max_length=128) _become_pass = models.CharField(default='', max_length=128)
CONNECTIVE_CACHE_KEY = '_JMS_ADMIN_USER_CONNECTIVE_{}'
def __str__(self): def __str__(self):
return self.name return self.name
...@@ -67,6 +68,23 @@ class AdminUser(AssetUser): ...@@ -67,6 +68,23 @@ class AdminUser(AssetUser):
def assets_amount(self): def assets_amount(self):
return self.get_related_assets().count() return self.get_related_assets().count()
@property
def connectivity(self):
from .asset import Asset
assets = self.get_related_assets().values_list('id', 'hostname', flat=True)
data = {
'unreachable': [],
'reachable': [],
}
for asset_id, hostname in assets:
key = Asset.CONNECTIVITY_CACHE_KEY.format(str(self.id))
value = cache.get(key, Asset.UNKNOWN)
if value == Asset.REACHABLE:
data['reachable'].append(hostname)
elif value == Asset.UNREACHABLE:
data['unreachable'].append(hostname)
return data
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
unique_together = [('name', 'org_id')] unique_together = [('name', 'org_id')]
...@@ -94,34 +112,36 @@ class AdminUser(AssetUser): ...@@ -94,34 +112,36 @@ class AdminUser(AssetUser):
class SystemUser(AssetUser): class SystemUser(AssetUser):
SSH_PROTOCOL = 'ssh' PROTOCOL_SSH = 'ssh'
RDP_PROTOCOL = 'rdp' PROTOCOL_RDP = 'rdp'
TELNET_PROTOCOL = 'telnet' PROTOCOL_TELNET = 'telnet'
PROTOCOL_VNC = 'vnc'
PROTOCOL_CHOICES = ( PROTOCOL_CHOICES = (
(SSH_PROTOCOL, 'ssh'), (PROTOCOL_SSH, 'ssh'),
(RDP_PROTOCOL, 'rdp'), (PROTOCOL_RDP, 'rdp'),
(TELNET_PROTOCOL, 'telnet (beta)'), (PROTOCOL_TELNET, 'telnet (beta)'),
(PROTOCOL_VNC, 'vnc'),
) )
AUTO_LOGIN = 'auto' LOGIN_AUTO = 'auto'
MANUAL_LOGIN = 'manual' LOGIN_MANUAL = 'manual'
LOGIN_MODE_CHOICES = ( LOGIN_MODE_CHOICES = (
(AUTO_LOGIN, _('Automatic login')), (LOGIN_AUTO, _('Automatic login')),
(MANUAL_LOGIN, _('Manually login')) (LOGIN_MANUAL, _('Manually login'))
) )
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes")) nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets")) assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
priority = models.IntegerField(default=20, verbose_name=_("Priority"), priority = models.IntegerField(default=20, verbose_name=_("Priority"), validators=[MinValueValidator(1), MaxValueValidator(100)])
validators=[MinValueValidator(1), MaxValueValidator(100)])
protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol')) protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol'))
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push')) auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo')) sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell')) shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=AUTO_LOGIN, max_length=10, verbose_name=_('Login mode')) login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode'))
cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True) cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True)
cache_key = "__SYSTEM_USER_CACHED_{}" SYSTEM_USER_CACHE_KEY = "__SYSTEM_USER_CACHED_{}"
CONNECTIVE_CACHE_KEY = '_JMS_SYSTEM_USER_CONNECTIVE_{}'
def __str__(self): def __str__(self):
return '{0.name}({0.username})'.format(self) return '{0.name}({0.username})'.format(self)
...@@ -136,34 +156,61 @@ class SystemUser(AssetUser): ...@@ -136,34 +156,61 @@ class SystemUser(AssetUser):
'auto_push': self.auto_push, 'auto_push': self.auto_push,
} }
def get_assets(self): def get_related_assets(self):
assets = set(self.assets.all()) assets = set(self.assets.all())
return assets return assets
@property @property
def assets_connective(self): def connectivity(self):
_result = cache.get(SYSTEM_USER_CONN_CACHE_KEY.format(self.name), {}) cache_key = self.CONNECTIVE_CACHE_KEY.format(str(self.id))
return _result value = cache.get(cache_key, None)
if not value or 'unreachable' not in value:
return {'unreachable': [], 'reachable': []}
else:
return value
@connectivity.setter
def connectivity(self, value):
data = self.connectivity
unreachable = data['unreachable']
reachable = data['reachable']
for host in value.get('dark', {}).keys():
if host not in unreachable:
unreachable.append(host)
if host in reachable:
reachable.remove(host)
for host in value.get('contacted'):
if host not in reachable:
reachable.append(host)
if host in unreachable:
unreachable.remove(host)
cache_key = self.CONNECTIVE_CACHE_KEY.format(str(self.id))
cache.set(cache_key, data, 3600)
@property
def assets_unreachable(self):
return self.connectivity.get('unreachable')
@property @property
def unreachable_assets(self): def assets_reachable(self):
return list(self.assets_connective.get('dark', {}).keys()) return self.connectivity.get('reachable')
@property @property
def reachable_assets(self): def login_mode_display(self):
return self.assets_connective.get('contacted', []) return self.get_login_mode_display()
def is_need_push(self): def is_need_push(self):
if self.auto_push and self.protocol == self.__class__.SSH_PROTOCOL: if self.auto_push and self.protocol == self.PROTOCOL_SSH:
return True return True
else: else:
return False return False
def set_cache(self): def set_cache(self):
cache.set(self.cache_key.format(self.id), self, 3600) cache.set(self.SYSTEM_USER_CACHE_KEY.format(self.id), self, 3600)
def expire_cache(self): def expire_cache(self):
cache.delete(self.cache_key.format(self.id)) cache.delete(self.SYSTEM_USER_CACHE_KEY.format(self.id))
@property @property
def cmd_filter_rules(self): def cmd_filter_rules(self):
...@@ -173,9 +220,18 @@ class SystemUser(AssetUser): ...@@ -173,9 +220,18 @@ class SystemUser(AssetUser):
).distinct() ).distinct()
return rules return rules
def is_command_can_run(self, command):
for rule in self.cmd_filter_rules:
action, matched_cmd = rule.match(command)
if action == rule.ACTION_ALLOW:
return True, None
elif action == rule.ACTION_DENY:
return False, matched_cmd
return True, None
@classmethod @classmethod
def get_system_user_by_id_or_cached(cls, sid): def get_system_user_by_id_or_cached(cls, sid):
cached = cache.get(cls.cache_key.format(sid)) cached = cache.get(cls.SYSTEM_USER_CACHE_KEY.format(sid))
if cached: if cached:
return cached return cached
try: try:
......
...@@ -9,6 +9,7 @@ from .system_user import AssetSystemUserSerializer ...@@ -9,6 +9,7 @@ from .system_user import AssetSystemUserSerializer
__all__ = [ __all__ = [
'AssetSerializer', 'AssetGrantedSerializer', 'MyAssetGrantedSerializer', 'AssetSerializer', 'AssetGrantedSerializer', 'MyAssetGrantedSerializer',
'AssetAsNodeSerializer', 'AssetSimpleSerializer',
] ]
...@@ -22,14 +23,27 @@ class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer): ...@@ -22,14 +23,27 @@ class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer):
fields = '__all__' fields = '__all__'
validators = [] validators = []
@classmethod
def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """
queryset = queryset.prefetch_related('labels', 'nodes')\
.select_related('admin_user')
return queryset
def get_field_names(self, declared_fields, info): def get_field_names(self, declared_fields, info):
fields = super().get_field_names(declared_fields, info) fields = super().get_field_names(declared_fields, info)
fields.extend([ fields.extend([
'hardware_info', 'is_connective', 'org_name' 'hardware_info', 'connectivity', 'org_name'
]) ])
return fields return fields
class AssetAsNodeSerializer(serializers.ModelSerializer):
class Meta:
model = Asset
fields = ['id', 'hostname', 'ip', 'port', 'platform', 'protocol']
class AssetGrantedSerializer(serializers.ModelSerializer): class AssetGrantedSerializer(serializers.ModelSerializer):
""" """
被授权资产的数据结构 被授权资产的数据结构
...@@ -64,3 +78,9 @@ class MyAssetGrantedSerializer(AssetGrantedSerializer): ...@@ -64,3 +78,9 @@ class MyAssetGrantedSerializer(AssetGrantedSerializer):
"is_active", "system_users_join", "org_name", "is_active", "system_users_join", "org_name",
"os", "platform", "comment", "org_id", "protocol" "os", "platform", "comment", "org_id", "protocol"
) )
class AssetSimpleSerializer(serializers.ModelSerializer):
class Meta:
model = Asset
fields = ['id', 'hostname', 'port', 'ip', 'connectivity']
...@@ -23,7 +23,6 @@ class DomainSerializer(serializers.ModelSerializer): ...@@ -23,7 +23,6 @@ class DomainSerializer(serializers.ModelSerializer):
class GatewaySerializer(serializers.ModelSerializer): class GatewaySerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Gateway model = Gateway
fields = [ fields = [
......
...@@ -8,85 +8,36 @@ from .asset import AssetGrantedSerializer ...@@ -8,85 +8,36 @@ from .asset import AssetGrantedSerializer
__all__ = [ __all__ = [
'NodeSerializer', "NodeGrantedSerializer", "NodeAddChildrenSerializer", 'NodeSerializer', "NodeAddChildrenSerializer",
"NodeAssetsSerializer", "NodeAssetsSerializer",
] ]
class NodeGrantedSerializer(BulkSerializerMixin, serializers.ModelSerializer):
"""
授权资产组
"""
assets_granted = AssetGrantedSerializer(many=True, read_only=True)
assets_amount = serializers.SerializerMethodField()
parent = serializers.SerializerMethodField()
name = serializers.SerializerMethodField()
class Meta:
model = Node
fields = [
'id', 'key', 'name', 'value', 'parent',
'assets_granted', 'assets_amount', 'org_id',
]
@staticmethod
def get_assets_amount(obj):
return len(obj.assets_granted)
@staticmethod
def get_name(obj):
return obj.name
@staticmethod
def get_parent(obj):
return obj.parent.id
class NodeSerializer(serializers.ModelSerializer): class NodeSerializer(serializers.ModelSerializer):
assets_amount = serializers.SerializerMethodField() assets_amount = serializers.IntegerField(read_only=True)
tree_id = serializers.SerializerMethodField()
tree_parent = serializers.SerializerMethodField()
class Meta: class Meta:
model = Node model = Node
fields = [ fields = [
'id', 'key', 'value', 'assets_amount', 'id', 'key', 'value', 'assets_amount', 'org_id',
'is_node', 'org_id', 'tree_id', 'tree_parent', ]
read_only_fields = [
'id', 'key', 'assets_amount', 'org_id',
] ]
list_serializer_class = BulkListSerializer
def validate(self, data): def validate_value(self, data):
value = data.get('value')
instance = self.instance if self.instance else Node.root() instance = self.instance if self.instance else Node.root()
children = instance.parent.get_children().exclude(key=instance.key) children = instance.parent.get_children().exclude(key=instance.key)
values = [child.value for child in children] values = [child.value for child in children]
if value in values: if data in values:
raise serializers.ValidationError( raise serializers.ValidationError(
'The same level node name cannot be the same' 'The same level node name cannot be the same'
) )
return data return data
@staticmethod
def get_assets_amount(obj):
return obj.get_all_assets().count()
@staticmethod
def get_tree_id(obj):
return obj.key
@staticmethod
def get_tree_parent(obj):
return obj.parent_key
def get_fields(self):
fields = super().get_fields()
field = fields["key"]
field.required = False
return fields
class NodeAssetsSerializer(serializers.ModelSerializer): class NodeAssetsSerializer(serializers.ModelSerializer):
assets = serializers.PrimaryKeyRelatedField(many=True, queryset = Asset.objects.all()) assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all())
class Meta: class Meta:
model = Node model = Node
...@@ -95,3 +46,4 @@ class NodeAssetsSerializer(serializers.ModelSerializer): ...@@ -95,3 +46,4 @@ class NodeAssetsSerializer(serializers.ModelSerializer):
class NodeAddChildrenSerializer(serializers.Serializer): class NodeAddChildrenSerializer(serializers.Serializer):
nodes = serializers.ListField() nodes = serializers.ListField()
from rest_framework import serializers from rest_framework import serializers
from ..models import SystemUser from ..models import SystemUser, Asset
from .base import AuthSerializer from .base import AuthSerializer
...@@ -21,17 +21,17 @@ class SystemUserSerializer(serializers.ModelSerializer): ...@@ -21,17 +21,17 @@ class SystemUserSerializer(serializers.ModelSerializer):
def get_field_names(self, declared_fields, info): def get_field_names(self, declared_fields, info):
fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info) fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info)
fields.extend([ fields.extend([
'get_login_mode_display', 'login_mode_display',
]) ])
return fields return fields
@staticmethod @staticmethod
def get_unreachable_assets(obj): def get_unreachable_assets(obj):
return obj.unreachable_assets return obj.assets_unreachable
@staticmethod @staticmethod
def get_reachable_assets(obj): def get_reachable_assets(obj):
return obj.reachable_assets return obj.assets_reachable
def get_unreachable_amount(self, obj): def get_unreachable_amount(self, obj):
return len(self.get_unreachable_assets(obj)) return len(self.get_unreachable_assets(obj))
...@@ -41,7 +41,7 @@ class SystemUserSerializer(serializers.ModelSerializer): ...@@ -41,7 +41,7 @@ class SystemUserSerializer(serializers.ModelSerializer):
@staticmethod @staticmethod
def get_assets_amount(obj): def get_assets_amount(obj):
return len(obj.get_assets()) return len(obj.get_related_assets())
class SystemUserAuthSerializer(AuthSerializer): class SystemUserAuthSerializer(AuthSerializer):
...@@ -75,4 +75,7 @@ class SystemUserSimpleSerializer(serializers.ModelSerializer): ...@@ -75,4 +75,7 @@ class SystemUserSimpleSerializer(serializers.ModelSerializer):
""" """
class Meta: class Meta:
model = SystemUser model = SystemUser
fields = ('id', 'name', 'username') fields = ('id', 'name', 'username')
\ No newline at end of file
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from collections import defaultdict from collections import defaultdict
from django.db.models.signals import post_save, m2m_changed from django.db.models.signals import post_save, m2m_changed, post_delete
from django.dispatch import receiver from django.dispatch import receiver
from common.utils import get_logger from common.utils import get_logger
from .models import Asset, SystemUser, Node from .models import Asset, SystemUser, Node
from .tasks import update_assets_hardware_info_util, \ from .tasks import update_assets_hardware_info_util, \
test_asset_connectability_util, push_system_user_to_assets test_asset_connectivity_util, push_system_user_to_assets
logger = get_logger(__file__) logger = get_logger(__file__)
...@@ -19,8 +19,8 @@ def update_asset_hardware_info_on_created(asset): ...@@ -19,8 +19,8 @@ def update_asset_hardware_info_on_created(asset):
def test_asset_conn_on_created(asset): def test_asset_conn_on_created(asset):
logger.debug("Test asset `{}` connectability".format(asset)) logger.debug("Test asset `{}` connectivity".format(asset))
test_asset_connectability_util.delay([asset]) test_asset_connectivity_util.delay([asset])
def set_asset_root_node(asset): def set_asset_root_node(asset):
...@@ -35,6 +35,17 @@ def on_asset_created_or_update(sender, instance=None, created=False, **kwargs): ...@@ -35,6 +35,17 @@ def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
update_asset_hardware_info_on_created(instance) update_asset_hardware_info_on_created(instance)
test_asset_conn_on_created(instance) test_asset_conn_on_created(instance)
# 过期节点资产数量
nodes = instance.nodes.all()
Node.expire_nodes_assets_amount(nodes)
@receiver(post_delete, sender=Asset, dispatch_uid="my_unique_identifier")
def on_asset_delete(sender, instance=None, **kwargs):
# 过期节点资产数量
nodes = instance.nodes.all()
Node.expire_nodes_assets_amount(nodes)
@receiver(post_save, sender=SystemUser, dispatch_uid="my_unique_identifier") @receiver(post_save, sender=SystemUser, dispatch_uid="my_unique_identifier")
def on_system_user_update(sender, instance=None, created=True, **kwargs): def on_system_user_update(sender, instance=None, created=True, **kwargs):
...@@ -58,15 +69,19 @@ def on_system_user_nodes_change(sender, instance=None, **kwargs): ...@@ -58,15 +69,19 @@ def on_system_user_nodes_change(sender, instance=None, **kwargs):
def on_system_user_assets_change(sender, instance=None, **kwargs): def on_system_user_assets_change(sender, instance=None, **kwargs):
if instance and kwargs["action"] == "post_add": if instance and kwargs["action"] == "post_add":
assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
push_system_user_to_assets(instance, assets) push_system_user_to_assets.delay(instance, assets)
@receiver(m2m_changed, sender=Asset.nodes.through) @receiver(m2m_changed, sender=Asset.nodes.through)
def on_asset_node_changed(sender, instance=None, **kwargs): def on_asset_node_changed(sender, instance=None, **kwargs):
logger.debug("Asset nodes change signal received")
if isinstance(instance, Asset): 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)
if kwargs['action'] == 'post_add': if kwargs['action'] == 'post_add':
logger.debug("Asset node change signal received")
nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
Node.expire_nodes_assets_amount(nodes)
system_users_assets = defaultdict(set) system_users_assets = defaultdict(set)
system_users = SystemUser.objects.filter(nodes__in=nodes) system_users = SystemUser.objects.filter(nodes__in=nodes)
# 清理节点缓存 # 清理节点缓存
...@@ -79,9 +94,11 @@ def on_asset_node_changed(sender, instance=None, **kwargs): ...@@ -79,9 +94,11 @@ def on_asset_node_changed(sender, instance=None, **kwargs):
@receiver(m2m_changed, sender=Asset.nodes.through) @receiver(m2m_changed, sender=Asset.nodes.through)
def on_node_assets_changed(sender, instance=None, **kwargs): def on_node_assets_changed(sender, instance=None, **kwargs):
if isinstance(instance, Node): 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']) assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
if kwargs['action'] == 'post_add': if kwargs['action'] == 'post_add':
logger.debug("Node assets change signal received")
# 重新关联系统用户和资产的关系 # 重新关联系统用户和资产的关系
system_users = SystemUser.objects.filter(nodes=instance) system_users = SystemUser.objects.filter(nodes=instance)
for system_user in system_users: for system_user in system_users:
......
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
import json import json
import re import re
import time
import os import os
from celery import shared_task from celery import shared_task
from django.core.cache import cache
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.core.cache import cache
from common.utils import get_object_or_none, capacity_convert, \ from common.utils import capacity_convert, \
sum_capacity, encrypt_password, get_logger sum_capacity, encrypt_password, get_logger
from ops.celery.utils import register_as_period_task, after_app_shutdown_clean, \ from ops.celery.utils import register_as_period_task, after_app_shutdown_clean
after_app_ready_start
from ops.celery import app as celery_app
from .models import SystemUser, AdminUser, Asset from .models import SystemUser, AdminUser, Asset
from . import const from . import const
...@@ -20,34 +19,51 @@ from . import const ...@@ -20,34 +19,51 @@ from . import const
FORKS = 10 FORKS = 10
TIMEOUT = 60 TIMEOUT = 60
logger = get_logger(__file__) logger = get_logger(__file__)
CACHE_MAX_TIME = 60*60*60 CACHE_MAX_TIME = 60*60*2
disk_pattern = re.compile(r'^hd|sd|xvd|vd') disk_pattern = re.compile(r'^hd|sd|xvd|vd')
PERIOD_TASK = os.environ.get("PERIOD_TASK", "off") PERIOD_TASK = os.environ.get("PERIOD_TASK", "off")
def clean_hosts(assets):
clean_assets = []
for asset in assets:
if not asset.is_active:
msg = _("Asset has been disabled, skipped: {}").format(asset)
logger.info(msg)
continue
if not asset.support_ansible():
msg = _("Asset may not be support ansible, skipped: {}").format(asset)
logger.info(msg)
continue
clean_assets.append(asset)
if not clean_assets:
logger.info(_("No assets matched, stop task"))
return clean_assets
@shared_task @shared_task
def set_assets_hardware_info(result, **kwargs): def set_assets_hardware_info(assets, result, **kwargs):
""" """
Using ops task run result, to update asset info Using ops task run result, to update asset info
@shared_task must be exit, because we using it as a task callback, is must @shared_task must be exit, because we using it as a task callback, is must
be a celery task also be a celery task also
:param assets:
:param result: :param result:
:param kwargs: {task_name: ""} :param kwargs: {task_name: ""}
:return: :return:
""" """
result_raw = result[0] result_raw = result[0]
assets_updated = [] assets_updated = []
for hostname, info in result_raw.get('ok', {}).items(): success_result = result_raw.get('ok', {})
for asset in assets:
hostname = asset.hostname
info = success_result.get(hostname, {})
info = info.get('setup', {}).get('ansible_facts', {}) info = info.get('setup', {}).get('ansible_facts', {})
if not info: if not info:
logger.error("Get asset info failed: {}".format(hostname)) logger.error(_("Get asset info failed: {}").format(hostname))
continue continue
asset = Asset.objects.get_object_by_fullname(hostname)
if not asset:
continue
___vendor = info.get('ansible_system_vendor', 'Unknown') ___vendor = info.get('ansible_system_vendor', 'Unknown')
___model = info.get('ansible_product_name', 'Unknown') ___model = info.get('ansible_product_name', 'Unknown')
___sn = info.get('ansible_product_serial', 'Unknown') ___sn = info.get('ansible_product_serial', 'Unknown')
...@@ -59,9 +75,12 @@ def set_assets_hardware_info(result, **kwargs): ...@@ -59,9 +75,12 @@ def set_assets_hardware_info(result, **kwargs):
___cpu_model = 'Unknown' ___cpu_model = 'Unknown'
___cpu_model = ___cpu_model[:64] ___cpu_model = ___cpu_model[:64]
___cpu_count = info.get('ansible_processor_count', 0) ___cpu_count = info.get('ansible_processor_count', 0)
___cpu_cores = info.get('ansible_processor_cores', None) or len(info.get('ansible_processor', [])) ___cpu_cores = info.get('ansible_processor_cores', None) or \
len(info.get('ansible_processor', []))
___cpu_vcpus = info.get('ansible_processor_vcpus', 0) ___cpu_vcpus = info.get('ansible_processor_vcpus', 0)
___memory = '%s %s' % capacity_convert('{} MB'.format(info.get('ansible_memtotal_mb'))) ___memory = '%s %s' % capacity_convert(
'{} MB'.format(info.get('ansible_memtotal_mb'))
)
disk_info = {} disk_info = {}
for dev, dev_info in info.get('ansible_devices', {}).items(): for dev, dev_info in info.get('ansible_devices', {}).items():
if disk_pattern.match(dev) and dev_info['removable'] == '0': if disk_pattern.match(dev) and dev_info['removable'] == '0':
...@@ -94,34 +113,31 @@ def update_assets_hardware_info_util(assets, task_name=None): ...@@ -94,34 +113,31 @@ def update_assets_hardware_info_util(assets, task_name=None):
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
if task_name is None: if task_name is None:
task_name = _("Update some assets hardware info") task_name = _("Update some assets hardware info")
# task_name = _("更新资产硬件信息")
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
hostname_list = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()] hosts = clean_hosts(assets)
if not hostname_list: if not hosts:
logger.info("Not hosts get, may be asset is not active or not unixlike platform")
return {} return {}
created_by = str(assets[0].org_id)
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name, hosts=hostname_list, tasks=tasks, pattern='all', task_name, hosts=hosts, tasks=tasks, created_by=created_by,
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System', pattern='all', options=const.TASK_OPTIONS, run_as_admin=True,
) )
result = task.run() result = task.run()
# Todo: may be somewhere using # Todo: may be somewhere using
# Manual run callback function # Manual run callback function
set_assets_hardware_info(result) set_assets_hardware_info(assets, result)
return result return result
@shared_task @shared_task
def update_asset_hardware_info_manual(asset): def update_asset_hardware_info_manual(asset):
task_name = _("Update asset hardware info") task_name = _("Update asset hardware info: {}").format(asset.hostname)
# task_name = _("更新资产硬件信息") return update_assets_hardware_info_util(
return update_assets_hardware_info_util([asset], task_name=task_name) [asset], task_name=task_name
)
@celery_app.task @shared_task
@register_as_period_task(interval=3600)
@after_app_ready_start
@after_app_shutdown_clean
def update_assets_hardware_info_period(): def update_assets_hardware_info_period():
""" """
Update asset hardware period task Update asset hardware period task
...@@ -131,148 +147,98 @@ def update_assets_hardware_info_period(): ...@@ -131,148 +147,98 @@ def update_assets_hardware_info_period():
logger.debug("Period task disabled, update assets hardware info pass") logger.debug("Period task disabled, update assets hardware info pass")
return return
from ops.utils import update_or_create_ansible_task
task_name = _("Update assets hardware info period")
# task_name = _("定期更新资产硬件信息")
hostname_list = [
asset.fullname for asset in Asset.objects.all()
if asset.is_active and asset.is_unixlike()
]
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
# Only create, schedule by celery beat
update_or_create_ansible_task(
task_name, hosts=hostname_list, tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System',
interval=60*60*24, is_periodic=True, callback=set_assets_hardware_info.name,
)
## ADMIN USER CONNECTIVE ## ## ADMIN USER CONNECTIVE ##
@shared_task @shared_task
def set_admin_user_connectability_info(result, **kwargs): def test_asset_connectivity_util(assets, task_name=None):
admin_user = kwargs.get("admin_user") from ops.utils import update_or_create_ansible_task
task_name = kwargs.get("task_name")
if admin_user is None and task_name is not None: if task_name is None:
admin_user = task_name.split(":")[-1] task_name = _("Test assets connectivity")
hosts = clean_hosts(assets)
if not hosts:
return {}
tasks = const.TEST_ADMIN_USER_CONN_TASKS
created_by = assets[0].org_id
task, created = update_or_create_ansible_task(
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True, created_by=created_by,
)
result = task.run()
summary = result[1]
for asset in assets:
if asset.hostname in summary.get('dark', {}):
asset.connectivity = asset.UNREACHABLE
elif asset.hostname in summary.get('contacted', []):
asset.connectivity = asset.REACHABLE
else:
asset.connectivity = asset.UNKNOWN
return summary
raw, summary = result
cache_key = const.ADMIN_USER_CONN_CACHE_KEY.format(admin_user)
cache.set(cache_key, summary, CACHE_MAX_TIME)
for i in summary.get('contacted', []): @shared_task
asset_conn_cache_key = const.ASSET_ADMIN_CONN_CACHE_KEY.format(i) def test_asset_connectivity_manual(asset):
cache.set(asset_conn_cache_key, 1, CACHE_MAX_TIME) task_name = _("Test assets connectivity: {}").format(asset)
summary = test_asset_connectivity_util([asset], task_name=task_name)
for i, msg in summary.get('dark', {}).items(): if summary.get('dark'):
asset_conn_cache_key = const.ASSET_ADMIN_CONN_CACHE_KEY.format(i) return False, summary['dark']
cache.set(asset_conn_cache_key, 0, CACHE_MAX_TIME) else:
logger.error(msg) return True, ""
@shared_task @shared_task
def test_admin_user_connectability_util(admin_user, task_name): def test_admin_user_connectivity_util(admin_user, task_name):
""" """
Test asset admin user can connect or not. Using ansible api do that Test asset admin user can connect or not. Using ansible api do that
:param admin_user: :param admin_user:
:param task_name: :param task_name:
:return: :return:
""" """
from ops.utils import update_or_create_ansible_task
assets = admin_user.get_related_assets() assets = admin_user.get_related_assets()
hosts = [asset.fullname for asset in assets hosts = clean_hosts(assets)
if asset.is_active and asset.is_unixlike()]
if not hosts: if not hosts:
return return {}
tasks = const.TEST_ADMIN_USER_CONN_TASKS summary = test_asset_connectivity_util(hosts, task_name)
task, created = update_or_create_ansible_task( return summary
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System',
)
result = task.run()
set_admin_user_connectability_info(result, admin_user=admin_user.name)
return result
@celery_app.task @shared_task
@register_as_period_task(interval=3600) @register_as_period_task(interval=3600)
@after_app_ready_start def test_admin_user_connectivity_period():
@after_app_shutdown_clean
def test_admin_user_connectability_period():
""" """
A period task that update the ansible task period A period task that update the ansible task period
""" """
if PERIOD_TASK != "on": key = '_JMS_TEST_ADMIN_USER_CONNECTIVITY_PERIOD'
logger.debug("Period task disabled, test admin user connectability pass") prev_execute_time = cache.get(key)
if prev_execute_time:
logger.debug("Test admin user connectivity, less than 40 minutes, skip")
return return
cache.set(key, 1, 60*40)
admin_users = AdminUser.objects.all() admin_users = AdminUser.objects.all()
for admin_user in admin_users: for admin_user in admin_users:
task_name = _("Test admin user connectability period: {}".format(admin_user.name)) task_name = _("Test admin user connectivity period: {}").format(admin_user.name)
# task_name = _("定期测试管理账号可连接性: {}".format(admin_user.name)) test_admin_user_connectivity_util(admin_user, task_name)
test_admin_user_connectability_util(admin_user, task_name)
@shared_task
def test_admin_user_connectability_manual(admin_user):
task_name = _("Test admin user connectability: {}").format(admin_user.name)
# task_name = _("测试管理行号可连接性: {}").format(admin_user.name)
return test_admin_user_connectability_util(admin_user, task_name)
@shared_task
def test_asset_connectability_util(assets, task_name=None):
from ops.utils import update_or_create_ansible_task
if task_name is None:
task_name = _("Test assets connectability")
# task_name = _("测试资产可连接性")
hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
if not hosts:
logger.info("No hosts, passed")
return {}
tasks = const.TEST_ADMIN_USER_CONN_TASKS
task, created = update_or_create_ansible_task(
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System',
)
result = task.run()
summary = result[1]
for k in summary.get('dark'):
cache.set(const.ASSET_ADMIN_CONN_CACHE_KEY.format(k), 0, CACHE_MAX_TIME)
for k in summary.get('contacted'):
cache.set(const.ASSET_ADMIN_CONN_CACHE_KEY.format(k), 1, CACHE_MAX_TIME)
return summary
@shared_task @shared_task
def test_asset_connectability_manual(asset): def test_admin_user_connectivity_manual(admin_user):
summary = test_asset_connectability_util([asset]) task_name = _("Test admin user connectivity: {}").format(admin_user.name)
return test_admin_user_connectivity_util(admin_user, task_name)
if summary.get('dark'):
return False, summary['dark']
else:
return True, ""
## System user connective ## ## System user connective ##
@shared_task @shared_task
def set_system_user_connectablity_info(result, **kwargs): def set_system_user_connectivity_info(system_user, result):
summary = result[1] summary = result[1]
task_name = kwargs.get("task_name") system_user.connectivity = summary
system_user = kwargs.get("system_user")
if system_user is None:
system_user = task_name.split(":")[-1]
cache_key = const.SYSTEM_USER_CONN_CACHE_KEY.format(system_user)
cache.set(cache_key, summary, CACHE_MAX_TIME)
@shared_task @shared_task
def test_system_user_connectability_util(system_user, assets, task_name): def test_system_user_connectivity_util(system_user, assets, task_name):
""" """
Test system cant connect his assets or not. Test system cant connect his assets or not.
:param system_user: :param system_user:
...@@ -281,51 +247,45 @@ def test_system_user_connectability_util(system_user, assets, task_name): ...@@ -281,51 +247,45 @@ def test_system_user_connectability_util(system_user, assets, task_name):
:return: :return:
""" """
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
# assets = system_user.get_assets()
hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
tasks = const.TEST_SYSTEM_USER_CONN_TASKS tasks = const.TEST_SYSTEM_USER_CONN_TASKS
hosts = clean_hosts(assets)
if not hosts: if not hosts:
logger.info("No hosts, passed")
return {} return {}
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name, hosts=hosts, tasks=tasks, pattern='all', task_name, hosts=hosts, tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, options=const.TASK_OPTIONS,
run_as=system_user.name, created_by="System", run_as=system_user, created_by=system_user.org_id,
) )
result = task.run() result = task.run()
set_system_user_connectablity_info(result, system_user=system_user.name) set_system_user_connectivity_info(system_user, result)
return result return result
@shared_task @shared_task
def test_system_user_connectability_manual(system_user): def test_system_user_connectivity_manual(system_user):
task_name = _("Test system user connectability: {}").format(system_user) task_name = _("Test system user connectivity: {}").format(system_user)
assets = system_user.get_assets() assets = system_user.get_related_assets()
return test_system_user_connectability_util(system_user, assets, task_name) return test_system_user_connectivity_util(system_user, assets, task_name)
@shared_task @shared_task
def test_system_user_connectability_a_asset(system_user, asset): def test_system_user_connectivity_a_asset(system_user, asset):
task_name = _("Test system user connectability: {} => {}").format( task_name = _("Test system user connectivity: {} => {}").format(
system_user, asset system_user, asset
) )
return test_system_user_connectability_util(system_user, [asset], task_name) return test_system_user_connectivity_util(system_user, [asset], task_name)
@shared_task @shared_task
@register_as_period_task(interval=3600) def test_system_user_connectivity_period():
@after_app_ready_start
@after_app_shutdown_clean
def test_system_user_connectability_period():
if PERIOD_TASK != "on": if PERIOD_TASK != "on":
logger.debug("Period task disabled, test system user connectability pass") logger.debug("Period task disabled, test system user connectivity pass")
return return
system_users = SystemUser.objects.all() system_users = SystemUser.objects.all()
for system_user in system_users: for system_user in system_users:
task_name = _("Test system user connectability period: {}".format(system_user)) task_name = _("Test system user connectivity period: {}").format(system_user)
# task_name = _("定期测试系统用户可连接性: {}".format(system_user)) assets = system_user.get_related_assets()
test_system_user_connectability_util(system_user, task_name) test_system_user_connectivity_util(system_user, assets, task_name)
#### Push system user tasks #### #### Push system user tasks ####
...@@ -347,6 +307,24 @@ def get_push_system_user_tasks(system_user): ...@@ -347,6 +307,24 @@ def get_push_system_user_tasks(system_user):
), ),
} }
}) })
tasks.extend([
{
'name': 'Check home dir exists',
'action': {
'module': 'stat',
'args': 'path=/home/{}'.format(system_user.username)
},
'register': 'home_existed'
},
{
'name': "Set home dir permission",
'action': {
'module': 'file',
'args': "path=/home/{0} owner={0} group={0} mode=700".format(system_user.username)
},
'when': 'home_existed.stat.exists == true'
}
])
if system_user.public_key: if system_user.public_key:
tasks.append({ tasks.append({
'name': 'Set {} authorized key'.format(system_user.username), 'name': 'Set {} authorized key'.format(system_user.username),
...@@ -374,53 +352,57 @@ def get_push_system_user_tasks(system_user): ...@@ -374,53 +352,57 @@ def get_push_system_user_tasks(system_user):
@shared_task @shared_task
def push_system_user_util(system_users, assets, task_name): def push_system_user_util(system_user, assets, task_name):
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
tasks = [] if not system_user.is_need_push():
for system_user in system_users: msg = _("Push system user task skip, auto push not enable or "
if not system_user.is_need_push(): "protocol is not ssh: {}").format(system_user.name)
msg = "push system user `{}` passed, may be not auto push or ssh " \ logger.info(msg)
"protocol is not ssh".format(system_user.name) return
logger.info(msg)
continue
tasks.extend(get_push_system_user_tasks(system_user))
if not tasks:
logger.info("Not tasks, passed")
return {}
hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()] tasks = get_push_system_user_tasks(system_user)
hosts = clean_hosts(assets)
if not hosts: if not hosts:
logger.info("Not hosts, passed")
return {} return {}
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all', task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System' options=const.TASK_OPTIONS, run_as_admin=True,
created_by=system_user.org_id,
) )
return task.run() return task.run()
@shared_task @shared_task
def push_system_user_to_assets_manual(system_user): def push_system_user_to_assets_manual(system_user):
assets = system_user.get_assets() assets = system_user.get_related_assets()
# task_name = "推送系统用户到入资产: {}".format(system_user.name)
task_name = _("Push system users to assets: {}").format(system_user.name) task_name = _("Push system users to assets: {}").format(system_user.name)
return push_system_user_util([system_user], assets, task_name=task_name) return push_system_user_util(system_user, assets, task_name=task_name)
@shared_task @shared_task
def push_system_user_a_asset_manual(system_user, asset): def push_system_user_a_asset_manual(system_user, asset):
task_name = _("Push system users to asset: {} => {}").format( task_name = _("Push system users to asset: {} => {}").format(
system_user.name, asset.fullname system_user.name, asset
) )
return push_system_user_util([system_user], [asset], task_name=task_name) return push_system_user_util(system_user, [asset], task_name=task_name)
@shared_task @shared_task
def push_system_user_to_assets(system_user, assets): def push_system_user_to_assets(system_user, assets):
# task_name = _("推送系统用户到入资产: {}").format(system_user.name)
task_name = _("Push system users to assets: {}").format(system_user.name) task_name = _("Push system users to assets: {}").format(system_user.name)
return push_system_user_util.delay([system_user], assets, task_name) return push_system_user_util(system_user, assets, task_name)
@shared_task
@after_app_shutdown_clean
def test_system_user_connectability_period():
pass
@shared_task
@after_app_shutdown_clean
def test_admin_user_connectability_period():
pass
# @shared_task # @shared_task
......
...@@ -57,6 +57,10 @@ ...@@ -57,6 +57,10 @@
<script> <script>
var zTree2, asset_table2 = 0; var zTree2, asset_table2 = 0;
function initTable2() { function initTable2() {
if(asset_table2){
return
}
var options = { var options = {
ele: $('#asset_list_modal_table'), ele: $('#asset_list_modal_table'),
ajax_url: '{% url "api-assets:asset-list" %}?show_current_asset=1', ajax_url: '{% url "api-assets:asset-list" %}?show_current_asset=1',
...@@ -69,16 +73,16 @@ function initTable2() { ...@@ -69,16 +73,16 @@ function initTable2() {
return asset_table2 return asset_table2
} }
function onSelected2(event, treeNode) { function onNodeSelected2(event, treeNode) {
var url = asset_table2.ajax.url(); var url = asset_table2.ajax.url();
url = setUrlParam(url, "node_id", treeNode.node_id); url = setUrlParam(url, "node_id", treeNode.meta.node.id);
setCookie('node_selected', treeNode.id);
asset_table2.ajax.url(url); asset_table2.ajax.url(url);
asset_table2.ajax.reload(); asset_table2.ajax.reload();
} }
function initTree2() { function initTree2() {
var url = '{% url 'api-assets:node-children-tree' %}?assets=0';
var setting = { var setting = {
view: { view: {
dblClickExpand: false, dblClickExpand: false,
...@@ -89,33 +93,22 @@ function initTree2() { ...@@ -89,33 +93,22 @@ function initTree2() {
enable: true enable: true
} }
}, },
async: {
enable: true,
url: url,
autoParam: ["id=key", "name=n", "level=lv"],
type: 'get'
},
callback: { callback: {
onSelected: onSelected2 onSelected: onNodeSelected2
} }
}; };
zTree2 = $.fn.zTree.init($("#assetTree2"), setting);
var zNodes = [];
$.get("{% url 'api-assets:node-list' %}", function(data, status){
$.each(data, function (index, value) {
value["node_id"] = value["id"];
value["id"] = value["tree_id"];
value["pId"] = value["tree_parent"];
{#value["open"] = true;#}
if (value["key"] === "0") {
value["open"] = true;
}
value["name"] = value["value"] + ' (' + value['assets_amount'] + ')';
});
zNodes = data;
$.fn.zTree.init($("#assetTree2"), setting, zNodes);
zTree2 = $.fn.zTree.getZTreeObj("assetTree2");
var root = zTree2.getNodes()[0];
zTree2.expandNode(root);
});
} }
$(document).ready(function(){ $(document).ready(function(){
}).on('show.bs.modal', function () {
initTable2(); initTable2();
initTree2(); initTree2();
}) })
......
...@@ -103,15 +103,17 @@ var need_change_field_login_mode = [ ...@@ -103,15 +103,17 @@ var need_change_field_login_mode = [
]; ];
function protocolChange() { function protocolChange() {
if ($(protocol_id + " option:selected").text() === 'rdp') { var protocol = $(protocol_id + " option:selected").text();
if (protocol === 'rdp' || protocol === 'vnc') {
$('.auth-fields').removeClass('hidden'); $('.auth-fields').removeClass('hidden');
$('#command-filter-block').addClass('hidden'); $('#command-filter-block').addClass('hidden');
$.each(need_change_field, function (index, value) { $.each(need_change_field, function (index, value) {
$(value).closest('.form-group').addClass('hidden') $(value).closest('.form-group').addClass('hidden')
}); });
} }
else if ($(protocol_id + " option:selected").text() === 'telnet (beta)') { else if (protocol === 'telnet (beta)') {
$('.auth-fields').removeClass('hidden'); $('.auth-fields').removeClass('hidden');
$('#command-filter-block').removeClass('hidden');
$.each(need_change_field, function (index, value) { $.each(need_change_field, function (index, value) {
$(value).closest('.form-group').addClass('hidden') $(value).closest('.form-group').addClass('hidden')
}); });
...@@ -123,6 +125,7 @@ function protocolChange() { ...@@ -123,6 +125,7 @@ function protocolChange() {
return return
} }
authFieldsDisplay(); authFieldsDisplay();
$('#command-filter-block').removeClass('hidden');
$.each(need_change_field, function (index, value) { $.each(need_change_field, function (index, value) {
$(value).closest('.form-group').removeClass('hidden') $(value).closest('.form-group').removeClass('hidden')
}); });
......
...@@ -45,13 +45,11 @@ ...@@ -45,13 +45,11 @@
<table class="table table-striped table-bordered table-hover" id="asset_list_table"> <table class="table table-striped table-bordered table-hover" id="asset_list_table">
<thead> <thead>
<tr> <tr>
<th class="text-center">
<input type="checkbox" id="check_all" class="ipt_check_all" >
</th>
<th>{% trans 'Hostname' %}</th> <th>{% trans 'Hostname' %}</th>
<th>{% trans 'IP' %}</th> <th>{% trans 'IP' %}</th>
<th>{% trans 'Port' %}</th> <th>{% trans 'Port' %}</th>
<th>{% trans 'Reachable' %}</th> <th>{% trans 'Reachable' %}</th>
<th>{% trans 'Action' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
...@@ -91,26 +89,36 @@ ...@@ -91,26 +89,36 @@
<script> <script>
function initTable() { function initTable() {
var reachable = {{ admin_user.REACHABLE }};
var unreachable = {{ admin_user.UNREACHABLE }};
var options = { var options = {
ele: $('#asset_list_table'), ele: $('#asset_list_table'),
buttons: [], buttons: [],
order: [], order: [],
columnDefs: [ columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) { {targets: 0, createdCell: function (td, cellData, rowData) {
var detail_btn = '<a href="{% url "assets:asset-detail" pk=DEFAULT_PK %}" data-aid="'+rowData.id+'">' + cellData + '</a>'; var detail_btn = '<a href="{% url "assets:asset-detail" pk=DEFAULT_PK %}" data-aid="'+rowData.id+'">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id)); $(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}}, }},
{targets: 4, createdCell: function (td, cellData) { {targets: 3, createdCell: function (td, cellData) {
if (!cellData) { if (cellData === unreachable) {
$(td).html('<i class="fa fa-times text-danger"></i>') $(td).html('<i class="fa fa-times text-danger"></i>')
} else { } else if (cellData === reachable) {
$(td).html('<i class="fa fa-check text-navy"></i>') $(td).html('<i class="fa fa-check text-navy"></i>')
} } else {
}}], $(td).html('')
ajax_url: '{% url "api-assets:asset-list" %}?admin_user_id={{ admin_user.id }}', }
}},
{targets: 4, createdCell: function (td, cellData) {
var test_btn = ' <a class="btn btn-xs btn-info btn-test-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Test" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
$(td).html(test_btn);
}}
],
ajax_url: '{% url "api-assets:admin-user-assets" pk=admin_user.id %}',
columns: [ columns: [
{data: function(){return ""}}, {data: "hostname" }, {data: "ip" }, {data: "hostname" }, {data: "ip" },
{data: "port" }, {data: "is_connective" }], {data: "port" }, {data: "connectivity" }, {data: "id"}],
op_html: $('#actions').html() op_html: $('#actions').html()
}; };
jumpserver.initServerSideDataTable(options); jumpserver.initServerSideDataTable(options);
...@@ -119,6 +127,21 @@ function initTable() { ...@@ -119,6 +127,21 @@ function initTable() {
$(document).ready(function () { $(document).ready(function () {
initTable(); initTable();
}) })
.on('click', '.btn-test-asset', function () {
var asset_id = $(this).data('uid');
var the_url = "{% url 'api-assets:asset-alive-test' pk=DEFAULT_PK %}".replace('{{ DEFAULT_PK }}', asset_id);
var success = function (data) {
var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
};
APIUpdateAttr({
url: the_url,
method: 'GET',
success: success,
flash_message: false
});
})
.on('click', '.btn-test-connective', function () { .on('click', '.btn-test-connective', function () {
var the_url = "{% url 'api-assets:admin-user-connective' pk=admin_user.id %}"; var the_url = "{% url 'api-assets:admin-user-connective' pk=admin_user.id %}";
var success = function (data) { var success = function (data) {
......
...@@ -86,15 +86,21 @@ $(document).ready(function () { ...@@ -86,15 +86,21 @@ $(document).ready(function () {
allowClear: true, allowClear: true,
templateSelection: format templateSelection: format
}); });
$('#id_nodes.select2').select2({
closeOnSelect: false
});
$("#id_protocol").change(function (){ $("#id_protocol").change(function (){
var protocol = $("#id_protocol option:selected").text(); var protocol = $("#id_protocol option:selected").text();
var port = 22; var port = 22;
if(protocol === 'rdp'){ if(protocol === 'rdp'){
port = 3389; port = 3389;
} }
if(protocol === 'telnet (beta)'){ else if(protocol === 'telnet (beta)'){
port = 23; port = 23;
} }
else if(protocol === 'vnc'){
port = 5901;
}
$("#id_port").val(port); $("#id_port").val(port);
}); });
}) })
......
...@@ -77,6 +77,10 @@ ...@@ -77,6 +77,10 @@
<td>{% trans 'Admin user' %}:</td> <td>{% trans 'Admin user' %}:</td>
<td><b>{{ asset.admin_user }}</b></td> <td><b>{{ asset.admin_user }}</b></td>
</tr> </tr>
<tr>
<td>{% trans 'Domain' %}:</td>
<td><b>{{ asset.domain|default:"" }}</b></td>
</tr>
<tr> <tr>
<td>{% trans 'Vendor' %}:</td> <td>{% trans 'Vendor' %}:</td>
<td><b>{{ asset.vendor|default:"" }}</b></td> <td><b>{{ asset.vendor|default:"" }}</b></td>
...@@ -159,7 +163,7 @@ ...@@ -159,7 +163,7 @@
</span> </span>
</td> </td>
</tr> </tr>
{% if asset.is_unixlike %} {% if asset.protocol == 'ssh' %}
<tr> <tr>
<td>{% trans 'Refresh hardware' %}:</td> <td>{% trans 'Refresh hardware' %}:</td>
<td> <td>
......
...@@ -92,8 +92,7 @@ ...@@ -92,8 +92,7 @@
<th class="text-center">{% trans 'Hostname' %}</th> <th class="text-center">{% trans 'Hostname' %}</th>
<th class="text-center">{% trans 'IP' %}</th> <th class="text-center">{% trans 'IP' %}</th>
<th class="text-center">{% trans 'Hardware' %}</th> <th class="text-center">{% trans 'Hardware' %}</th>
<th class="text-center">{% trans 'Active' %}</th> <th class="text-center">{% trans 'Reachable' %}</th>
{# <th class="text-center">{% trans 'Reachable' %}</th>#}
<th class="text-center">{% trans 'Action' %}</th> <th class="text-center">{% trans 'Action' %}</th>
</tr> </tr>
</thead> </thead>
...@@ -134,8 +133,11 @@ ...@@ -134,8 +133,11 @@
<li id="menu_refresh_hardware_info" class="btn-refresh-hardware" tabindex="-1"><a><i class="fa fa-refresh"></i> {% trans 'Refresh node hardware info' %}</a></li> <li id="menu_refresh_hardware_info" class="btn-refresh-hardware" tabindex="-1"><a><i class="fa fa-refresh"></i> {% trans 'Refresh node hardware info' %}</a></li>
<li id="menu_test_connective" class="btn-test-connective" tabindex="-1"><a><i class="fa fa-chain"></i> {% trans 'Test node connective' %}</a></li> <li id="menu_test_connective" class="btn-test-connective" tabindex="-1"><a><i class="fa fa-chain"></i> {% trans 'Test node connective' %}</a></li>
<li class="divider"></li> <li class="divider"></li>
<li id="menu_refresh_assets_amount" class="btn-refresh-assets-amount" tabindex="-1"><a><i class="fa fa-refresh"></i> {% trans 'Refresh all node assets amount' %}</a></li>
<li class="divider"></li>
<li id="show_current_asset" class="btn-show-current-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-hand-o-up"></i> {% trans 'Display only current node assets' %}</a></li> <li id="show_current_asset" class="btn-show-current-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-hand-o-up"></i> {% trans 'Display only current node assets' %}</a></li>
<li id="show_all_asset" class="btn-show-all-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-th"></i> {% trans 'Displays all child node assets' %}</a></li> <li id="show_all_asset" class="btn-show-all-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-th"></i> {% trans 'Displays all child node assets' %}</a></li>
{# <li id="fresh_tree" class="btn-refresh-tree" tabindex="-1"><a><i class="fa fa-refresh"></i> {% trans 'Refresh' %}</a></li>#}
</ul> </ul>
</div> </div>
...@@ -147,6 +149,8 @@ ...@@ -147,6 +149,8 @@
<script> <script>
var zTree, rMenu, asset_table, show = 0; var zTree, rMenu, asset_table, show = 0;
var update_node_action = ""; var update_node_action = "";
var current_node_id = null;
var current_node = null;
function initTable() { function initTable() {
var options = { var options = {
ele: $('#asset_list_table'), ele: $('#asset_list_table'),
...@@ -160,10 +164,12 @@ function initTable() { ...@@ -160,10 +164,12 @@ function initTable() {
$(td).html(rowData.hardware_info) $(td).html(rowData.hardware_info)
}}, }},
{targets: 4, createdCell: function (td, cellData) { {targets: 4, createdCell: function (td, cellData) {
if (!cellData) { if (cellData === 1){
$(td).html('<i class="fa fa-times text-danger"></i>') $(td).html('<i class="fa fa-circle text-navy"></i>')
} else if (cellData === 0) {
$(td).html('<i class="fa fa-circle text-danger"></i>')
} else { } else {
$(td).html('<i class="fa fa-check text-navy"></i>') $(td).html('<i class="fa fa-circle text-warning"></i>')
} }
}}, }},
...@@ -176,8 +182,8 @@ function initTable() { ...@@ -176,8 +182,8 @@ function initTable() {
ajax_url: '{% url "api-assets:asset-list" %}', ajax_url: '{% url "api-assets:asset-list" %}',
columns: [ columns: [
{data: "id"}, {data: "hostname" }, {data: "ip" }, {data: "id"}, {data: "hostname" }, {data: "ip" },
{data: "cpu_cores"}, {data: "is_active", orderable: false }, {data: "cpu_cores", orderable: false},
{data: "id", orderable: false } {data: "connectivity", orderable: false}, {data: "id", orderable: false }
], ],
op_html: $('#actions').html() op_html: $('#actions').html()
}; };
...@@ -191,18 +197,20 @@ function addTreeNode() { ...@@ -191,18 +197,20 @@ function addTreeNode() {
if (!parentNode){ if (!parentNode){
return return
} }
var url = "{% url 'api-assets:node-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", parentNode.node_id ); var url = "{% url 'api-assets:node-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", parentNode.meta.node.id);
$.post(url, {}, function (data, status){ $.post(url, {}, function (data, status){
if (status === "success") { if (status === "success") {
var newNode = { var newNode = {
id: data["key"], id: data["key"],
name: data["value"], name: data["value"],
node_id: data["id"], pId: parentNode.id,
pId: parentNode.id meta: {
"node": data
}
}; };
newNode.checked = zTree.getSelectedNodes()[0].checked; newNode.checked = zTree.getSelectedNodes()[0].checked;
zTree.addNodes(parentNode, 0, newNode); zTree.addNodes(parentNode, 0, newNode);
var node = zTree.getNodeByParam('id', newNode.node_id, parentNode); var node = zTree.getNodeByParam('id', newNode.id, parentNode);
zTree.editName(node); zTree.editName(node);
} else { } else {
alert("{% trans 'Create node failed' %}") alert("{% trans 'Create node failed' %}")
...@@ -218,10 +226,10 @@ function removeTreeNode() { ...@@ -218,10 +226,10 @@ function removeTreeNode() {
} }
if (current_node.children && current_node.children.length > 0) { if (current_node.children && current_node.children.length > 0) {
toastr.error("{% trans 'Have child node, cancel' %}"); toastr.error("{% trans 'Have child node, cancel' %}");
} else if (current_node.assets_amount !== 0) { } else if (current_node.meta.node.assets_amount !== 0) {
toastr.error("{% trans 'Have assets, cancel' %}"); toastr.error("{% trans 'Have assets, cancel' %}");
} else { } else {
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.node_id ); var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
$.ajax({ $.ajax({
url: url, url: url,
method: "DELETE", method: "DELETE",
...@@ -238,8 +246,8 @@ function editTreeNode() { ...@@ -238,8 +246,8 @@ function editTreeNode() {
if (!current_node){ if (!current_node){
return return
} }
if (current_node.value) { if (current_node) {
current_node.name = current_node.value; current_node.name = current_node.meta.node.value;
} }
zTree.editName(current_node); zTree.editName(current_node);
} }
...@@ -281,7 +289,7 @@ function onBodyMouseDown(event){ ...@@ -281,7 +289,7 @@ function onBodyMouseDown(event){
function onRename(event, treeId, treeNode, isCancel){ function onRename(event, treeId, treeNode, isCancel){
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", treeNode.node_id); var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
var data = {"value": treeNode.name}; var data = {"value": treeNode.name};
if (isCancel){ if (isCancel){
return return
...@@ -291,13 +299,20 @@ function onRename(event, treeId, treeNode, isCancel){ ...@@ -291,13 +299,20 @@ function onRename(event, treeId, treeNode, isCancel){
body: JSON.stringify(data), body: JSON.stringify(data),
method: "PATCH", method: "PATCH",
success_message: "{% trans 'Rename success' %}", success_message: "{% trans 'Rename success' %}",
fail_message: "{% trans 'Rename failed, do not change the root node name' %}" fail_message: "{% trans 'Rename failed, do not change the root node name' %}",
success: function () {
treeNode.name = treeNode.name + ' (' + treeNode.meta.node.assets_amount + ')'
zTree.updateNode(treeNode);
console.log("Success: " + treeNode.name)
}
}) })
} }
function onSelected(event, treeNode) { function onSelected(event, treeNode) {
current_node = treeNode;
current_node_id = treeNode.meta.node.id;
var url = asset_table.ajax.url(); var url = asset_table.ajax.url();
url = setUrlParam(url, "node_id", treeNode.node_id); url = setUrlParam(url, "node_id", current_node_id);
url = setUrlParam(url, "show_current_asset", getCookie('show_current_asset')); url = setUrlParam(url, "show_current_asset", getCookie('show_current_asset'));
setCookie('node_selected', treeNode.node_id); setCookie('node_selected', treeNode.node_id);
asset_table.ajax.url(url); asset_table.ajax.url(url);
...@@ -305,6 +320,9 @@ function onSelected(event, treeNode) { ...@@ -305,6 +320,9 @@ function onSelected(event, treeNode) {
} }
function selectQueryNode() { function selectQueryNode() {
// TODO: 是否应该添加
// 暂时忽略之前选中的内容
return
var query_node_id = $.getUrlParam("node"); var query_node_id = $.getUrlParam("node");
var cookie_node_id = getCookie('node_selected'); var cookie_node_id = getCookie('node_selected');
var node; var node;
...@@ -329,10 +347,10 @@ function beforeDrag() { ...@@ -329,10 +347,10 @@ function beforeDrag() {
function beforeDrop(treeId, treeNodes, targetNode, moveType) { function beforeDrop(treeId, treeNodes, targetNode, moveType) {
var treeNodesNames = []; var treeNodesNames = [];
$.each(treeNodes, function (index, value) { $.each(treeNodes, function (index, value) {
treeNodesNames.push(value.value); treeNodesNames.push(value.name);
}); });
var msg = "你想移动节点: `" + treeNodesNames.join(",") + "` 到 `" + targetNode.value + "` 下吗?"; var msg = "你想移动节点: `" + treeNodesNames.join(",") + "` 到 `" + targetNode.name + "` 下吗?";
return confirm(msg); return confirm(msg);
} }
...@@ -342,10 +360,10 @@ function onDrag(event, treeId, treeNodes) { ...@@ -342,10 +360,10 @@ function onDrag(event, treeId, treeNodes) {
function onDrop(event, treeId, treeNodes, targetNode, moveType) { function onDrop(event, treeId, treeNodes, targetNode, moveType) {
var treeNodesIds = []; var treeNodesIds = [];
$.each(treeNodes, function (index, value) { $.each(treeNodes, function (index, value) {
treeNodesIds.push(value.node_id); treeNodesIds.push(value.meta.node.id);
}); });
var the_url = "{% url 'api-assets:node-add-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", targetNode.node_id); var the_url = "{% url 'api-assets:node-add-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", targetNode.meta.node.id);
var body = {nodes: treeNodesIds}; var body = {nodes: treeNodesIds};
APIUpdateAttr({ APIUpdateAttr({
url: the_url, url: the_url,
...@@ -355,6 +373,14 @@ function onDrop(event, treeId, treeNodes, targetNode, moveType) { ...@@ -355,6 +373,14 @@ function onDrop(event, treeId, treeNodes, targetNode, moveType) {
} }
function initTree() { function initTree() {
if (zTree) {
return
}
var url = '{% url 'api-assets:node-children-tree' %}?assets=0&all=';
var showCurrentAsset = getCookie('show_current_asset');
if (!showCurrentAsset) {
url += '1'
}
var setting = { var setting = {
view: { view: {
dblClickExpand: false, dblClickExpand: false,
...@@ -365,6 +391,12 @@ function initTree() { ...@@ -365,6 +391,12 @@ function initTree() {
enable: true enable: true
} }
}, },
async: {
enable: true,
url: url,
autoParam: ["id=key", "name=n", "level=lv"],
type: 'get'
},
edit: { edit: {
enable: true, enable: true,
showRemoveBtn: false, showRemoveBtn: false,
...@@ -387,26 +419,8 @@ function initTree() { ...@@ -387,26 +419,8 @@ function initTree() {
}; };
var zNodes = []; var zNodes = [];
$.get("{% url 'api-assets:node-list' %}", function(data, status){ zTree = $.fn.zTree.init($("#assetTree"), setting, zNodes);
$.each(data, function (index, value) { rMenu = $("#rMenu");
value["node_id"] = value["id"];
value["id"] = value["tree_id"];
if (value["tree_id"] !== value["tree_parent"]){
value["pId"] = value["tree_parent"];
} else {
value["isParent"] = true;
}
value["name"] = value["value"] + ' (' + value['assets_amount'] + ')';
value['value'] = value['value'];
});
zNodes = data;
$.fn.zTree.init($("#assetTree"), setting, zNodes);
zTree = $.fn.zTree.getZTreeObj("assetTree");
var root = zTree.getNodes()[0];
zTree.expandNode(root);
rMenu = $("#rMenu");
selectQueryNode();
});
} }
function toggle() { function toggle() {
...@@ -443,20 +457,15 @@ $(document).ready(function(){ ...@@ -443,20 +457,15 @@ $(document).ready(function(){
.on('click', '.btn_export', function () { .on('click', '.btn_export', function () {
var $data_table = $('#asset_list_table').DataTable(); var $data_table = $('#asset_list_table').DataTable();
var rows = $data_table.rows('.selected').data(); var rows = $data_table.rows('.selected').data();
var nodes = zTree.getSelectedNodes();
var current_node;
if (nodes && nodes.length === 1) {
current_node = nodes[0];
}
var assets = []; var assets = [];
$.each(rows, function (index, obj) { $.each(rows, function (index, obj) {
assets.push(obj.id) assets.push(obj.id)
}); });
var _node_id = current_node ? current_node : null;
$.ajax({ $.ajax({
url: "{% url "assets:asset-export" %}", url: "{% url "assets:asset-export" %}",
method: 'POST', method: 'POST',
data: JSON.stringify({assets_id: assets, node_id: _node_id}), data: JSON.stringify({assets_id: assets, node_id: current_node_id}),
dataType: "json", dataType: "json",
success: function (data, textStatus) { success: function (data, textStatus) {
window.open(data.redirect) window.open(data.redirect)
...@@ -469,12 +478,8 @@ $(document).ready(function(){ ...@@ -469,12 +478,8 @@ $(document).ready(function(){
.on('click', '#btn_asset_import', function () { .on('click', '#btn_asset_import', function () {
var $form = $('#fm_asset_import'); var $form = $('#fm_asset_import');
var action = $form.attr("action"); var action = $form.attr("action");
var nodes = zTree.getSelectedNodes(); if (current_node_id){
var current_node; action = setUrlParam(action, 'node_id', current_node_id);
if (nodes && nodes.length ===1 ){
current_node = nodes[0];
action = setUrlParam(action, 'node_id', current_node.node_id);
{#action += "?node_id=" + current_node.node_id;#}
$form.attr("action", action) $form.attr("action", action)
} }
$form.find('.help-block').remove(); $form.find('.help-block').remove();
...@@ -496,25 +501,14 @@ $(document).ready(function(){ ...@@ -496,25 +501,14 @@ $(document).ready(function(){
}) })
.on('click', '.btn-create-asset', function () { .on('click', '.btn-create-asset', function () {
var url = "{% url 'assets:asset-create' %}"; var url = "{% url 'assets:asset-create' %}";
var nodes = zTree.getSelectedNodes(); if (current_node_id) {
var current_node; url += "?node_id=" + current_node_id;
if (nodes && nodes.length ===1 ){
current_node = nodes[0];
url += "?node_id=" + current_node.node_id;
} }
window.open(url, '_self'); window.open(url, '_self');
}) })
.on('click', '.btn-refresh-hardware', function () { .on('click', '.btn-refresh-hardware', function () {
var url = "{% url 'api-assets:node-refresh-hardware-info' pk=DEFAULT_PK %}"; var url = "{% url 'api-assets:node-refresh-hardware-info' pk=DEFAULT_PK %}";
var nodes = zTree.getSelectedNodes(); var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
var current_node;
if (nodes && nodes.length ===1 ){
current_node = nodes[0];
} else {
return null;
}
var the_url = url.replace("{{ DEFAULT_PK }}", current_node.node_id);
function success(data) { function success(data) {
rMenu.css({"visibility" : "hidden"}); rMenu.css({"visibility" : "hidden"});
var task_id = data.task; var task_id = data.task;
...@@ -531,15 +525,10 @@ $(document).ready(function(){ ...@@ -531,15 +525,10 @@ $(document).ready(function(){
}) })
.on('click', '.btn-test-connective', function () { .on('click', '.btn-test-connective', function () {
var url = "{% url 'api-assets:node-test-connective' pk=DEFAULT_PK %}"; var url = "{% url 'api-assets:node-test-connective' pk=DEFAULT_PK %}";
var nodes = zTree.getSelectedNodes(); if (!current_node_id) {
var current_node;
if (nodes && nodes.length ===1 ){
current_node = nodes[0];
} else {
return null; return null;
} }
var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
var the_url = url.replace("{{ DEFAULT_PK }}", current_node.node_id);
function success(data) { function success(data) {
rMenu.css({"visibility" : "hidden"}); rMenu.css({"visibility" : "hidden"});
var task_id = data.task; var task_id = data.task;
...@@ -567,6 +556,19 @@ $(document).ready(function(){ ...@@ -567,6 +556,19 @@ $(document).ready(function(){
setCookie('show_current_asset', ''); setCookie('show_current_asset', '');
location.reload(); location.reload();
}) })
.on('click', '.btn-test-connective', function () {
hideRMenu();
})
.on('click', '#menu_refresh_assets_amount', function () {
hideRMenu();
var url = "{% url 'api-assets:refresh-assets-amount' %}";
APIUpdateAttr({
'url': url,
'method': 'GET'
});
window.location.reload();
})
.on('click', '.btn_asset_delete', function () { .on('click', '.btn_asset_delete', function () {
var $this = $(this); var $this = $(this);
var $data_table = $("#asset_list_table").DataTable(); var $data_table = $("#asset_list_table").DataTable();
...@@ -660,11 +662,8 @@ $(document).ready(function(){ ...@@ -660,11 +662,8 @@ $(document).ready(function(){
} }
function doRemove() { function doRemove() {
var current_node;
var nodes = zTree.getSelectedNodes(); var nodes = zTree.getSelectedNodes();
if (nodes && nodes.length === 1) { if (!current_node_id) {
current_node = nodes[0]
} else {
return return
} }
...@@ -677,7 +676,7 @@ $(document).ready(function(){ ...@@ -677,7 +676,7 @@ $(document).ready(function(){
}; };
APIUpdateAttr({ APIUpdateAttr({
'url': '/api/assets/v1/nodes/' + current_node.node_id + '/assets/remove/', 'url': '/api/assets/v1/nodes/' + current_node_id + '/assets/remove/',
'method': 'PUT', 'method': 'PUT',
'body': JSON.stringify(data), 'body': JSON.stringify(data),
'success': success 'success': success
...@@ -706,11 +705,7 @@ $(document).ready(function(){ ...@@ -706,11 +705,7 @@ $(document).ready(function(){
}) })
.on('click', '#btn_asset_modal_confirm', function () { .on('click', '#btn_asset_modal_confirm', function () {
var assets_selected = asset_table2.selected; var assets_selected = asset_table2.selected;
var current_node; if (!current_node_id) {
var nodes = zTree.getSelectedNodes();
if (nodes && nodes.length === 1) {
current_node = nodes[0]
} else {
return return
} }
...@@ -722,9 +717,9 @@ $(document).ready(function(){ ...@@ -722,9 +717,9 @@ $(document).ready(function(){
var url = ''; var url = '';
if (update_node_action === "move") { if (update_node_action === "move") {
url = "{% url 'api-assets:node-replace-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.node_id); url = "{% url 'api-assets:node-replace-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
} else { } else {
url = "{% url 'api-assets:node-add-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.node_id); url = "{% url 'api-assets:node-add-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
} }
APIUpdateAttr({ APIUpdateAttr({
...@@ -740,6 +735,7 @@ $(document).ready(function(){ ...@@ -740,6 +735,7 @@ $(document).ready(function(){
}).on('click', '#menu_asset_move', function () { }).on('click', '#menu_asset_move', function () {
update_node_action = "move" update_node_action = "move"
}) })
</script> </script>
{% endblock %} {% endblock %}
\ No newline at end of file
...@@ -24,17 +24,27 @@ ...@@ -24,17 +24,27 @@
{% block custom_foot_js %} {% block custom_foot_js %}
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function () { $(document).ready(function () {
console.log($.fn.select2.defaults);
$('.select2').select2().off("select2:open"); $('.select2').select2().off("select2:open");
}).on('click', '.select2-selection__rendered', function (e) { }).on('click', '.select2-selection__rendered', function (e) {
e.preventDefault(); e.preventDefault();
$("#asset_list_modal").modal(); $("#asset_list_modal").modal();
initSelectedAssets2Table();
}) })
.on('click', '#btn_asset_modal_confirm', function () { .on('click', '#btn_asset_modal_confirm', function () {
var assets = asset_table2.selected; var assets = asset_table2.selected;
$.each(assets, function (id, data) { var options = [];
$('.select2').val(assets).trigger('change'); $('#id_assets option').each(function (i, v) {
options.push(v.value)
}); });
asset_table2.selected_rows.forEach(function (i) {
var name = i.hostname + '(' + i.ip + ')';
var option = new Option(name, i.id, false, true);
if (options.indexOf(i.id) === -1) {
$('#id_assets').append(option).trigger('change');
}
});
$('.select2').val(assets).trigger('change');
$("#asset_list_modal").modal('hide'); $("#asset_list_modal").modal('hide');
}) })
......
...@@ -32,15 +32,23 @@ $(document).ready(function () { ...@@ -32,15 +32,23 @@ $(document).ready(function () {
}).on('click', '.select2-selection__rendered', function (e) { }).on('click', '.select2-selection__rendered', function (e) {
e.preventDefault(); e.preventDefault();
$("#asset_list_modal").modal(); $("#asset_list_modal").modal();
initSelectedAssets2Table();
}) })
.on('click', '#btn_asset_modal_confirm', function () { .on('click', '#btn_asset_modal_confirm', function () {
var assets = asset_table2.selected; var assets = asset_table2.selected;
$('.select2 option:selected').each(function (i, data) { var options = [];
assets.push($(data).attr('value')) $('#id_assets option').each(function (i, v) {
options.push(v.value)
}); });
$.each(assets, function (id, data) { asset_table2.selected_rows.forEach(function (i) {
$('.select2').val(assets).trigger('change'); var name = i.hostname + '(' + i.ip + ')';
var option = new Option(name, i.id, false, true);
if (options.indexOf(i.id) === -1) {
$('#id_assets').append(option).trigger('change');
}
}); });
$('#id_assets').val(assets).trigger('change');
$("#asset_list_modal").modal('hide'); $("#asset_list_modal").modal('hide');
}) })
</script> </script>
......
...@@ -136,7 +136,7 @@ ...@@ -136,7 +136,7 @@
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
function initAssetsTable() { function initAssetsTable() {
var unreachable = {{ system_user.unreachable_assets|safe }}; var connectivity = {{ system_user.connectivity | safe }};
var options = { var options = {
ele: $('#system_user_list'), ele: $('#system_user_list'),
buttons: [], buttons: [],
...@@ -147,11 +147,13 @@ function initAssetsTable() { ...@@ -147,11 +147,13 @@ function initAssetsTable() {
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id)); $(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}}, }},
{targets: 3, createdCell: function (td, cellData) { {targets: 3, createdCell: function (td, cellData) {
if (unreachable.indexOf(cellData) >= 0) { if (connectivity.unreachable.indexOf(cellData) >= 0) {
$(td).html('<i class="fa fa-times text-danger"></i>') $(td).html('<i class="fa fa-times text-danger"></i>')
} else { } else if (connectivity.reachable.indexOf(cellData) >= 0 ) {
$(td).html('<i class="fa fa-check text-navy"></i>') $(td).html('<i class="fa fa-check text-navy"></i>')
} } else {
$(td).html('')
}
}}, }},
{targets: 4, createdCell: function (td, cellData) { {targets: 4, createdCell: function (td, cellData) {
var push_btn = ''; var push_btn = '';
......
...@@ -95,7 +95,7 @@ function initTable() { ...@@ -95,7 +95,7 @@ function initTable() {
}}], }}],
ajax_url: '{% url "api-assets:system-user-list" %}', ajax_url: '{% url "api-assets:system-user-list" %}',
columns: [ columns: [
{data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "get_login_mode_display"}, {data: "assets_amount" }, {data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "login_mode_display"}, {data: "assets_amount" },
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" } {data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" }
], ],
op_html: $('#actions').html() op_html: $('#actions').html()
......
...@@ -71,7 +71,6 @@ function initTable() { ...@@ -71,7 +71,6 @@ function initTable() {
} else { } else {
inited = true; inited = true;
} }
console.log("init table")
url = "{% url 'api-perms:my-assets' %}"; url = "{% url 'api-perms:my-assets' %}";
var options = { var options = {
ele: $('#user_assets_table'), ele: $('#user_assets_table'),
...@@ -108,7 +107,8 @@ function initTable() { ...@@ -108,7 +107,8 @@ function initTable() {
function onSelected(event, treeNode) { function onSelected(event, treeNode) {
url = '{% url "api-perms:my-node-assets" node_id=DEFAULT_PK %}'; url = '{% url "api-perms:my-node-assets" node_id=DEFAULT_PK %}';
url = url.replace("{{ DEFAULT_PK }}", treeNode.node_id); var node_id = treeNode.meta.node.id;
url = url.replace("{{ DEFAULT_PK }}", node_id);
setCookie('node_selected', treeNode.id); setCookie('node_selected', treeNode.id);
asset_table.ajax.url(url); asset_table.ajax.url(url);
asset_table.ajax.reload(); asset_table.ajax.reload();
...@@ -131,21 +131,10 @@ function initTree() { ...@@ -131,21 +131,10 @@ function initTree() {
}; };
var zNodes = []; var zNodes = [];
$.get("{% url 'api-perms:my-nodes' %}", function(data, status){ $.get("{% url 'api-perms:my-nodes-assets-as-tree' %}?show_assets=0", function(data, status){
$.each(data, function (index, value) {
value["node_id"] = value["id"];
value["id"] = value["tree_id"];
if (value["tree_id"] !== value["tree_parent"]) {
value["pId"] = value["tree_parent"];
}
value["isParent"] = value["is_node"];
value['name'] = value['value'];
});
zNodes = data; zNodes = data;
$.fn.zTree.init($("#assetTree"), setting, zNodes); $.fn.zTree.init($("#assetTree"), setting, zNodes);
zTree = $.fn.zTree.getZTreeObj("assetTree"); zTree = $.fn.zTree.getZTreeObj("assetTree");
var root = zTree.getNodes()[0];
zTree.expandNode(root);
}); });
} }
...@@ -170,7 +159,7 @@ $(document).ready(function () { ...@@ -170,7 +159,7 @@ $(document).ready(function () {
'comment': "{% trans 'Comment' %}" 'comment': "{% trans 'Comment' %}"
{#'date_joined': "{% trans 'Date joined' %}",#} {#'date_joined': "{% trans 'Date joined' %}",#}
}; };
$.each(data, function(index, value){ $.each(data.results, function(index, value){
if(value.id === asset_id){ if(value.id === asset_id){
for(var i in desc){ for(var i in desc){
trs += "<tr class='no-borders-tr'>\n" + trs += "<tr class='no-borders-tr'>\n" +
......
...@@ -24,35 +24,39 @@ cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-r ...@@ -24,35 +24,39 @@ cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-r
urlpatterns = [ urlpatterns = [
path('assets-bulk/', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'), path('assets-bulk/', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'),
path('system-user/<uuid:pk>/auth-info/',
api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
path('system-user/<uuid:pk>/assets/',
api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
path('assets/<uuid:pk>/refresh/', path('assets/<uuid:pk>/refresh/',
api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'), api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'),
path('assets/<uuid:pk>/alive/', path('assets/<uuid:pk>/alive/',
api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'), api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'),
path('assets/<uuid:pk>/gateway/', path('assets/<uuid:pk>/gateway/',
api.AssetGatewayApi.as_view(), name='asset-gateway'), api.AssetGatewayApi.as_view(), name='asset-gateway'),
path('admin-user/<uuid:pk>/nodes/', path('admin-user/<uuid:pk>/nodes/',
api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'), api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'),
path('admin-user/<uuid:pk>/auth/', path('admin-user/<uuid:pk>/auth/',
api.AdminUserAuthApi.as_view(), name='admin-user-auth'), api.AdminUserAuthApi.as_view(), name='admin-user-auth'),
path('admin-user/<uuid:pk>/connective/', path('admin-user/<uuid:pk>/connective/',
api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'), api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'),
path('admin-user/<uuid:pk>/assets/',
api.AdminUserAssetsListView.as_view(), name='admin-user-assets'),
path('system-user/<uuid:pk>/auth-info/',
api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
path('system-user/<uuid:pk>/assets/',
api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
path('system-user/<uuid:pk>/push/', path('system-user/<uuid:pk>/push/',
api.SystemUserPushApi.as_view(), name='system-user-push'), api.SystemUserPushApi.as_view(), name='system-user-push'),
path('system-user/<uuid:pk>/asset/<uuid:aid>/push/', path('system-user/<uuid:pk>/asset/<uuid:aid>/push/',
api.SystemUserPushToAssetApi.as_view(), name='system-user-push-to-asset'), api.SystemUserPushToAssetApi.as_view(), name='system-user-push-to-asset'),
path('system-user/<uuid:pk>/asset/<uuid:aid>/test/', path('system-user/<uuid:pk>/asset/<uuid:aid>/test/',
api.SystemUserTestAssetConnectabilityApi.as_view(), name='system-user-test-to-asset'), api.SystemUserTestAssetConnectivityApi.as_view(), name='system-user-test-to-asset'),
path('system-user/<uuid:pk>/connective/', path('system-user/<uuid:pk>/connective/',
api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'), api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'),
path('system-user/<uuid:pk>/cmd-filter-rules/', path('system-user/<uuid:pk>/cmd-filter-rules/',
api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'), api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'),
path('nodes/tree/', api.NodeListAsTreeApi.as_view(), name='node-tree'),
path('nodes/children/tree/', api.NodeChildrenAsTreeApi.as_view(), name='node-children-tree'),
path('nodes/<uuid:pk>/children/', path('nodes/<uuid:pk>/children/',
api.NodeChildrenApi.as_view(), name='node-children'), api.NodeChildrenApi.as_view(), name='node-children'),
path('nodes/children/', api.NodeChildrenApi.as_view(), name='node-children-2'), path('nodes/children/', api.NodeChildrenApi.as_view(), name='node-children-2'),
...@@ -70,6 +74,8 @@ urlpatterns = [ ...@@ -70,6 +74,8 @@ urlpatterns = [
api.RefreshNodeHardwareInfoApi.as_view(), name='node-refresh-hardware-info'), api.RefreshNodeHardwareInfoApi.as_view(), name='node-refresh-hardware-info'),
path('nodes/<uuid:pk>/test-connective/', path('nodes/<uuid:pk>/test-connective/',
api.TestNodeConnectiveApi.as_view(), name='node-test-connective'), api.TestNodeConnectiveApi.as_view(), name='node-test-connective'),
path('nodes/refresh-assets-amount/',
api.RefreshAssetsAmount.as_view(), name='refresh-assets-amount'),
path('gateway/<uuid:pk>/test-connective/', path('gateway/<uuid:pk>/test-connective/',
api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'), api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
......
...@@ -9,7 +9,11 @@ from .models import Asset, SystemUser, Label ...@@ -9,7 +9,11 @@ from .models import Asset, SystemUser, Label
def get_assets_by_id_list(id_list): def get_assets_by_id_list(id_list):
return Asset.objects.filter(id__in=id_list) return Asset.objects.filter(id__in=id_list).filter(is_active=True)
def get_system_users_by_id_list(id_list):
return SystemUser.objects.filter(id__in=id_list)
def get_assets_by_fullname_list(hostname_list): def get_assets_by_fullname_list(hostname_list):
...@@ -21,6 +25,11 @@ def get_system_user_by_name(name): ...@@ -21,6 +25,11 @@ def get_system_user_by_name(name):
return system_user return system_user
def get_system_user_by_id(id):
system_user = get_object_or_none(SystemUser, id=id)
return system_user
class LabelFilter: class LabelFilter:
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset) queryset = super().filter_queryset(queryset)
...@@ -40,44 +49,3 @@ class LabelFilter: ...@@ -40,44 +49,3 @@ class LabelFilter:
for kwargs in conditions: for kwargs in conditions:
queryset = queryset.filter(**kwargs) queryset = queryset.filter(**kwargs)
return queryset return queryset
def test_gateway_connectability(gateway):
"""
Test system cant connect his assets or not.
:param gateway:
:return:
"""
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
proxy = paramiko.SSHClient()
proxy.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
proxy.connect(gateway.ip, gateway.port,
username=gateway.username,
password=gateway.password,
pkey=gateway.private_key_obj)
except(paramiko.AuthenticationException,
paramiko.BadAuthenticationType,
SSHException) as e:
return False, str(e)
sock = proxy.get_transport().open_channel(
'direct-tcpip', ('127.0.0.1', gateway.port), ('127.0.0.1', 0)
)
try:
client.connect("127.0.0.1", port=gateway.port,
username=gateway.username,
password=gateway.password,
key_filename=gateway.private_key_file,
sock=sock,
timeout=5
)
except (paramiko.SSHException, paramiko.ssh_exception.SSHException,
paramiko.AuthenticationException, TimeoutError) as e:
return False, str(e)
finally:
client.close()
return True, None
...@@ -102,7 +102,7 @@ class AdminUserAssetsView(AdminUserRequiredMixin, SingleObjectMixin, ListView): ...@@ -102,7 +102,7 @@ class AdminUserAssetsView(AdminUserRequiredMixin, SingleObjectMixin, ListView):
'app': _('Assets'), 'app': _('Assets'),
'action': _('Admin user detail'), 'action': _('Admin user detail'),
"total_amount": len(self.queryset), "total_amount": len(self.queryset),
'unreachable_amount': len([asset for asset in self.queryset if asset.is_connective is False]) 'unreachable_amount': len([asset for asset in self.queryset if asset.connectivity is False])
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
......
...@@ -13,6 +13,6 @@ class Migration(migrations.Migration): ...@@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='ftplog', model_name='ftplog',
name='org_id', name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True), field=models.CharField(blank=True, db_index=True, default=None, max_length=36, null=True),
), ),
] ]
...@@ -13,6 +13,6 @@ class Migration(migrations.Migration): ...@@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='ftplog', model_name='ftplog',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
] ]
...@@ -15,7 +15,7 @@ class Migration(migrations.Migration): ...@@ -15,7 +15,7 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='OperateLog', name='OperateLog',
fields=[ fields=[
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')), ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('user', models.CharField(max_length=128, verbose_name='User')), ('user', models.CharField(max_length=128, verbose_name='User')),
('action', models.CharField(choices=[('create', 'Create'), ('update', 'Update'), ('delete', 'Delete')], max_length=16, verbose_name='Action')), ('action', models.CharField(choices=[('create', 'Create'), ('update', 'Update'), ('delete', 'Delete')], max_length=16, verbose_name='Action')),
......
...@@ -13,4 +13,5 @@ urlpatterns = [ ...@@ -13,4 +13,5 @@ urlpatterns = [
path('ftp-log/', views.FTPLogListView.as_view(), name='ftp-log-list'), path('ftp-log/', views.FTPLogListView.as_view(), name='ftp-log-list'),
path('operate-log/', views.OperateLogListView.as_view(), name='operate-log-list'), path('operate-log/', views.OperateLogListView.as_view(), name='operate-log-list'),
path('password-change-log/', views.PasswordChangeLogList.as_view(), name='password-change-log-list'), path('password-change-log/', views.PasswordChangeLogList.as_view(), name='password-change-log-list'),
path('command-execution-log/', views.CommandExecutionListView.as_view(), name='command-execution-log-list'),
] ]
...@@ -7,6 +7,8 @@ from common.mixins import DatetimeSearchMixin ...@@ -7,6 +7,8 @@ from common.mixins import DatetimeSearchMixin
from common.permissions import AdminUserRequiredMixin from common.permissions import AdminUserRequiredMixin
from orgs.utils import current_org from orgs.utils import current_org
from ops.views import CommandExecutionListView as UserCommandExecutionListView
from users.models import User
from .models import FTPLog, OperateLog, PasswordChangeLog, UserLoginLog from .models import FTPLog, OperateLog, PasswordChangeLog, UserLoginLog
...@@ -122,7 +124,10 @@ class PasswordChangeLogList(AdminUserRequiredMixin, DatetimeSearchMixin, ListVie ...@@ -122,7 +124,10 @@ class PasswordChangeLogList(AdminUserRequiredMixin, DatetimeSearchMixin, ListVie
date_from = date_to = None date_from = date_to = None
def get_queryset(self): def get_queryset(self):
self.queryset = super().get_queryset() users = current_org.get_org_users()
self.queryset = super().get_queryset().filter(
user__in=[user.__str__() for user in users]
)
self.user = self.request.GET.get('user') self.user = self.request.GET.get('user')
filter_kwargs = dict() filter_kwargs = dict()
...@@ -184,7 +189,7 @@ class LoginLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): ...@@ -184,7 +189,7 @@ class LoginLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
'app': _('Users'), 'app': _('Audits'),
'action': _('Login log'), 'action': _('Login log'),
'date_from': self.date_from, 'date_from': self.date_from,
'date_to': self.date_to, 'date_to': self.date_to,
...@@ -193,4 +198,35 @@ class LoginLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): ...@@ -193,4 +198,35 @@ class LoginLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
'user_list': self.get_org_users(), 'user_list': self.get_org_users(),
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
\ No newline at end of file
class CommandExecutionListView(UserCommandExecutionListView):
user_id = None
def get_queryset(self):
queryset = self._get_queryset()
self.user_id = self.request.GET.get('user')
org_users = self.get_user_list()
if self.user_id:
queryset = queryset.filter(user=self.user_id)
else:
queryset = queryset.filter(user__in=org_users)
return queryset
def get_user_list(self):
users = current_org.get_org_users()
return users
def get_context_data(self, **kwargs):
context = {
'app': _('Audits'),
'action': _('Command execution list'),
'date_from': self.date_from,
'date_to': self.date_to,
'user_list': self.get_user_list(),
'keyword': self.keyword,
'user_id': self.user_id,
}
kwargs.update(context)
return super().get_context_data(**kwargs)
# coding:utf-8
#
import ldap
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django_auth_ldap.backend import _LDAPUser, LDAPBackend
from django_auth_ldap.config import _LDAPConfig, LDAPSearch, LDAPSearchUnion
logger = _LDAPConfig.get_logger()
class LDAPAuthorizationBackend(LDAPBackend):
"""
Override this class to override _LDAPUser to LDAPUser
"""
def authenticate(self, request=None, username=None, password=None, **kwargs):
if password or self.settings.PERMIT_EMPTY_PASSWORD:
ldap_user = LDAPUser(self, username=username.strip(), request=request)
user = self.authenticate_ldap_user(ldap_user, password)
else:
logger.debug('Rejecting empty password for {}'.format(username))
user = None
return user
def get_user(self, user_id):
user = None
try:
user = self.get_user_model().objects.get(pk=user_id)
LDAPUser(self, user=user) # This sets user.ldap_user
except ObjectDoesNotExist:
pass
return user
def get_group_permissions(self, user, obj=None):
if not hasattr(user, 'ldap_user') and self.settings.AUTHORIZE_ALL_USERS:
LDAPUser(self, user=user) # This sets user.ldap_user
if hasattr(user, 'ldap_user'):
permissions = user.ldap_user.get_group_permissions()
else:
permissions = set()
return permissions
def populate_user(self, username):
ldap_user = LDAPUser(self, username=username)
user = ldap_user.populate_user()
return user
class LDAPUser(_LDAPUser):
def _search_for_user_dn(self):
"""
This method was overridden because the AUTH_LDAP_USER_SEARCH
configuration in the settings.py file
is configured with a `lambda` problem value
"""
user_search_union = [
LDAPSearch(
USER_SEARCH, ldap.SCOPE_SUBTREE,
settings.AUTH_LDAP_SEARCH_FILTER
)
for USER_SEARCH in str(settings.AUTH_LDAP_SEARCH_OU).split("|")
]
search = LDAPSearchUnion(*user_search_union)
if search is None:
raise ImproperlyConfigured(
'AUTH_LDAP_USER_SEARCH must be an LDAPSearch instance.'
)
results = search.execute(self.connection, {'user': self._username})
if results is not None and len(results) == 1:
(user_dn, self._user_attrs) = next(iter(results))
else:
user_dn = None
return user_dn
def _populate_user_from_attributes(self):
super()._populate_user_from_attributes()
if not hasattr(self._user, 'email') or '@' not in self._user.email:
email = '{}@{}'.format(self._user.username, settings.EMAIL_SUFFIX)
setattr(self._user, 'email', email)
from django.http.request import QueryDict from django.http.request import QueryDict
from django.contrib.auth.signals import user_logged_out
from django.dispatch import receiver
from django.conf import settings from django.conf import settings
from django.dispatch import receiver
from django.contrib.auth.signals import user_logged_out
from django_auth_ldap.backend import populate_user
from .openid import client from .openid import client
from .signals import post_create_openid_user from .signals import post_create_openid_user
...@@ -31,3 +32,9 @@ def on_post_create_openid_user(sender, user=None, **kwargs): ...@@ -31,3 +32,9 @@ def on_post_create_openid_user(sender, user=None, **kwargs):
user.source = user.SOURCE_OPENID user.source = user.SOURCE_OPENID
user.save() user.save()
@receiver(populate_user)
def on_ldap_create_user(sender, user, ldap_user, **kwargs):
if user and user.name != 'admin':
user.source = user.SOURCE_LDAP
user.save()
...@@ -104,7 +104,11 @@ class ReplayStorageCreateAPI(APIView): ...@@ -104,7 +104,11 @@ class ReplayStorageCreateAPI(APIView):
data = {storage_name: storage_data} data = {storage_name: storage_data}
if not self.is_valid(storage_data): if not self.is_valid(storage_data):
return Response({"error": _("Error: Account invalid")}, status=401) return Response({
"error": _("Error: Account invalid (Please make sure the "
"information such as Access key or Secret key is correct)")},
status=401
)
Setting.save_storage('TERMINAL_REPLAY_STORAGE', data) Setting.save_storage('TERMINAL_REPLAY_STORAGE', data)
return Response({"msg": _('Create succeed')}, status=200) return Response({"msg": _('Create succeed')}, status=200)
...@@ -136,7 +140,11 @@ class CommandStorageCreateAPI(APIView): ...@@ -136,7 +140,11 @@ class CommandStorageCreateAPI(APIView):
storage_name = storage_data.pop('NAME') storage_name = storage_data.pop('NAME')
data = {storage_name: storage_data} data = {storage_name: storage_data}
if not self.is_valid(storage_data): if not self.is_valid(storage_data):
return Response({"error": _("Error: Account invalid")}, status=401) return Response(
{"error": _("Error: Account invalid (Please make sure the "
"information such as Access key or Secret key is correct)")},
status=401
)
Setting.save_storage('TERMINAL_COMMAND_STORAGE', data) Setting.save_storage('TERMINAL_COMMAND_STORAGE', data)
return Response({"msg": _('Create succeed')}, status=200) return Response({"msg": _('Create succeed')}, status=200)
......
...@@ -15,8 +15,6 @@ class BaseForm(forms.Form): ...@@ -15,8 +15,6 @@ class BaseForm(forms.Form):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
for name, field in self.fields.items(): for name, field in self.fields.items():
value = getattr(settings, name, None) value = getattr(settings, name, None)
# django_value = getattr(settings, name) if hasattr(settings, name) else None
if value is None: # and django_value is None: if value is None: # and django_value is None:
continue continue
...@@ -24,8 +22,6 @@ class BaseForm(forms.Form): ...@@ -24,8 +22,6 @@ class BaseForm(forms.Form):
if isinstance(value, dict): if isinstance(value, dict):
value = json.dumps(value) value = json.dumps(value)
initial_value = value initial_value = value
# elif django_value is False or django_value:
# initial_value = django_value
else: else:
initial_value = '' initial_value = ''
field.initial = initial_value field.initial = initial_value
...@@ -134,17 +130,34 @@ class TerminalSettingForm(BaseForm): ...@@ -134,17 +130,34 @@ class TerminalSettingForm(BaseForm):
('hostname', _('Hostname')), ('hostname', _('Hostname')),
('ip', _('IP')), ('ip', _('IP')),
) )
PAGE_SIZE_CHOICES = (
('all', _('All')),
('auto', _('Auto')),
(10, 10),
(15, 15),
(25, 25),
(50, 50),
)
TERMINAL_PASSWORD_AUTH = forms.BooleanField( TERMINAL_PASSWORD_AUTH = forms.BooleanField(
initial=True, required=False, label=_("Password auth") required=False, label=_("Password auth")
) )
TERMINAL_PUBLIC_KEY_AUTH = forms.BooleanField( TERMINAL_PUBLIC_KEY_AUTH = forms.BooleanField(
initial=True, required=False, label=_("Public key auth") required=False, label=_("Public key auth")
) )
TERMINAL_HEARTBEAT_INTERVAL = forms.IntegerField( TERMINAL_HEARTBEAT_INTERVAL = forms.IntegerField(
initial=5, label=_("Heartbeat interval"), help_text=_("Units: seconds") min_value=5, label=_("Heartbeat interval"),
help_text=_("Units: seconds")
) )
TERMINAL_ASSET_LIST_SORT_BY = forms.ChoiceField( TERMINAL_ASSET_LIST_SORT_BY = forms.ChoiceField(
choices=SORT_BY_CHOICES, initial='hostname', label=_("List sort by") choices=SORT_BY_CHOICES, label=_("List sort by")
)
TERMINAL_ASSET_LIST_PAGE_SIZE = forms.ChoiceField(
choices=PAGE_SIZE_CHOICES, label=_("List page size"),
)
TERMINAL_SESSION_KEEP_DURATION = forms.IntegerField(
min_value=1, label=_("Session keep duration"),
help_text=_("Units: days, Session, record, command will be delete "
"if more than duration, only in database")
) )
...@@ -155,8 +168,7 @@ class TerminalCommandStorage(BaseForm): ...@@ -155,8 +168,7 @@ class TerminalCommandStorage(BaseForm):
class SecuritySettingForm(BaseForm): class SecuritySettingForm(BaseForm):
# MFA global setting # MFA global setting
SECURITY_MFA_AUTH = forms.BooleanField( SECURITY_MFA_AUTH = forms.BooleanField(
initial=False, required=False, required=False, label=_("MFA Secondary certification"),
label=_("MFA Secondary certification"),
help_text=_( help_text=_(
'After opening, the user login must use MFA secondary ' 'After opening, the user login must use MFA secondary '
'authentication (valid for all users, including administrators)' 'authentication (valid for all users, including administrators)'
...@@ -164,13 +176,11 @@ class SecuritySettingForm(BaseForm): ...@@ -164,13 +176,11 @@ class SecuritySettingForm(BaseForm):
) )
# limit login count # limit login count
SECURITY_LOGIN_LIMIT_COUNT = forms.IntegerField( SECURITY_LOGIN_LIMIT_COUNT = forms.IntegerField(
initial=7, min_value=3, min_value=3, label=_("Limit the number of login failures")
label=_("Limit the number of login failures")
) )
# limit login time # limit login time
SECURITY_LOGIN_LIMIT_TIME = forms.IntegerField( SECURITY_LOGIN_LIMIT_TIME = forms.IntegerField(
initial=30, min_value=5, min_value=5, label=_("No logon interval"),
label=_("No logon interval"),
help_text=_( help_text=_(
"Tip: (unit/minute) if the user has failed to log in for a limited " "Tip: (unit/minute) if the user has failed to log in for a limited "
"number of times, no login is allowed during this time interval." "number of times, no login is allowed during this time interval."
...@@ -178,8 +188,7 @@ class SecuritySettingForm(BaseForm): ...@@ -178,8 +188,7 @@ class SecuritySettingForm(BaseForm):
) )
# ssh max idle time # ssh max idle time
SECURITY_MAX_IDLE_TIME = forms.IntegerField( SECURITY_MAX_IDLE_TIME = forms.IntegerField(
initial=30, required=False, required=False, label=_("Connection max idle time"),
label=_("Connection max idle time"),
help_text=_( help_text=_(
'If idle time more than it, disconnect connection(only ssh now) ' 'If idle time more than it, disconnect connection(only ssh now) '
'Unit: minute' 'Unit: minute'
...@@ -187,10 +196,10 @@ class SecuritySettingForm(BaseForm): ...@@ -187,10 +196,10 @@ class SecuritySettingForm(BaseForm):
) )
# password expiration time # password expiration time
SECURITY_PASSWORD_EXPIRATION_TIME = forms.IntegerField( SECURITY_PASSWORD_EXPIRATION_TIME = forms.IntegerField(
initial=9999, label=_("Password expiration time"), label=_("Password expiration time"),
min_value=1, min_value=1, max_value=99999,
help_text=_( help_text=_(
"Tip: (unit/day) " "Tip: (unit: day) "
"If the user does not update the password during the time, " "If the user does not update the password during the time, "
"the user password will expire failure;" "the user password will expire failure;"
"The password expiration reminder mail will be automatic sent to the user " "The password expiration reminder mail will be automatic sent to the user "
...@@ -199,35 +208,30 @@ class SecuritySettingForm(BaseForm): ...@@ -199,35 +208,30 @@ class SecuritySettingForm(BaseForm):
) )
# min length # min length
SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField( SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField(
initial=6, label=_("Password minimum length"), min_value=6, label=_("Password minimum length"),
min_value=6
) )
# upper case # upper case
SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField( SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField(
initial=False, required=False, required=False, label=_("Must contain capital letters"),
label=_("Must contain capital letters"),
help_text=_( help_text=_(
'After opening, the user password changes ' 'After opening, the user password changes '
'and resets must contain uppercase letters') 'and resets must contain uppercase letters')
) )
# lower case # lower case
SECURITY_PASSWORD_LOWER_CASE = forms.BooleanField( SECURITY_PASSWORD_LOWER_CASE = forms.BooleanField(
initial=False, required=False, required=False, label=_("Must contain lowercase letters"),
label=_("Must contain lowercase letters"),
help_text=_('After opening, the user password changes ' help_text=_('After opening, the user password changes '
'and resets must contain lowercase letters') 'and resets must contain lowercase letters')
) )
# number # number
SECURITY_PASSWORD_NUMBER = forms.BooleanField( SECURITY_PASSWORD_NUMBER = forms.BooleanField(
initial=False, required=False, required=False, label=_("Must contain numeric characters"),
label=_("Must contain numeric characters"),
help_text=_('After opening, the user password changes ' help_text=_('After opening, the user password changes '
'and resets must contain numeric characters') 'and resets must contain numeric characters')
) )
# special char # special char
SECURITY_PASSWORD_SPECIAL_CHAR = forms.BooleanField( SECURITY_PASSWORD_SPECIAL_CHAR = forms.BooleanField(
initial=False, required=False, required=False, label=_("Must contain special characters"),
label=_("Must contain special characters"),
help_text=_('After opening, the user password changes ' help_text=_('After opening, the user password changes '
'and resets must contain special characters') 'and resets must contain special characters')
) )
......
...@@ -47,6 +47,8 @@ class Setting(models.Model): ...@@ -47,6 +47,8 @@ class Setting(models.Model):
value = self.value value = self.value
if self.encrypted: if self.encrypted:
value = signer.unsign(value) value = signer.unsign(value)
if not value:
return None
value = json.loads(value) value = json.loads(value)
return value return value
except json.JSONDecodeError: except json.JSONDecodeError:
...@@ -64,6 +66,11 @@ class Setting(models.Model): ...@@ -64,6 +66,11 @@ class Setting(models.Model):
@classmethod @classmethod
def save_storage(cls, name, data): def save_storage(cls, name, data):
"""
:param name: TERMINAL_REPLAY_STORAGE or TERMINAL_COMMAND_STORAGE
:param data: {}
:return: Setting object
"""
obj = cls.objects.filter(name=name).first() obj = cls.objects.filter(name=name).first()
if not obj: if not obj:
obj = cls() obj = cls()
...@@ -79,6 +86,11 @@ class Setting(models.Model): ...@@ -79,6 +86,11 @@ class Setting(models.Model):
@classmethod @classmethod
def delete_storage(cls, name, storage_name): def delete_storage(cls, name, storage_name):
"""
:param name: TERMINAL_REPLAY_STORAGE or TERMINAL_COMMAND_STORAGE
:param storage_name: ""
:return: bool
"""
obj = cls.objects.filter(name=name).first() obj = cls.objects.filter(name=name).first()
if not obj: if not obj:
return False return False
......
...@@ -26,21 +26,20 @@ def refresh_settings_on_changed(sender, instance=None, **kwargs): ...@@ -26,21 +26,20 @@ def refresh_settings_on_changed(sender, instance=None, **kwargs):
def refresh_all_settings_on_django_ready(sender, **kwargs): def refresh_all_settings_on_django_ready(sender, **kwargs):
logger.debug("Receive django ready signal") logger.debug("Receive django ready signal")
logger.debug(" - fresh all settings") logger.debug(" - fresh all settings")
CACHE_KEY_PREFIX = '_SETTING_' cache_key_prefix = '_SETTING_'
def monkey_patch_getattr(self, name): def monkey_patch_getattr(self, name):
key = CACHE_KEY_PREFIX + name key = cache_key_prefix + name
cached = cache.get(key) cached = cache.get(key)
if cached is not None: if cached is not None:
return cached return cached
if self._wrapped is empty: if self._wrapped is empty:
self._setup(name) self._setup(name)
val = getattr(self._wrapped, name) val = getattr(self._wrapped, name)
# self.__dict__[name] = val # Never set it
return val return val
def monkey_patch_setattr(self, name, value): def monkey_patch_setattr(self, name, value):
key = CACHE_KEY_PREFIX + name key = cache_key_prefix + name
cache.set(key, value, None) cache.set(key, value, None)
if name == '_wrapped': if name == '_wrapped':
self.__dict__.clear() self.__dict__.clear()
...@@ -51,7 +50,7 @@ def refresh_all_settings_on_django_ready(sender, **kwargs): ...@@ -51,7 +50,7 @@ def refresh_all_settings_on_django_ready(sender, **kwargs):
def monkey_patch_delattr(self, name): def monkey_patch_delattr(self, name):
super(LazySettings, self).__delattr__(name) super(LazySettings, self).__delattr__(name)
self.__dict__.pop(name, None) self.__dict__.pop(name, None)
key = CACHE_KEY_PREFIX + name key = cache_key_prefix + name
cache.delete(key) cache.delete(key)
try: try:
...@@ -65,6 +64,8 @@ def refresh_all_settings_on_django_ready(sender, **kwargs): ...@@ -65,6 +64,8 @@ def refresh_all_settings_on_django_ready(sender, **kwargs):
@receiver(pre_save, dispatch_uid="my_unique_identifier") @receiver(pre_save, dispatch_uid="my_unique_identifier")
def on_create_set_created_by(sender, instance=None, **kwargs): def on_create_set_created_by(sender, instance=None, **kwargs):
if getattr(instance, '_ignore_auto_created_by', False) is True:
return
if hasattr(instance, 'created_by') and not instance.created_by: if hasattr(instance, 'created_by') and not instance.created_by:
if current_request and current_request.user.is_authenticated: if current_request and current_request.user.is_authenticated:
instance.created_by = current_request.user.name instance.created_by = current_request.user.name
...@@ -41,7 +41,6 @@ ...@@ -41,7 +41,6 @@
<label class="col-md-2 control-label" for="id_name">{% trans "Name" %}</label> <label class="col-md-2 control-label" for="id_name">{% trans "Name" %}</label>
<div class="col-md-9"> <div class="col-md-9">
<input id="id_name" class="form-control" type="text" name="NAME" value=""> <input id="id_name" class="form-control" type="text" name="NAME" value="">
<div class="help-block">* required</div>
<div id="id_error" style="color: red;"></div> <div id="id_error" style="color: red;"></div>
</div> </div>
</div> </div>
......
...@@ -44,7 +44,6 @@ ...@@ -44,7 +44,6 @@
<label class="col-md-2 control-label" for="id_name">{% trans "Name" %}</label> <label class="col-md-2 control-label" for="id_name">{% trans "Name" %}</label>
<div class="col-md-9"> <div class="col-md-9">
<input id="id_name" class="form-control" type="text" name="NAME" value=""> <input id="id_name" class="form-control" type="text" name="NAME" value="">
<div class="help-block">* required</div>
<div id="id_error" style="color: red;"></div> <div id="id_error" style="color: red;"></div>
</div> </div>
</div> </div>
......
...@@ -39,9 +39,9 @@ ...@@ -39,9 +39,9 @@
{% endif %} {% endif %}
{% csrf_token %} {% csrf_token %}
<h3>{% trans "User login settings" %}</h3> <h3>{% trans "Security setting" %}</h3>
{% for field in form %} {% for field in form %}
{% if forloop.counter == 5 %} {% if forloop.counter == 6 %}
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
<h3>{% trans "Password check rule" %}</h3> <h3>{% trans "Password check rule" %}</h3>
{% endif %} {% endif %}
......
...@@ -87,7 +87,7 @@ ...@@ -87,7 +87,7 @@
<tr> <tr>
<td>{{ name }}</td> <td>{{ name }}</td>
<td>{{ setting.TYPE }}</td> <td>{{ setting.TYPE }}</td>
<td><a class="btn btn-xs btn-danger m-l-xs btn-del-command" data-name="{{ name }}">{% trans 'Delete' %}</a></td> <td><a class="btn btn-xs btn-danger m-l-xs btn-del-command" {% if setting.TYPE == 'server' and name == 'default' %} disabled {% endif %} data-name="{{ name }}">{% trans 'Delete' %}</a></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
...@@ -109,7 +109,7 @@ ...@@ -109,7 +109,7 @@
<tr> <tr>
<td>{{ name }}</td> <td>{{ name }}</td>
<td>{{ setting.TYPE }}</td> <td>{{ setting.TYPE }}</td>
<td><a class="btn btn-xs btn-danger m-l-xs btn-del-replay" data-name="{{ name }}">{% trans 'Delete' %}</a></td> <td><a class="btn btn-xs btn-danger m-l-xs btn-del-replay" {% if setting.TYPE == 'server' and name == 'default' %} disabled {% endif %} data-name="{{ name }}">{% trans 'Delete' %}</a></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
......
...@@ -111,3 +111,13 @@ def sort(data): ...@@ -111,3 +111,13 @@ def sort(data):
@register.filter @register.filter
def subtract(value, arg): def subtract(value, arg):
return value - arg return value - arg
@register.filter
def state_show(state):
success = '<i class ="fa fa-check text-navy"> </i>'
failed = '<i class ="fa fa-times text-danger"> </i>'
if state:
return success
else:
return failed
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
class TreeNode:
id = ""
name = ""
comment = ""
title = ""
isParent = False
pId = ""
open = False
iconSkin = ""
meta = {}
_tree = None
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
@classmethod
def root(cls):
return cls(id="#", name='Root', title='Root', isParent=True, open=True)
def get_parent(self):
return self._tree.get_node(self.pId)
def get_parents(self):
parent = self.get_parent()
if parent == self._tree.root:
return []
parents = [parent]
parents.extend(parent.get_parents())
return parents
def add_child(self, child):
self._tree.add_node(child, self)
def __str__(self):
return '<{}: {}>'.format(self.id, self.name)
__repr__ = __str__
def __gt__(self, other):
if self.isParent and not other.isParent:
return False
elif not self.isParent and other.isParent:
return True
if self.pId != other.pId:
return self.pId > other.pId
return self.name > other.name
def __eq__(self, other):
return self.id == other.id
class Tree:
def __init__(self):
self.nodes = {}
self.root = TreeNode.root()
self.root._tree = self
def add_node(self, node, parent=None):
node._tree = self
if not parent:
parent = self.root
if parent.id not in self.nodes and parent != self.root:
raise ValueError("Parent not in tree")
elif node in parent.get_parents():
raise ValueError("Parent must not be node parent")
node.pId = parent.id
parent.isParent = True
self.nodes[node.id] = node
def get_nodes(self):
return sorted(self.nodes.values())
def get_node(self, tid):
return self.nodes.get(tid) or TreeNode.root()
class TreeNodeSerializer(serializers.Serializer):
id = serializers.CharField(max_length=128)
name = serializers.CharField(max_length=128)
title = serializers.CharField(max_length=128)
pId = serializers.CharField(max_length=128)
isParent = serializers.BooleanField(default=False)
open = serializers.BooleanField(default=False)
iconSkin = serializers.CharField(max_length=128, allow_blank=True)
meta = serializers.JSONField()
...@@ -391,6 +391,8 @@ def get_request_ip(request): ...@@ -391,6 +391,8 @@ def get_request_ip(request):
def get_command_storage_setting(): def get_command_storage_setting():
default = settings.DEFAULT_TERMINAL_COMMAND_STORAGE default = settings.DEFAULT_TERMINAL_COMMAND_STORAGE
value = settings.TERMINAL_COMMAND_STORAGE value = settings.TERMINAL_COMMAND_STORAGE
if not value:
return default
value.update(default) value.update(default)
return value return value
...@@ -398,6 +400,8 @@ def get_command_storage_setting(): ...@@ -398,6 +400,8 @@ def get_command_storage_setting():
def get_replay_storage_setting(): def get_replay_storage_setting():
default = settings.DEFAULT_TERMINAL_REPLAY_STORAGE default = settings.DEFAULT_TERMINAL_REPLAY_STORAGE
value = settings.TERMINAL_REPLAY_STORAGE value = settings.TERMINAL_REPLAY_STORAGE
if not value:
return default
value.update(default) value.update(default)
return value return value
......
...@@ -52,7 +52,7 @@ class EmailSettingView(SuperUserRequiredMixin, TemplateView): ...@@ -52,7 +52,7 @@ class EmailSettingView(SuperUserRequiredMixin, TemplateView):
form = self.form_class(request.POST) form = self.form_class(request.POST)
if form.is_valid(): if form.is_valid():
form.save() form.save()
msg = _("Update setting successfully, please restart program") msg = _("Update setting successfully")
messages.success(request, msg) messages.success(request, msg)
return redirect('settings:email-setting') return redirect('settings:email-setting')
else: else:
......
...@@ -301,16 +301,35 @@ defaults = { ...@@ -301,16 +301,35 @@ defaults = {
'REDIS_HOST': '127.0.0.1', 'REDIS_HOST': '127.0.0.1',
'REDIS_PORT': 6379, 'REDIS_PORT': 6379,
'REDIS_PASSWORD': '', 'REDIS_PASSWORD': '',
'REDIS_DB_CELERY_BROKER': 3, 'REDIS_DB_CELERY': 3,
'REDIS_DB_CACHE': 4, 'REDIS_DB_CACHE': 4,
'CAPTCHA_TEST_MODE': None, 'CAPTCHA_TEST_MODE': None,
'TOKEN_EXPIRATION': 3600, 'TOKEN_EXPIRATION': 3600 * 24,
'DISPLAY_PER_PAGE': 25, 'DISPLAY_PER_PAGE': 25,
'DEFAULT_EXPIRED_YEARS': 70, 'DEFAULT_EXPIRED_YEARS': 70,
'SESSION_COOKIE_DOMAIN': None, 'SESSION_COOKIE_DOMAIN': None,
'CSRF_COOKIE_DOMAIN': None, 'CSRF_COOKIE_DOMAIN': None,
'SESSION_COOKIE_AGE': 3600 * 24, 'SESSION_COOKIE_AGE': 3600 * 24,
'SESSION_EXPIRE_AT_BROWSER_CLOSE': False,
'AUTH_OPENID': False, 'AUTH_OPENID': False,
'OTP_ISSUER_NAME': 'Jumpserver',
'EMAIL_SUFFIX': 'jumpserver.org',
'TERMINAL_PASSWORD_AUTH': True,
'TERMINAL_PUBLIC_KEY_AUTH': True,
'TERMINAL_HEARTBEAT_INTERVAL': 20,
'TERMINAL_ASSET_LIST_SORT_BY': 'hostname',
'TERMINAL_ASSET_LIST_PAGE_SIZE': 'auto',
'TERMINAL_SESSION_KEEP_DURATION': 9999,
'SECURITY_MFA_AUTH': False,
'SECURITY_LOGIN_LIMIT_COUNT': 7,
'SECURITY_LOGIN_LIMIT_TIME': 30,
'SECURITY_MAX_IDLE_TIME': 30,
'SECURITY_PASSWORD_EXPIRATION_TIME': 9999,
'SECURITY_PASSWORD_MIN_LENGTH': 6,
'SECURITY_PASSWORD_UPPER_CASE': False,
'SECURITY_PASSWORD_LOWER_CASE': False,
'SECURITY_PASSWORD_NUMBER': False,
'SECURITY_PASSWORD_SPECIAL_CHAR': False,
} }
......
...@@ -14,7 +14,7 @@ import os ...@@ -14,7 +14,7 @@ import os
import sys import sys
import ldap import ldap
from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion # from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
from django.urls import reverse_lazy from django.urls import reverse_lazy
from .conf import load_user_config from .conf import load_user_config
...@@ -146,6 +146,7 @@ LOGIN_URL = reverse_lazy('users:login') ...@@ -146,6 +146,7 @@ LOGIN_URL = reverse_lazy('users:login')
SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN
CSRF_COOKIE_DOMAIN = CONFIG.CSRF_COOKIE_DOMAIN CSRF_COOKIE_DOMAIN = CONFIG.CSRF_COOKIE_DOMAIN
SESSION_COOKIE_AGE = CONFIG.SESSION_COOKIE_AGE SESSION_COOKIE_AGE = CONFIG.SESSION_COOKIE_AGE
SESSION_EXPIRE_AT_BROWSER_CLOSE = CONFIG.SESSION_EXPIRE_AT_BROWSER_CLOSE
MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
# Database # Database
...@@ -353,6 +354,10 @@ AUTH_USER_MODEL = 'users.User' ...@@ -353,6 +354,10 @@ AUTH_USER_MODEL = 'users.User'
FILE_UPLOAD_PERMISSIONS = 0o644 FILE_UPLOAD_PERMISSIONS = 0o644
FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755 FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755
# OTP settings
OTP_ISSUER_NAME = CONFIG.OTP_ISSUER_NAME
OTP_VALID_WINDOW = CONFIG.OTP_VALID_WINDOW
# Auth LDAP settings # Auth LDAP settings
AUTH_LDAP = False AUTH_LDAP = False
AUTH_LDAP_SERVER_URI = 'ldap://localhost:389' AUTH_LDAP_SERVER_URI = 'ldap://localhost:389'
...@@ -362,22 +367,17 @@ AUTH_LDAP_SEARCH_OU = 'ou=tech,dc=jumpserver,dc=org' ...@@ -362,22 +367,17 @@ AUTH_LDAP_SEARCH_OU = 'ou=tech,dc=jumpserver,dc=org'
AUTH_LDAP_SEARCH_FILTER = '(cn=%(user)s)' AUTH_LDAP_SEARCH_FILTER = '(cn=%(user)s)'
AUTH_LDAP_START_TLS = False AUTH_LDAP_START_TLS = False
AUTH_LDAP_USER_ATTR_MAP = {"username": "cn", "name": "sn", "email": "mail"} AUTH_LDAP_USER_ATTR_MAP = {"username": "cn", "name": "sn", "email": "mail"}
AUTH_LDAP_USER_SEARCH_UNION = lambda: [ # AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU
LDAPSearch(USER_SEARCH, ldap.SCOPE_SUBTREE, AUTH_LDAP_SEARCH_FILTER) # AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER
for USER_SEARCH in str(AUTH_LDAP_SEARCH_OU).split("|") # AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
] # AUTH_LDAP_GROUP_SEARCH_OU, ldap.SCOPE_SUBTREE, AUTH_LDAP_GROUP_SEARCH_FILTER
AUTH_LDAP_USER_SEARCH = lambda: LDAPSearchUnion(*AUTH_LDAP_USER_SEARCH_UNION()) # )
AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU
AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
AUTH_LDAP_GROUP_SEARCH_OU, ldap.SCOPE_SUBTREE, AUTH_LDAP_GROUP_SEARCH_FILTER
)
AUTH_LDAP_CONNECTION_OPTIONS = { AUTH_LDAP_CONNECTION_OPTIONS = {
ldap.OPT_TIMEOUT: 5 ldap.OPT_TIMEOUT: 5
} }
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 1 AUTH_LDAP_GROUP_CACHE_TIMEOUT = 1
AUTH_LDAP_ALWAYS_UPDATE_USER = True AUTH_LDAP_ALWAYS_UPDATE_USER = True
AUTH_LDAP_BACKEND = 'django_auth_ldap.backend.LDAPBackend' AUTH_LDAP_BACKEND = 'authentication.ldap.backends.LDAPAuthorizationBackend'
if AUTH_LDAP: if AUTH_LDAP:
AUTHENTICATION_BACKENDS.insert(0, AUTH_LDAP_BACKEND) AUTHENTICATION_BACKENDS.insert(0, AUTH_LDAP_BACKEND)
...@@ -402,10 +402,10 @@ if AUTH_OPENID: ...@@ -402,10 +402,10 @@ if AUTH_OPENID:
# Celery using redis as broker # Celery using redis as broker
CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % { CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % {
'password': CONFIG.REDIS_PASSWORD if CONFIG.REDIS_PASSWORD else '', 'password': CONFIG.REDIS_PASSWORD,
'host': CONFIG.REDIS_HOST or '127.0.0.1', 'host': CONFIG.REDIS_HOST,
'port': CONFIG.REDIS_PORT or 6379, 'port': CONFIG.REDIS_PORT,
'db': CONFIG.REDIS_DB_CELERY_BROKER or 3, 'db': CONFIG.REDIS_DB_CELERY,
} }
CELERY_TASK_SERIALIZER = 'pickle' CELERY_TASK_SERIALIZER = 'pickle'
CELERY_RESULT_SERIALIZER = 'pickle' CELERY_RESULT_SERIALIZER = 'pickle'
...@@ -467,6 +467,7 @@ DEFAULT_TERMINAL_REPLAY_STORAGE = { ...@@ -467,6 +467,7 @@ DEFAULT_TERMINAL_REPLAY_STORAGE = {
TERMINAL_REPLAY_STORAGE = { TERMINAL_REPLAY_STORAGE = {
} }
SECURITY_MFA_AUTH = False SECURITY_MFA_AUTH = False
SECURITY_LOGIN_LIMIT_COUNT = 7 SECURITY_LOGIN_LIMIT_COUNT = 7
SECURITY_LOGIN_LIMIT_TIME = 30 # Unit: minute SECURITY_LOGIN_LIMIT_TIME = 30 # Unit: minute
...@@ -485,14 +486,22 @@ SECURITY_PASSWORD_RULES = [ ...@@ -485,14 +486,22 @@ SECURITY_PASSWORD_RULES = [
'SECURITY_PASSWORD_SPECIAL_CHAR' 'SECURITY_PASSWORD_SPECIAL_CHAR'
] ]
TERMINAL_PASSWORD_AUTH = CONFIG.TERMINAL_PASSWORD_AUTH
TERMINAL_PUBLIC_KEY_AUTH = CONFIG.TERMINAL_PUBLIC_KEY_AUTH
TERMINAL_HEARTBEAT_INTERVAL = CONFIG.TERMINAL_HEARTBEAT_INTERVAL
TERMINAL_ASSET_LIST_SORT_BY = CONFIG.TERMINAL_ASSET_LIST_SORT_BY
TERMINAL_ASSET_LIST_PAGE_SIZE = CONFIG.TERMINAL_ASSET_LIST_PAGE_SIZE
TERMINAL_SESSION_KEEP_DURATION = CONFIG.TERMINAL_SESSION_KEEP_DURATION
# Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html # Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html
BOOTSTRAP3 = { BOOTSTRAP3 = {
'horizontal_label_class': 'col-md-2', 'horizontal_label_class': 'col-md-2',
# Field class to use in horizontal forms # Field class to use in horizontal forms
'horizontal_field_class': 'col-md-9', 'horizontal_field_class': 'col-md-9',
# Set placeholder attributes to label if no placeholder is provided # Set placeholder attributes to label if no placeholder is provided
'set_placeholder': True, 'set_placeholder': False,
'success_css_class': '', 'success_css_class': '',
'required_css_class': 'required',
} }
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
...@@ -510,3 +519,5 @@ SWAGGER_SETTINGS = { ...@@ -510,3 +519,5 @@ SWAGGER_SETTINGS = {
}, },
} }
# Default email suffix
EMAIL_SUFFIX = CONFIG.EMAIL_SUFFIX
This source diff could not be displayed because it is too large. You can view the blob instead.
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
import sys import datetime
import json
from collections import defaultdict
from ansible import constants as C
from ansible.plugins.callback import CallbackBase from ansible.plugins.callback import CallbackBase
from ansible.plugins.callback.default import CallbackModule from ansible.plugins.callback.default import CallbackModule
from ansible.plugins.callback.minimal import CallbackModule as CMDCallBackModule
from .display import TeeObj
class CallbackMixin:
class AdHocResultCallback(CallbackModule): def __init__(self, display=None):
"""
Task result Callback
"""
def __init__(self, display=None, options=None, file_obj=None):
# result_raw example: { # result_raw example: {
# "ok": {"hostname": {"task_name": {},...},..}, # "ok": {"hostname": {"task_name": {},...},..},
# "failed": {"hostname": {"task_name": {}..}, ..}, # "failed": {"hostname": {"task_name": {}..}, ..},
...@@ -20,71 +19,138 @@ class AdHocResultCallback(CallbackModule): ...@@ -20,71 +19,138 @@ class AdHocResultCallback(CallbackModule):
# "skipped": {"hostname": {"task_name": {}, ..}, ..}, # "skipped": {"hostname": {"task_name": {}, ..}, ..},
# } # }
# results_summary example: { # results_summary example: {
# "contacted": {"hostname",...}, # "contacted": {"hostname": {"task_name": {}}, "hostname": {}},
# "dark": {"hostname": {"task_name": {}, "task_name": {}},...,}, # "dark": {"hostname": {"task_name": {}, "task_name": {}},...,},
# "success": True
# } # }
self.results_raw = dict(ok={}, failed={}, unreachable={}, skipped={}) self.results_raw = dict(
self.results_summary = dict(contacted=[], dark={}) ok=defaultdict(dict),
failed=defaultdict(dict),
unreachable=defaultdict(dict),
skippe=defaultdict(dict),
)
self.results_summary = dict(
contacted=defaultdict(dict),
dark=defaultdict(dict),
success=True
)
self.results = {
'raw': self.results_raw,
'summary': self.results_summary,
}
super().__init__() super().__init__()
if file_obj is not None: if display:
sys.stdout = TeeObj(file_obj) self._display = display
self._display.columns = 79
def gather_result(self, t, res): def display(self, msg):
self._clean_results(res._result, res._task.action) self._display.display(msg)
host = res._host.get_name()
task_name = res.task_name
task_result = res._result
if self.results_raw[t].get(host): def gather_result(self, t, result):
self.results_raw[t][host][task_name] = task_result self._clean_results(result._result, result._task.action)
else: host = result._host.get_name()
self.results_raw[t][host] = {task_name: task_result} task_name = result.task_name
task_result = result._result
self.results_raw[t][host][task_name] = task_result
self.clean_result(t, host, task_name, task_result) self.clean_result(t, host, task_name, task_result)
class AdHocResultCallback(CallbackMixin, CallbackModule, CMDCallBackModule):
"""
Task result Callback
"""
def clean_result(self, t, host, task_name, task_result): def clean_result(self, t, host, task_name, task_result):
contacted = self.results_summary["contacted"] contacted = self.results_summary["contacted"]
dark = self.results_summary["dark"] dark = self.results_summary["dark"]
if t in ("ok", "skipped") and host not in dark:
if host not in contacted: if task_result.get('rc') is not None:
contacted.append(host) cmd = task_result.get('cmd')
else: if isinstance(cmd, list):
if dark.get(host): cmd = " ".join(cmd)
dark[host][task_name] = task_result.values
else: else:
dark[host] = {task_name: task_result} cmd = str(cmd)
if host in contacted: detail = {
contacted.remove(host) 'cmd': cmd,
'stderr': task_result.get('stderr'),
'stdout': task_result.get('stdout'),
'rc': task_result.get('rc'),
'delta': task_result.get('delta'),
'msg': task_result.get('msg', '')
}
else:
detail = {
"changed": task_result.get('changed', False),
"msg": task_result.get('msg', '')
}
if t in ("ok", "skipped"):
contacted[host][task_name] = detail
else:
dark[host][task_name] = detail
def v2_runner_on_failed(self, result, ignore_errors=False): def v2_runner_on_failed(self, result, ignore_errors=False):
self.results_summary['success'] = False
self.gather_result("failed", result) self.gather_result("failed", result)
super().v2_runner_on_failed(result, ignore_errors=ignore_errors)
if result._task.action in C.MODULE_NO_JSON:
CMDCallBackModule.v2_runner_on_failed(self,
result, ignore_errors=ignore_errors
)
else:
super().v2_runner_on_failed(
result, ignore_errors=ignore_errors
)
def v2_runner_on_ok(self, result): def v2_runner_on_ok(self, result):
self.gather_result("ok", result) self.gather_result("ok", result)
super().v2_runner_on_ok(result) if result._task.action in C.MODULE_NO_JSON:
CMDCallBackModule.v2_runner_on_ok(self, result)
else:
super().v2_runner_on_ok(result)
def v2_runner_on_skipped(self, result): def v2_runner_on_skipped(self, result):
self.gather_result("skipped", result) self.gather_result("skipped", result)
super().v2_runner_on_skipped(result) super().v2_runner_on_skipped(result)
def v2_runner_on_unreachable(self, result): def v2_runner_on_unreachable(self, result):
self.results_summary['success'] = False
self.gather_result("unreachable", result) self.gather_result("unreachable", result)
super().v2_runner_on_unreachable(result) super().v2_runner_on_unreachable(result)
def on_playbook_start(self, name):
date_start = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.display(
"{} Start task: {}\r\n".format(date_start, name)
)
def on_playbook_end(self, name):
date_finished = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.display(
"{} Task finish\r\n".format(date_finished)
)
def display_skipped_hosts(self):
pass
def display_ok_hosts(self):
pass
class CommandResultCallback(AdHocResultCallback): class CommandResultCallback(AdHocResultCallback):
""" """
Command result callback Command result callback
results_command: {
"cmd": "",
"stderr": "",
"stdout": "",
"rc": 0,
"delta": 0:0:0.123
}
""" """
def __init__(self, display=None): def __init__(self, display=None, **kwargs):
# results_command: {
# "cmd": "",
# "stderr": "",
# "stdout": "",
# "rc": 0,
# "delta": 0:0:0.123
# }
#
self.results_command = dict() self.results_command = dict()
super().__init__(display) super().__init__(display)
...@@ -92,6 +158,43 @@ class CommandResultCallback(AdHocResultCallback): ...@@ -92,6 +158,43 @@ class CommandResultCallback(AdHocResultCallback):
super().gather_result(t, res) super().gather_result(t, res)
self.gather_cmd(t, res) self.gather_cmd(t, res)
def v2_playbook_on_play_start(self, play):
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
msg = '$ {} ({})'.format(play.name, now)
self._play = play
self._display.banner(msg)
def v2_runner_on_unreachable(self, result):
self.results_summary['success'] = False
self.gather_result("unreachable", result)
msg = result._result.get("msg")
if not msg:
msg = json.dumps(result._result, indent=4)
self._display.display("%s | FAILED! => \n%s" % (
result._host.get_name(),
msg,
), color=C.COLOR_ERROR)
def v2_runner_on_failed(self, result, ignore_errors=False):
self.results_summary['success'] = False
self.gather_result("failed", result)
msg = result._result.get("msg", '')
stderr = result._result.get("stderr")
if stderr:
msg += '\n' + stderr
module_stdout = result._result.get("module_stdout")
if module_stdout:
msg += '\n' + module_stdout
if not msg:
msg = json.dumps(result._result, indent=4)
self._display.display("%s | FAILED! => \n%s" % (
result._host.get_name(),
msg,
), color=C.COLOR_ERROR)
def _print_task_banner(self, task):
pass
def gather_cmd(self, t, res): def gather_cmd(self, t, res):
host = res._host.get_name() host = res._host.get_name()
cmd = {} cmd = {}
......
...@@ -17,7 +17,7 @@ from common.utils import get_logger ...@@ -17,7 +17,7 @@ from common.utils import get_logger
from .exceptions import AnsibleError from .exceptions import AnsibleError
__all__ = ["AdHocRunner", "PlayBookRunner"] __all__ = ["AdHocRunner", "PlayBookRunner", "CommandRunner"]
C.HOST_KEY_CHECKING = False C.HOST_KEY_CHECKING = False
logger = get_logger(__name__) logger = get_logger(__name__)
...@@ -45,7 +45,7 @@ def get_default_options(): ...@@ -45,7 +45,7 @@ def get_default_options():
listtasks=False, listtasks=False,
listhosts=False, listhosts=False,
syntax=False, syntax=False,
timeout=60, timeout=30,
connection='ssh', connection='ssh',
module_path='', module_path='',
forks=10, forks=10,
...@@ -135,6 +135,7 @@ class AdHocRunner: ...@@ -135,6 +135,7 @@ class AdHocRunner:
loader_class = DataLoader loader_class = DataLoader
variable_manager_class = VariableManager variable_manager_class = VariableManager
default_options = get_default_options() default_options = get_default_options()
command_modules_choices = ('shell', 'raw', 'command', 'script', 'win_shell')
def __init__(self, inventory, options=None): def __init__(self, inventory, options=None):
self.options = self.update_options(options) self.options = self.update_options(options)
...@@ -145,7 +146,7 @@ class AdHocRunner: ...@@ -145,7 +146,7 @@ class AdHocRunner:
) )
def get_result_callback(self, file_obj=None): def get_result_callback(self, file_obj=None):
return self.__class__.results_callback_class(file_obj=file_obj) return self.__class__.results_callback_class()
@staticmethod @staticmethod
def check_module_args(module_name, module_args=''): def check_module_args(module_name, module_args=''):
...@@ -163,10 +164,30 @@ class AdHocRunner: ...@@ -163,10 +164,30 @@ class AdHocRunner:
"pattern: %s dose not match any hosts." % pattern "pattern: %s dose not match any hosts." % pattern
) )
def clean_args(self, module, args):
if not args:
return ''
if module not in self.command_modules_choices:
return args
if isinstance(args, str):
if args.startswith('executable='):
_args = args.split(' ')
executable, command = _args[0].split('=')[1], ' '.join(_args[1:])
args = {'executable': executable, '_raw_params': command}
else:
args = {'_raw_params': args}
return args
else:
return args
def clean_tasks(self, tasks): def clean_tasks(self, tasks):
cleaned_tasks = [] cleaned_tasks = []
for task in tasks: for task in tasks:
self.check_module_args(task['action']['module'], task['action'].get('args')) module = task['action']['module']
args = task['action'].get('args')
cleaned_args = self.clean_args(module, args)
task['action']['args'] = cleaned_args
self.check_module_args(module, cleaned_args)
cleaned_tasks.append(task) cleaned_tasks.append(task)
return cleaned_tasks return cleaned_tasks
...@@ -177,17 +198,16 @@ class AdHocRunner: ...@@ -177,17 +198,16 @@ class AdHocRunner:
options = self.__class__.default_options options = self.__class__.default_options
return options return options
def run(self, tasks, pattern, play_name='Ansible Ad-hoc', gather_facts='no', file_obj=None): def run(self, tasks, pattern, play_name='Ansible Ad-hoc', gather_facts='no'):
""" """
:param tasks: [{'action': {'module': 'shell', 'args': 'ls'}, ...}, ] :param tasks: [{'action': {'module': 'shell', 'args': 'ls'}, ...}, ]
:param pattern: all, *, or others :param pattern: all, *, or others
:param play_name: The play name :param play_name: The play name
:param gather_facts: :param gather_facts:
:param file_obj: logging to file_obj
:return: :return:
""" """
self.check_pattern(pattern) self.check_pattern(pattern)
self.results_callback = self.get_result_callback(file_obj) self.results_callback = self.get_result_callback()
cleaned_tasks = self.clean_tasks(tasks) cleaned_tasks = self.clean_tasks(tasks)
play_source = dict( play_source = dict(
...@@ -211,10 +231,6 @@ class AdHocRunner: ...@@ -211,10 +231,6 @@ class AdHocRunner:
stdout_callback=self.results_callback, stdout_callback=self.results_callback,
passwords=self.options.passwords, passwords=self.options.passwords,
) )
print("Get matched hosts: {}".format(
self.inventory.get_matched_hosts(pattern)
))
try: try:
tqm.run(play) tqm.run(play)
return self.results_callback return self.results_callback
...@@ -229,16 +245,12 @@ class CommandRunner(AdHocRunner): ...@@ -229,16 +245,12 @@ class CommandRunner(AdHocRunner):
results_callback_class = CommandResultCallback results_callback_class = CommandResultCallback
modules_choices = ('shell', 'raw', 'command', 'script') modules_choices = ('shell', 'raw', 'command', 'script')
def execute(self, cmd, pattern, module=None): def execute(self, cmd, pattern, module='shell'):
if module and module not in self.modules_choices: if module and module not in self.modules_choices:
raise AnsibleError("Module should in {}".format(self.modules_choices)) raise AnsibleError("Module should in {}".format(self.modules_choices))
else:
module = "shell"
tasks = [ tasks = [
{"action": {"module": module, "args": cmd}} {"action": {"module": module, "args": cmd}}
] ]
hosts = self.inventory.get_hosts(pattern=pattern) return self.run(tasks, pattern, play_name=cmd)
name = "Run command {} on {}".format(cmd, ", ".join([host.name for host in hosts]))
return self.run(tasks, pattern, play_name=name)
# -*- coding: utf-8 -*-
#
from .adhoc import *
from .celery import *
from .command import *
# ~*~ coding: utf-8 ~*~ # -*- coding: utf-8 -*-
import uuid #
import os
from django.core.cache import cache
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _
from rest_framework import viewsets, generics from rest_framework import viewsets, generics
from rest_framework.views import Response from rest_framework.views import Response
from common.permissions import IsOrgAdmin from common.permissions import IsOrgAdmin
from .models import Task, AdHoc, AdHocRunHistory, CeleryTask from orgs.utils import current_org
from .serializers import TaskSerializer, AdHocSerializer, \ from ..models import Task, AdHoc, AdHocRunHistory
from ..serializers import TaskSerializer, AdHocSerializer, \
AdHocRunHistorySerializer AdHocRunHistorySerializer
from .tasks import run_ansible_task from ..tasks import run_ansible_task
__all__ = [
'TaskViewSet', 'TaskRun', 'AdHocViewSet', 'AdHocRunHistoryViewSet'
]
class TaskViewSet(viewsets.ModelViewSet): class TaskViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all() queryset = Task.objects.all()
serializer_class = TaskSerializer serializer_class = TaskSerializer
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
# label = None
# help_text = '' def get_queryset(self):
queryset = super().get_queryset()
if current_org.is_real():
queryset = queryset.filter(created_by=current_org.id)
else:
queryset = queryset.filter(created_by='')
return queryset
class TaskRun(generics.RetrieveAPIView): class TaskRun(generics.RetrieveAPIView):
...@@ -47,7 +55,7 @@ class AdHocViewSet(viewsets.ModelViewSet): ...@@ -47,7 +55,7 @@ class AdHocViewSet(viewsets.ModelViewSet):
return self.queryset return self.queryset
class AdHocRunHistorySet(viewsets.ModelViewSet): class AdHocRunHistoryViewSet(viewsets.ModelViewSet):
queryset = AdHocRunHistory.objects.all() queryset = AdHocRunHistory.objects.all()
serializer_class = AdHocRunHistorySerializer serializer_class = AdHocRunHistorySerializer
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
...@@ -66,28 +74,6 @@ class AdHocRunHistorySet(viewsets.ModelViewSet): ...@@ -66,28 +74,6 @@ class AdHocRunHistorySet(viewsets.ModelViewSet):
return self.queryset return self.queryset
class CeleryTaskLogApi(generics.RetrieveAPIView):
permission_classes = (IsOrgAdmin,)
buff_size = 1024 * 10
end = False
queryset = CeleryTask.objects.all()
def get(self, request, *args, **kwargs):
mark = request.query_params.get("mark") or str(uuid.uuid4())
task = self.get_object()
log_path = task.full_log_path
if not log_path or not os.path.isfile(log_path):
return Response({"data": _("Waiting ...")}, status=203)
with open(log_path, 'r') as f:
offset = cache.get(mark, 0)
f.seek(offset)
data = f.read(self.buff_size).replace('\n', '\r\n')
mark = str(uuid.uuid4())
cache.set(mark, f.tell(), 5)
if data == '' and task.is_finished():
self.end = True
return Response({"data": data, 'end': self.end, 'mark': mark})
# -*- coding: utf-8 -*-
#
import uuid
import os
from celery.result import AsyncResult
from django.core.cache import cache
from django.utils.translation import ugettext as _
from rest_framework import generics
from rest_framework.views import Response
from common.permissions import IsOrgAdmin, IsValidUser
from ..models import CeleryTask
from ..serializers import CeleryResultSerializer
__all__ = ['CeleryTaskLogApi', 'CeleryResultApi']
class CeleryTaskLogApi(generics.RetrieveAPIView):
permission_classes = (IsValidUser,)
buff_size = 1024 * 10
end = False
queryset = CeleryTask.objects.all()
def get(self, request, *args, **kwargs):
mark = request.query_params.get("mark") or str(uuid.uuid4())
task = self.get_object()
log_path = task.full_log_path
if not log_path or not os.path.isfile(log_path):
return Response({"data": _("Waiting ...")}, status=203)
with open(log_path, 'r') as f:
offset = cache.get(mark, 0)
f.seek(offset)
data = f.read(self.buff_size).replace('\n', '\r\n')
mark = str(uuid.uuid4())
cache.set(mark, f.tell(), 5)
if data == '' and task.is_finished():
self.end = True
return Response({"data": data, 'end': self.end, 'mark': mark})
class CeleryResultApi(generics.RetrieveAPIView):
permission_classes = (IsValidUser,)
serializer_class = CeleryResultSerializer
def get_object(self):
pk = self.kwargs.get('pk')
return AsyncResult(pk)
# -*- coding: utf-8 -*-
#
from rest_framework import viewsets
from common.permissions import IsValidUser
from ..models import CommandExecution
from ..serializers import CommandExecutionSerializer
from ..tasks import run_command_execution
class CommandExecutionViewSet(viewsets.ModelViewSet):
serializer_class = CommandExecutionSerializer
permission_classes = (IsValidUser,)
task = None
def get_queryset(self):
return CommandExecution.objects.filter(
user_id=str(self.request.user.id)
)
def perform_create(self, serializer):
instance = serializer.save()
instance.user = self.request.user
instance.save()
run_command_execution.apply_async(
args=(instance.id,), task_id=str(instance.id)
)
...@@ -27,7 +27,6 @@ def on_app_ready(sender=None, headers=None, body=None, **kwargs): ...@@ -27,7 +27,6 @@ def on_app_ready(sender=None, headers=None, body=None, **kwargs):
if cache.get("CELERY_APP_READY", 0) == 1: if cache.get("CELERY_APP_READY", 0) == 1:
return return
cache.set("CELERY_APP_READY", 1, 10) cache.set("CELERY_APP_READY", 1, 10)
logger.debug("App ready signal recv")
tasks = get_after_app_ready_tasks() tasks = get_after_app_ready_tasks()
logger.debug("Start need start task: [{}]".format( logger.debug("Start need start task: [{}]".format(
", ".join(tasks)) ", ".join(tasks))
......
# -*- coding: utf-8 -*-
#
from django import forms
from assets.models import SystemUser
from .models import CommandExecution
class CommandExecutionForm(forms.ModelForm):
class Meta:
model = CommandExecution
fields = ['run_as', 'command']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
run_as_field = self.fields.get('run_as')
run_as_field.queryset = SystemUser.objects.all()
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
# #
from .ansible.inventory import BaseInventory from .ansible.inventory import BaseInventory
from assets.utils import get_assets_by_fullname_list, get_system_user_by_name from assets.utils import get_assets_by_id_list, get_system_user_by_id
__all__ = [ __all__ = [
'JMSInventory' 'JMSInventory'
...@@ -14,19 +14,18 @@ class JMSInventory(BaseInventory): ...@@ -14,19 +14,18 @@ class JMSInventory(BaseInventory):
JMS Inventory is the manager with jumpserver assets, so you can JMS Inventory is the manager with jumpserver assets, so you can
write you own manager, construct you inventory write you own manager, construct you inventory
""" """
def __init__(self, hostname_list, run_as_admin=False, run_as=None, become_info=None): def __init__(self, assets, run_as_admin=False, run_as=None, become_info=None):
""" """
:param hostname_list: ["test1", ] :param host_id_list: ["test1", ]
:param run_as_admin: True 是否使用管理用户去执行, 每台服务器的管理用户可能不同 :param run_as_admin: True 是否使用管理用户去执行, 每台服务器的管理用户可能不同
:param run_as: 是否统一使用某个系统用户去执行 :param run_as: 是否统一使用某个系统用户去执行
:param become_info: 是否become成某个用户去执行 :param become_info: 是否become成某个用户去执行
""" """
self.hostname_list = hostname_list self.assets = assets
self.using_admin = run_as_admin self.using_admin = run_as_admin
self.run_as = run_as self.run_as = run_as
self.become_info = become_info self.become_info = become_info
assets = self.get_jms_assets()
host_list = [] host_list = []
for asset in assets: for asset in assets:
...@@ -43,14 +42,10 @@ class JMSInventory(BaseInventory): ...@@ -43,14 +42,10 @@ class JMSInventory(BaseInventory):
host.update(become_info) host.update(become_info)
super().__init__(host_list=host_list) super().__init__(host_list=host_list)
def get_jms_assets(self):
assets = get_assets_by_fullname_list(self.hostname_list)
return assets
def convert_to_ansible(self, asset, run_as_admin=False): def convert_to_ansible(self, asset, run_as_admin=False):
info = { info = {
'id': asset.id, 'id': asset.id,
'hostname': asset.fullname, 'hostname': asset.hostname,
'ip': asset.ip, 'ip': asset.ip,
'port': asset.port, 'port': asset.port,
'vars': dict(), 'vars': dict(),
...@@ -75,7 +70,7 @@ class JMSInventory(BaseInventory): ...@@ -75,7 +70,7 @@ class JMSInventory(BaseInventory):
return info return info
def get_run_user_info(self): def get_run_user_info(self):
system_user = get_system_user_by_name(self.run_as) system_user = self.run_as
if not system_user: if not system_user:
return {} return {}
else: else:
......
# Generated by Django 2.1.4 on 2018-12-07 09:44
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0023_auto_20181016_1650'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('ops', '0002_celerytask'),
]
operations = [
migrations.CreateModel(
name='CommandExecution',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('command', models.TextField(verbose_name='Command')),
('_result', models.TextField(blank=True, null=True, verbose_name='Result')),
('is_finished', models.BooleanField(default=False)),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_start', models.DateTimeField(null=True)),
('date_finished', models.DateTimeField(null=True)),
('hosts', models.ManyToManyField(to='assets.Asset')),
('run_as', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.SystemUser')),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.RemoveField(
model_name='adhoc',
name='run_as',
),
migrations.AddField(
model_name='adhoc',
name='hosts',
field=models.ManyToManyField(to='assets.Asset', verbose_name='Host'),
),
migrations.AlterField(
model_name='task',
name='created_by',
field=models.CharField(blank=True, default='', max_length=128),
),
migrations.AlterField(
model_name='task',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterUniqueTogether(
name='task',
unique_together={('name', 'created_by')},
),
]
# Generated by Django 2.1.4 on 2018-12-07 09:44
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0023_auto_20181016_1650'),
('ops', '0003_auto_20181207_1744'),
]
operations = [
migrations.AddField(
model_name='adhoc',
name='run_as',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.SystemUser'),
),
]
# Generated by Django 2.1 on 2018-12-19 10:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ops', '0004_adhoc_run_as'),
]
operations = [
migrations.AlterField(
model_name='task',
name='date_created',
field=models.DateTimeField(auto_now_add=True, db_index=True),
),
migrations.AlterField(
model_name='adhoc',
name='date_created',
field=models.DateTimeField(auto_now_add=True, db_index=True),
),
migrations.AlterField(
model_name='celerytask',
name='status',
field=models.CharField(
choices=[('waiting', 'waiting'), ('running', 'running'),
('finished', 'finished')], db_index=True,
max_length=128),
),
]
...@@ -2,4 +2,5 @@ ...@@ -2,4 +2,5 @@
# #
from .adhoc import * from .adhoc import *
from .celery import * from .celery import *
\ No newline at end of file from .command import *
...@@ -34,16 +34,17 @@ class Task(models.Model): ...@@ -34,16 +34,17 @@ class Task(models.Model):
One task can have some versions of adhoc, run a task only run the latest version adhoc One task can have some versions of adhoc, run a task only run the latest version adhoc
""" """
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name')) name = models.CharField(max_length=128, verbose_name=_('Name'))
interval = models.IntegerField(verbose_name=_("Interval"), null=True, blank=True, help_text=_("Units: seconds")) interval = models.IntegerField(verbose_name=_("Interval"), null=True, blank=True, help_text=_("Units: seconds"))
crontab = models.CharField(verbose_name=_("Crontab"), null=True, blank=True, max_length=128, help_text=_("5 * * * *")) crontab = models.CharField(verbose_name=_("Crontab"), null=True, blank=True, max_length=128, help_text=_("5 * * * *"))
is_periodic = models.BooleanField(default=False) is_periodic = models.BooleanField(default=False)
callback = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Callback")) # Callback must be a registered celery task callback = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Callback")) # Callback must be a registered celery task
is_deleted = models.BooleanField(default=False) is_deleted = models.BooleanField(default=False)
comment = models.TextField(blank=True, verbose_name=_("Comment")) comment = models.TextField(blank=True, verbose_name=_("Comment"))
created_by = models.CharField(max_length=128, blank=True, null=True, default='') created_by = models.CharField(max_length=128, blank=True, default='')
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True, db_index=True)
__latest_adhoc = None __latest_adhoc = None
_ignore_auto_created_by = True
@property @property
def short_id(self): def short_id(self):
...@@ -94,7 +95,7 @@ class Task(models.Model): ...@@ -94,7 +95,7 @@ class Task(models.Model):
update_fields=None): update_fields=None):
from ..tasks import run_ansible_task from ..tasks import run_ansible_task
super().save( super().save(
force_insert=force_insert, force_update=force_update, force_insert=force_insert, force_update=force_update,
using=using, update_fields=update_fields, using=using, update_fields=update_fields,
) )
...@@ -108,7 +109,7 @@ class Task(models.Model): ...@@ -108,7 +109,7 @@ class Task(models.Model):
crontab = self.crontab crontab = self.crontab
tasks = { tasks = {
self.name: { self.__str__(): {
"task": run_ansible_task.name, "task": run_ansible_task.name,
"interval": interval, "interval": interval,
"crontab": crontab, "crontab": crontab,
...@@ -119,11 +120,11 @@ class Task(models.Model): ...@@ -119,11 +120,11 @@ class Task(models.Model):
} }
create_or_update_celery_periodic_tasks(tasks) create_or_update_celery_periodic_tasks(tasks)
else: else:
disable_celery_periodic_task(self.name) disable_celery_periodic_task(self.__str__())
def delete(self, using=None, keep_parents=False): def delete(self, using=None, keep_parents=False):
super().delete(using=using, keep_parents=keep_parents) super().delete(using=using, keep_parents=keep_parents)
delete_celery_periodic_task(self.name) delete_celery_periodic_task(self.__str__())
@property @property
def schedule(self): def schedule(self):
...@@ -133,10 +134,11 @@ class Task(models.Model): ...@@ -133,10 +134,11 @@ class Task(models.Model):
return None return None
def __str__(self): def __str__(self):
return self.name return self.name + '@' + str(self.created_by)
class Meta: class Meta:
db_table = 'ops_task' db_table = 'ops_task'
unique_together = ('name', 'created_by')
get_latest_by = 'date_created' get_latest_by = 'date_created'
...@@ -157,15 +159,19 @@ class AdHoc(models.Model): ...@@ -157,15 +159,19 @@ class AdHoc(models.Model):
pattern = models.CharField(max_length=64, default='{}', verbose_name=_('Pattern')) pattern = models.CharField(max_length=64, default='{}', verbose_name=_('Pattern'))
_options = models.CharField(max_length=1024, default='', verbose_name=_('Options')) _options = models.CharField(max_length=1024, default='', verbose_name=_('Options'))
_hosts = models.TextField(blank=True, verbose_name=_('Hosts')) # ['hostname1', 'hostname2'] _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_admin = models.BooleanField(default=False, verbose_name=_('Run as admin'))
run_as = models.CharField(max_length=128, default='', verbose_name=_("Run as")) run_as = models.ForeignKey('assets.SystemUser', null=True, on_delete=models.CASCADE)
_become = models.CharField(max_length=1024, default='', verbose_name=_("Become")) _become = models.CharField(max_length=1024, default='', verbose_name=_("Become"))
created_by = models.CharField(max_length=64, default='', null=True, verbose_name=_('Create by')) created_by = models.CharField(max_length=64, default='', null=True, verbose_name=_('Create by'))
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True, db_index=True)
@property @property
def tasks(self): def tasks(self):
return json.loads(self._tasks) try:
return json.loads(self._tasks)
except:
return []
@tasks.setter @tasks.setter
def tasks(self, item): def tasks(self, item):
...@@ -174,14 +180,6 @@ class AdHoc(models.Model): ...@@ -174,14 +180,6 @@ class AdHoc(models.Model):
else: else:
raise SyntaxError('Tasks should be a list: {}'.format(item)) raise SyntaxError('Tasks should be a list: {}'.format(item))
@property
def hosts(self):
return json.loads(self._hosts)
@hosts.setter
def hosts(self, item):
self._hosts = json.dumps(item)
@property @property
def inventory(self): def inventory(self):
if self.become: if self.become:
...@@ -194,7 +192,7 @@ class AdHoc(models.Model): ...@@ -194,7 +192,7 @@ class AdHoc(models.Model):
become_info = None become_info = None
inventory = JMSInventory( inventory = JMSInventory(
self.hosts, run_as_admin=self.run_as_admin, self.hosts.all(), run_as_admin=self.run_as_admin,
run_as=self.run_as, become_info=become_info run_as=self.run_as, become_info=become_info
) )
return inventory return inventory
...@@ -242,14 +240,13 @@ class AdHoc(models.Model): ...@@ -242,14 +240,13 @@ class AdHoc(models.Model):
history.timedelta = time.time() - time_start history.timedelta = time.time() - time_start
history.save() history.save()
def _run_only(self, file_obj=None): def _run_only(self):
runner = AdHocRunner(self.inventory, options=self.options) runner = AdHocRunner(self.inventory, options=self.options)
try: try:
result = runner.run( result = runner.run(
self.tasks, self.tasks,
self.pattern, self.pattern,
self.task.name, self.task.name,
file_obj=file_obj,
) )
return result.results_raw, result.results_summary return result.results_raw, result.results_summary
except AnsibleError as e: except AnsibleError as e:
......
...@@ -19,7 +19,7 @@ class CeleryTask(models.Model): ...@@ -19,7 +19,7 @@ class CeleryTask(models.Model):
) )
id = models.UUIDField(primary_key=True, default=uuid.uuid4) id = models.UUIDField(primary_key=True, default=uuid.uuid4)
name = models.CharField(max_length=1024) name = models.CharField(max_length=1024)
status = models.CharField(max_length=128, choices=STATUS_CHOICES) status = models.CharField(max_length=128, choices=STATUS_CHOICES, db_index=True)
log_path = models.CharField(max_length=256, blank=True, null=True) log_path = models.CharField(max_length=256, blank=True, null=True)
date_published = models.DateTimeField(auto_now_add=True) date_published = models.DateTimeField(auto_now_add=True)
date_start = models.DateTimeField(null=True) date_start = models.DateTimeField(null=True)
......
# -*- coding: utf-8 -*-
#
import uuid
import json
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.db import models
from ..ansible.runner import CommandRunner
from ..inventory import JMSInventory
class CommandExecution(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
hosts = models.ManyToManyField('assets.Asset')
run_as = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE)
command = models.TextField(verbose_name=_("Command"))
_result = models.TextField(blank=True, null=True, verbose_name=_('Result'))
user = models.ForeignKey('users.User', on_delete=models.CASCADE, null=True)
is_finished = models.BooleanField(default=False)
date_created = models.DateTimeField(auto_now_add=True)
date_start = models.DateTimeField(null=True)
date_finished = models.DateTimeField(null=True)
def __str__(self):
return self.command[:10]
@property
def inventory(self):
return JMSInventory(self.hosts.all(), run_as=self.run_as)
@property
def result(self):
if self._result:
return json.loads(self._result)
else:
return {}
@result.setter
def result(self, item):
self._result = json.dumps(item)
@property
def is_success(self):
if 'error' in self.result:
return False
return True
def get_hosts_names(self):
return ','.join(self.hosts.all().values_list('hostname', flat=True))
def run(self):
print('-'*10 + ' ' + ugettext('Task start') + ' ' + '-'*10)
self.date_start = timezone.now()
ok, msg = self.run_as.is_command_can_run(self.command)
if ok:
runner = CommandRunner(self.inventory)
try:
result = runner.execute(self.command, 'all')
self.result = result.results_command
except Exception as e:
print("Error occur: {}".format(e))
self.result = {"error": str(e)}
else:
msg = _("Command `{}` is forbidden ........").format(self.command)
print('\033[31m' + msg + '\033[0m')
self.result = {"error": msg}
self.is_finished = True
self.date_finished = timezone.now()
self.save()
print('-'*10 + ' ' + ugettext('Task end') + ' ' + '-'*10)
return self.result
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework import serializers from rest_framework import serializers
from django.shortcuts import reverse
from .models import Task, AdHoc, AdHocRunHistory from .models import Task, AdHoc, AdHocRunHistory, CommandExecution
class CeleryResultSerializer(serializers.Serializer):
id = serializers.UUIDField()
result = serializers.JSONField()
state = serializers.CharField(max_length=16)
class CeleryTaskSerializer(serializers.Serializer):
pass
class TaskSerializer(serializers.ModelSerializer): class TaskSerializer(serializers.ModelSerializer):
...@@ -42,7 +53,7 @@ class AdHocRunHistorySerializer(serializers.ModelSerializer): ...@@ -42,7 +53,7 @@ class AdHocRunHistorySerializer(serializers.ModelSerializer):
@staticmethod @staticmethod
def get_stat(obj): def get_stat(obj):
return { return {
"total": len(obj.adhoc.hosts), "total": obj.adhoc.hosts.count(),
"success": len(obj.summary.get("contacted", [])), "success": len(obj.summary.get("contacted", [])),
"failed": len(obj.summary.get("dark", [])), "failed": len(obj.summary.get("dark", [])),
} }
...@@ -51,3 +62,23 @@ class AdHocRunHistorySerializer(serializers.ModelSerializer): ...@@ -51,3 +62,23 @@ class AdHocRunHistorySerializer(serializers.ModelSerializer):
fields = super().get_field_names(declared_fields, info) fields = super().get_field_names(declared_fields, info)
fields.extend(['summary', 'short_id']) fields.extend(['summary', 'short_id'])
return fields return fields
class CommandExecutionSerializer(serializers.ModelSerializer):
result = serializers.JSONField(read_only=True)
log_url = serializers.SerializerMethodField()
class Meta:
model = CommandExecution
fields = [
'id', 'hosts', 'run_as', 'command', 'result', 'log_url',
'is_finished', 'date_created', 'date_finished'
]
read_only_fields = [
'id', 'result', 'is_finished', 'log_url', 'date_created',
'date_finished'
]
@staticmethod
def get_log_url(obj):
return reverse('api-ops:celery-task-log', kwargs={'pk': obj.id})
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
from celery import shared_task, subtask from celery import shared_task, subtask
from common.utils import get_logger, get_object_or_none from common.utils import get_logger, get_object_or_none
from .models import Task from .celery.utils import register_as_period_task, after_app_shutdown_clean
from .models import Task, CommandExecution
logger = get_logger(__file__) logger = get_logger(__file__)
...@@ -28,6 +29,25 @@ def run_ansible_task(tid, callback=None, **kwargs): ...@@ -28,6 +29,25 @@ def run_ansible_task(tid, callback=None, **kwargs):
logger.error("No task found") logger.error("No task found")
@shared_task
def run_command_execution(cid, **kwargs):
execution = get_object_or_none(CommandExecution, id=cid)
return execution.run()
@shared_task
@register_as_period_task(interval=3600*24)
@after_app_shutdown_clean
def clean_tasks_adhoc_period():
logger.debug("Start clean task adhoc and run history")
tasks = Task.objects.all()
for task in tasks:
adhoc = task.adhoc.all().order_by('-date_created')[5:]
for ad in adhoc:
ad.history.all().delete()
ad.delete()
@shared_task @shared_task
def hello(name, callback=None): def hello(name, callback=None):
print("Hello {}".format(name)) print("Hello {}".format(name))
......
...@@ -186,6 +186,19 @@ ...@@ -186,6 +186,19 @@
</div> </div>
</div> </div>
</div> </div>
{% include 'users/_user_update_pk_modal.html' %} {% endblock %}
{% block custom_foot_js %}
<script>
$(document).ready(function () {
}).on('click', '.celery-task-log', function () {
var history_pk = "{{ object.latest_history.pk }}";
if (!history_pk) {
alert("没有运行历史");
return
}
var url = '{% url 'ops:celery-task-log' pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', history_pk);
window.open(url, '', 'width=800,height=600,left=400,top=400')
})
</script>
{% endblock %} {% endblock %}
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
<a href="{% url 'ops:adhoc-history-detail' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history detail' %} </a> <a href="{% url 'ops:adhoc-history-detail' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history detail' %} </a>
</li> </li>
<li> <li>
<a class="text-center celery-task-log" onclick="window.open('{% url 'ops:celery-task-log' pk=object.pk %}','', 'width=800,height=600,left=400,top=400')"><i class="fa fa-laptop"></i> {% trans 'Output' %} </a> <a class="text-center celery-task-log" ><i class="fa fa-laptop"></i> {% trans 'Output' %} </a>
</li> </li>
</ul> </ul>
</div> </div>
...@@ -141,4 +141,14 @@ ...@@ -141,4 +141,14 @@
</div> </div>
{% include 'users/_user_update_pk_modal.html' %} {% include 'users/_user_update_pk_modal.html' %}
{% endblock %} {% endblock %}
{% block custom_foot_js %}
<script>
$(document).ready(function () {
}).on('click', '.celery-task-log', function () {
var url = '{% url 'ops:celery-task-log' pk=object.pk %}';
window.open(url, '', 'width=800,height=600,left=400,top=400')
})
</script>
{% endblock %}
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
var end = false; var end = false;
var error = false; var error = false;
var interval = 200; var interval = 200;
var success = true;
function calWinSize() { function calWinSize() {
var t = $('#marker'); var t = $('#marker');
...@@ -34,20 +35,19 @@ ...@@ -34,20 +35,19 @@
{#colWidth = 1.00 * t.width() / 6;#} {#colWidth = 1.00 * t.width() / 6;#}
} }
function resize() { function resize() {
{#console.log(rowHeight, window.innerHeight);#}
{#console.log(colWidth, window.innerWidth);#}
var rows = Math.floor(window.innerHeight / rowHeight) - 1; var rows = Math.floor(window.innerHeight / rowHeight) - 1;
var cols = Math.floor(window.innerWidth / colWidth) - 2; var cols = Math.floor(window.innerWidth / colWidth) - 2;
console.log(rows, cols);
term.resize(cols, rows); term.resize(cols, rows);
} }
function requestAndWrite() { function requestAndWrite() {
if (!end) { if (!end && success) {
success = false;
$.ajax({ $.ajax({
url: url + '?mark=' + mark, url: url + '?mark=' + mark,
method: "GET", method: "GET",
contentType: "application/json; charset=utf-8" contentType: "application/json; charset=utf-8"
}).done(function(data, textStatue, jqXHR) { }).done(function(data, textStatue, jqXHR) {
success = true;
if (jqXHR.status === 203) { if (jqXHR.status === 203) {
error = true; error = true;
term.write('.'); term.write('.');
...@@ -64,7 +64,14 @@ ...@@ -64,7 +64,14 @@
} }
} }
$(document).ready(function () { $(document).ready(function () {
term = new Terminal(); term = new Terminal({
cursorBlink: false,
screenKeys: false,
fontFamily: '"Monaco", "Consolas", "monospace"',
fontSize: 12,
rightClickSelectsWord: true,
disableStdin: true
});
term.open(document.getElementById('term')); term.open(document.getElementById('term'));
term.resize(80, 24); term.resize(80, 24);
resize(); resize();
......
{% extends 'base.html' %}
{% load static %}
{% load i18n %}
{% load bootstrap3 %}
{% block custom_head_css_js %}
<link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.exhide.min.js' %}"></script>
<script src="{% static 'js/jquery.form.min.js' %}"></script>
<script src="{% static 'js/plugins/xterm/xterm.js' %}"></script>
<link rel="stylesheet" href="{% static 'js/plugins/xterm/xterm.css' %}" />
<script src="{% static 'js/plugins/xterm/addons/fit/fit.js' %}"></script>
<script src="{% static 'js/plugins/codemirror/codemirror.js' %}"></script>
<script src="{% static 'js/plugins/codemirror/mode/shell/shell.js' %}"></script>
<link href="{% static 'css/plugins/codemirror/codemirror.css' %}" rel="stylesheet">
<link href="{% static 'css/plugins/codemirror/ambiance.css' %}" rel="stylesheet">
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
<style type="text/css">
.xterm .xterm-screen canvas {
position: absolute;
left: 0;
top: 0;
padding: 10px;
}
.select2-container .select2-selection--single {
height: 34px;
}
</style>
{% endblock %}
{% block content %}
<div class="wrapper wrapper-content">
<div class="row">
<div class="col-lg-3" id="split-left" style="padding-left: 3px">
<div class="ibox float-e-margins">
<div class="ibox-content mailbox-content" style="padding-top: 0;padding-left: 1px">
<div class="file-manager ">
<div id="assetTree" class="ztree"></div>
<div class="clearfix"></div>
</div>
</div>
</div>
</div>
<div class="col-lg-9 animated fadeInRight" id="split-right">
<div class="tree-toggle">
<div class="btn btn-sm btn-primary tree-toggle-btn" onclick="toggle()">
<i class="fa fa-angle-left fa-x" id="toggle-icon"></i>
</div>
</div>
<div class="mail-box-header" style="padding-top: 5px;">
<form enctype="multipart/form-data" method="post" class="form-horizontal" action="" onsubmit="return execute()">
<div class="form-group">
<div id="term" style="height: 100%;width: 100%"></div>
</div>
<div class="row">
<div class="col-lg-10">
<div class="input-group" style="height: 100%; width: 100%">
<textarea class="form-control" id="command-text"></textarea>
</div>
</div>
<div class="col-lg-2">
<select class="select2 form-control" id="system-users-select">
{% for s in system_users %}
{% if s.protocol == 'ssh' and s.login_mode == 'auto' %}
<option value="{{ s.id }}">{{ s }}</option>
{% endif %}
{% endfor %}
</select>
<button type="button" class="btn btn-primary btn-execute" style="margin-top: 30px; width: 100%">{% trans 'Go' %}</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
var zTree, show = 0;
var systemUserId = null;
function initTree() {
var setting = {
check: {
enable: true
},
view: {
dblClickExpand: false,
showLine: true
},
data: {
simpleData: {
enable: true
}
},
edit: {
enable: true,
showRemoveBtn: false,
showRenameBtn: false,
drag: {
isCopy: true,
isMove: true
}
},
callback: {
onCheck: onCheck
}
};
var url = "{% url 'api-perms:my-nodes-assets-as-tree' %}";
if (systemUserId) {
url += '?system_user=' + systemUserId
}
$.get(url, function(data, status){
$.fn.zTree.init($("#assetTree"), setting, data);
zTree = $.fn.zTree.getZTreeObj("assetTree");
});
}
function getSelectedAssetsNode() {
var nodes = zTree.getCheckedNodes(true);
var assetsNodeId = [];
var assetsNode = [];
nodes.forEach(function (node) {
if (node.meta.type === 'asset' && !node.isHidden && node.meta.asset.protocol === 'ssh') {
if (assetsNodeId.indexOf(node.id) === -1) {
assetsNodeId.push(node.id);
assetsNode.push(node)
}
}
});
return assetsNode;
}
function onCheck(e, treeId, treeNode) {
var nodes = getSelectedAssetsNode();
var nodes_names = nodes.map(function (node) {
return node.name;
});
var message = "已选择资产: ";
message += nodes_names.join(", ");
message += "\r\n";
message += "总共: " + nodes_names.length + "个\r\n";
term.clear();
term.write(message)
}
function toggle() {
if (show === 0) {
$("#split-left").hide(500, function () {
$("#split-right").attr("class", "col-lg-12");
$("#toggle-icon").attr("class", "fa fa-angle-right fa-x");
show = 1;
});
} else {
$("#split-right").attr("class", "col-lg-9");
$("#toggle-icon").attr("class", "fa fa-angle-left fa-x");
$("#split-left").show(500);
show = 0;
}
}
var term = null;
function initResultTerminal() {
term = new Terminal({
cursorBlink: false,
screenKeys: false,
fontFamily: '"Monaco", "Consolas", "monospace"',
fontSize: 13,
rightClickSelectsWord: true,
disableStdin: true,
theme: {
background: '#1f1b1b'
}
});
term.open(document.getElementById('term'));
term.write("选择左侧资产, 选择运行的系统用户,批量执行命令\r\n")
}
function wrapperError(msg) {
return '\033[31m' + msg + '\033[0m' + '\r\n';
}
function execute() {
if (!term) {
initResultTerminal()
}
var url = '{% url "api-ops:command-execution-list" %}';
var run_as = systemUserId;
var command = editor.getValue();
var hosts = getSelectedAssetsNode().map(function (node) {
return node.id;
});
if (hosts.length === 0) {
term.write(wrapperError('没有选中资产'));
return
}
if (!command) {
term.write(wrapperError('没有输入命令'));
return
}
if (!run_as) {
term.write(wrapperError('没有选择运行用户'));
return
}
var data = {
hosts: hosts,
run_as: run_as,
command: command
};
var mark = '';
var log_url = null;
var end = false;
var error = false;
var int = null;
var interval = 200;
function writeExecutionOutput() {
if (!end) {
$.ajax({
url: log_url + '?mark=' + mark,
method: "GET",
contentType: "application/json; charset=utf-8"
}).done(function(data, textStatue, jqXHR) {
if (jqXHR.status === 203) {
error = true;
term.write('.');
interval = 500;
}
if (jqXHR.status === 200){
term.write(data.data);
mark = data.mark;
if (data.end){
end = true;
window.clearInterval(int)
}
}
})
}
}
APIUpdateAttr({
url: url,
body: JSON.stringify(data),
method: 'POST',
flash_message: false,
success: function (resp) {
term.write("{% trans 'Pending' %}" + "...\r\n");
log_url = resp.log_url;
int = setInterval(function () {
writeExecutionOutput()
}, interval);
}
});
return false;
}
var editor;
$(document).ready(function(){
systemUserId = $('#system-users-select').val();
$(".select2").select2({
dropdownAutoWidth : true,
}).on('select2:select', function(evt) {
var data = evt.params.data;
systemUserId = data.id;
initTree();
});
editor = CodeMirror.fromTextArea(document.getElementById("command-text"), {
lineNumbers: true,
lineWrapping: true,
mode: "shell"
});
editor.setSize(600, 100);
var charWidth = editor.defaultCharWidth(), basePadding = 4;
editor.on("renderLine", function(cm, line, elt) {
var off = CodeMirror.countColumn(line.text, null, cm.getOption("tabSize")) * charWidth;
elt.style.textIndent = "-" + off + "px";
elt.style.paddingLeft = (basePadding + off) + "px";
});
editor.refresh();
initTree();
initResultTerminal();
}).on('click', '.btn-execute', function () {
execute()
})
</script>
{% endblock %}
\ No newline at end of file
{% extends '_base_list.html' %}
{% load i18n %}
{% load static %}
{% load common_tags %}
{% block custom_head_css_js %}
<link href="{% static "css/plugins/footable/footable.core.css" %}" rel="stylesheet">
<link href="{% static 'css/plugins/datepicker/datepicker3.css' %}" rel="stylesheet">
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
<style>
#search_btn {
margin-bottom: 0;
}
</style>
{% endblock %}
{% block content_left_head %}
{% endblock %}
{% block table_search %}
<form id="search_form" method="get" action="" class="pull-right form-inline">
<div class="form-group" id="date">
<div class="input-daterange input-group" id="datepicker">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" value="{{ date_from|date:'Y-m-d' }}">
<span class="input-group-addon">to</span>
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_to" value="{{ date_to|date:'Y-m-d' }}">
</div>
</div>
{% if user_list %}
<div class="input-group">
<select class="select2 form-control" name="user">
<option value="">{% trans 'User' %}</option>
{% for u in user_list %}
<option value="{{ u.id }}" {% if u.id == user_id %} selected {% endif %}>{{ u }}</option>
{% endfor %}
</select>
</div>
{% endif %}
<div class="input-group">
<input type="text" class="form-control input-sm" name="keyword" placeholder="{% trans 'Search' %}" value="{{ keyword }}">
</div>
<div class="input-group">
<div class="input-group-btn">
<button id='search_btn' type="submit" class="btn btn-sm btn-primary">
{% trans "Search" %}
</button>
</div>
</div>
</form>
{% endblock %}
{% block table_container %}
<table class="footable table table-stripped table-bordered toggle-arrow-tiny" data-page="false" >
<thead>
<th class="text-center"></th>
<th class="text-center">{% trans 'Hosts' %}</th>
<th class="text-center">{% trans 'User' %}</th>
<th class="text-center">{% trans 'Command' %}</th>
<th class="text-center">{% trans 'Run as' %}</th>
<th class="text-center">{% trans 'Output' %}</th>
<th class="text-center">{% trans 'Finished' %}</th>
<th class="text-center">{% trans 'Success' %}</th>
<th class="text-center">{% trans 'Date start' %}</th>
</thead>
<tbody>
{% for object in object_list %}
<tr class="gradeX">
<td>{{ forloop.counter }}</td>
<td class="text-center hosts" value="{{ object.get_hosts_names }}"></td>
<td class="text-center">{{ object.user.name }}</td>
<td class="text-center">{{ object.command| truncatechars:16 }}</td>
<td class="text-center">{{ object.run_as.username }}</td>
<td class="text-center"><a href="{% url "ops:celery-task-log" pk=object.id %}" target="_blank">查看</a></td>
<td class="text-center">{{ object.is_finished | state_show | safe }}</td>
<td class="text-center">{{ object.is_success | state_show | safe }}</td>
<td class="text-center">{{ object.date_start }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
{% block custom_foot_js %}
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
<script src="{% static "js/plugins/footable/footable.all.min.js" %}"></script>
<script>
$(document).ready(function() {
{#$('table').DataTable({#}
{# "searching": false,#}
{# "paging": false,#}
{# "bInfo" : false,#}
{# "order": []#}
{# });#}
{#$('.footable').footable();#}
$('.select2').select2({
dropdownAutoWidth : true,
width: 'auto'
});
$('#date .input-daterange').datepicker({
format: "yyyy-mm-dd",
todayBtn: "linked",
keyboardNavigation: false,
forceParse: false,
calendarWeeks: true,
autoclose: true
});
$(".hosts").each(function (i) {
var data = $(this).attr('value');
var data_list = data.split(",");
if (data_list.length === 1 && data_list[0] === "") {
data_list.pop();
}
var html = createPopover(data_list);
$(this).html(html);
});
$('[data-toggle="popover"]').popover();
})
</script>
{% endblock %}
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
<a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a> <a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a>
</li> </li>
<li> <li>
<a class="text-center celery-task-log" onclick="window.open('{% url 'ops:celery-task-log' pk=object.latest_history.pk %}','', 'width=800,height=600,left=400,top=400')"><i class="fa fa-laptop"></i> {% trans 'Last run output' %} </a> <a class="text-center celery-task-log" ><i class="fa fa-laptop"></i> {% trans 'Last run output' %} </a>
</li> </li>
</ul> </ul>
</div> </div>
...@@ -78,52 +78,60 @@ ...@@ -78,52 +78,60 @@
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
$(document).ready(function () { $(document).ready(function () {
var options = { var options = {
ele: $('#task-version-list-table'), ele: $('#task-version-list-table'),
buttons: [], buttons: [],
order: [], order: [],
select: [], select: [],
columnDefs: [ columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) { {targets: 1, createdCell: function (td, cellData, rowData) {
{# var detail_btn = '<a href="' + cellData + '</a>';#} {# var detail_btn = '<a href="' + cellData + '</a>';#}
$(td).html(cellData); $(td).html(cellData);
}}, }},
{targets: 2, createdCell: function (td, cellData, rowData) { {targets: 2, createdCell: function (td, cellData, rowData) {
var dataLength = cellData.length; var dataLength = cellData.length;
$(td).html(dataLength); $(td).html(dataLength);
}}, }},
{targets: 4, createdCell: function (td, cellData) { {targets: 4, createdCell: function (td, cellData) {
if (!cellData) { if (!cellData) {
$(td).html("Admin") $(td).html("Admin")
} else { } else {
$(td).html(cellData) $(td).html(cellData)
} }
}}, }},
{targets: 5, createdCell: function (td, cellData, rowData) { {targets: 5, createdCell: function (td, cellData, rowData) {
if (!cellData) { if (!cellData) {
$(td).html("") $(td).html("")
} else { } else {
$(td).html(cellData.user) $(td).html(cellData.user)
} }
}}, }},
{targets: 6, createdCell: function (td, cellData) { {targets: 6, createdCell: function (td, cellData) {
var d = new Date(cellData); var d = new Date(cellData);
$(td).html(d.toLocaleString()) $(td).html(d.toLocaleString())
}}, }},
{targets: 7, createdCell: function (td, cellData, rowData) { {targets: 7, createdCell: function (td, cellData, rowData) {
var detail_btn = '<a class="btn btn-xs btn-primary m-l-xs btn-run" href="{% url 'ops:adhoc-detail' pk=DEFAULT_PK %}">{% trans "Detail" %}</a>'.replace('{{ DEFAULT_PK }}', cellData); var detail_btn = '<a class="btn btn-xs btn-primary m-l-xs btn-run" href="{% url 'ops:adhoc-detail' pk=DEFAULT_PK %}">{% trans "Detail" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
if (cellData) { if (cellData) {
$(td).html(detail_btn); $(td).html(detail_btn);
} }
}} }}
], ],
ajax_url: '{% url "api-ops:adhoc-list" %}?task={{ object.pk }}', ajax_url: '{% url "api-ops:adhoc-list" %}?task={{ object.pk }}',
columns: [{data: function(){return ""}}, {data: "short_id" }, {data: "hosts"}, {data: "pattern"}, columns: [{data: function(){return ""}}, {data: "short_id" }, {data: "hosts"}, {data: "pattern"},
{data: "run_as"}, {data: "become"}, {data: "date_created"}, {data: "id"}] {data: "run_as"}, {data: "become"}, {data: "date_created"}, {data: "id"}]
}; };
jumpserver.initDataTable(options); jumpserver.initDataTable(options);
}) }).on('click', '.celery-task-log', function () {
</script> var history_pk = "{{ object.latest_history.pk }}";
if (!history_pk) {
alert("没有运行历史");
return
}
var url = '{% url 'ops:celery-task-log' pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', history_pk);
window.open(url, '', 'width=800,height=600,left=400,top=400')
})
</script>
{% endblock %} {% endblock %}
...@@ -4,10 +4,11 @@ ...@@ -4,10 +4,11 @@
{% block custom_head_css_js %} {% block custom_head_css_js %}
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet"> <link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<link href="{% static "css/plugins/sweetalert/sweetalert.css" %}" rel="stylesheet"> <link href="{% static 'css/plugins/sweetalert/sweetalert.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script> <script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
<script src="{% static "js/plugins/sweetalert/sweetalert.min.js" %}"></script> <script src="{% static 'js/plugins/sweetalert/sweetalert.min.js' %}"></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="wrapper wrapper-content animated fadeInRight"> <div class="wrapper wrapper-content animated fadeInRight">
<div class="row"> <div class="row">
...@@ -25,7 +26,7 @@ ...@@ -25,7 +26,7 @@
<a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a> <a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a>
</li> </li>
<li> <li>
<a class="text-center celery-task-log" onclick="window.open('{% url 'ops:celery-task-log' pk=object.latest_history.pk %}','', 'width=800,height=600,left=400,top=400')"><i class="fa fa-laptop"></i> {% trans 'Last run output' %} </a> <a class="text-center celery-task-log"><i class="fa fa-laptop"></i> {% trans 'Last run output' %} </a>
</li> </li>
</ul> </ul>
</div> </div>
...@@ -164,4 +165,19 @@ ...@@ -164,4 +165,19 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block custom_foot_js %}
<script>
$(document).ready(function () {
}).on('click', '.celery-task-log', function () {
var history_pk = "{{ object.latest_history.pk }}";
if (!history_pk) {
alert("没有运行历史");
return
}
var url = '{% url 'ops:celery-task-log' pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', history_pk);
window.open(url, '', 'width=800,height=600,left=400,top=400')
})
</script>
{% endblock %}
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
<a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a> <a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a>
</li> </li>
<li> <li>
<a class="text-center celery-task-log" onclick="window.open('{% url 'ops:celery-task-log' pk=object.latest_history.pk %}','', 'width=800,height=600,left=400,top=400')"><i class="fa fa-laptop"></i> {% trans 'Last run output' %} </a> <a class="text-center celery-task-log"><i class="fa fa-laptop"></i> {% trans 'Last run output' %} </a>
</li> </li>
</ul> </ul>
</div> </div>
...@@ -148,6 +148,14 @@ function initTable() { ...@@ -148,6 +148,14 @@ function initTable() {
$(document).ready(function () { $(document).ready(function () {
initTable(); initTable();
}).on('click', '.celery-task-log', function () {
var history_pk = "{{ object.latest_history.pk }}";
if (!history_pk) {
alert("没有运行历史");
return
}
var url = '{% url 'ops:celery-task-log' pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', history_pk);
window.open(url, '', 'width=800,height=600,left=400,top=400')
}) })
</script> </script>
......
...@@ -11,11 +11,13 @@ app_name = "ops" ...@@ -11,11 +11,13 @@ app_name = "ops"
router = DefaultRouter() router = DefaultRouter()
router.register(r'tasks', api.TaskViewSet, 'task') router.register(r'tasks', api.TaskViewSet, 'task')
router.register(r'adhoc', api.AdHocViewSet, 'adhoc') router.register(r'adhoc', api.AdHocViewSet, 'adhoc')
router.register(r'history', api.AdHocRunHistorySet, 'history') router.register(r'history', api.AdHocRunHistoryViewSet, 'history')
router.register(r'command-executions', api.CommandExecutionViewSet, 'command-execution')
urlpatterns = [ urlpatterns = [
path('tasks/<uuid:pk>/run/', api.TaskRun.as_view(), name='task-run'), path('tasks/<uuid:pk>/run/', api.TaskRun.as_view(), name='task-run'),
path('celery/task/<uuid:pk>/log/', api.CeleryTaskLogApi.as_view(), name='celery-task-log'), path('celery/task/<uuid:pk>/log/', api.CeleryTaskLogApi.as_view(), name='celery-task-log'),
path('celery/task/<uuid:pk>/result/', api.CeleryResultApi.as_view(), name='celery-result'),
] ]
urlpatterns += router.urls urlpatterns += router.urls
...@@ -18,4 +18,7 @@ urlpatterns = [ ...@@ -18,4 +18,7 @@ urlpatterns = [
path('adhoc/<uuid:pk>/history/', views.AdHocHistoryView.as_view(), name='adhoc-history'), path('adhoc/<uuid:pk>/history/', views.AdHocHistoryView.as_view(), name='adhoc-history'),
path('adhoc/history/<uuid:pk>/', views.AdHocHistoryDetailView.as_view(), name='adhoc-history-detail'), path('adhoc/history/<uuid:pk>/', views.AdHocHistoryDetailView.as_view(), name='adhoc-history-detail'),
path('celery/task/<uuid:pk>/log/', views.CeleryTaskLogView.as_view(), name='celery-task-log'), path('celery/task/<uuid:pk>/log/', views.CeleryTaskLogView.as_view(), name='celery-task-log'),
path('command-execution/', views.CommandExecutionListView.as_view(), name='command-execution-list'),
path('command-execution/start/', views.CommandExecutionStartView.as_view(), name='command-execution-start'),
] ]
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
from django.utils.translation import ugettext_lazy as _
from common.utils import get_logger, get_object_or_none from common.utils import get_logger, get_object_or_none
from orgs.utils import set_to_root_org
from .models import Task, AdHoc from .models import Task, AdHoc
logger = get_logger(__file__) logger = get_logger(__file__)
...@@ -10,15 +12,14 @@ def get_task_by_id(task_id): ...@@ -10,15 +12,14 @@ def get_task_by_id(task_id):
def update_or_create_ansible_task( def update_or_create_ansible_task(
task_name, hosts, tasks, task_name, hosts, tasks, created_by,
interval=None, crontab=None, is_periodic=False, interval=None, crontab=None, is_periodic=False,
callback=None, pattern='all', options=None, callback=None, pattern='all', options=None,
run_as_admin=False, run_as="", become_info=None, run_as_admin=False, run_as=None, become_info=None,
created_by=None,
): ):
if not hosts or not tasks or not task_name: if not hosts or not tasks or not task_name:
return return
set_to_root_org()
defaults = { defaults = {
'name': task_name, 'name': task_name,
'interval': interval, 'interval': interval,
...@@ -29,22 +30,27 @@ def update_or_create_ansible_task( ...@@ -29,22 +30,27 @@ def update_or_create_ansible_task(
} }
created = False created = False
task, _ = Task.objects.update_or_create( task, ok = Task.objects.update_or_create(
defaults=defaults, name=task_name, defaults=defaults, name=task_name, created_by=created_by
) )
adhoc = task.get_latest_adhoc()
adhoc = task.latest_adhoc
new_adhoc = AdHoc(task=task, pattern=pattern, new_adhoc = AdHoc(task=task, pattern=pattern,
run_as_admin=run_as_admin, run_as_admin=run_as_admin,
run_as=run_as) run_as=run_as)
new_adhoc.hosts = hosts
new_adhoc.tasks = tasks new_adhoc.tasks = tasks
new_adhoc.options = options new_adhoc.options = options
new_adhoc.become = become_info new_adhoc.become = become_info
if not adhoc or adhoc != new_adhoc: hosts_same = True
print("Task create new adhoc: {}".format(task_name)) if adhoc:
old_hosts = set([str(asset.id) for asset in adhoc.hosts.all()])
new_hosts = set([str(asset.id) for asset in hosts])
hosts_same = old_hosts == new_hosts
if not adhoc or adhoc != new_adhoc or not hosts_same:
logger.info(_("Update task content: {}").format(task_name))
new_adhoc.save() new_adhoc.save()
new_adhoc.hosts.set(hosts)
task.latest_adhoc = new_adhoc task.latest_adhoc = new_adhoc
created = True created = True
return task, created return task, created
......
from .adhoc import *
from .celery import *
from .command import *
\ No newline at end of file
...@@ -2,14 +2,22 @@ ...@@ -2,14 +2,22 @@
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.conf import settings from django.conf import settings
from django.views.generic import ListView, DetailView, TemplateView from django.views.generic import ListView, DetailView
from common.mixins import DatetimeSearchMixin from common.mixins import DatetimeSearchMixin
from .models import Task, AdHoc, AdHocRunHistory, CeleryTask from common.permissions import AdminUserRequiredMixin
from common.permissions import SuperUserRequiredMixin, AdminUserRequiredMixin from orgs.utils import current_org
from ..models import Task, AdHoc, AdHocRunHistory
class TaskListView(SuperUserRequiredMixin, DatetimeSearchMixin, ListView): __all__ = [
'TaskListView', 'TaskDetailView', 'TaskHistoryView',
'TaskAdhocView', 'AdHocDetailView', 'AdHocHistoryDetailView',
'AdHocHistoryView'
]
class TaskListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
paginate_by = settings.DISPLAY_PER_PAGE paginate_by = settings.DISPLAY_PER_PAGE
model = Task model = Task
ordering = ('-date_created',) ordering = ('-date_created',)
...@@ -18,18 +26,23 @@ class TaskListView(SuperUserRequiredMixin, DatetimeSearchMixin, ListView): ...@@ -18,18 +26,23 @@ class TaskListView(SuperUserRequiredMixin, DatetimeSearchMixin, ListView):
keyword = '' keyword = ''
def get_queryset(self): def get_queryset(self):
self.queryset = super().get_queryset() queryset = super().get_queryset()
if current_org.is_real():
queryset = queryset.filter(created_by=current_org.id)
else:
queryset = queryset.filter(created_by='')
self.keyword = self.request.GET.get('keyword', '') self.keyword = self.request.GET.get('keyword', '')
self.queryset = self.queryset.filter( queryset = queryset.filter(
date_created__gt=self.date_from, date_created__gt=self.date_from,
date_created__lt=self.date_to date_created__lt=self.date_to
) )
if self.keyword: if self.keyword:
self.queryset = self.queryset.filter( queryset = queryset.filter(
name__icontains=self.keyword, name__icontains=self.keyword,
) )
return self.queryset return queryset
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
...@@ -43,10 +56,19 @@ class TaskListView(SuperUserRequiredMixin, DatetimeSearchMixin, ListView): ...@@ -43,10 +56,19 @@ class TaskListView(SuperUserRequiredMixin, DatetimeSearchMixin, ListView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class TaskDetailView(SuperUserRequiredMixin, DetailView): class TaskDetailView(AdminUserRequiredMixin, DetailView):
model = Task model = Task
template_name = 'ops/task_detail.html' template_name = 'ops/task_detail.html'
def get_queryset(self):
queryset = super().get_queryset()
# Todo: 需要整理默认组织等东西
if current_org.is_real():
queryset = queryset.filter(created_by=current_org.id)
else:
queryset = queryset.filter(created_by='')
return queryset
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
'app': _('Ops'), 'app': _('Ops'),
...@@ -56,7 +78,7 @@ class TaskDetailView(SuperUserRequiredMixin, DetailView): ...@@ -56,7 +78,7 @@ class TaskDetailView(SuperUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class TaskAdhocView(SuperUserRequiredMixin, DetailView): class TaskAdhocView(AdminUserRequiredMixin, DetailView):
model = Task model = Task
template_name = 'ops/task_adhoc.html' template_name = 'ops/task_adhoc.html'
...@@ -69,7 +91,7 @@ class TaskAdhocView(SuperUserRequiredMixin, DetailView): ...@@ -69,7 +91,7 @@ class TaskAdhocView(SuperUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class TaskHistoryView(SuperUserRequiredMixin, DetailView): class TaskHistoryView(AdminUserRequiredMixin, DetailView):
model = Task model = Task
template_name = 'ops/task_history.html' template_name = 'ops/task_history.html'
...@@ -82,7 +104,7 @@ class TaskHistoryView(SuperUserRequiredMixin, DetailView): ...@@ -82,7 +104,7 @@ class TaskHistoryView(SuperUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class AdHocDetailView(SuperUserRequiredMixin, DetailView): class AdHocDetailView(AdminUserRequiredMixin, DetailView):
model = AdHoc model = AdHoc
template_name = 'ops/adhoc_detail.html' template_name = 'ops/adhoc_detail.html'
...@@ -95,7 +117,7 @@ class AdHocDetailView(SuperUserRequiredMixin, DetailView): ...@@ -95,7 +117,7 @@ class AdHocDetailView(SuperUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class AdHocHistoryView(SuperUserRequiredMixin, DetailView): class AdHocHistoryView(AdminUserRequiredMixin, DetailView):
model = AdHoc model = AdHoc
template_name = 'ops/adhoc_history.html' template_name = 'ops/adhoc_history.html'
...@@ -108,7 +130,7 @@ class AdHocHistoryView(SuperUserRequiredMixin, DetailView): ...@@ -108,7 +130,7 @@ class AdHocHistoryView(SuperUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class AdHocHistoryDetailView(SuperUserRequiredMixin, DetailView): class AdHocHistoryDetailView(AdminUserRequiredMixin, DetailView):
model = AdHocRunHistory model = AdHocRunHistory
template_name = 'ops/adhoc_history_detail.html' template_name = 'ops/adhoc_history_detail.html'
...@@ -121,6 +143,5 @@ class AdHocHistoryDetailView(SuperUserRequiredMixin, DetailView): ...@@ -121,6 +143,5 @@ class AdHocHistoryDetailView(SuperUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class CeleryTaskLogView(AdminUserRequiredMixin, DetailView):
template_name = 'ops/celery_task_log.html'
model = CeleryTask
# -*- coding: utf-8 -*-
#
from django.views.generic import DetailView
from common.permissions import AdminUserRequiredMixin
from ..models import CeleryTask
__all__ = ['CeleryTaskLogView']
class CeleryTaskLogView(AdminUserRequiredMixin, DetailView):
template_name = 'ops/celery_task_log.html'
model = CeleryTask
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext as _
from django.conf import settings
from django.views.generic import ListView, TemplateView
from common.mixins import DatetimeSearchMixin
from ..models import CommandExecution
from ..forms import CommandExecutionForm
__all__ = [
'CommandExecutionListView', 'CommandExecutionStartView'
]
class CommandExecutionListView(DatetimeSearchMixin, ListView):
template_name = 'ops/command_execution_list.html'
model = CommandExecution
paginate_by = settings.DISPLAY_PER_PAGE
ordering = ('-date_created',)
context_object_name = 'task_list'
keyword = ''
def _get_queryset(self):
self.keyword = self.request.GET.get('keyword', '')
queryset = super().get_queryset()
if self.date_from:
queryset = queryset.filter(date_start__gte=self.date_from)
if self.date_to:
queryset = queryset.filter(date_start__lte=self.date_to)
if self.keyword:
queryset = queryset.filter(command__icontains=self.keyword)
return queryset
def get_queryset(self):
queryset = self._get_queryset().filter(user=self.request.user)
return queryset
def get_context_data(self, **kwargs):
context = {
'app': _('Ops'),
'action': _('Command execution list'),
'date_from': self.date_from,
'date_to': self.date_to,
'keyword': self.keyword,
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class CommandExecutionStartView(TemplateView):
template_name = 'ops/command_execution_create.html'
form_class = CommandExecutionForm
def get_user_system_users(self):
from perms.utils import AssetPermissionUtil
user = self.request.user
util = AssetPermissionUtil(user)
system_users = [s for s in util.get_system_users() if s.protocol == 'ssh']
return system_users
def get_context_data(self, **kwargs):
system_users = self.get_user_system_users()
context = {
'app': _('Ops'),
'action': _('Command execution'),
'form': self.get_form(),
'system_users': system_users
}
kwargs.update(context)
return super().get_context_data(**kwargs)
def get_form(self):
return self.form_class()
...@@ -74,7 +74,7 @@ class OrgManager(models.Manager): ...@@ -74,7 +74,7 @@ class OrgManager(models.Manager):
class OrgModelMixin(models.Model): class OrgModelMixin(models.Model):
org_id = models.CharField(max_length=36, blank=True, default='', verbose_name=_("Organization")) org_id = models.CharField(max_length=36, blank=True, default='', verbose_name=_("Organization"), db_index=True)
objects = OrgManager() objects = OrgManager()
sep = '@' sep = '@'
......
...@@ -68,14 +68,10 @@ class Organization(models.Model): ...@@ -68,14 +68,10 @@ class Organization(models.Model):
def get_org_users(self, include_app=False): def get_org_users(self, include_app=False):
from users.models import User from users.models import User
if self.is_default(): if self.is_real():
users = User.objects.filter(orgs__isnull=True)
elif not self.is_real():
users = User.objects.all()
elif self.is_root():
users = User.objects.all()
else:
users = self.users.all() users = self.users.all()
else:
users = User.objects.all()
if not include_app: if not include_app:
users = users.exclude(role=User.ROLE_APP) users = users.exclude(role=User.ROLE_APP)
return users return users
......
...@@ -3,18 +3,20 @@ ...@@ -3,18 +3,20 @@
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework.views import APIView, Response from rest_framework.views import APIView, Response
from rest_framework.generics import ListAPIView, get_object_or_404, RetrieveUpdateAPIView from rest_framework.generics import ListAPIView, get_object_or_404, \
RetrieveUpdateAPIView
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.pagination import LimitOffsetPagination from rest_framework.pagination import LimitOffsetPagination
from common.utils import set_or_append_attr_bulk from common.utils import set_or_append_attr_bulk
from common.permissions import IsValidUser, IsOrgAdmin, IsOrgAdminOrAppUser from common.permissions import IsValidUser, IsOrgAdmin, IsOrgAdminOrAppUser
from common.tree import TreeNode, TreeNodeSerializer
from orgs.mixins import RootOrgViewMixin from orgs.mixins import RootOrgViewMixin
from orgs.utils import set_to_root_org
from .utils import AssetPermissionUtil from .utils import AssetPermissionUtil
from .models import AssetPermission from .models import AssetPermission
from .hands import AssetGrantedSerializer, User, UserGroup, Asset, Node, \ from .hands import AssetGrantedSerializer, User, UserGroup, Asset, Node, \
NodeGrantedSerializer, SystemUser, NodeSerializer SystemUser, NodeSerializer
from orgs.utils import set_to_root_org
from . import serializers from . import serializers
from .mixins import AssetsFilterMixin from .mixins import AssetsFilterMixin
...@@ -25,6 +27,7 @@ __all__ = [ ...@@ -25,6 +27,7 @@ __all__ = [
'UserGroupGrantedNodesApi', 'UserGroupGrantedNodesWithAssetsApi', 'UserGroupGrantedNodeAssetsApi', 'UserGroupGrantedNodesApi', 'UserGroupGrantedNodesWithAssetsApi', 'UserGroupGrantedNodeAssetsApi',
'ValidateUserAssetPermissionApi', 'AssetPermissionRemoveUserApi', 'AssetPermissionAddUserApi', 'ValidateUserAssetPermissionApi', 'AssetPermissionRemoveUserApi', 'AssetPermissionAddUserApi',
'AssetPermissionRemoveAssetApi', 'AssetPermissionAddAssetApi', 'UserGrantedNodeChildrenApi', 'AssetPermissionRemoveAssetApi', 'AssetPermissionAddAssetApi', 'UserGrantedNodeChildrenApi',
'UserGrantedNodesWithAssetsAsTreeApi',
] ]
...@@ -147,7 +150,7 @@ class UserGrantedNodesWithAssetsApi(AssetsFilterMixin, ListAPIView): ...@@ -147,7 +150,7 @@ class UserGrantedNodesWithAssetsApi(AssetsFilterMixin, ListAPIView):
用户授权的节点并带着节点下资产的api 用户授权的节点并带着节点下资产的api
""" """
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = NodeGrantedSerializer serializer_class = serializers.NodeGrantedSerializer
def change_org_if_need(self): def change_org_if_need(self):
if self.request.user.is_superuser or \ if self.request.user.is_superuser or \
...@@ -186,6 +189,117 @@ class UserGrantedNodesWithAssetsApi(AssetsFilterMixin, ListAPIView): ...@@ -186,6 +189,117 @@ class UserGrantedNodesWithAssetsApi(AssetsFilterMixin, ListAPIView):
return super().get_permissions() return super().get_permissions()
class UserGrantedNodesWithAssetsAsTreeApi(ListAPIView):
serializer_class = TreeNodeSerializer
permission_classes = (IsOrgAdminOrAppUser,)
show_assets = True
system_user_id = None
def change_org_if_need(self):
if self.request.user.is_superuser or \
self.request.user.is_app or \
self.kwargs.get('pk') is None:
set_to_root_org()
def get(self, request, *args, **kwargs):
self.show_assets = request.query_params.get('show_assets', '1') == '1'
self.system_user_id = request.query_params.get('system_user')
return super().get(request, *args, **kwargs)
@staticmethod
def parse_node_to_tree_node(node):
name = '{} ({})'.format(node.value, node.assets_amount)
node_serializer = serializers.GrantedNodeSerializer(node)
data = {
'id': node.key,
'name': name,
'title': name,
'pId': node.parent_key,
'isParent': True,
'open': node.is_root(),
'meta': {
'node': node_serializer.data,
'type': 'node'
}
}
tree_node = TreeNode(**data)
return tree_node
@staticmethod
def parse_asset_to_tree_node(node, asset, system_users):
system_users_protocol_matched = [s for s in system_users if s.protocol == asset.protocol]
icon_skin = 'file'
if asset.platform.lower() == 'windows':
icon_skin = 'windows'
elif asset.platform.lower() == 'linux':
icon_skin = 'linux'
system_users = []
for system_user in system_users_protocol_matched:
system_users.append({
'id': system_user.id,
'name': system_user.name,
'username': system_user.username,
'protocol': system_user.protocol,
'priority': system_user.priority,
'login_mode': system_user.login_mode,
'comment': system_user.comment,
})
data = {
'id': str(asset.id),
'name': asset.hostname,
'title': asset.ip,
'pId': node.key,
'isParent': False,
'open': False,
'iconSkin': icon_skin,
'meta': {
'system_users': system_users,
'type': 'asset',
'asset': {
'id': asset.id,
'hostname': asset.hostname,
'ip': asset.ip,
'port': asset.port,
'protocol': asset.protocol,
'platform': asset.platform,
'domain': None if not asset.domain else asset.domain.id,
'is_active': asset.is_active,
'comment': asset.comment
},
}
}
tree_node = TreeNode(**data)
return tree_node
def get_permissions(self):
if self.kwargs.get('pk') is None:
self.permission_classes = (IsValidUser,)
return super().get_permissions()
def get_queryset(self):
self.change_org_if_need()
user_id = self.kwargs.get('pk', '')
queryset = []
if not user_id:
user = self.request.user
else:
user = get_object_or_404(User, id=user_id)
util = AssetPermissionUtil(user)
if self.system_user_id:
util.filter_permission_with_system_user(system_user=self.system_user_id)
nodes = util.get_nodes_with_assets()
for node, assets in nodes.items():
data = self.parse_node_to_tree_node(node)
queryset.append(data)
if not self.show_assets:
continue
for asset, system_users in assets.items():
data = self.parse_asset_to_tree_node(node, asset, system_users)
queryset.append(data)
queryset = sorted(queryset)
return queryset
class UserGrantedNodeAssetsApi(AssetsFilterMixin, ListAPIView): class UserGrantedNodeAssetsApi(AssetsFilterMixin, ListAPIView):
""" """
查询用户授权的节点下的资产的api, 与上面api不同的是,只返回某个节点下的资产 查询用户授权的节点下的资产的api, 与上面api不同的是,只返回某个节点下的资产
...@@ -263,7 +377,7 @@ class UserGroupGrantedNodesApi(ListAPIView): ...@@ -263,7 +377,7 @@ class UserGroupGrantedNodesApi(ListAPIView):
class UserGroupGrantedNodesWithAssetsApi(ListAPIView): class UserGroupGrantedNodesWithAssetsApi(ListAPIView):
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
serializer_class = NodeGrantedSerializer serializer_class = serializers.NodeGrantedSerializer
def get_queryset(self): def get_queryset(self):
user_group_id = self.kwargs.get('pk', '') user_group_id = self.kwargs.get('pk', '')
......
...@@ -7,16 +7,24 @@ from django.utils.translation import ugettext_lazy as _ ...@@ -7,16 +7,24 @@ from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgModelForm from orgs.mixins import OrgModelForm
from orgs.utils import current_org from orgs.utils import current_org
from .models import AssetPermission from .models import AssetPermission
from assets.models import Asset
class AssetPermissionForm(OrgModelForm): class AssetPermissionForm(OrgModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if 'initial' not in kwargs:
return
users_field = self.fields.get('users') users_field = self.fields.get('users')
if hasattr(users_field, 'queryset'): if hasattr(users_field, 'queryset'):
users_field.queryset = current_org.get_org_users() users_field.queryset = current_org.get_org_users()
assets_field = self.fields.get('assets')
# 前端渲染优化, 防止过多资产
if not self.data:
instance = kwargs.get('instance')
if instance:
assets_field.queryset = instance.assets.all()
else:
assets_field.queryset = Asset.objects.none()
class Meta: class Meta:
model = AssetPermission model = AssetPermission
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
from common.permissions import AdminUserRequiredMixin from common.permissions import AdminUserRequiredMixin
from users.models import User, UserGroup from users.models import User, UserGroup
from assets.models import Asset, SystemUser, Node from assets.models import Asset, SystemUser, Node
from assets.serializers import AssetGrantedSerializer, NodeGrantedSerializer, NodeSerializer from assets.serializers import AssetGrantedSerializer, NodeSerializer
...@@ -13,11 +13,11 @@ class Migration(migrations.Migration): ...@@ -13,11 +13,11 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='assetpermission', model_name='assetpermission',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='nodepermission', model_name='nodepermission',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
] ]
...@@ -5,9 +5,16 @@ from rest_framework import serializers ...@@ -5,9 +5,16 @@ from rest_framework import serializers
from common.fields import StringManyToManyField from common.fields import StringManyToManyField
from .models import AssetPermission from .models import AssetPermission
from assets.models import Node from assets.models import Node, Asset, SystemUser
from assets.serializers import AssetGrantedSerializer from assets.serializers import AssetGrantedSerializer
__all__ = [
'AssetPermissionCreateUpdateSerializer', 'AssetPermissionListSerializer',
'AssetPermissionUpdateUserSerializer', 'AssetPermissionUpdateAssetSerializer',
'AssetPermissionNodeSerializer', 'GrantedNodeSerializer',
'GrantedAssetSerializer', 'GrantedSystemUserSerializer',
]
class AssetPermissionCreateUpdateSerializer(serializers.ModelSerializer): class AssetPermissionCreateUpdateSerializer(serializers.ModelSerializer):
class Meta: class Meta:
...@@ -74,3 +81,58 @@ class AssetPermissionNodeSerializer(serializers.ModelSerializer): ...@@ -74,3 +81,58 @@ class AssetPermissionNodeSerializer(serializers.ModelSerializer):
@staticmethod @staticmethod
def get_tree_parent(obj): def get_tree_parent(obj):
return obj.parent_key return obj.parent_key
class NodeGrantedSerializer(serializers.ModelSerializer):
"""
授权资产组
"""
assets_granted = AssetGrantedSerializer(many=True, read_only=True)
assets_amount = serializers.SerializerMethodField()
parent = serializers.SerializerMethodField()
name = serializers.SerializerMethodField()
class Meta:
model = Node
fields = [
'id', 'key', 'name', 'value', 'parent',
'assets_granted', 'assets_amount', 'org_id',
]
@staticmethod
def get_assets_amount(obj):
return len(obj.assets_granted)
@staticmethod
def get_name(obj):
return obj.name
@staticmethod
def get_parent(obj):
return obj.parent.id
class GrantedNodeSerializer(serializers.ModelSerializer):
class Meta:
model = Node
fields = [
'id', 'name', 'key', 'value',
]
class GrantedAssetSerializer(serializers.ModelSerializer):
class Meta:
model = Asset
fields = [
'id', 'hostname', 'ip', 'port', 'protocol', 'platform',
'domain', 'is_active', 'comment'
]
class GrantedSystemUserSerializer(serializers.ModelSerializer):
class Meta:
model = SystemUser
fields = [
'id', 'name', 'username', 'protocol', 'priority',
'login_mode', 'comment'
]
...@@ -29,6 +29,11 @@ ...@@ -29,6 +29,11 @@
</div> </div>
<div class="ibox-content"> <div class="ibox-content">
<form method="post" class="form-horizontal" action="" > <form method="post" class="form-horizontal" action="" >
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}
{% csrf_token %} {% csrf_token %}
<h3>{% trans 'Basic' %}</h3> <h3>{% trans 'Basic' %}</h3>
{% bootstrap_field form.name layout="horizontal" %} {% bootstrap_field form.name layout="horizontal" %}
...@@ -54,9 +59,15 @@ ...@@ -54,9 +59,15 @@
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-daterange input-group" id="datepicker"> <div class="input-daterange input-group" id="datepicker">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span> <span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text" class="input-sm form-control" id="date_start" name="date_start" value="{{ form.date_start.value|date:'Y-m-d H:i' }}"> {% if form.errors %}
<span class="input-group-addon">to</span> <input type="text" class="input-sm form-control" id="date_start" name="date_start" value="{{ form.date_start.value }}">
<input type="text" class="input-sm form-control" id="date_expired" name="date_expired" value="{{ form.date_expired.value|date:'Y-m-d H:i' }}"> <span class="input-group-addon">to</span>
<input type="text" class="input-sm form-control" id="date_expired" name="date_expired" value="{{ form.date_expired.value }}">
{% else %}
<input type="text" class="input-sm form-control" id="date_start" name="date_start" value="{{ form.date_start.value|date:'Y-m-d H:i' }}">
<span class="input-group-addon">to</span>
<input type="text" class="input-sm form-control" id="date_expired" name="date_expired" value="{{ form.date_expired.value|date:'Y-m-d H:i' }}">
{% endif %}
</div> </div>
<span class="help-block ">{{ form.date_expired.errors }}</span> <span class="help-block ">{{ form.date_expired.errors }}</span>
<span class="help-block ">{{ form.date_start.errors }}</span> <span class="help-block ">{{ form.date_start.errors }}</span>
...@@ -107,17 +118,27 @@ $(document).ready(function () { ...@@ -107,17 +118,27 @@ $(document).ready(function () {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
$("#asset_list_modal").modal(); $("#asset_list_modal").modal();
initSelectedAssets2Table();
} }
}) })
}) })
.on('click', '#btn_asset_modal_confirm', function () { .on('click', '#btn_asset_modal_confirm', function () {
var assets = asset_table2.selected; var assets = asset_table2.selected;
$('.select2 option:selected').each(function (i, data) {
assets.push($(data).attr('value')) var options = [];
$('#id_assets option').each(function (i, v) {
options.push(v.value)
}); });
$.each(assets, function (id, data) { asset_table2.selected_rows.forEach(function (i) {
$('.select2').val(assets).trigger('change'); var name = i.hostname + '(' + i.ip + ')';
var option = new Option(name, i.id, false, true);
if (options.indexOf(i.id) === -1) {
$('#id_assets').append(option).trigger('change');
}
}); });
$('#id_assets').val(assets).trigger('change');
$("#asset_list_modal").modal('hide'); $("#asset_list_modal").modal('hide');
}); });
</script> </script>
......
...@@ -78,58 +78,24 @@ var zTree, table, show = 0; ...@@ -78,58 +78,24 @@ var zTree, table, show = 0;
function onSelected(event, treeNode) { function onSelected(event, treeNode) {
setCookie('node_selected', treeNode.id); setCookie('node_selected', treeNode.id);
var url = table.ajax.url(); var url = table.ajax.url();
if (treeNode.is_node) { if (treeNode.meta.type === 'node') {
url = setUrlParam(url, 'asset', ""); url = setUrlParam(url, 'asset', "");
url = setUrlParam(url, 'node', treeNode.node_id) url = setUrlParam(url, 'node', treeNode.meta.node.id)
} else { } else {
url = setUrlParam(url, 'node', ""); url = setUrlParam(url, 'node', "");
url = setUrlParam(url, 'asset', treeNode.node_id) url = setUrlParam(url, 'asset', treeNode.meta.asset.id)
} }
setCookie('node_selected', treeNode.node_id); setCookie('node_selected', treeNode.node_id);
table.ajax.url(url); table.ajax.url(url);
table.ajax.reload(); table.ajax.reload();
} }
function selectQueryNode() {
var query_node_id = $.getUrlParam("node");
var cookie_node_id = getCookie('node_selected');
var node;
var node_id;
if (query_node_id !== null) {
node_id = query_node_id
} else if (cookie_node_id !== null) {
node_id = cookie_node_id;
}
node = zTree.getNodesByParam("id", node_id, null);
if (node){
zTree.selectNode(node[0]);
node.open = true;
}
}
function filter(treeId, parentNode, childNodes) {
$.each(childNodes, function (index, value) {
value["node_id"] = value["id"];
value["id"] = value["tree_id"];
if (value["tree_id"] !== value["tree_parent"]) {
value["pId"] = value["tree_parent"];
} else {
value["isParent"] = true;
}
value['name'] = value['value'];
value["iconSkin"] = value["is_node"] ? null : 'file';
{#value["pId"] = value["parent"];#}
{#value["name"] = value["value"];#}
value["isParent"] = value["is_node"];
});
return childNodes;
}
function beforeAsync(treeId, treeNode) { function beforeAsync(treeId, treeNode) {
return treeNode.is_node if (treeNode) {
return treeNode.meta.type === 'node'
}
return true
} }
function makeLabel(data) { function makeLabel(data) {
...@@ -235,9 +201,8 @@ function initTree() { ...@@ -235,9 +201,8 @@ function initTree() {
}, },
async: { async: {
enable: true, enable: true,
url: "{% url 'api-assets:node-children-2' %}?assets=1&all=", url: "{% url 'api-assets:node-children-tree' %}?assets=1",
autoParam:["node_id=id", "name=n", "level=lv"], autoParam:["id=key", "name=n", "level=lv"],
dataFilter: filter,
type: 'get' type: 'get'
}, },
callback: { callback: {
...@@ -245,25 +210,7 @@ function initTree() { ...@@ -245,25 +210,7 @@ function initTree() {
beforeAsync: beforeAsync beforeAsync: beforeAsync
} }
}; };
zTree = $.fn.zTree.init($("#assetTree"), setting);
var zNodes = [];
$.get("{% url 'api-assets:node-children-2' %}?assets=1", function(data, status){
$.each(data, function (index, value) {
value["node_id"] = value["id"];
value["id"] = value["tree_id"];
if (value["tree_id"] !== value["tree_parent"]) {
value["pId"] = value["tree_parent"];
}
value["isParent"] = value["is_node"];
value['name'] = value['value'];
value["iconSkin"] = value["is_node"] ? null : 'file';
});
zNodes = data;
$.fn.zTree.init($("#assetTree"), setting, zNodes);
zTree = $.fn.zTree.getZTreeObj("assetTree");
var root = zTree.getNodes()[0];
zTree.expandNode(root);
});
} }
function toggle() { function toggle() {
...@@ -299,10 +246,10 @@ $(document).ready(function(){ ...@@ -299,10 +246,10 @@ $(document).ready(function(){
var _nodes = []; var _nodes = [];
var _assets = []; var _assets = [];
$.each(nodes, function (id, node) { $.each(nodes, function (id, node) {
if (node.is_node) { if (node.meta.type === 'node') {
_nodes.push(node.node_id) _nodes.push(node.meta.node.id)
} else { } else {
_assets.push(node.node_id) _assets.push(node.meta.asset.id)
} }
}); });
url += "?assets=" + _assets.join(",") + "&nodes=" + _nodes.join(","); url += "?assets=" + _assets.join(",") + "&nodes=" + _nodes.join(",");
......
...@@ -29,6 +29,11 @@ urlpatterns = [ ...@@ -29,6 +29,11 @@ urlpatterns = [
api.UserGrantedNodesWithAssetsApi.as_view(), name='user-nodes-assets'), api.UserGrantedNodesWithAssetsApi.as_view(), name='user-nodes-assets'),
path('user/nodes-assets/', api.UserGrantedNodesWithAssetsApi.as_view(), path('user/nodes-assets/', api.UserGrantedNodesWithAssetsApi.as_view(),
name='my-nodes-assets'), name='my-nodes-assets'),
path('user/<uuid:pk>/nodes-assets/tree/',
api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='user-nodes-assets-as-tree'),
path('user/nodes-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(),
name='my-nodes-assets-as-tree'),
# 查询某个用户组授权的资产和资产组 # 查询某个用户组授权的资产和资产组
path('user-group/<uuid:pk>/assets/', path('user-group/<uuid:pk>/assets/',
......
...@@ -11,36 +11,40 @@ from .hands import Node ...@@ -11,36 +11,40 @@ from .hands import Node
logger = get_logger(__file__) logger = get_logger(__file__)
class Tree: class GenerateTree:
def __init__(self): def __init__(self):
self.__all_nodes = Node.objects.all().prefetch_related('assets') """
self.__node_asset_map = defaultdict(set) nodes: {"node_instance": {
"asset_instance": set("system_user")
}
"""
self.__all_nodes = list(Node.objects.all())
self.nodes = defaultdict(dict) self.nodes = defaultdict(dict)
self.root = Node.root()
self.init_node_asset_map()
def init_node_asset_map(self):
for node in self.__all_nodes:
assets = [a.id for a in node.assets.all()]
for asset in assets:
self.__node_asset_map[str(asset)].add(node)
def add_asset(self, asset, system_users): def add_asset(self, asset, system_users):
nodes = self.__node_asset_map.get(str(asset.id), []) nodes = asset.nodes.all()
self.add_nodes(nodes) self.add_nodes(nodes)
for node in nodes: for node in nodes:
self.nodes[node][asset].update(system_users) self.nodes[node][asset].update(system_users)
def get_nodes(self):
for node in self.nodes:
assets = set(self.nodes.get(node).keys())
for n in self.nodes.keys():
if n.key.startswith(node.key + ':'):
assets.update(set(self.nodes[n].keys()))
node.assets_amount = len(assets)
return self.nodes
def add_node(self, node): def add_node(self, node):
if node in self.nodes: if node in self.nodes:
return return
else: else:
self.nodes[node] = defaultdict(set) self.nodes[node] = defaultdict(set)
if node.key == self.root.key: if node.is_root():
return return
parent_key = ':'.join(node.key.split(':')[:-1])
for n in self.__all_nodes: for n in self.__all_nodes:
if n.key == parent_key: if n.key == node.parent_key:
self.add_node(n) self.add_node(n)
break break
...@@ -107,6 +111,9 @@ class AssetPermissionUtil: ...@@ -107,6 +111,9 @@ class AssetPermissionUtil:
self._permissions = permissions self._permissions = permissions
return permissions return permissions
def filter_permission_with_system_user(self, system_user):
self._permissions = self.permissions.filter(system_users=system_user)
def get_nodes_direct(self): def get_nodes_direct(self):
""" """
返回用户/组授权规则直接关联的节点 返回用户/组授权规则直接关联的节点
...@@ -128,7 +135,9 @@ class AssetPermissionUtil: ...@@ -128,7 +135,9 @@ class AssetPermissionUtil:
permissions = self.permissions.prefetch_related('assets', 'system_users') permissions = self.permissions.prefetch_related('assets', 'system_users')
for perm in permissions: for perm in permissions:
for asset in perm.assets.all().valid().prefetch_related('nodes'): for asset in perm.assets.all().valid().prefetch_related('nodes'):
assets[asset].update(perm.system_users.all()) assets[asset].update(
perm.system_users.filter(protocol=asset.protocol)
)
return assets return assets
def get_assets(self): def get_assets(self):
...@@ -139,7 +148,9 @@ class AssetPermissionUtil: ...@@ -139,7 +148,9 @@ class AssetPermissionUtil:
for node, system_users in nodes.items(): for node, system_users in nodes.items():
_assets = node.get_all_assets().valid().prefetch_related('nodes') _assets = node.get_all_assets().valid().prefetch_related('nodes')
for asset in _assets: for asset in _assets:
assets[asset].update(system_users) assets[asset].update(
[s for s in system_users if s.protocol == asset.protocol]
)
self._assets = assets self._assets = assets
return self._assets return self._assets
...@@ -150,10 +161,17 @@ class AssetPermissionUtil: ...@@ -150,10 +161,17 @@ class AssetPermissionUtil:
:return: :return:
""" """
assets = self.get_assets() assets = self.get_assets()
tree = Tree() tree = GenerateTree()
for asset, system_users in assets.items(): for asset, system_users in assets.items():
tree.add_asset(asset, system_users) tree.add_asset(asset, system_users)
return tree.nodes return tree.get_nodes()
def get_system_users(self):
system_users = set()
permissions = self.permissions.prefetch_related('system_users')
for perm in permissions:
system_users.update(perm.system_users.all())
return system_users
def is_obj_attr_has(obj, val, attrs=("hostname", "ip", "comment")): def is_obj_attr_has(obj, val, attrs=("hostname", "ip", "comment")):
......
...@@ -34,8 +34,8 @@ th a { ...@@ -34,8 +34,8 @@ th a {
} }
.select2-container--default .select2-results__option--highlighted[aria-selected] { .select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: #d2d2d2 !important; background-color: #1ab394 !important;
color: #333 !important; /*color: #333 !important;*/
} }
.select2-selection--single, .select2-selection--single,
...@@ -438,4 +438,7 @@ div.dataTables_wrapper div.dataTables_filter { ...@@ -438,4 +438,7 @@ div.dataTables_wrapper div.dataTables_filter {
white-space: nowrap; white-space: nowrap;
} }
.form-group .required .control-label:after {
content:"*";
color:red;
}
/* ambiance theme for codemirror */
/* Color scheme */
.cm-s-ambiance .cm-keyword { color: #cda869; }
.cm-s-ambiance .cm-atom { color: #CF7EA9; }
.cm-s-ambiance .cm-number { color: #78CF8A; }
.cm-s-ambiance .cm-def { color: #aac6e3; }
.cm-s-ambiance .cm-variable { color: #ffb795; }
.cm-s-ambiance .cm-variable-2 { color: #eed1b3; }
.cm-s-ambiance .cm-variable-3 { color: #faded3; }
.cm-s-ambiance .cm-property { color: #eed1b3; }
.cm-s-ambiance .cm-operator {color: #fa8d6a;}
.cm-s-ambiance .cm-comment { color: #555; font-style:italic; }
.cm-s-ambiance .cm-string { color: #8f9d6a; }
.cm-s-ambiance .cm-string-2 { color: #9d937c; }
.cm-s-ambiance .cm-meta { color: #D2A8A1; }
.cm-s-ambiance .cm-qualifier { color: yellow; }
.cm-s-ambiance .cm-builtin { color: #9999cc; }
.cm-s-ambiance .cm-bracket { color: #24C2C7; }
.cm-s-ambiance .cm-tag { color: #fee4ff }
.cm-s-ambiance .cm-attribute { color: #9B859D; }
.cm-s-ambiance .cm-header {color: blue;}
.cm-s-ambiance .cm-quote { color: #24C2C7; }
.cm-s-ambiance .cm-hr { color: pink; }
.cm-s-ambiance .cm-link { color: #F4C20B; }
.cm-s-ambiance .cm-special { color: #FF9D00; }
.cm-s-ambiance .cm-error { color: #AF2018; }
.cm-s-ambiance .CodeMirror-matchingbracket { color: #0f0; }
.cm-s-ambiance .CodeMirror-nonmatchingbracket { color: #f22; }
.cm-s-ambiance .CodeMirror-selected {
background: rgba(255, 255, 255, 0.15);
}
.cm-s-ambiance.CodeMirror-focused .CodeMirror-selected {
background: rgba(255, 255, 255, 0.10);
}
/* Editor styling */
.cm-s-ambiance.CodeMirror {
line-height: 1.40em;
color: #E6E1DC;
background-color: #202020;
-webkit-box-shadow: inset 0 0 10px black;
-moz-box-shadow: inset 0 0 10px black;
box-shadow: inset 0 0 10px black;
}
.cm-s-ambiance .CodeMirror-gutters {
background: #3D3D3D;
border-right: 1px solid #4D4D4D;
box-shadow: 0 10px 20px black;
}
.cm-s-ambiance .CodeMirror-linenumber {
text-shadow: 0px 1px 1px #4d4d4d;
color: #111;
padding: 0 5px;
}
.cm-s-ambiance .CodeMirror-guttermarker { color: #aaa; }
.cm-s-ambiance .CodeMirror-guttermarker-subtle { color: #111; }
.cm-s-ambiance .CodeMirror-lines .CodeMirror-cursor {
border-left: 1px solid #7991E8;
}
.cm-s-ambiance .CodeMirror-activeline-background {
background: none repeat scroll 0% 0% rgba(255, 255, 255, 0.031);
}
.cm-s-ambiance.CodeMirror,
.cm-s-ambiance .CodeMirror-gutters {
background-image: url("data:image/png;base64,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");
}
/* BASICS */
.CodeMirror {
/* Set height, width, borders, and global font properties here */
font-family: monospace;
height: 300px;
}
.CodeMirror-scroll {
/* Set scrolling behaviour here */
overflow: auto;
}
/* PADDING */
.CodeMirror-lines {
padding: 4px 0; /* Vertical padding around content */
}
.CodeMirror pre {
padding: 0 4px; /* Horizontal padding of content */
}
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
background-color: white; /* The little square between H and V scrollbars */
}
/* GUTTER */
.CodeMirror-gutters {
border-right: 1px solid #ddd;
background-color: #f7f7f7;
white-space: nowrap;
}
.CodeMirror-linenumbers {}
.CodeMirror-linenumber {
padding: 0 3px 0 5px;
min-width: 20px;
text-align: right;
color: #999;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-guttermarker { color: black; }
.CodeMirror-guttermarker-subtle { color: #999; }
/* CURSOR */
.CodeMirror div.CodeMirror-cursor {
border-left: 1px solid black;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor {
width: auto;
border: 0;
background: #7e7;
}
.CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursors {
z-index: 1;
}
.cm-animate-fat-cursor {
width: auto;
border: 0;
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
}
@-moz-keyframes blink {
0% { background: #7e7; }
50% { background: none; }
100% { background: #7e7; }
}
@-webkit-keyframes blink {
0% { background: #7e7; }
50% { background: none; }
100% { background: #7e7; }
}
@keyframes blink {
0% { background: #7e7; }
50% { background: none; }
100% { background: #7e7; }
}
/* Can style cursor different in overwrite (non-insert) mode */
div.CodeMirror-overwrite div.CodeMirror-cursor {}
.cm-tab { display: inline-block; text-decoration: inherit; }
.CodeMirror-ruler {
border-left: 1px solid #ccc;
position: absolute;
}
/* DEFAULT THEME */
.cm-s-default .cm-keyword {color: #708;}
.cm-s-default .cm-atom {color: #219;}
.cm-s-default .cm-number {color: #164;}
.cm-s-default .cm-def {color: #00f;}
.cm-s-default .cm-variable,
.cm-s-default .cm-punctuation,
.cm-s-default .cm-property,
.cm-s-default .cm-operator {}
.cm-s-default .cm-variable-2 {color: #05a;}
.cm-s-default .cm-variable-3 {color: #085;}
.cm-s-default .cm-comment {color: #a50;}
.cm-s-default .cm-string {color: #a11;}
.cm-s-default .cm-string-2 {color: #f50;}
.cm-s-default .cm-meta {color: #555;}
.cm-s-default .cm-qualifier {color: #555;}
.cm-s-default .cm-builtin {color: #30a;}
.cm-s-default .cm-bracket {color: #997;}
.cm-s-default .cm-tag {color: #170;}
.cm-s-default .cm-attribute {color: #00c;}
.cm-s-default .cm-header {color: blue;}
.cm-s-default .cm-quote {color: #090;}
.cm-s-default .cm-hr {color: #999;}
.cm-s-default .cm-link {color: #00c;}
.cm-negative {color: #d44;}
.cm-positive {color: #292;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-link {text-decoration: underline;}
.cm-s-default .cm-error {color: #f00;}
.cm-invalidchar {color: #f00;}
/* Default styles for common addons */
div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
.CodeMirror-activeline-background {background: #e8f2ff;}
/* STOP */
/* The rest of this file contains styles related to the mechanics of
the editor. You probably shouldn't touch them. */
.CodeMirror {
line-height: 1;
position: relative;
overflow: hidden;
background: white;
color: black;
}
.CodeMirror-scroll {
/* 30px is the magic margin used to hide the element's real scrollbars */
/* See overflow: hidden in .CodeMirror */
margin-bottom: -30px; margin-right: -30px;
padding-bottom: 30px;
height: 100%;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-sizer {
position: relative;
border-right: 30px solid transparent;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
before actuall scrolling happens, thus preventing shaking and
flickering artifacts. */
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
position: absolute;
z-index: 6;
display: none;
}
.CodeMirror-vscrollbar {
right: 0; top: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.CodeMirror-hscrollbar {
bottom: 0; left: 0;
overflow-y: hidden;
overflow-x: scroll;
}
.CodeMirror-scrollbar-filler {
right: 0; bottom: 0;
}
.CodeMirror-gutter-filler {
left: 0; bottom: 0;
}
.CodeMirror-gutters {
position: absolute; left: 0; top: 0;
padding-bottom: 30px;
z-index: 3;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
-moz-box-sizing: content-box;
box-sizing: content-box;
padding-bottom: 30px;
margin-bottom: -32px;
display: inline-block;
/* Hack to make IE7 behave */
*zoom:1;
*display:inline;
}
.CodeMirror-gutter-elt {
position: absolute;
cursor: default;
z-index: 4;
}
.CodeMirror-lines {
cursor: text;
min-height: 1px; /* prevents collapsing before first draw */
}
.CodeMirror pre {
/* Reset some styles that the rest of the page might have set */
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
word-wrap: normal;
line-height: inherit;
color: inherit;
z-index: 2;
position: relative;
overflow: visible;
}
.CodeMirror-wrap pre {
word-wrap: break-word;
white-space: pre-wrap;
word-break: normal;
}
.CodeMirror-linebackground {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
z-index: 0;
}
.CodeMirror-linewidget {
position: relative;
z-index: 2;
overflow: auto;
}
.CodeMirror-widget {}
.CodeMirror-wrap .CodeMirror-scroll {
overflow-x: hidden;
}
.CodeMirror-measure {
position: absolute;
width: 100%;
height: 0;
overflow: hidden;
visibility: hidden;
}
.CodeMirror-measure pre { position: static; }
.CodeMirror div.CodeMirror-cursor {
position: absolute;
border-right: none;
width: 0;
}
div.CodeMirror-cursors {
visibility: hidden;
position: relative;
z-index: 3;
}
.CodeMirror-focused div.CodeMirror-cursors {
visibility: visible;
}
.CodeMirror-selected { background: #d9d9d9; }
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
.CodeMirror-crosshair { cursor: crosshair; }
.cm-searching {
background: #ffa;
background: rgba(255, 255, 0, .4);
}
/* IE7 hack to prevent it from returning funny offsetTops on the spans */
.CodeMirror span { *vertical-align: text-bottom; }
/* Used to force a border model for a node */
.cm-force-border { padding-right: .1px; }
@media print {
/* Hide the cursor when printing */
.CodeMirror div.CodeMirror-cursors {
visibility: hidden;
}
}
/* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext { background: none; }
...@@ -528,6 +528,7 @@ jumpserver.initServerSideDataTable = function (options) { ...@@ -528,6 +528,7 @@ jumpserver.initServerSideDataTable = function (options) {
lengthMenu: [[10, 15, 25, 50], [10, 15, 25, 50]] lengthMenu: [[10, 15, 25, 50], [10, 15, 25, 50]]
}); });
table.selected = []; table.selected = [];
table.selected_rows = [];
table.on('select', function(e, dt, type, indexes) { table.on('select', function(e, dt, type, indexes) {
var $node = table[ type ]( indexes ).nodes().to$(); var $node = table[ type ]( indexes ).nodes().to$();
$node.find('input.ipt_check').prop('checked', true); $node.find('input.ipt_check').prop('checked', true);
...@@ -535,7 +536,8 @@ jumpserver.initServerSideDataTable = function (options) { ...@@ -535,7 +536,8 @@ jumpserver.initServerSideDataTable = function (options) {
if (type === 'row') { if (type === 'row') {
var rows = table.rows(indexes).data(); var rows = table.rows(indexes).data();
$.each(rows, function (id, row) { $.each(rows, function (id, row) {
if (row.id){ table.selected_rows.push(row);
if (row.id && $.inArray(row.id, table.selected) === -1){
table.selected.push(row.id) table.selected.push(row.id)
} }
}) })
...@@ -812,3 +814,32 @@ function initPopover($container, $progress, $idPassword, $el, password_check_rul ...@@ -812,3 +814,32 @@ function initPopover($container, $progress, $idPassword, $el, password_check_rul
$idPassword.pwstrength(options); $idPassword.pwstrength(options);
popoverPasswordRules(password_check_rules, $el); popoverPasswordRules(password_check_rules, $el);
} }
// 解决input框中的资产和弹出表格中资产的显示不一致
function initSelectedAssets2Table(){
var inputAssets = $('#id_assets').val();
var selectedAssets = asset_table2.selected.concat();
// input assets无,table assets选中,则取消勾选(再次click)
if (selectedAssets.length !== 0){
$.each(selectedAssets, function (index, assetId){
if ($.inArray(assetId, inputAssets) === -1){
$('#'+assetId).trigger('click'); // 取消勾选
}
});
}
// input assets有,table assets没选,则选中(click)
if (inputAssets !== null){
asset_table2.selected = inputAssets;
$.each(inputAssets, function(index, assetId){
var dom = document.getElementById(assetId);
if (dom !== null){
var selected = dom.parentElement.parentElement.className.indexOf('selected')
}
if (selected === -1){
$('#'+assetId).trigger('click');
}
});
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
<!doctype html>
<title>CodeMirror: Language Modes</title>
<meta charset="utf-8"/>
<link rel=stylesheet href="../doc/docs.css">
<div id=nav>
<a href="http://codemirror.net"><h1>CodeMirror</h1><img id=logo src="../doc/logo.png"></a>
<ul>
<li><a href="../index.html">Home</a>
<li><a href="../doc/manual.html">Manual</a>
<li><a href="https://github.com/codemirror/codemirror">Code</a>
</ul>
<ul>
<li><a class=active href="#">Language modes</a>
</ul>
</div>
<article>
<h2>Language modes</h2>
<p>This is a list of every mode in the distribution. Each mode lives
in a subdirectory of the <code>mode/</code> directory, and typically
defines a single JavaScript file that implements the mode. Loading
such file will make the language available to CodeMirror, through
the <a href="manual.html#option_mode"><code>mode</code></a>
option.</p>
<div style="-webkit-columns: 100px 2; -moz-columns: 100px 2; columns: 100px 2;">
<ul style="margin-top: 0">
<li><a href="apl/index.html">APL</a></li>
<li><a href="asterisk/index.html">Asterisk dialplan</a></li>
<li><a href="clike/index.html">C, C++, C#</a></li>
<li><a href="clojure/index.html">Clojure</a></li>
<li><a href="cobol/index.html">COBOL</a></li>
<li><a href="coffeescript/index.html">CoffeeScript</a></li>
<li><a href="commonlisp/index.html">Common Lisp</a></li>
<li><a href="css/index.html">CSS</a></li>
<li><a href="cypher/index.html">Cypher</a></li>
<li><a href="python/index.html">Cython</a></li>
<li><a href="d/index.html">D</a></li>
<li><a href="django/index.html">Django</a> (templating language)</li>
<li><a href="diff/index.html">diff</a></li>
<li><a href="dtd/index.html">DTD</a></li>
<li><a href="dylan/index.html">Dylan</a></li>
<li><a href="ecl/index.html">ECL</a></li>
<li><a href="eiffel/index.html">Eiffel</a></li>
<li><a href="erlang/index.html">Erlang</a></li>
<li><a href="fortran/index.html">Fortran</a></li>
<li><a href="mllike/index.html">F#</a></li>
<li><a href="gas/index.html">Gas</a> (AT&amp;T-style assembly)</li>
<li><a href="gherkin/index.html">Gherkin</a></li>
<li><a href="go/index.html">Go</a></li>
<li><a href="groovy/index.html">Groovy</a></li>
<li><a href="haml/index.html">HAML</a></li>
<li><a href="haskell/index.html">Haskell</a></li>
<li><a href="haxe/index.html">Haxe</a></li>
<li><a href="htmlembedded/index.html">HTML embedded scripts</a></li>
<li><a href="htmlmixed/index.html">HTML mixed-mode</a></li>
<li><a href="http/index.html">HTTP</a></li>
<li><a href="clike/index.html">Java</a></li>
<li><a href="jade/index.html">Jade</a></li>
<li><a href="javascript/index.html">JavaScript</a></li>
<li><a href="jinja2/index.html">Jinja2</a></li>
<li><a href="julia/index.html">Julia</a></li>
<li><a href="kotlin/index.html">Kotlin</a></li>
<li><a href="css/less.html">LESS</a></li>
<li><a href="livescript/index.html">LiveScript</a></li>
<li><a href="lua/index.html">Lua</a></li>
<li><a href="markdown/index.html">Markdown</a> (<a href="gfm/index.html">GitHub-flavour</a>)</li>
<li><a href="mirc/index.html">mIRC</a></li>
<li><a href="modelica/index.html">Modelica</a></li>
<li><a href="nginx/index.html">Nginx</a></li>
<li><a href="ntriples/index.html">NTriples</a></li>
<li><a href="mllike/index.html">OCaml</a></li>
<li><a href="octave/index.html">Octave</a> (MATLAB)</li>
<li><a href="pascal/index.html">Pascal</a></li>
<li><a href="pegjs/index.html">PEG.js</a></li>
<li><a href="perl/index.html">Perl</a></li>
<li><a href="php/index.html">PHP</a></li>
<li><a href="pig/index.html">Pig Latin</a></li>
<li><a href="properties/index.html">Properties files</a></li>
<li><a href="puppet/index.html">Puppet</a></li>
<li><a href="python/index.html">Python</a></li>
<li><a href="q/index.html">Q</a></li>
<li><a href="r/index.html">R</a></li>
<li><a href="rpm/index.html">RPM</a></li>
<li><a href="rst/index.html">reStructuredText</a></li>
<li><a href="ruby/index.html">Ruby</a></li>
<li><a href="rust/index.html">Rust</a></li>
<li><a href="sass/index.html">Sass</a></li>
<li><a href="clike/scala.html">Scala</a></li>
<li><a href="scheme/index.html">Scheme</a></li>
<li><a href="css/scss.html">SCSS</a></li>
<li><a href="shell/index.html">Shell</a></li>
<li><a href="sieve/index.html">Sieve</a></li>
<li><a href="slim/index.html">Slim</a></li>
<li><a href="smalltalk/index.html">Smalltalk</a></li>
<li><a href="smarty/index.html">Smarty</a></li>
<li><a href="smartymixed/index.html">Smarty/HTML mixed</a></li>
<li><a href="solr/index.html">Solr</a></li>
<li><a href="sql/index.html">SQL</a> (several dialects)</li>
<li><a href="sparql/index.html">SPARQL</a></li>
<li><a href="stex/index.html">sTeX, LaTeX</a></li>
<li><a href="tcl/index.html">Tcl</a></li>
<li><a href="textile/index.html">Textile</a></li>
<li><a href="tiddlywiki/index.html">Tiddlywiki</a></li>
<li><a href="tiki/index.html">Tiki wiki</a></li>
<li><a href="toml/index.html">TOML</a></li>
<li><a href="tornado/index.html">Tornado</a> (templating language)</li>
<li><a href="turtle/index.html">Turtle</a></li>
<li><a href="vb/index.html">VB.NET</a></li>
<li><a href="vbscript/index.html">VBScript</a></li>
<li><a href="velocity/index.html">Velocity</a></li>
<li><a href="verilog/index.html">Verilog/SystemVerilog</a></li>
<li><a href="xml/index.html">XML/HTML</a></li>
<li><a href="xquery/index.html">XQuery</a></li>
<li><a href="yaml/index.html">YAML</a></li>
<li><a href="z80/index.html">Z80</a></li>
</ul>
</div>
</article>
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
CodeMirror.modeInfo = [
{name: "APL", mime: "text/apl", mode: "apl", ext: ["dyalog", "apl"]},
{name: "Asterisk", mime: "text/x-asterisk", mode: "asterisk"},
{name: "C", mime: "text/x-csrc", mode: "clike", ext: ["c", "h"]},
{name: "C++", mime: "text/x-c++src", mode: "clike", ext: ["cpp", "c++", "hpp", "h++"]},
{name: "Cobol", mime: "text/x-cobol", mode: "cobol", ext: ["cob", "cpy"]},
{name: "C#", mime: "text/x-csharp", mode: "clike", ext: ["cs"]},
{name: "Clojure", mime: "text/x-clojure", mode: "clojure", ext: ["clj"]},
{name: "CoffeeScript", mime: "text/x-coffeescript", mode: "coffeescript", ext: ["coffee"]},
{name: "Common Lisp", mime: "text/x-common-lisp", mode: "commonlisp", ext: ["cl", "lisp", "el"]},
{name: "Cypher", mime: "application/x-cypher-query", mode: "cypher"},
{name: "Cython", mime: "text/x-cython", mode: "python", ext: ["pyx", "pxd", "pxi"]},
{name: "CSS", mime: "text/css", mode: "css", ext: ["css"]},
{name: "CQL", mime: "text/x-cassandra", mode: "sql", ext: ["cql"]},
{name: "D", mime: "text/x-d", mode: "d", ext: ["d"]},
{name: "diff", mime: "text/x-diff", mode: "diff", ext: ["diff", "patch"]},
{name: "DTD", mime: "application/xml-dtd", mode: "dtd", ext: ["dtd"]},
{name: "Dylan", mime: "text/x-dylan", mode: "dylan", ext: ["dylan", "dyl", "intr"]},
{name: "ECL", mime: "text/x-ecl", mode: "ecl", ext: ["ecl"]},
{name: "Eiffel", mime: "text/x-eiffel", mode: "eiffel", ext: ["e"]},
{name: "Embedded Javascript", mime: "application/x-ejs", mode: "htmlembedded", ext: ["ejs"]},
{name: "Erlang", mime: "text/x-erlang", mode: "erlang", ext: ["erl"]},
{name: "Fortran", mime: "text/x-fortran", mode: "fortran", ext: ["f", "for", "f77", "f90"]},
{name: "F#", mime: "text/x-fsharp", mode: "mllike", ext: ["fs"]},
{name: "Gas", mime: "text/x-gas", mode: "gas", ext: ["s"]},
{name: "Gherkin", mime: "text/x-feature", mode: "gherkin", ext: ["feature"]},
{name: "GitHub Flavored Markdown", mime: "text/x-gfm", mode: "gfm"},
{name: "Go", mime: "text/x-go", mode: "go", ext: ["go"]},
{name: "Groovy", mime: "text/x-groovy", mode: "groovy", ext: ["groovy"]},
{name: "HAML", mime: "text/x-haml", mode: "haml", ext: ["haml"]},
{name: "Haskell", mime: "text/x-haskell", mode: "haskell", ext: ["hs"]},
{name: "Haxe", mime: "text/x-haxe", mode: "haxe", ext: ["hx"]},
{name: "HXML", mime: "text/x-hxml", mode: "haxe", ext: ["hxml"]},
{name: "ASP.NET", mime: "application/x-aspx", mode: "htmlembedded", ext: ["aspx"]},
{name: "HTML", mime: "text/html", mode: "htmlmixed", ext: ["html", "htm"]},
{name: "HTTP", mime: "message/http", mode: "http"},
{name: "Jade", mime: "text/x-jade", mode: "jade", ext: ["jade"]},
{name: "Java", mime: "text/x-java", mode: "clike", ext: ["java"]},
{name: "Java Server Pages", mime: "application/x-jsp", mode: "htmlembedded", ext: ["jsp"]},
{name: "JavaScript", mimes: ["text/javascript", "text/ecmascript", "application/javascript", "application/x-javascript", "application/ecmascript"],
mode: "javascript", ext: ["js"]},
{name: "JSON", mimes: ["application/json", "application/x-json"], mode: "javascript", ext: ["json", "map"]},
{name: "JSON-LD", mime: "application/ld+json", mode: "javascript"},
{name: "Jinja2", mime: "null", mode: "jinja2"},
{name: "Julia", mime: "text/x-julia", mode: "julia", ext: ["jl"]},
{name: "Kotlin", mime: "text/x-kotlin", mode: "kotlin", ext: ["kt"]},
{name: "LESS", mime: "text/x-less", mode: "css", ext: ["less"]},
{name: "LiveScript", mime: "text/x-livescript", mode: "livescript", ext: ["ls"]},
{name: "Lua", mime: "text/x-lua", mode: "lua", ext: ["lua"]},
{name: "Markdown (GitHub-flavour)", mime: "text/x-markdown", mode: "markdown", ext: ["markdown", "md", "mkd"]},
{name: "mIRC", mime: "text/mirc", mode: "mirc"},
{name: "MariaDB SQL", mime: "text/x-mariadb", mode: "sql"},
{name: "Modelica", mime: "text/x-modelica", mode: "modelica", ext: ["mo"]},
{name: "MS SQL", mime: "text/x-mssql", mode: "sql"},
{name: "MySQL", mime: "text/x-mysql", mode: "sql"},
{name: "Nginx", mime: "text/x-nginx-conf", mode: "nginx"},
{name: "NTriples", mime: "text/n-triples", mode: "ntriples", ext: ["nt"]},
{name: "OCaml", mime: "text/x-ocaml", mode: "mllike", ext: ["ml", "mli", "mll", "mly"]},
{name: "Octave", mime: "text/x-octave", mode: "octave", ext: ["m"]},
{name: "Pascal", mime: "text/x-pascal", mode: "pascal", ext: ["p", "pas"]},
{name: "PEG.js", mime: "null", mode: "pegjs"},
{name: "Perl", mime: "text/x-perl", mode: "perl", ext: ["pl", "pm"]},
{name: "PHP", mime: "application/x-httpd-php", mode: "php", ext: ["php", "php3", "php4", "php5", "phtml"]},
{name: "Pig", mime: "text/x-pig", mode: "pig"},
{name: "Plain Text", mime: "text/plain", mode: "null", ext: ["txt", "text", "conf", "def", "list", "log"]},
{name: "PLSQL", mime: "text/x-plsql", mode: "sql"},
{name: "Properties files", mime: "text/x-properties", mode: "properties", ext: ["properties", "ini", "in"]},
{name: "Python", mime: "text/x-python", mode: "python", ext: ["py", "pyw"]},
{name: "Puppet", mime: "text/x-puppet", mode: "puppet", ext: ["pp"]},
{name: "Q", mime: "text/x-q", mode: "q", ext: ["q"]},
{name: "R", mime: "text/x-rsrc", mode: "r", ext: ["r"]},
{name: "reStructuredText", mime: "text/x-rst", mode: "rst", ext: ["rst"]},
{name: "Ruby", mime: "text/x-ruby", mode: "ruby", ext: ["rb"]},
{name: "Rust", mime: "text/x-rustsrc", mode: "rust", ext: ["rs"]},
{name: "Sass", mime: "text/x-sass", mode: "sass", ext: ["sass"]},
{name: "Scala", mime: "text/x-scala", mode: "clike", ext: ["scala"]},
{name: "Scheme", mime: "text/x-scheme", mode: "scheme", ext: ["scm", "ss"]},
{name: "SCSS", mime: "text/x-scss", mode: "css", ext: ["scss"]},
{name: "Shell", mime: "text/x-sh", mode: "shell", ext: ["sh", "ksh", "bash"]},
{name: "Sieve", mime: "application/sieve", mode: "sieve"},
{name: "Slim", mimes: ["text/x-slim", "application/x-slim"], mode: "slim"},
{name: "Smalltalk", mime: "text/x-stsrc", mode: "smalltalk", ext: ["st"]},
{name: "Smarty", mime: "text/x-smarty", mode: "smarty", ext: ["tpl"]},
{name: "SmartyMixed", mime: "text/x-smarty", mode: "smartymixed"},
{name: "Solr", mime: "text/x-solr", mode: "solr"},
{name: "SPARQL", mime: "application/x-sparql-query", mode: "sparql", ext: ["sparql"]},
{name: "SQL", mime: "text/x-sql", mode: "sql", ext: ["sql"]},
{name: "MariaDB", mime: "text/x-mariadb", mode: "sql"},
{name: "sTeX", mime: "text/x-stex", mode: "stex"},
{name: "LaTeX", mime: "text/x-latex", mode: "stex", ext: ["text", "ltx"]},
{name: "SystemVerilog", mime: "text/x-systemverilog", mode: "verilog", ext: ["v"]},
{name: "Tcl", mime: "text/x-tcl", mode: "tcl", ext: ["tcl"]},
{name: "Textile", mime: "text/x-textile", mode: "textile"},
{name: "TiddlyWiki ", mime: "text/x-tiddlywiki", mode: "tiddlywiki"},
{name: "Tiki wiki", mime: "text/tiki", mode: "tiki"},
{name: "TOML", mime: "text/x-toml", mode: "toml"},
{name: "Tornado", mime: "text/x-tornado", mode: "tornado"},
{name: "Turtle", mime: "text/turtle", mode: "turtle", ext: ["ttl"]},
{name: "TypeScript", mime: "application/typescript", mode: "javascript", ext: ["ts"]},
{name: "VB.NET", mime: "text/x-vb", mode: "vb", ext: ["vb"]},
{name: "VBScript", mime: "text/vbscript", mode: "vbscript"},
{name: "Velocity", mime: "text/velocity", mode: "velocity", ext: ["vtl"]},
{name: "Verilog", mime: "text/x-verilog", mode: "verilog", ext: ["v"]},
{name: "XML", mimes: ["application/xml", "text/xml"], mode: "xml", ext: ["xml", "xsl", "xsd"]},
{name: "XQuery", mime: "application/xquery", mode: "xquery", ext: ["xy", "xquery"]},
{name: "YAML", mime: "text/x-yaml", mode: "yaml", ext: ["yaml"]},
{name: "Z80", mime: "text/x-z80", mode: "z80", ext: ["z80"]}
];
// Ensure all modes have a mime property for backwards compatibility
for (var i = 0; i < CodeMirror.modeInfo.length; i++) {
var info = CodeMirror.modeInfo[i];
if (info.mimes) info.mime = info.mimes[0];
}
CodeMirror.findModeByMIME = function(mime) {
for (var i = 0; i < CodeMirror.modeInfo.length; i++) {
var info = CodeMirror.modeInfo[i];
if (info.mime == mime) return info;
if (info.mimes) for (var j = 0; j < info.mimes.length; j++)
if (info.mimes[j] == mime) return info;
}
};
CodeMirror.findModeByExtension = function(ext) {
for (var i = 0; i < CodeMirror.modeInfo.length; i++) {
var info = CodeMirror.modeInfo[i];
if (info.ext) for (var j = 0; j < info.ext.length; j++)
if (info.ext[j] == ext) return info;
}
};
});
<!doctype html>
<title>CodeMirror: Shell mode</title>
<meta charset="utf-8"/>
<link rel=stylesheet href="../../doc/docs.css">
<link rel=stylesheet href=../../lib/codemirror.css>
<script src=../../lib/codemirror.js></script>
<script src="../../addon/edit/matchbrackets.js"></script>
<script src=shell.js></script>
<style type=text/css>
.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}
</style>
<div id=nav>
<a href="http://codemirror.net"><h1>CodeMirror</h1><img id=logo src="../../doc/logo.png"></a>
<ul>
<li><a href="../../index.html">Home</a>
<li><a href="../../doc/manual.html">Manual</a>
<li><a href="https://github.com/codemirror/codemirror">Code</a>
</ul>
<ul>
<li><a href="../index.html">Language modes</a>
<li><a class=active href="#">Shell</a>
</ul>
</div>
<article>
<h2>Shell mode</h2>
<textarea id=code>
#!/bin/bash
# clone the repository
git clone http://github.com/garden/tree
# generate HTTPS credentials
cd tree
openssl genrsa -aes256 -out https.key 1024
openssl req -new -nodes -key https.key -out https.csr
openssl x509 -req -days 365 -in https.csr -signkey https.key -out https.crt
cp https.key{,.orig}
openssl rsa -in https.key.orig -out https.key
# start the server in HTTPS mode
cd web
sudo node ../server.js 443 'yes' &gt;&gt; ../node.log &amp;
# here is how to stop the server
for pid in `ps aux | grep 'node ../server.js' | awk '{print $2}'` ; do
sudo kill -9 $pid 2&gt; /dev/null
done
exit 0</textarea>
<script>
var editor = CodeMirror.fromTextArea(document.getElementById('code'), {
mode: 'shell',
lineNumbers: true,
matchBrackets: true
});
</script>
<p><strong>MIME types defined:</strong> <code>text/x-sh</code>.</p>
</article>
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
CodeMirror.defineMode('shell', function() {
var words = {};
function define(style, string) {
var split = string.split(' ');
for(var i = 0; i < split.length; i++) {
words[split[i]] = style;
}
};
// Atoms
define('atom', 'true false');
// Keywords
define('keyword', 'if then do else elif while until for in esac fi fin ' +
'fil done exit set unset export function');
// Commands
define('builtin', 'ab awk bash beep cat cc cd chown chmod chroot clear cp ' +
'curl cut diff echo find gawk gcc get git grep kill killall ln ls make ' +
'mkdir openssl mv nc node npm ping ps restart rm rmdir sed service sh ' +
'shopt shred source sort sleep ssh start stop su sudo tee telnet top ' +
'touch vi vim wall wc wget who write yes zsh');
function tokenBase(stream, state) {
if (stream.eatSpace()) return null;
var sol = stream.sol();
var ch = stream.next();
if (ch === '\\') {
stream.next();
return null;
}
if (ch === '\'' || ch === '"' || ch === '`') {
state.tokens.unshift(tokenString(ch));
return tokenize(stream, state);
}
if (ch === '#') {
if (sol && stream.eat('!')) {
stream.skipToEnd();
return 'meta'; // 'comment'?
}
stream.skipToEnd();
return 'comment';
}
if (ch === '$') {
state.tokens.unshift(tokenDollar);
return tokenize(stream, state);
}
if (ch === '+' || ch === '=') {
return 'operator';
}
if (ch === '-') {
stream.eat('-');
stream.eatWhile(/\w/);
return 'attribute';
}
if (/\d/.test(ch)) {
stream.eatWhile(/\d/);
if(stream.eol() || !/\w/.test(stream.peek())) {
return 'number';
}
}
stream.eatWhile(/[\w-]/);
var cur = stream.current();
if (stream.peek() === '=' && /\w+/.test(cur)) return 'def';
return words.hasOwnProperty(cur) ? words[cur] : null;
}
function tokenString(quote) {
return function(stream, state) {
var next, end = false, escaped = false;
while ((next = stream.next()) != null) {
if (next === quote && !escaped) {
end = true;
break;
}
if (next === '$' && !escaped && quote !== '\'') {
escaped = true;
stream.backUp(1);
state.tokens.unshift(tokenDollar);
break;
}
escaped = !escaped && next === '\\';
}
if (end || !escaped) {
state.tokens.shift();
}
return (quote === '`' || quote === ')' ? 'quote' : 'string');
};
};
var tokenDollar = function(stream, state) {
if (state.tokens.length > 1) stream.eat('$');
var ch = stream.next(), hungry = /\w/;
if (ch === '{') hungry = /[^}]/;
if (ch === '(') {
state.tokens[0] = tokenString(')');
return tokenize(stream, state);
}
if (!/\d/.test(ch)) {
stream.eatWhile(hungry);
stream.eat('}');
}
state.tokens.shift();
return 'def';
};
function tokenize(stream, state) {
return (state.tokens[0] || tokenBase) (stream, state);
};
return {
startState: function() {return {tokens:[]};},
token: function(stream, state) {
return tokenize(stream, state);
},
lineComment: '#'
};
});
CodeMirror.defineMIME('text/x-sh', 'shell');
});
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
(function() {
var mode = CodeMirror.getMode({}, "shell");
function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); }
MT("var",
"text [def $var] text");
MT("varBraces",
"text[def ${var}]text");
MT("varVar",
"text [def $a$b] text");
MT("varBracesVarBraces",
"text[def ${a}${b}]text");
MT("singleQuotedVar",
"[string 'text $var text']");
MT("singleQuotedVarBraces",
"[string 'text ${var} text']");
MT("doubleQuotedVar",
'[string "text ][def $var][string text"]');
MT("doubleQuotedVarBraces",
'[string "text][def ${var}][string text"]');
MT("doubleQuotedVarPunct",
'[string "text ][def $@][string text"]');
MT("doubleQuotedVarVar",
'[string "][def $a$b][string "]');
MT("doubleQuotedVarBracesVarBraces",
'[string "][def ${a}${b}][string "]');
MT("notAString",
"text\\'text");
MT("escapes",
"outside\\'\\\"\\`\\\\[string \"inside\\`\\'\\\"\\\\`\\$notAVar\"]outside\\$\\(notASubShell\\)");
MT("subshell",
"[builtin echo] [quote $(whoami)] s log, stardate [quote `date`].");
MT("doubleQuotedSubshell",
"[builtin echo] [string \"][quote $(whoami)][string 's log, stardate `date`.\"]");
MT("hashbang",
"[meta #!/bin/bash]");
MT("comment",
"text [comment # Blurb]");
MT("numbers",
"[number 0] [number 1] [number 2]");
MT("keywords",
"[keyword while] [atom true]; [keyword do]",
" [builtin sleep] [number 3]",
"[keyword done]");
MT("options",
"[builtin ls] [attribute -l] [attribute --human-readable]");
MT("operator",
"[def var][operator =]value");
})();
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.attach = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function attach(term, socket, bidirectional, buffered) {
var addonTerminal = term;
bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional;
addonTerminal.__socket = socket;
addonTerminal.__flushBuffer = function () {
addonTerminal.write(addonTerminal.__attachSocketBuffer);
addonTerminal.__attachSocketBuffer = null;
};
addonTerminal.__pushToBuffer = function (data) {
if (addonTerminal.__attachSocketBuffer) {
addonTerminal.__attachSocketBuffer += data;
}
else {
addonTerminal.__attachSocketBuffer = data;
setTimeout(addonTerminal.__flushBuffer, 10);
}
};
var myTextDecoder;
addonTerminal.__getMessage = function (ev) {
var _this = this;
var str;
if (typeof ev.data === 'object') {
if (!myTextDecoder) {
myTextDecoder = new TextDecoder();
}
if (ev.data instanceof ArrayBuffer) {
str = myTextDecoder.decode(ev.data);
displayData(str);
}
else {
var fileReader = new FileReader();
fileReader.addEventListener('load', function () {
str = myTextDecoder.decode(_this.result);
displayData(str);
});
fileReader.readAsArrayBuffer(ev.data);
}
}
else if (typeof ev.data === 'string') {
displayData(ev.data);
}
else {
throw Error("Cannot handle \"" + typeof ev.data + "\" websocket message.");
}
};
function displayData(str, data) {
if (buffered) {
addonTerminal.__pushToBuffer(str || data);
}
else {
addonTerminal.write(str || data);
}
}
addonTerminal.__sendData = function (data) {
if (socket.readyState !== 1) {
return;
}
socket.send(data);
};
addonTerminal._core.register(addSocketListener(socket, 'message', addonTerminal.__getMessage));
if (bidirectional) {
addonTerminal._core.register(addonTerminal.addDisposableListener('data', addonTerminal.__sendData));
}
addonTerminal._core.register(addSocketListener(socket, 'close', function () { return detach(addonTerminal, socket); }));
addonTerminal._core.register(addSocketListener(socket, 'error', function () { return detach(addonTerminal, socket); }));
}
exports.attach = attach;
function addSocketListener(socket, type, handler) {
socket.addEventListener(type, handler);
return {
dispose: function () {
if (!handler) {
return;
}
socket.removeEventListener(type, handler);
handler = null;
}
};
}
function detach(term, socket) {
var addonTerminal = term;
addonTerminal.off('data', addonTerminal.__sendData);
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
if (socket) {
socket.removeEventListener('message', addonTerminal.__getMessage);
}
delete addonTerminal.__socket;
}
exports.detach = detach;
function apply(terminalConstructor) {
terminalConstructor.prototype.attach = function (socket, bidirectional, buffered) {
attach(this, socket, bidirectional, buffered);
};
terminalConstructor.prototype.detach = function (socket) {
detach(this, socket);
};
}
exports.apply = apply;
},{}]},{},[1])(1)
});
//# sourceMappingURL=attach.js.map
{"version":3,"file":"attach.js","sources":["../../../src/addons/attach/attach.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2014 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Implements the attach method, that attaches the terminal to a WebSocket stream.\n */\n\nimport { Terminal, IDisposable } from 'xterm';\nimport { IAttachAddonTerminal } from './Interfaces';\n\n/**\n * Attaches the given terminal to the given socket.\n *\n * @param term The terminal to be attached to the given socket.\n * @param socket The socket to attach the current terminal.\n * @param bidirectional Whether the terminal should send data to the socket as well.\n * @param buffered Whether the rendering of incoming data should happen instantly or at a maximum\n * frequency of 1 rendering per 10ms.\n */\nexport function attach(term: Terminal, socket: WebSocket, bidirectional: boolean, buffered: boolean): void {\n const addonTerminal = <IAttachAddonTerminal>term;\n bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional;\n addonTerminal.__socket = socket;\n\n addonTerminal.__flushBuffer = () => {\n addonTerminal.write(addonTerminal.__attachSocketBuffer);\n addonTerminal.__attachSocketBuffer = null;\n };\n\n addonTerminal.__pushToBuffer = (data: string) => {\n if (addonTerminal.__attachSocketBuffer) {\n addonTerminal.__attachSocketBuffer += data;\n } else {\n addonTerminal.__attachSocketBuffer = data;\n setTimeout(addonTerminal.__flushBuffer, 10);\n }\n };\n\n // TODO: This should be typed but there seem to be issues importing the type\n let myTextDecoder: any;\n\n addonTerminal.__getMessage = function(ev: MessageEvent): void {\n let str: string;\n\n if (typeof ev.data === 'object') {\n if (!myTextDecoder) {\n myTextDecoder = new TextDecoder();\n }\n if (ev.data instanceof ArrayBuffer) {\n str = myTextDecoder.decode(ev.data);\n displayData(str);\n } else {\n const fileReader = new FileReader();\n\n fileReader.addEventListener('load', () => {\n str = myTextDecoder.decode(this.result);\n displayData(str);\n });\n fileReader.readAsArrayBuffer(ev.data);\n }\n } else if (typeof ev.data === 'string') {\n displayData(ev.data);\n } else {\n throw Error(`Cannot handle \"${typeof ev.data}\" websocket message.`);\n }\n };\n\n /**\n * Push data to buffer or write it in the terminal.\n * This is used as a callback for FileReader.onload.\n *\n * @param str String decoded by FileReader.\n * @param data The data of the EventMessage.\n */\n function displayData(str?: string, data?: string): void {\n if (buffered) {\n addonTerminal.__pushToBuffer(str || data);\n } else {\n addonTerminal.write(str || data);\n }\n }\n\n addonTerminal.__sendData = (data: string) => {\n if (socket.readyState !== 1) {\n return;\n }\n socket.send(data);\n };\n\n addonTerminal._core.register(addSocketListener(socket, 'message', addonTerminal.__getMessage));\n\n if (bidirectional) {\n addonTerminal._core.register(addonTerminal.addDisposableListener('data', addonTerminal.__sendData));\n }\n\n addonTerminal._core.register(addSocketListener(socket, 'close', () => detach(addonTerminal, socket)));\n addonTerminal._core.register(addSocketListener(socket, 'error', () => detach(addonTerminal, socket)));\n}\n\nfunction addSocketListener(socket: WebSocket, type: string, handler: (this: WebSocket, ev: Event) => any): IDisposable {\n socket.addEventListener(type, handler);\n return {\n dispose: () => {\n if (!handler) {\n // Already disposed\n return;\n }\n socket.removeEventListener(type, handler);\n handler = null;\n }\n };\n}\n\n/**\n * Detaches the given terminal from the given socket\n *\n * @param term The terminal to be detached from the given socket.\n * @param socket The socket from which to detach the current terminal.\n */\nexport function detach(term: Terminal, socket: WebSocket): void {\n const addonTerminal = <IAttachAddonTerminal>term;\n addonTerminal.off('data', addonTerminal.__sendData);\n\n socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;\n\n if (socket) {\n socket.removeEventListener('message', addonTerminal.__getMessage);\n }\n\n delete addonTerminal.__socket;\n}\n\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n /**\n * Attaches the current terminal to the given socket\n *\n * @param socket The socket to attach the current terminal.\n * @param bidirectional Whether the terminal should send data to the socket as well.\n * @param buffered Whether the rendering of incoming data should happen instantly or at a maximum\n * frequency of 1 rendering per 10ms.\n */\n (<any>terminalConstructor.prototype).attach = function (socket: WebSocket, bidirectional: boolean, buffered: boolean): void {\n attach(this, socket, bidirectional, buffered);\n };\n\n /**\n * Detaches the current terminal from the given socket.\n *\n * @param socket The socket from which to detach the current terminal.\n */\n (<any>terminalConstructor.prototype).detach = function (socket: WebSocket): void {\n detach(this, socket);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADmBA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAGA;AAEA;AAAA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AAAA;AACA;AACA;AACA;AASA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AAEA;AACA;AACA;AAEA;AACA;AACA;AA9EA;AAgFA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAQA;AACA;AACA;AAEA;AAEA;AACA;AACA;AAEA;AACA;AAXA;AAcA;AASA;AACA;AACA;AAOA;AACA;AACA;AACA;AArBA;"}
\ No newline at end of file
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.fit = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function proposeGeometry(term) {
if (!term.element.parentElement) {
return null;
}
var parentElementStyle = window.getComputedStyle(term.element.parentElement);
var parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
var parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
var elementStyle = window.getComputedStyle(term.element);
var elementPadding = {
top: parseInt(elementStyle.getPropertyValue('padding-top')),
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
right: parseInt(elementStyle.getPropertyValue('padding-right')),
left: parseInt(elementStyle.getPropertyValue('padding-left'))
};
var elementPaddingVer = elementPadding.top + elementPadding.bottom;
var elementPaddingHor = elementPadding.right + elementPadding.left;
var availableHeight = parentElementHeight - elementPaddingVer;
var availableWidth = parentElementWidth - elementPaddingHor - term._core.viewport.scrollBarWidth;
var geometry = {
cols: Math.floor(availableWidth / term._core.renderer.dimensions.actualCellWidth),
rows: Math.floor(availableHeight / term._core.renderer.dimensions.actualCellHeight)
};
return geometry;
}
exports.proposeGeometry = proposeGeometry;
function fit(term) {
var geometry = proposeGeometry(term);
if (geometry) {
if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
term._core.renderer.clear();
term.resize(geometry.cols, geometry.rows);
}
}
}
exports.fit = fit;
function apply(terminalConstructor) {
terminalConstructor.prototype.proposeGeometry = function () {
return proposeGeometry(this);
};
terminalConstructor.prototype.fit = function () {
fit(this);
};
}
exports.apply = apply;
},{}]},{},[1])(1)
});
//# sourceMappingURL=fit.js.map
{"version":3,"file":"fit.js","sources":["../../../src/addons/fit/fit.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2014 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Fit terminal columns and rows to the dimensions of its DOM element.\n *\n * ## Approach\n *\n * Rows: Truncate the division of the terminal parent element height by the\n * terminal row height.\n * Columns: Truncate the division of the terminal parent element width by the\n * terminal character width (apply display: inline at the terminal\n * row and truncate its width with the current number of columns).\n */\n\nimport { Terminal } from 'xterm';\n\nexport interface IGeometry {\n rows: number;\n cols: number;\n}\n\nexport function proposeGeometry(term: Terminal): IGeometry {\n if (!term.element.parentElement) {\n return null;\n }\n const parentElementStyle = window.getComputedStyle(term.element.parentElement);\n const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));\n const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));\n const elementStyle = window.getComputedStyle(term.element);\n const elementPadding = {\n top: parseInt(elementStyle.getPropertyValue('padding-top')),\n bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),\n right: parseInt(elementStyle.getPropertyValue('padding-right')),\n left: parseInt(elementStyle.getPropertyValue('padding-left'))\n };\n const elementPaddingVer = elementPadding.top + elementPadding.bottom;\n const elementPaddingHor = elementPadding.right + elementPadding.left;\n const availableHeight = parentElementHeight - elementPaddingVer;\n const availableWidth = parentElementWidth - elementPaddingHor - (<any>term)._core.viewport.scrollBarWidth;\n const geometry = {\n cols: Math.floor(availableWidth / (<any>term)._core.renderer.dimensions.actualCellWidth),\n rows: Math.floor(availableHeight / (<any>term)._core.renderer.dimensions.actualCellHeight)\n };\n return geometry;\n}\n\nexport function fit(term: Terminal): void {\n const geometry = proposeGeometry(term);\n if (geometry) {\n // Force a full render\n if (term.rows !== geometry.rows || term.cols !== geometry.cols) {\n (<any>term)._core.renderer.clear();\n term.resize(geometry.cols, geometry.rows);\n }\n }\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n (<any>terminalConstructor.prototype).proposeGeometry = function (): IGeometry {\n return proposeGeometry(this);\n };\n\n (<any>terminalConstructor.prototype).fit = function (): void {\n fit(this);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADsBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAvBA;AAyBA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AATA;AAWA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AARA;"}
\ No newline at end of file
.xterm.fullscreen {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: auto;
height: auto;
z-index: 255;
}
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.fullscreen = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function toggleFullScreen(term, fullscreen) {
var fn;
if (typeof fullscreen === 'undefined') {
fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';
}
else if (!fullscreen) {
fn = 'remove';
}
else {
fn = 'add';
}
term.element.classList[fn]('fullscreen');
}
exports.toggleFullScreen = toggleFullScreen;
function apply(terminalConstructor) {
terminalConstructor.prototype.toggleFullScreen = function (fullscreen) {
toggleFullScreen(this, fullscreen);
};
}
exports.apply = apply;
},{}]},{},[1])(1)
});
//# sourceMappingURL=fullscreen.js.map
{"version":3,"file":"fullscreen.js","sources":["../../../src/addons/fullscreen/fullscreen.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2014 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Terminal } from 'xterm';\n\n/**\n * Toggle the given terminal's fullscreen mode.\n * @param term The terminal to toggle full screen mode\n * @param fullscreen Toggle fullscreen on (true) or off (false)\n */\nexport function toggleFullScreen(term: Terminal, fullscreen: boolean): void {\n let fn: string;\n\n if (typeof fullscreen === 'undefined') {\n fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';\n } else if (!fullscreen) {\n fn = 'remove';\n } else {\n fn = 'add';\n }\n\n term.element.classList[fn]('fullscreen');\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n (<any>terminalConstructor.prototype).toggleFullScreen = function (fullscreen: boolean): void {\n toggleFullScreen(this, fullscreen);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADYA;AACA;AAEA;AACA;AACA;AAAA;AACA;AACA;AAAA;AACA;AACA;AAEA;AACA;AAZA;AAcA;AACA;AACA;AACA;AACA;AAJA;"}
\ No newline at end of file
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.search = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var SearchHelper = (function () {
function SearchHelper(_terminal) {
this._terminal = _terminal;
}
SearchHelper.prototype.findNext = function (term) {
if (!term || term.length === 0) {
return false;
}
var result;
var startRow = this._terminal._core.buffer.ydisp;
if (this._terminal._core.selectionManager.selectionEnd) {
startRow = this._terminal._core.selectionManager.selectionEnd[1];
}
for (var y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
result = this._findInLine(term, y);
if (result) {
break;
}
}
if (!result) {
for (var y = 0; y < startRow; y++) {
result = this._findInLine(term, y);
if (result) {
break;
}
}
}
return this._selectResult(result);
};
SearchHelper.prototype.findPrevious = function (term) {
if (!term || term.length === 0) {
return false;
}
var result;
var startRow = this._terminal._core.buffer.ydisp;
if (this._terminal._core.selectionManager.selectionStart) {
startRow = this._terminal._core.selectionManager.selectionStart[1];
}
for (var y = startRow - 1; y >= 0; y--) {
result = this._findInLine(term, y);
if (result) {
break;
}
}
if (!result) {
for (var y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
result = this._findInLine(term, y);
if (result) {
break;
}
}
}
return this._selectResult(result);
};
SearchHelper.prototype._findInLine = function (term, y) {
var lowerStringLine = this._terminal._core.buffer.translateBufferLineToString(y, true).toLowerCase();
var lowerTerm = term.toLowerCase();
var searchIndex = lowerStringLine.indexOf(lowerTerm);
if (searchIndex >= 0) {
var line = this._terminal._core.buffer.lines.get(y);
for (var i = 0; i < searchIndex; i++) {
var charData = line[i];
var char = charData[1];
if (char.length > 1) {
searchIndex -= char.length - 1;
}
var charWidth = charData[2];
if (charWidth === 0) {
searchIndex++;
}
}
return {
term: term,
col: searchIndex,
row: y
};
}
};
SearchHelper.prototype._selectResult = function (result) {
if (!result) {
return false;
}
this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length);
this._terminal.scrollLines(result.row - this._terminal._core.buffer.ydisp);
return true;
};
return SearchHelper;
}());
exports.SearchHelper = SearchHelper;
},{}],2:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var SearchHelper_1 = require("./SearchHelper");
function findNext(terminal, term) {
var addonTerminal = terminal;
if (!addonTerminal.__searchHelper) {
addonTerminal.__searchHelper = new SearchHelper_1.SearchHelper(addonTerminal);
}
return addonTerminal.__searchHelper.findNext(term);
}
exports.findNext = findNext;
function findPrevious(terminal, term) {
var addonTerminal = terminal;
if (!addonTerminal.__searchHelper) {
addonTerminal.__searchHelper = new SearchHelper_1.SearchHelper(addonTerminal);
}
return addonTerminal.__searchHelper.findPrevious(term);
}
exports.findPrevious = findPrevious;
function apply(terminalConstructor) {
terminalConstructor.prototype.findNext = function (term) {
return findNext(this, term);
};
terminalConstructor.prototype.findPrevious = function (term) {
return findPrevious(this, term);
};
}
exports.apply = apply;
},{"./SearchHelper":1}]},{},[2])(2)
});
//# sourceMappingURL=search.js.map
{"version":3,"file":"search.js","sources":["../../../src/addons/search/search.ts","../../../src/addons/search/SearchHelper.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { SearchHelper } from './SearchHelper';\nimport { Terminal } from 'xterm';\nimport { ISearchAddonTerminal } from './Interfaces';\n\n/**\n * Find the next instance of the term, then scroll to and select it. If it\n * doesn't exist, do nothing.\n * @param term Tne search term.\n * @return Whether a result was found.\n */\nexport function findNext(terminal: Terminal, term: string): boolean {\n const addonTerminal = <ISearchAddonTerminal>terminal;\n if (!addonTerminal.__searchHelper) {\n addonTerminal.__searchHelper = new SearchHelper(addonTerminal);\n }\n return addonTerminal.__searchHelper.findNext(term);\n}\n\n/**\n * Find the previous instance of the term, then scroll to and select it. If it\n * doesn't exist, do nothing.\n * @param term Tne search term.\n * @return Whether a result was found.\n */\nexport function findPrevious(terminal: Terminal, term: string): boolean {\n const addonTerminal = <ISearchAddonTerminal>terminal;\n if (!addonTerminal.__searchHelper) {\n addonTerminal.__searchHelper = new SearchHelper(addonTerminal);\n }\n return addonTerminal.__searchHelper.findPrevious(term);\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n (<any>terminalConstructor.prototype).findNext = function(term: string): boolean {\n return findNext(this, term);\n };\n\n (<any>terminalConstructor.prototype).findPrevious = function(term: string): boolean {\n return findPrevious(this, term);\n };\n}\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { ISearchHelper, ISearchAddonTerminal } from './Interfaces';\n\ninterface ISearchResult {\n term: string;\n col: number;\n row: number;\n}\n\n/**\n * A class that knows how to search the terminal and how to display the results.\n */\nexport class SearchHelper implements ISearchHelper {\n constructor(private _terminal: ISearchAddonTerminal) {\n // TODO: Search for multiple instances on 1 line\n // TODO: Don't use the actual selection, instead use a \"find selection\" so multiple instances can be highlighted\n // TODO: Highlight other instances in the viewport\n // TODO: Support regex, case sensitivity, etc.\n }\n\n /**\n * Find the next instance of the term, then scroll to and select it. If it\n * doesn't exist, do nothing.\n * @param term Tne search term.\n * @return Whether a result was found.\n */\n public findNext(term: string): boolean {\n if (!term || term.length === 0) {\n return false;\n }\n\n let result: ISearchResult;\n\n let startRow = this._terminal._core.buffer.ydisp;\n if (this._terminal._core.selectionManager.selectionEnd) {\n // Start from the selection end if there is a selection\n startRow = this._terminal._core.selectionManager.selectionEnd[1];\n }\n\n // Search from ydisp + 1 to end\n for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {\n result = this._findInLine(term, y);\n if (result) {\n break;\n }\n }\n\n // Search from the top to the current ydisp\n if (!result) {\n for (let y = 0; y < startRow; y++) {\n result = this._findInLine(term, y);\n if (result) {\n break;\n }\n }\n }\n\n // Set selection and scroll if a result was found\n return this._selectResult(result);\n }\n\n /**\n * Find the previous instance of the term, then scroll to and select it. If it\n * doesn't exist, do nothing.\n * @param term Tne search term.\n * @return Whether a result was found.\n */\n public findPrevious(term: string): boolean {\n if (!term || term.length === 0) {\n return false;\n }\n\n let result: ISearchResult;\n\n let startRow = this._terminal._core.buffer.ydisp;\n if (this._terminal._core.selectionManager.selectionStart) {\n // Start from the selection end if there is a selection\n startRow = this._terminal._core.selectionManager.selectionStart[1];\n }\n\n // Search from ydisp + 1 to end\n for (let y = startRow - 1; y >= 0; y--) {\n result = this._findInLine(term, y);\n if (result) {\n break;\n }\n }\n\n // Search from the top to the current ydisp\n if (!result) {\n for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {\n result = this._findInLine(term, y);\n if (result) {\n break;\n }\n }\n }\n\n // Set selection and scroll if a result was found\n return this._selectResult(result);\n }\n\n /**\n * Searches a line for a search term.\n * @param term Tne search term.\n * @param y The line to search.\n * @return The search result if it was found.\n */\n private _findInLine(term: string, y: number): ISearchResult {\n const lowerStringLine = this._terminal._core.buffer.translateBufferLineToString(y, true).toLowerCase();\n const lowerTerm = term.toLowerCase();\n let searchIndex = lowerStringLine.indexOf(lowerTerm);\n if (searchIndex >= 0) {\n const line = this._terminal._core.buffer.lines.get(y);\n for (let i = 0; i < searchIndex; i++) {\n const charData = line[i];\n // Adjust the searchIndex to normalize emoji into single chars\n const char = charData[1/*CHAR_DATA_CHAR_INDEX*/];\n if (char.length > 1) {\n searchIndex -= char.length - 1;\n }\n // Adjust the searchIndex for empty characters following wide unicode\n // chars (eg. CJK)\n const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/];\n if (charWidth === 0) {\n searchIndex++;\n }\n }\n return {\n term,\n col: searchIndex,\n row: y\n };\n }\n }\n\n /**\n * Selects and scrolls to a result.\n * @param result The result to select.\n * @return Whethera result was selected.\n */\n private _selectResult(result: ISearchResult): boolean {\n if (!result) {\n return false;\n }\n this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length);\n this._terminal.scrollLines(result.row - this._terminal._core.buffer.ydisp);\n return true;\n }\n}\n",null],"names":[],"mappings":"AEAA;;;ADgBA;AACA;AAAA;AAKA;AAQA;AACA;AACA;AACA;AAEA;AAEA;AACA;AAEA;AACA;AAGA;AACA;AACA;AACA;AACA;AACA;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAGA;AACA;AAQA;AACA;AACA;AACA;AAEA;AAEA;AACA;AAEA;AACA;AAGA;AACA;AACA;AACA;AACA;AACA;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAGA;AACA;AAQA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAOA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AAzIa;;;;;ADXb;AAUA;AACA;AACA;AACA;AACA;AACA;AACA;AANA;AAcA;AACA;AACA;AACA;AACA;AACA;AACA;AANA;AAQA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AARA;"}
\ No newline at end of file
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.terminado = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function terminadoAttach(term, socket, bidirectional, buffered) {
var addonTerminal = term;
bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional;
addonTerminal.__socket = socket;
addonTerminal.__flushBuffer = function () {
addonTerminal.write(addonTerminal.__attachSocketBuffer);
addonTerminal.__attachSocketBuffer = null;
};
addonTerminal.__pushToBuffer = function (data) {
if (addonTerminal.__attachSocketBuffer) {
addonTerminal.__attachSocketBuffer += data;
}
else {
addonTerminal.__attachSocketBuffer = data;
setTimeout(addonTerminal.__flushBuffer, 10);
}
};
addonTerminal.__getMessage = function (ev) {
var data = JSON.parse(ev.data);
if (data[0] === 'stdout') {
if (buffered) {
addonTerminal.__pushToBuffer(data[1]);
}
else {
addonTerminal.write(data[1]);
}
}
};
addonTerminal.__sendData = function (data) {
socket.send(JSON.stringify(['stdin', data]));
};
addonTerminal.__setSize = function (size) {
socket.send(JSON.stringify(['set_size', size.rows, size.cols]));
};
socket.addEventListener('message', addonTerminal.__getMessage);
if (bidirectional) {
addonTerminal.on('data', addonTerminal.__sendData);
}
addonTerminal.on('resize', addonTerminal.__setSize);
socket.addEventListener('close', function () { return terminadoDetach(addonTerminal, socket); });
socket.addEventListener('error', function () { return terminadoDetach(addonTerminal, socket); });
}
exports.terminadoAttach = terminadoAttach;
function terminadoDetach(term, socket) {
var addonTerminal = term;
addonTerminal.off('data', addonTerminal.__sendData);
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
if (socket) {
socket.removeEventListener('message', addonTerminal.__getMessage);
}
delete addonTerminal.__socket;
}
exports.terminadoDetach = terminadoDetach;
function apply(terminalConstructor) {
terminalConstructor.prototype.terminadoAttach = function (socket, bidirectional, buffered) {
return terminadoAttach(this, socket, bidirectional, buffered);
};
terminalConstructor.prototype.terminadoDetach = function (socket) {
return terminadoDetach(this, socket);
};
}
exports.apply = apply;
},{}]},{},[1])(1)
});
//# sourceMappingURL=terminado.js.map
{"version":3,"file":"terminado.js","sources":["../../../src/addons/terminado/terminado.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2016 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * This module provides methods for attaching a terminal to a terminado\n * WebSocket stream.\n */\n\nimport { Terminal } from 'xterm';\nimport { ITerminadoAddonTerminal } from './Interfaces';\n\n/**\n * Attaches the given terminal to the given socket.\n *\n * @param term The terminal to be attached to the given socket.\n * @param socket The socket to attach the current terminal.\n * @param bidirectional Whether the terminal should send data to the socket as well.\n * @param buffered Whether the rendering of incoming data should happen instantly or at a maximum\n * frequency of 1 rendering per 10ms.\n */\nexport function terminadoAttach(term: Terminal, socket: WebSocket, bidirectional: boolean, buffered: boolean): void {\n const addonTerminal = <ITerminadoAddonTerminal>term;\n bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional;\n addonTerminal.__socket = socket;\n\n addonTerminal.__flushBuffer = () => {\n addonTerminal.write(addonTerminal.__attachSocketBuffer);\n addonTerminal.__attachSocketBuffer = null;\n };\n\n addonTerminal.__pushToBuffer = (data: string) => {\n if (addonTerminal.__attachSocketBuffer) {\n addonTerminal.__attachSocketBuffer += data;\n } else {\n addonTerminal.__attachSocketBuffer = data;\n setTimeout(addonTerminal.__flushBuffer, 10);\n }\n };\n\n addonTerminal.__getMessage = (ev: MessageEvent) => {\n const data = JSON.parse(ev.data);\n if (data[0] === 'stdout') {\n if (buffered) {\n addonTerminal.__pushToBuffer(data[1]);\n } else {\n addonTerminal.write(data[1]);\n }\n }\n };\n\n addonTerminal.__sendData = (data: string) => {\n socket.send(JSON.stringify(['stdin', data]));\n };\n\n addonTerminal.__setSize = (size: {rows: number, cols: number}) => {\n socket.send(JSON.stringify(['set_size', size.rows, size.cols]));\n };\n\n socket.addEventListener('message', addonTerminal.__getMessage);\n\n if (bidirectional) {\n addonTerminal.on('data', addonTerminal.__sendData);\n }\n addonTerminal.on('resize', addonTerminal.__setSize);\n\n socket.addEventListener('close', () => terminadoDetach(addonTerminal, socket));\n socket.addEventListener('error', () => terminadoDetach(addonTerminal, socket));\n}\n\n/**\n * Detaches the given terminal from the given socket\n *\n * @param term The terminal to be detached from the given socket.\n * @param socket The socket from which to detach the current terminal.\n */\nexport function terminadoDetach(term: Terminal, socket: WebSocket): void {\n const addonTerminal = <ITerminadoAddonTerminal>term;\n addonTerminal.off('data', addonTerminal.__sendData);\n\n socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;\n\n if (socket) {\n socket.removeEventListener('message', addonTerminal.__getMessage);\n }\n\n delete addonTerminal.__socket;\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n /**\n * Attaches the current terminal to the given socket\n *\n * @param socket - The socket to attach the current terminal.\n * @param bidirectional - Whether the terminal should send data to the socket as well.\n * @param buffered - Whether the rendering of incoming data should happen instantly or at a\n * maximum frequency of 1 rendering per 10ms.\n */\n (<any>terminalConstructor.prototype).terminadoAttach = function (socket: WebSocket, bidirectional: boolean, buffered: boolean): void {\n return terminadoAttach(this, socket, bidirectional, buffered);\n };\n\n /**\n * Detaches the current terminal from the given socket.\n *\n * @param socket The socket from which to detach the current terminal.\n */\n (<any>terminalConstructor.prototype).terminadoDetach = function (socket: WebSocket): void {\n return terminadoDetach(this, socket);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADoBA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;AAEA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AA/CA;AAuDA;AACA;AACA;AAEA;AAEA;AACA;AACA;AAEA;AACA;AAXA;AAaA;AASA;AACA;AACA;AAOA;AACA;AACA;AACA;AArBA;"}
\ No newline at end of file
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.webLinks = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var protocolClause = '(https?:\\/\\/)';
var domainCharacterSet = '[\\da-z\\.-]+';
var negatedDomainCharacterSet = '[^\\da-z\\.-]+';
var domainBodyClause = '(' + domainCharacterSet + ')';
var tldClause = '([a-z\\.]{2,6})';
var ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
var localHostClause = '(localhost)';
var portClause = '(:\\d{1,5})';
var hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
var pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
var queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
var queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
var hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
var negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
var bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
var start = '(?:^|' + negatedDomainCharacterSet + ')(';
var end = ')($|' + negatedPathCharacterSet + ')';
var strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
function handleLink(event, uri) {
window.open(uri, '_blank');
}
function webLinksInit(term, handler, options) {
if (handler === void 0) { handler = handleLink; }
if (options === void 0) { options = {}; }
options.matchIndex = 1;
term.registerLinkMatcher(strictUrlRegex, handler, options);
}
exports.webLinksInit = webLinksInit;
function apply(terminalConstructor) {
terminalConstructor.prototype.webLinksInit = function (handler, options) {
webLinksInit(this, handler, options);
};
}
exports.apply = apply;
},{}]},{},[1])(1)
});
//# sourceMappingURL=webLinks.js.map
{"version":3,"file":"webLinks.js","sources":["../../../src/addons/webLinks/webLinks.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Terminal, ILinkMatcherOptions } from 'xterm';\n\nconst protocolClause = '(https?:\\\\/\\\\/)';\nconst domainCharacterSet = '[\\\\da-z\\\\.-]+';\nconst negatedDomainCharacterSet = '[^\\\\da-z\\\\.-]+';\nconst domainBodyClause = '(' + domainCharacterSet + ')';\nconst tldClause = '([a-z\\\\.]{2,6})';\nconst ipClause = '((\\\\d{1,3}\\\\.){3}\\\\d{1,3})';\nconst localHostClause = '(localhost)';\nconst portClause = '(:\\\\d{1,5})';\nconst hostClause = '((' + domainBodyClause + '\\\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';\nconst pathClause = '(\\\\/[\\\\/\\\\w\\\\.\\\\-%~]*)*';\nconst queryStringHashFragmentCharacterSet = '[0-9\\\\w\\\\[\\\\]\\\\(\\\\)\\\\/\\\\?\\\\!#@$%&\\'*+,:;~\\\\=\\\\.\\\\-]*';\nconst queryStringClause = '(\\\\?' + queryStringHashFragmentCharacterSet + ')?';\nconst hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';\nconst negatedPathCharacterSet = '[^\\\\/\\\\w\\\\.\\\\-%]+';\nconst bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;\nconst start = '(?:^|' + negatedDomainCharacterSet + ')(';\nconst end = ')($|' + negatedPathCharacterSet + ')';\nconst strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);\n\nfunction handleLink(event: MouseEvent, uri: string): void {\n window.open(uri, '_blank');\n}\n\n/**\n * Initialize the web links addon, registering the link matcher.\n * @param term The terminal to use web links within.\n * @param handler A custom handler to use.\n * @param options Custom options to use, matchIndex will always be ignored.\n */\nexport function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void {\n options.matchIndex = 1;\n term.registerLinkMatcher(strictUrlRegex, handler, options);\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n (<any>terminalConstructor.prototype).webLinksInit = function (handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): void {\n webLinksInit(this, handler, options);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADOA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAQA;AAAA;AAAA;AACA;AACA;AACA;AAHA;AAKA;AACA;AACA;AACA;AACA;AAJA;"}
\ No newline at end of file
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.winptyCompat = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function winptyCompatInit(terminal) {
var addonTerminal = terminal;
var isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
if (!isWindows) {
return;
}
addonTerminal.on('linefeed', function () {
var line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1);
var lastChar = line[addonTerminal.cols - 1];
if (lastChar[3] !== 32) {
var nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);
nextLine.isWrapped = true;
}
});
}
exports.winptyCompatInit = winptyCompatInit;
function apply(terminalConstructor) {
terminalConstructor.prototype.winptyCompatInit = function () {
winptyCompatInit(this);
};
}
exports.apply = apply;
},{}]},{},[1])(1)
});
//# sourceMappingURL=winptyCompat.js.map
{"version":3,"file":"winptyCompat.js","sources":["../../../src/addons/winptyCompat/winptyCompat.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Terminal } from 'xterm';\nimport { IWinptyCompatAddonTerminal } from './Interfaces';\n\nexport function winptyCompatInit(terminal: Terminal): void {\n const addonTerminal = <IWinptyCompatAddonTerminal>terminal;\n\n // Don't do anything when the platform is not Windows\n const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;\n if (!isWindows) {\n return;\n }\n\n // Winpty does not support wraparound mode which means that lines will never\n // be marked as wrapped. This causes issues for things like copying a line\n // retaining the wrapped new line characters or if consumers are listening\n // in on the data stream.\n //\n // The workaround for this is to listen to every incoming line feed and mark\n // the line as wrapped if the last character in the previous line is not a\n // space. This is certainly not without its problems, but generally on\n // Windows when text reaches the end of the terminal it's likely going to be\n // wrapped.\n addonTerminal.on('linefeed', () => {\n const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1);\n const lastChar = line[addonTerminal.cols - 1];\n\n if (lastChar[3] !== 32 /* ' ' */) {\n const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);\n (<any>nextLine).isWrapped = true;\n }\n });\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n (<any>terminalConstructor.prototype).winptyCompatInit = function (): void {\n winptyCompatInit(this);\n };\n}\n",null],"names":[],"mappings":"ACAA;;;ADQA;AACA;AAGA;AACA;AACA;AACA;AAYA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AA5BA;AA8BA;AACA;AACA;AACA;AACA;AAJA;"}
\ No newline at end of file
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.zmodem = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var zmodem;
function zmodemAttach(ws, opts) {
if (opts === void 0) { opts = {}; }
var term = this;
var senderFunc = function (octets) { return ws.send(new Uint8Array(octets)); };
var zsentry;
function shouldWrite() {
return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession;
}
zsentry = new zmodem.Sentry({
to_terminal: function (octets) {
if (shouldWrite()) {
term.write(String.fromCharCode.apply(String, octets));
}
},
sender: senderFunc,
on_retract: function () { return term.emit('zmodemRetract'); },
on_detect: function (detection) { return term.emit('zmodemDetect', detection); }
});
function handleWSMessage(evt) {
if (typeof evt.data === 'string') {
if (shouldWrite()) {
term.write(evt.data);
}
}
else {
zsentry.consume(evt.data);
}
}
ws.binaryType = 'arraybuffer';
ws.addEventListener('message', handleWSMessage);
}
function apply(terminalConstructor) {
zmodem = (typeof window === 'object') ? window.Zmodem : { Browser: null };
terminalConstructor.prototype.zmodemAttach = zmodemAttach;
terminalConstructor.prototype.zmodemBrowser = zmodem.Browser;
}
exports.apply = apply;
},{}]},{},[1])(1)
});
//# sourceMappingURL=zmodem.js.map
{"version":3,"file":"zmodem.js","sources":["../../../src/addons/zmodem/zmodem.ts","../../../node_modules/browser-pack/_prelude.js"],"sourcesContent":["/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Terminal } from 'xterm';\n\n/**\n *\n * Allow xterm.js to handle ZMODEM uploads and downloads.\n *\n * This addon is a wrapper around zmodem.js. It adds the following to the\n * Terminal class:\n *\n * - function `zmodemAttach(<WebSocket>, <Object>)` - creates a Zmodem.Sentry\n * on the passed WebSocket object. The Object passed is optional and\n * can contain:\n * - noTerminalWriteOutsideSession: Suppress writes from the Sentry\n * object to the Terminal while there is no active Session. This\n * is necessary for compatibility with, for example, the\n * `attach.js` addon.\n *\n * - event `zmodemDetect` - fired on Zmodem.Sentry’s `on_detect` callback.\n * Passes the zmodem.js Detection object.\n *\n * - event `zmodemRetract` - fired on Zmodem.Sentry’s `on_retract` callback.\n *\n * You’ll need to provide logic to handle uploads and downloads.\n * See zmodem.js’s documentation for more details.\n *\n * **IMPORTANT:** After you confirm() a zmodem.js Detection, if you have\n * used the `attach` or `terminado` addons, you’ll need to suspend their\n * operation for the duration of the ZMODEM session. (The demo does this\n * via `detach()` and a re-`attach()`.)\n */\n\nlet zmodem;\n\nexport interface IZmodemOptions {\n noTerminalWriteOutsideSession?: boolean;\n}\n\nfunction zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void {\n const term = this;\n const senderFunc = (octets: ArrayLike<number>) => ws.send(new Uint8Array(octets));\n\n let zsentry;\n\n function shouldWrite(): boolean {\n return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession;\n }\n\n zsentry = new zmodem.Sentry({\n to_terminal: (octets: ArrayLike<number>) => {\n if (shouldWrite()) {\n term.write(\n String.fromCharCode.apply(String, octets)\n );\n }\n },\n sender: senderFunc,\n on_retract: () => (<any>term).emit('zmodemRetract'),\n on_detect: (detection: any) => (<any>term).emit('zmodemDetect', detection)\n });\n\n function handleWSMessage(evt: MessageEvent): void {\n\n // In testing with xterm.js’s demo the first message was\n // always text even if the rest were binary. While that\n // may be specific to xterm.js’s demo, ultimately we\n // should reject anything that isn’t binary.\n if (typeof evt.data === 'string') {\n if (shouldWrite()) {\n term.write(evt.data);\n }\n }\n else {\n zsentry.consume(evt.data);\n }\n }\n\n ws.binaryType = 'arraybuffer';\n ws.addEventListener('message', handleWSMessage);\n}\n\nexport function apply(terminalConstructor: typeof Terminal): void {\n zmodem = (typeof window === 'object') ? (<any>window).Zmodem : {Browser: null}; // Nullify browser for tests\n\n (<any>terminalConstructor.prototype).zmodemAttach = zmodemAttach;\n (<any>terminalConstructor.prototype).zmodemBrowser = zmodem.Browser;\n}\n",null],"names":[],"mappings":"ACAA;;;ADoCA;AAMA;AAAA;AACA;AACA;AAEA;AAEA;AACA;AACA;AAEA;AACA;AACA;AACA;AAGA;AACA;AACA;AACA;AACA;AACA;AAEA;AAMA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AALA;"}
\ No newline at end of file
/** /**
* xterm.js: xterm, in the browser * Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License)
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* https://github.com/chjj/term.js * https://github.com/chjj/term.js
* @license MIT
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
...@@ -31,13 +31,11 @@ ...@@ -31,13 +31,11 @@
* other features. * other features.
*/ */
/* /**
* Default style for xterm.js * Default styles for xterm.js
*/ */
.terminal { .xterm {
background-color: #000;
color: #fff;
font-family: courier-new, courier, monospace; font-family: courier-new, courier, monospace;
font-feature-settings: "liga" 0; font-feature-settings: "liga" 0;
position: relative; position: relative;
...@@ -46,17 +44,22 @@ ...@@ -46,17 +44,22 @@
-webkit-user-select: none; -webkit-user-select: none;
} }
.terminal.focus, .xterm.focus,
.terminal:focus { .xterm:focus {
outline: none; outline: none;
} }
.terminal .xterm-helpers { .xterm .xterm-helpers {
position: absolute; position: absolute;
top: 0; top: 0;
/**
* The z-index of the helpers must be higher than the canvases in order for
* IMEs to appear on top.
*/
z-index: 10;
} }
.terminal .xterm-helper-textarea { .xterm .xterm-helper-textarea {
/* /*
* HACK: to fix IE's blinking cursor * HACK: to fix IE's blinking cursor
* Move textarea out of the screen to the far left, so that the cursor is not visible. * Move textarea out of the screen to the far left, so that the cursor is not visible.
...@@ -74,57 +77,8 @@ ...@@ -74,57 +77,8 @@
resize: none; resize: none;
} }
.terminal a { .xterm .composition-view {
color: inherit; /* TODO: Composition position got messed up somewhere */
text-decoration: none;
}
.terminal a:hover {
cursor: pointer;
text-decoration: underline;
}
.terminal a.xterm-invalid-link:hover {
cursor: text;
text-decoration: none;
}
.terminal .terminal-cursor {
position: relative;
}
.terminal:not(.focus) .terminal-cursor {
outline: 1px solid #fff;
outline-offset: -1px;
}
.terminal.xterm-cursor-style-block.focus:not(.xterm-cursor-blink-on) .terminal-cursor {
background-color: #fff;
color: #000;
}
.terminal.focus.xterm-cursor-style-bar:not(.xterm-cursor-blink-on) .terminal-cursor::before,
.terminal.focus.xterm-cursor-style-underline:not(.xterm-cursor-blink-on) .terminal-cursor::before {
content: '';
position: absolute;
background-color: #fff;
}
.terminal.focus.xterm-cursor-style-bar:not(.xterm-cursor-blink-on) .terminal-cursor::before {
top: 0;
left: 0;
bottom: 0;
width: 1px;
}
.terminal.focus.xterm-cursor-style-underline:not(.xterm-cursor-blink-on) .terminal-cursor::before {
bottom: 0;
left: 0;
right: 0;
height: 1px;
}
.terminal .composition-view {
background: #000; background: #000;
color: #FFF; color: #FFF;
display: none; display: none;
...@@ -133,2129 +87,78 @@ ...@@ -133,2129 +87,78 @@
z-index: 1; z-index: 1;
} }
.terminal .composition-view.active { .xterm .composition-view.active {
display: block; display: block;
} }
.terminal .xterm-viewport { .xterm .xterm-viewport {
/* On OS X this is required in order for the scroll bar to appear fully opaque */ /* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: #000; background-color: #000;
overflow-y: scroll; overflow-y: scroll;
cursor: default;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
} }
.terminal .xterm-wide-char, .xterm .xterm-screen {
.terminal .xterm-normal-char { position: relative;
display: inline-block;
} }
.terminal .xterm-rows { .xterm .xterm-screen canvas {
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
} }
.terminal .xterm-rows > div { .xterm .xterm-scroll-area {
/* Lines containing spans and text nodes ocassionally wrap despite being the same width (#327) */
white-space: nowrap;
}
.terminal .xterm-scroll-area {
visibility: hidden; visibility: hidden;
} }
.terminal .xterm-char-measure-element { .xterm-char-measure-element {
display: inline-block; display: inline-block;
visibility: hidden; visibility: hidden;
position: absolute;
left: -9999em;
}
.terminal.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
}
.terminal .xterm-selection {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: -9999em;
z-index: 1; line-height: normal;
opacity: 0.3;
pointer-events: none;
}
.terminal .xterm-selection div {
position: absolute;
background-color: #fff;
}
/*
* Determine default colors for xterm.js
*/
.terminal .xterm-bold {
font-weight: bold;
}
.terminal .xterm-underline {
text-decoration: underline;
}
.terminal .xterm-blink {
text-decoration: blink;
}
.terminal .xterm-blink.xterm-underline {
text-decoration: blink underline;
}
.terminal .xterm-hidden {
visibility: hidden;
}
.terminal .xterm-color-0 {
color: #2e3436;
}
.terminal .xterm-bg-color-0 {
background-color: #2e3436;
}
.terminal .xterm-color-1 {
color: #cc0000;
}
.terminal .xterm-bg-color-1 {
background-color: #cc0000;
}
.terminal .xterm-color-2 {
color: #4e9a06;
}
.terminal .xterm-bg-color-2 {
background-color: #4e9a06;
}
.terminal .xterm-color-3 {
color: #c4a000;
}
.terminal .xterm-bg-color-3 {
background-color: #c4a000;
}
.terminal .xterm-color-4 {
color: #3465a4;
}
.terminal .xterm-bg-color-4 {
background-color: #3465a4;
}
.terminal .xterm-color-5 {
color: #75507b;
}
.terminal .xterm-bg-color-5 {
background-color: #75507b;
}
.terminal .xterm-color-6 {
color: #06989a;
}
.terminal .xterm-bg-color-6 {
background-color: #06989a;
}
.terminal .xterm-color-7 {
color: #d3d7cf;
}
.terminal .xterm-bg-color-7 {
background-color: #d3d7cf;
}
.terminal .xterm-color-8 {
color: #555753;
}
.terminal .xterm-bg-color-8 {
background-color: #555753;
}
.terminal .xterm-color-9 {
color: #ef2929;
}
.terminal .xterm-bg-color-9 {
background-color: #ef2929;
}
.terminal .xterm-color-10 {
color: #8ae234;
}
.terminal .xterm-bg-color-10 {
background-color: #8ae234;
}
.terminal .xterm-color-11 {
color: #fce94f;
}
.terminal .xterm-bg-color-11 {
background-color: #fce94f;
}
.terminal .xterm-color-12 {
color: #729fcf;
}
.terminal .xterm-bg-color-12 {
background-color: #729fcf;
}
.terminal .xterm-color-13 {
color: #ad7fa8;
}
.terminal .xterm-bg-color-13 {
background-color: #ad7fa8;
}
.terminal .xterm-color-14 {
color: #34e2e2;
}
.terminal .xterm-bg-color-14 {
background-color: #34e2e2;
}
.terminal .xterm-color-15 {
color: #eeeeec;
}
.terminal .xterm-bg-color-15 {
background-color: #eeeeec;
}
.terminal .xterm-color-16 {
color: #000000;
}
.terminal .xterm-bg-color-16 {
background-color: #000000;
}
.terminal .xterm-color-17 {
color: #00005f;
}
.terminal .xterm-bg-color-17 {
background-color: #00005f;
}
.terminal .xterm-color-18 {
color: #000087;
}
.terminal .xterm-bg-color-18 {
background-color: #000087;
}
.terminal .xterm-color-19 {
color: #0000af;
}
.terminal .xterm-bg-color-19 {
background-color: #0000af;
}
.terminal .xterm-color-20 {
color: #0000d7;
}
.terminal .xterm-bg-color-20 {
background-color: #0000d7;
}
.terminal .xterm-color-21 {
color: #0000ff;
}
.terminal .xterm-bg-color-21 {
background-color: #0000ff;
}
.terminal .xterm-color-22 {
color: #005f00;
}
.terminal .xterm-bg-color-22 {
background-color: #005f00;
}
.terminal .xterm-color-23 {
color: #005f5f;
}
.terminal .xterm-bg-color-23 {
background-color: #005f5f;
}
.terminal .xterm-color-24 {
color: #005f87;
}
.terminal .xterm-bg-color-24 {
background-color: #005f87;
}
.terminal .xterm-color-25 {
color: #005faf;
}
.terminal .xterm-bg-color-25 {
background-color: #005faf;
}
.terminal .xterm-color-26 {
color: #005fd7;
}
.terminal .xterm-bg-color-26 {
background-color: #005fd7;
}
.terminal .xterm-color-27 {
color: #005fff;
}
.terminal .xterm-bg-color-27 {
background-color: #005fff;
}
.terminal .xterm-color-28 {
color: #008700;
}
.terminal .xterm-bg-color-28 {
background-color: #008700;
}
.terminal .xterm-color-29 {
color: #00875f;
}
.terminal .xterm-bg-color-29 {
background-color: #00875f;
}
.terminal .xterm-color-30 {
color: #008787;
}
.terminal .xterm-bg-color-30 {
background-color: #008787;
}
.terminal .xterm-color-31 {
color: #0087af;
}
.terminal .xterm-bg-color-31 {
background-color: #0087af;
}
.terminal .xterm-color-32 {
color: #0087d7;
}
.terminal .xterm-bg-color-32 {
background-color: #0087d7;
}
.terminal .xterm-color-33 {
color: #0087ff;
}
.terminal .xterm-bg-color-33 {
background-color: #0087ff;
}
.terminal .xterm-color-34 {
color: #00af00;
}
.terminal .xterm-bg-color-34 {
background-color: #00af00;
}
.terminal .xterm-color-35 {
color: #00af5f;
}
.terminal .xterm-bg-color-35 {
background-color: #00af5f;
}
.terminal .xterm-color-36 {
color: #00af87;
}
.terminal .xterm-bg-color-36 {
background-color: #00af87;
}
.terminal .xterm-color-37 {
color: #00afaf;
}
.terminal .xterm-bg-color-37 {
background-color: #00afaf;
}
.terminal .xterm-color-38 {
color: #00afd7;
}
.terminal .xterm-bg-color-38 {
background-color: #00afd7;
}
.terminal .xterm-color-39 {
color: #00afff;
}
.terminal .xterm-bg-color-39 {
background-color: #00afff;
}
.terminal .xterm-color-40 {
color: #00d700;
}
.terminal .xterm-bg-color-40 {
background-color: #00d700;
}
.terminal .xterm-color-41 {
color: #00d75f;
}
.terminal .xterm-bg-color-41 {
background-color: #00d75f;
}
.terminal .xterm-color-42 {
color: #00d787;
}
.terminal .xterm-bg-color-42 {
background-color: #00d787;
}
.terminal .xterm-color-43 {
color: #00d7af;
}
.terminal .xterm-bg-color-43 {
background-color: #00d7af;
} }
.terminal .xterm-color-44 { .xterm {
color: #00d7d7; cursor: text;
} }
.terminal .xterm-bg-color-44 { .xterm.enable-mouse-events {
background-color: #00d7d7; /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
} }
.terminal .xterm-color-45 { .xterm.xterm-cursor-pointer {
color: #00d7ff; cursor: pointer;
} }
.terminal .xterm-bg-color-45 { .xterm.xterm-cursor-crosshair {
background-color: #00d7ff; /* Column selection mode */
cursor: crosshair;
} }
.terminal .xterm-color-46 { .xterm .xterm-accessibility,
color: #00ff00; .xterm .xterm-message {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 100;
color: transparent;
} }
.terminal .xterm-bg-color-46 { .xterm .live-region {
background-color: #00ff00; position: absolute;
} left: -9999px;
width: 1px;
.terminal .xterm-color-47 { height: 1px;
color: #00ff5f; overflow: hidden;
}
.terminal .xterm-bg-color-47 {
background-color: #00ff5f;
}
.terminal .xterm-color-48 {
color: #00ff87;
}
.terminal .xterm-bg-color-48 {
background-color: #00ff87;
}
.terminal .xterm-color-49 {
color: #00ffaf;
}
.terminal .xterm-bg-color-49 {
background-color: #00ffaf;
}
.terminal .xterm-color-50 {
color: #00ffd7;
}
.terminal .xterm-bg-color-50 {
background-color: #00ffd7;
}
.terminal .xterm-color-51 {
color: #00ffff;
}
.terminal .xterm-bg-color-51 {
background-color: #00ffff;
}
.terminal .xterm-color-52 {
color: #5f0000;
}
.terminal .xterm-bg-color-52 {
background-color: #5f0000;
}
.terminal .xterm-color-53 {
color: #5f005f;
}
.terminal .xterm-bg-color-53 {
background-color: #5f005f;
}
.terminal .xterm-color-54 {
color: #5f0087;
}
.terminal .xterm-bg-color-54 {
background-color: #5f0087;
}
.terminal .xterm-color-55 {
color: #5f00af;
}
.terminal .xterm-bg-color-55 {
background-color: #5f00af;
}
.terminal .xterm-color-56 {
color: #5f00d7;
}
.terminal .xterm-bg-color-56 {
background-color: #5f00d7;
}
.terminal .xterm-color-57 {
color: #5f00ff;
}
.terminal .xterm-bg-color-57 {
background-color: #5f00ff;
}
.terminal .xterm-color-58 {
color: #5f5f00;
}
.terminal .xterm-bg-color-58 {
background-color: #5f5f00;
}
.terminal .xterm-color-59 {
color: #5f5f5f;
}
.terminal .xterm-bg-color-59 {
background-color: #5f5f5f;
}
.terminal .xterm-color-60 {
color: #5f5f87;
}
.terminal .xterm-bg-color-60 {
background-color: #5f5f87;
}
.terminal .xterm-color-61 {
color: #5f5faf;
}
.terminal .xterm-bg-color-61 {
background-color: #5f5faf;
}
.terminal .xterm-color-62 {
color: #5f5fd7;
}
.terminal .xterm-bg-color-62 {
background-color: #5f5fd7;
}
.terminal .xterm-color-63 {
color: #5f5fff;
}
.terminal .xterm-bg-color-63 {
background-color: #5f5fff;
}
.terminal .xterm-color-64 {
color: #5f8700;
}
.terminal .xterm-bg-color-64 {
background-color: #5f8700;
}
.terminal .xterm-color-65 {
color: #5f875f;
}
.terminal .xterm-bg-color-65 {
background-color: #5f875f;
}
.terminal .xterm-color-66 {
color: #5f8787;
}
.terminal .xterm-bg-color-66 {
background-color: #5f8787;
}
.terminal .xterm-color-67 {
color: #5f87af;
}
.terminal .xterm-bg-color-67 {
background-color: #5f87af;
}
.terminal .xterm-color-68 {
color: #5f87d7;
}
.terminal .xterm-bg-color-68 {
background-color: #5f87d7;
}
.terminal .xterm-color-69 {
color: #5f87ff;
}
.terminal .xterm-bg-color-69 {
background-color: #5f87ff;
}
.terminal .xterm-color-70 {
color: #5faf00;
}
.terminal .xterm-bg-color-70 {
background-color: #5faf00;
}
.terminal .xterm-color-71 {
color: #5faf5f;
}
.terminal .xterm-bg-color-71 {
background-color: #5faf5f;
}
.terminal .xterm-color-72 {
color: #5faf87;
}
.terminal .xterm-bg-color-72 {
background-color: #5faf87;
}
.terminal .xterm-color-73 {
color: #5fafaf;
}
.terminal .xterm-bg-color-73 {
background-color: #5fafaf;
}
.terminal .xterm-color-74 {
color: #5fafd7;
}
.terminal .xterm-bg-color-74 {
background-color: #5fafd7;
}
.terminal .xterm-color-75 {
color: #5fafff;
}
.terminal .xterm-bg-color-75 {
background-color: #5fafff;
}
.terminal .xterm-color-76 {
color: #5fd700;
}
.terminal .xterm-bg-color-76 {
background-color: #5fd700;
}
.terminal .xterm-color-77 {
color: #5fd75f;
}
.terminal .xterm-bg-color-77 {
background-color: #5fd75f;
}
.terminal .xterm-color-78 {
color: #5fd787;
}
.terminal .xterm-bg-color-78 {
background-color: #5fd787;
}
.terminal .xterm-color-79 {
color: #5fd7af;
}
.terminal .xterm-bg-color-79 {
background-color: #5fd7af;
}
.terminal .xterm-color-80 {
color: #5fd7d7;
}
.terminal .xterm-bg-color-80 {
background-color: #5fd7d7;
}
.terminal .xterm-color-81 {
color: #5fd7ff;
}
.terminal .xterm-bg-color-81 {
background-color: #5fd7ff;
}
.terminal .xterm-color-82 {
color: #5fff00;
}
.terminal .xterm-bg-color-82 {
background-color: #5fff00;
}
.terminal .xterm-color-83 {
color: #5fff5f;
}
.terminal .xterm-bg-color-83 {
background-color: #5fff5f;
}
.terminal .xterm-color-84 {
color: #5fff87;
}
.terminal .xterm-bg-color-84 {
background-color: #5fff87;
}
.terminal .xterm-color-85 {
color: #5fffaf;
}
.terminal .xterm-bg-color-85 {
background-color: #5fffaf;
}
.terminal .xterm-color-86 {
color: #5fffd7;
}
.terminal .xterm-bg-color-86 {
background-color: #5fffd7;
}
.terminal .xterm-color-87 {
color: #5fffff;
}
.terminal .xterm-bg-color-87 {
background-color: #5fffff;
}
.terminal .xterm-color-88 {
color: #870000;
}
.terminal .xterm-bg-color-88 {
background-color: #870000;
}
.terminal .xterm-color-89 {
color: #87005f;
}
.terminal .xterm-bg-color-89 {
background-color: #87005f;
}
.terminal .xterm-color-90 {
color: #870087;
}
.terminal .xterm-bg-color-90 {
background-color: #870087;
}
.terminal .xterm-color-91 {
color: #8700af;
}
.terminal .xterm-bg-color-91 {
background-color: #8700af;
}
.terminal .xterm-color-92 {
color: #8700d7;
}
.terminal .xterm-bg-color-92 {
background-color: #8700d7;
}
.terminal .xterm-color-93 {
color: #8700ff;
}
.terminal .xterm-bg-color-93 {
background-color: #8700ff;
}
.terminal .xterm-color-94 {
color: #875f00;
}
.terminal .xterm-bg-color-94 {
background-color: #875f00;
}
.terminal .xterm-color-95 {
color: #875f5f;
}
.terminal .xterm-bg-color-95 {
background-color: #875f5f;
}
.terminal .xterm-color-96 {
color: #875f87;
}
.terminal .xterm-bg-color-96 {
background-color: #875f87;
}
.terminal .xterm-color-97 {
color: #875faf;
}
.terminal .xterm-bg-color-97 {
background-color: #875faf;
}
.terminal .xterm-color-98 {
color: #875fd7;
}
.terminal .xterm-bg-color-98 {
background-color: #875fd7;
}
.terminal .xterm-color-99 {
color: #875fff;
}
.terminal .xterm-bg-color-99 {
background-color: #875fff;
}
.terminal .xterm-color-100 {
color: #878700;
}
.terminal .xterm-bg-color-100 {
background-color: #878700;
}
.terminal .xterm-color-101 {
color: #87875f;
}
.terminal .xterm-bg-color-101 {
background-color: #87875f;
}
.terminal .xterm-color-102 {
color: #878787;
}
.terminal .xterm-bg-color-102 {
background-color: #878787;
}
.terminal .xterm-color-103 {
color: #8787af;
}
.terminal .xterm-bg-color-103 {
background-color: #8787af;
}
.terminal .xterm-color-104 {
color: #8787d7;
}
.terminal .xterm-bg-color-104 {
background-color: #8787d7;
}
.terminal .xterm-color-105 {
color: #8787ff;
}
.terminal .xterm-bg-color-105 {
background-color: #8787ff;
}
.terminal .xterm-color-106 {
color: #87af00;
}
.terminal .xterm-bg-color-106 {
background-color: #87af00;
}
.terminal .xterm-color-107 {
color: #87af5f;
}
.terminal .xterm-bg-color-107 {
background-color: #87af5f;
}
.terminal .xterm-color-108 {
color: #87af87;
}
.terminal .xterm-bg-color-108 {
background-color: #87af87;
}
.terminal .xterm-color-109 {
color: #87afaf;
}
.terminal .xterm-bg-color-109 {
background-color: #87afaf;
}
.terminal .xterm-color-110 {
color: #87afd7;
}
.terminal .xterm-bg-color-110 {
background-color: #87afd7;
}
.terminal .xterm-color-111 {
color: #87afff;
}
.terminal .xterm-bg-color-111 {
background-color: #87afff;
}
.terminal .xterm-color-112 {
color: #87d700;
}
.terminal .xterm-bg-color-112 {
background-color: #87d700;
}
.terminal .xterm-color-113 {
color: #87d75f;
}
.terminal .xterm-bg-color-113 {
background-color: #87d75f;
}
.terminal .xterm-color-114 {
color: #87d787;
}
.terminal .xterm-bg-color-114 {
background-color: #87d787;
}
.terminal .xterm-color-115 {
color: #87d7af;
}
.terminal .xterm-bg-color-115 {
background-color: #87d7af;
}
.terminal .xterm-color-116 {
color: #87d7d7;
}
.terminal .xterm-bg-color-116 {
background-color: #87d7d7;
}
.terminal .xterm-color-117 {
color: #87d7ff;
}
.terminal .xterm-bg-color-117 {
background-color: #87d7ff;
}
.terminal .xterm-color-118 {
color: #87ff00;
}
.terminal .xterm-bg-color-118 {
background-color: #87ff00;
}
.terminal .xterm-color-119 {
color: #87ff5f;
}
.terminal .xterm-bg-color-119 {
background-color: #87ff5f;
}
.terminal .xterm-color-120 {
color: #87ff87;
}
.terminal .xterm-bg-color-120 {
background-color: #87ff87;
}
.terminal .xterm-color-121 {
color: #87ffaf;
}
.terminal .xterm-bg-color-121 {
background-color: #87ffaf;
}
.terminal .xterm-color-122 {
color: #87ffd7;
}
.terminal .xterm-bg-color-122 {
background-color: #87ffd7;
}
.terminal .xterm-color-123 {
color: #87ffff;
}
.terminal .xterm-bg-color-123 {
background-color: #87ffff;
}
.terminal .xterm-color-124 {
color: #af0000;
}
.terminal .xterm-bg-color-124 {
background-color: #af0000;
}
.terminal .xterm-color-125 {
color: #af005f;
}
.terminal .xterm-bg-color-125 {
background-color: #af005f;
}
.terminal .xterm-color-126 {
color: #af0087;
}
.terminal .xterm-bg-color-126 {
background-color: #af0087;
}
.terminal .xterm-color-127 {
color: #af00af;
}
.terminal .xterm-bg-color-127 {
background-color: #af00af;
}
.terminal .xterm-color-128 {
color: #af00d7;
}
.terminal .xterm-bg-color-128 {
background-color: #af00d7;
}
.terminal .xterm-color-129 {
color: #af00ff;
}
.terminal .xterm-bg-color-129 {
background-color: #af00ff;
}
.terminal .xterm-color-130 {
color: #af5f00;
}
.terminal .xterm-bg-color-130 {
background-color: #af5f00;
}
.terminal .xterm-color-131 {
color: #af5f5f;
}
.terminal .xterm-bg-color-131 {
background-color: #af5f5f;
}
.terminal .xterm-color-132 {
color: #af5f87;
}
.terminal .xterm-bg-color-132 {
background-color: #af5f87;
}
.terminal .xterm-color-133 {
color: #af5faf;
}
.terminal .xterm-bg-color-133 {
background-color: #af5faf;
}
.terminal .xterm-color-134 {
color: #af5fd7;
}
.terminal .xterm-bg-color-134 {
background-color: #af5fd7;
}
.terminal .xterm-color-135 {
color: #af5fff;
}
.terminal .xterm-bg-color-135 {
background-color: #af5fff;
}
.terminal .xterm-color-136 {
color: #af8700;
}
.terminal .xterm-bg-color-136 {
background-color: #af8700;
}
.terminal .xterm-color-137 {
color: #af875f;
}
.terminal .xterm-bg-color-137 {
background-color: #af875f;
}
.terminal .xterm-color-138 {
color: #af8787;
}
.terminal .xterm-bg-color-138 {
background-color: #af8787;
}
.terminal .xterm-color-139 {
color: #af87af;
}
.terminal .xterm-bg-color-139 {
background-color: #af87af;
}
.terminal .xterm-color-140 {
color: #af87d7;
}
.terminal .xterm-bg-color-140 {
background-color: #af87d7;
}
.terminal .xterm-color-141 {
color: #af87ff;
}
.terminal .xterm-bg-color-141 {
background-color: #af87ff;
}
.terminal .xterm-color-142 {
color: #afaf00;
}
.terminal .xterm-bg-color-142 {
background-color: #afaf00;
}
.terminal .xterm-color-143 {
color: #afaf5f;
}
.terminal .xterm-bg-color-143 {
background-color: #afaf5f;
}
.terminal .xterm-color-144 {
color: #afaf87;
}
.terminal .xterm-bg-color-144 {
background-color: #afaf87;
}
.terminal .xterm-color-145 {
color: #afafaf;
}
.terminal .xterm-bg-color-145 {
background-color: #afafaf;
}
.terminal .xterm-color-146 {
color: #afafd7;
}
.terminal .xterm-bg-color-146 {
background-color: #afafd7;
}
.terminal .xterm-color-147 {
color: #afafff;
}
.terminal .xterm-bg-color-147 {
background-color: #afafff;
}
.terminal .xterm-color-148 {
color: #afd700;
}
.terminal .xterm-bg-color-148 {
background-color: #afd700;
}
.terminal .xterm-color-149 {
color: #afd75f;
}
.terminal .xterm-bg-color-149 {
background-color: #afd75f;
}
.terminal .xterm-color-150 {
color: #afd787;
}
.terminal .xterm-bg-color-150 {
background-color: #afd787;
}
.terminal .xterm-color-151 {
color: #afd7af;
}
.terminal .xterm-bg-color-151 {
background-color: #afd7af;
}
.terminal .xterm-color-152 {
color: #afd7d7;
}
.terminal .xterm-bg-color-152 {
background-color: #afd7d7;
}
.terminal .xterm-color-153 {
color: #afd7ff;
}
.terminal .xterm-bg-color-153 {
background-color: #afd7ff;
}
.terminal .xterm-color-154 {
color: #afff00;
}
.terminal .xterm-bg-color-154 {
background-color: #afff00;
}
.terminal .xterm-color-155 {
color: #afff5f;
}
.terminal .xterm-bg-color-155 {
background-color: #afff5f;
}
.terminal .xterm-color-156 {
color: #afff87;
}
.terminal .xterm-bg-color-156 {
background-color: #afff87;
}
.terminal .xterm-color-157 {
color: #afffaf;
}
.terminal .xterm-bg-color-157 {
background-color: #afffaf;
}
.terminal .xterm-color-158 {
color: #afffd7;
}
.terminal .xterm-bg-color-158 {
background-color: #afffd7;
}
.terminal .xterm-color-159 {
color: #afffff;
}
.terminal .xterm-bg-color-159 {
background-color: #afffff;
}
.terminal .xterm-color-160 {
color: #d70000;
}
.terminal .xterm-bg-color-160 {
background-color: #d70000;
}
.terminal .xterm-color-161 {
color: #d7005f;
}
.terminal .xterm-bg-color-161 {
background-color: #d7005f;
}
.terminal .xterm-color-162 {
color: #d70087;
}
.terminal .xterm-bg-color-162 {
background-color: #d70087;
}
.terminal .xterm-color-163 {
color: #d700af;
}
.terminal .xterm-bg-color-163 {
background-color: #d700af;
}
.terminal .xterm-color-164 {
color: #d700d7;
}
.terminal .xterm-bg-color-164 {
background-color: #d700d7;
}
.terminal .xterm-color-165 {
color: #d700ff;
}
.terminal .xterm-bg-color-165 {
background-color: #d700ff;
}
.terminal .xterm-color-166 {
color: #d75f00;
}
.terminal .xterm-bg-color-166 {
background-color: #d75f00;
}
.terminal .xterm-color-167 {
color: #d75f5f;
}
.terminal .xterm-bg-color-167 {
background-color: #d75f5f;
}
.terminal .xterm-color-168 {
color: #d75f87;
}
.terminal .xterm-bg-color-168 {
background-color: #d75f87;
}
.terminal .xterm-color-169 {
color: #d75faf;
}
.terminal .xterm-bg-color-169 {
background-color: #d75faf;
}
.terminal .xterm-color-170 {
color: #d75fd7;
}
.terminal .xterm-bg-color-170 {
background-color: #d75fd7;
}
.terminal .xterm-color-171 {
color: #d75fff;
}
.terminal .xterm-bg-color-171 {
background-color: #d75fff;
}
.terminal .xterm-color-172 {
color: #d78700;
}
.terminal .xterm-bg-color-172 {
background-color: #d78700;
}
.terminal .xterm-color-173 {
color: #d7875f;
}
.terminal .xterm-bg-color-173 {
background-color: #d7875f;
}
.terminal .xterm-color-174 {
color: #d78787;
}
.terminal .xterm-bg-color-174 {
background-color: #d78787;
}
.terminal .xterm-color-175 {
color: #d787af;
}
.terminal .xterm-bg-color-175 {
background-color: #d787af;
}
.terminal .xterm-color-176 {
color: #d787d7;
}
.terminal .xterm-bg-color-176 {
background-color: #d787d7;
}
.terminal .xterm-color-177 {
color: #d787ff;
}
.terminal .xterm-bg-color-177 {
background-color: #d787ff;
}
.terminal .xterm-color-178 {
color: #d7af00;
}
.terminal .xterm-bg-color-178 {
background-color: #d7af00;
}
.terminal .xterm-color-179 {
color: #d7af5f;
}
.terminal .xterm-bg-color-179 {
background-color: #d7af5f;
}
.terminal .xterm-color-180 {
color: #d7af87;
}
.terminal .xterm-bg-color-180 {
background-color: #d7af87;
}
.terminal .xterm-color-181 {
color: #d7afaf;
}
.terminal .xterm-bg-color-181 {
background-color: #d7afaf;
}
.terminal .xterm-color-182 {
color: #d7afd7;
}
.terminal .xterm-bg-color-182 {
background-color: #d7afd7;
}
.terminal .xterm-color-183 {
color: #d7afff;
}
.terminal .xterm-bg-color-183 {
background-color: #d7afff;
}
.terminal .xterm-color-184 {
color: #d7d700;
}
.terminal .xterm-bg-color-184 {
background-color: #d7d700;
}
.terminal .xterm-color-185 {
color: #d7d75f;
}
.terminal .xterm-bg-color-185 {
background-color: #d7d75f;
}
.terminal .xterm-color-186 {
color: #d7d787;
}
.terminal .xterm-bg-color-186 {
background-color: #d7d787;
}
.terminal .xterm-color-187 {
color: #d7d7af;
}
.terminal .xterm-bg-color-187 {
background-color: #d7d7af;
}
.terminal .xterm-color-188 {
color: #d7d7d7;
}
.terminal .xterm-bg-color-188 {
background-color: #d7d7d7;
}
.terminal .xterm-color-189 {
color: #d7d7ff;
}
.terminal .xterm-bg-color-189 {
background-color: #d7d7ff;
}
.terminal .xterm-color-190 {
color: #d7ff00;
}
.terminal .xterm-bg-color-190 {
background-color: #d7ff00;
}
.terminal .xterm-color-191 {
color: #d7ff5f;
}
.terminal .xterm-bg-color-191 {
background-color: #d7ff5f;
}
.terminal .xterm-color-192 {
color: #d7ff87;
}
.terminal .xterm-bg-color-192 {
background-color: #d7ff87;
}
.terminal .xterm-color-193 {
color: #d7ffaf;
}
.terminal .xterm-bg-color-193 {
background-color: #d7ffaf;
}
.terminal .xterm-color-194 {
color: #d7ffd7;
}
.terminal .xterm-bg-color-194 {
background-color: #d7ffd7;
}
.terminal .xterm-color-195 {
color: #d7ffff;
}
.terminal .xterm-bg-color-195 {
background-color: #d7ffff;
}
.terminal .xterm-color-196 {
color: #ff0000;
}
.terminal .xterm-bg-color-196 {
background-color: #ff0000;
}
.terminal .xterm-color-197 {
color: #ff005f;
}
.terminal .xterm-bg-color-197 {
background-color: #ff005f;
}
.terminal .xterm-color-198 {
color: #ff0087;
}
.terminal .xterm-bg-color-198 {
background-color: #ff0087;
}
.terminal .xterm-color-199 {
color: #ff00af;
}
.terminal .xterm-bg-color-199 {
background-color: #ff00af;
}
.terminal .xterm-color-200 {
color: #ff00d7;
}
.terminal .xterm-bg-color-200 {
background-color: #ff00d7;
}
.terminal .xterm-color-201 {
color: #ff00ff;
}
.terminal .xterm-bg-color-201 {
background-color: #ff00ff;
}
.terminal .xterm-color-202 {
color: #ff5f00;
}
.terminal .xterm-bg-color-202 {
background-color: #ff5f00;
}
.terminal .xterm-color-203 {
color: #ff5f5f;
}
.terminal .xterm-bg-color-203 {
background-color: #ff5f5f;
}
.terminal .xterm-color-204 {
color: #ff5f87;
}
.terminal .xterm-bg-color-204 {
background-color: #ff5f87;
}
.terminal .xterm-color-205 {
color: #ff5faf;
}
.terminal .xterm-bg-color-205 {
background-color: #ff5faf;
}
.terminal .xterm-color-206 {
color: #ff5fd7;
}
.terminal .xterm-bg-color-206 {
background-color: #ff5fd7;
}
.terminal .xterm-color-207 {
color: #ff5fff;
}
.terminal .xterm-bg-color-207 {
background-color: #ff5fff;
}
.terminal .xterm-color-208 {
color: #ff8700;
}
.terminal .xterm-bg-color-208 {
background-color: #ff8700;
}
.terminal .xterm-color-209 {
color: #ff875f;
}
.terminal .xterm-bg-color-209 {
background-color: #ff875f;
}
.terminal .xterm-color-210 {
color: #ff8787;
}
.terminal .xterm-bg-color-210 {
background-color: #ff8787;
}
.terminal .xterm-color-211 {
color: #ff87af;
}
.terminal .xterm-bg-color-211 {
background-color: #ff87af;
}
.terminal .xterm-color-212 {
color: #ff87d7;
}
.terminal .xterm-bg-color-212 {
background-color: #ff87d7;
}
.terminal .xterm-color-213 {
color: #ff87ff;
}
.terminal .xterm-bg-color-213 {
background-color: #ff87ff;
}
.terminal .xterm-color-214 {
color: #ffaf00;
}
.terminal .xterm-bg-color-214 {
background-color: #ffaf00;
}
.terminal .xterm-color-215 {
color: #ffaf5f;
}
.terminal .xterm-bg-color-215 {
background-color: #ffaf5f;
}
.terminal .xterm-color-216 {
color: #ffaf87;
}
.terminal .xterm-bg-color-216 {
background-color: #ffaf87;
}
.terminal .xterm-color-217 {
color: #ffafaf;
}
.terminal .xterm-bg-color-217 {
background-color: #ffafaf;
}
.terminal .xterm-color-218 {
color: #ffafd7;
}
.terminal .xterm-bg-color-218 {
background-color: #ffafd7;
}
.terminal .xterm-color-219 {
color: #ffafff;
}
.terminal .xterm-bg-color-219 {
background-color: #ffafff;
}
.terminal .xterm-color-220 {
color: #ffd700;
}
.terminal .xterm-bg-color-220 {
background-color: #ffd700;
}
.terminal .xterm-color-221 {
color: #ffd75f;
}
.terminal .xterm-bg-color-221 {
background-color: #ffd75f;
}
.terminal .xterm-color-222 {
color: #ffd787;
}
.terminal .xterm-bg-color-222 {
background-color: #ffd787;
}
.terminal .xterm-color-223 {
color: #ffd7af;
}
.terminal .xterm-bg-color-223 {
background-color: #ffd7af;
}
.terminal .xterm-color-224 {
color: #ffd7d7;
}
.terminal .xterm-bg-color-224 {
background-color: #ffd7d7;
}
.terminal .xterm-color-225 {
color: #ffd7ff;
}
.terminal .xterm-bg-color-225 {
background-color: #ffd7ff;
}
.terminal .xterm-color-226 {
color: #ffff00;
}
.terminal .xterm-bg-color-226 {
background-color: #ffff00;
}
.terminal .xterm-color-227 {
color: #ffff5f;
}
.terminal .xterm-bg-color-227 {
background-color: #ffff5f;
}
.terminal .xterm-color-228 {
color: #ffff87;
}
.terminal .xterm-bg-color-228 {
background-color: #ffff87;
}
.terminal .xterm-color-229 {
color: #ffffaf;
}
.terminal .xterm-bg-color-229 {
background-color: #ffffaf;
}
.terminal .xterm-color-230 {
color: #ffffd7;
}
.terminal .xterm-bg-color-230 {
background-color: #ffffd7;
}
.terminal .xterm-color-231 {
color: #ffffff;
}
.terminal .xterm-bg-color-231 {
background-color: #ffffff;
}
.terminal .xterm-color-232 {
color: #080808;
}
.terminal .xterm-bg-color-232 {
background-color: #080808;
}
.terminal .xterm-color-233 {
color: #121212;
}
.terminal .xterm-bg-color-233 {
background-color: #121212;
}
.terminal .xterm-color-234 {
color: #1c1c1c;
}
.terminal .xterm-bg-color-234 {
background-color: #1c1c1c;
}
.terminal .xterm-color-235 {
color: #262626;
}
.terminal .xterm-bg-color-235 {
background-color: #262626;
}
.terminal .xterm-color-236 {
color: #303030;
}
.terminal .xterm-bg-color-236 {
background-color: #303030;
}
.terminal .xterm-color-237 {
color: #3a3a3a;
}
.terminal .xterm-bg-color-237 {
background-color: #3a3a3a;
}
.terminal .xterm-color-238 {
color: #444444;
}
.terminal .xterm-bg-color-238 {
background-color: #444444;
}
.terminal .xterm-color-239 {
color: #4e4e4e;
}
.terminal .xterm-bg-color-239 {
background-color: #4e4e4e;
}
.terminal .xterm-color-240 {
color: #585858;
}
.terminal .xterm-bg-color-240 {
background-color: #585858;
}
.terminal .xterm-color-241 {
color: #626262;
}
.terminal .xterm-bg-color-241 {
background-color: #626262;
}
.terminal .xterm-color-242 {
color: #6c6c6c;
}
.terminal .xterm-bg-color-242 {
background-color: #6c6c6c;
}
.terminal .xterm-color-243 {
color: #767676;
}
.terminal .xterm-bg-color-243 {
background-color: #767676;
}
.terminal .xterm-color-244 {
color: #808080;
}
.terminal .xterm-bg-color-244 {
background-color: #808080;
}
.terminal .xterm-color-245 {
color: #8a8a8a;
}
.terminal .xterm-bg-color-245 {
background-color: #8a8a8a;
}
.terminal .xterm-color-246 {
color: #949494;
}
.terminal .xterm-bg-color-246 {
background-color: #949494;
}
.terminal .xterm-color-247 {
color: #9e9e9e;
}
.terminal .xterm-bg-color-247 {
background-color: #9e9e9e;
}
.terminal .xterm-color-248 {
color: #a8a8a8;
}
.terminal .xterm-bg-color-248 {
background-color: #a8a8a8;
}
.terminal .xterm-color-249 {
color: #b2b2b2;
}
.terminal .xterm-bg-color-249 {
background-color: #b2b2b2;
}
.terminal .xterm-color-250 {
color: #bcbcbc;
}
.terminal .xterm-bg-color-250 {
background-color: #bcbcbc;
}
.terminal .xterm-color-251 {
color: #c6c6c6;
}
.terminal .xterm-bg-color-251 {
background-color: #c6c6c6;
}
.terminal .xterm-color-252 {
color: #d0d0d0;
}
.terminal .xterm-bg-color-252 {
background-color: #d0d0d0;
}
.terminal .xterm-color-253 {
color: #dadada;
}
.terminal .xterm-bg-color-253 {
background-color: #dadada;
}
.terminal .xterm-color-254 {
color: #e4e4e4;
}
.terminal .xterm-bg-color-254 {
background-color: #e4e4e4;
}
.terminal .xterm-color-255 {
color: #eeeeee;
}
.terminal .xterm-bg-color-255 {
background-color: #eeeeee;
} }
This source diff could not be displayed because it is too large. You can view the blob instead.
/*
* JQuery zTree exHideNodes v3.5.33
* http://treejs.cn/
*
* Copyright (c) 2010 Hunter.z
*
* Licensed same as jquery - MIT License
* http://www.opensource.org/licenses/mit-license.php
*
* email: hunter.z@263.net
* Date: 2018-01-30
*/
(function(j){j.extend(!0,j.fn.zTree._z,{view:{clearOldFirstNode:function(c,a){for(var b=a.getNextNode();b;){if(b.isFirstNode){b.isFirstNode=!1;e.setNodeLineIcos(c,b);break}if(b.isLastNode)break;b=b.getNextNode()}},clearOldLastNode:function(c,a,b){for(a=a.getPreNode();a;){if(a.isLastNode){a.isLastNode=!1;b&&e.setNodeLineIcos(c,a);break}if(a.isFirstNode)break;a=a.getPreNode()}},makeDOMNodeMainBefore:function(c,a,b){a=d.isHidden(a,b);c.push("<li ",a?"style='display:none;' ":"","id='",b.tId,"' class='",
l.className.LEVEL,b.level,"' tabindex='0' hidefocus='true' treenode>")},showNode:function(c,a){d.isHidden(c,a,!1);d.initShowForExCheck(c,a);k(a,c).show()},showNodes:function(c,a,b){if(a&&a.length!=0){var f={},g,i;for(g=0,i=a.length;g<i;g++){var h=a[g];if(!f[h.parentTId]){var u=h.getParentNode();f[h.parentTId]=u===null?d.getRoot(c):h.getParentNode()}e.showNode(c,h,b)}for(var j in f)a=d.nodeChildren(c,f[j]),e.setFirstNodeForShow(c,a),e.setLastNodeForShow(c,a)}},hideNode:function(c,a){d.isHidden(c,a,
!0);a.isFirstNode=!1;a.isLastNode=!1;d.initHideForExCheck(c,a);e.cancelPreSelectedNode(c,a);k(a,c).hide()},hideNodes:function(c,a,b){if(a&&a.length!=0){var f={},g,i;for(g=0,i=a.length;g<i;g++){var h=a[g];if((h.isFirstNode||h.isLastNode)&&!f[h.parentTId]){var j=h.getParentNode();f[h.parentTId]=j===null?d.getRoot(c):h.getParentNode()}e.hideNode(c,h,b)}for(var k in f)a=d.nodeChildren(c,f[k]),e.setFirstNodeForHide(c,a),e.setLastNodeForHide(c,a)}},setFirstNode:function(c,a){var b=d.nodeChildren(c,a),f=
d.isHidden(c,b[0],!1);b.length>0&&!f?b[0].isFirstNode=!0:b.length>0&&e.setFirstNodeForHide(c,b)},setLastNode:function(c,a){var b=d.nodeChildren(c,a),f=d.isHidden(c,b[0]);b.length>0&&!f?b[b.length-1].isLastNode=!0:b.length>0&&e.setLastNodeForHide(c,b)},setFirstNodeForHide:function(c,a){var b,f,g;for(f=0,g=a.length;f<g;f++){b=a[f];if(b.isFirstNode)break;if(!d.isHidden(c,b)&&!b.isFirstNode){b.isFirstNode=!0;e.setNodeLineIcos(c,b);break}else b=null}return b},setFirstNodeForShow:function(c,a){var b,f,
g,i,h;for(f=0,g=a.length;f<g;f++){b=a[f];var j=d.isHidden(c,b);if(!i&&!j&&b.isFirstNode){i=b;break}else if(!i&&!j&&!b.isFirstNode)b.isFirstNode=!0,i=b,e.setNodeLineIcos(c,b);else if(i&&b.isFirstNode){b.isFirstNode=!1;h=b;e.setNodeLineIcos(c,b);break}}return{"new":i,old:h}},setLastNodeForHide:function(c,a){var b,f;for(f=a.length-1;f>=0;f--){b=a[f];if(b.isLastNode)break;if(!d.isHidden(c,b)&&!b.isLastNode){b.isLastNode=!0;e.setNodeLineIcos(c,b);break}else b=null}return b},setLastNodeForShow:function(c,
a){var b,f,g,i;for(f=a.length-1;f>=0;f--){b=a[f];var h=d.isHidden(c,b);if(!g&&!h&&b.isLastNode){g=b;break}else if(!g&&!h&&!b.isLastNode)b.isLastNode=!0,g=b,e.setNodeLineIcos(c,b);else if(g&&b.isLastNode){b.isLastNode=!1;i=b;e.setNodeLineIcos(c,b);break}}return{"new":g,old:i}}},data:{initHideForExCheck:function(c,a){if(d.isHidden(c,a)&&c.check&&c.check.enable){if(typeof a._nocheck=="undefined")a._nocheck=!!a.nocheck,a.nocheck=!0;a.check_Child_State=-1;e.repairParentChkClassWithSelf&&e.repairParentChkClassWithSelf(c,
a)}},initShowForExCheck:function(c,a){if(!d.isHidden(c,a)&&c.check&&c.check.enable){if(typeof a._nocheck!="undefined")a.nocheck=a._nocheck,delete a._nocheck;if(e.setChkClass){var b=k(a,l.id.CHECK,c);e.setChkClass(c,b,a)}e.repairParentChkClassWithSelf&&e.repairParentChkClassWithSelf(c,a)}}}});var j=j.fn.zTree,m=j._z.tools,l=j.consts,e=j._z.view,d=j._z.data,k=m.$;d.isHidden=function(c,a,b){if(!a)return!1;c=c.data.key.isHidden;typeof b!=="undefined"&&(typeof b==="string"&&(b=m.eqs(checked,"true")),a[c]=
!!b);return a[c]};d.exSetting({data:{key:{isHidden:"isHidden"}}});d.addInitNode(function(c,a,b){a=d.isHidden(c,b);d.isHidden(c,b,a);d.initHideForExCheck(c,b)});d.addBeforeA(function(){});d.addZTreeTools(function(c,a){a.showNodes=function(a,b){e.showNodes(c,a,b)};a.showNode=function(a,b){a&&e.showNodes(c,[a],b)};a.hideNodes=function(a,b){e.hideNodes(c,a,b)};a.hideNode=function(a,b){a&&e.hideNodes(c,[a],b)};var b=a.checkNode;if(b)a.checkNode=function(f,e,i,h){(!f||!d.isHidden(c,f))&&b.apply(a,arguments)}});
var n=d.initNode;d.initNode=function(c,a,b,f,g,i,h){var j=(f?f:d.getRoot(c))[c.data.key.children];d.tmpHideFirstNode=e.setFirstNodeForHide(c,j);d.tmpHideLastNode=e.setLastNodeForHide(c,j);h&&(e.setNodeLineIcos(c,d.tmpHideFirstNode),e.setNodeLineIcos(c,d.tmpHideLastNode));g=d.tmpHideFirstNode===b;i=d.tmpHideLastNode===b;n&&n.apply(d,arguments);h&&i&&e.clearOldLastNode(c,b,h)};var o=d.makeChkFlag;if(o)d.makeChkFlag=function(c,a){(!a||!d.isHidden(c,a))&&o.apply(d,arguments)};var p=d.getTreeCheckedNodes;
if(p)d.getTreeCheckedNodes=function(c,a,b,f){if(a&&a.length>0){var e=a[0].getParentNode();if(e&&d.isHidden(c,e))return[]}return p.apply(d,arguments)};var q=d.getTreeChangeCheckedNodes;if(q)d.getTreeChangeCheckedNodes=function(c,a,b){if(a&&a.length>0){var e=a[0].getParentNode();if(e&&d.isHidden(c,e))return[]}return q.apply(d,arguments)};var r=e.expandCollapseSonNode;if(r)e.expandCollapseSonNode=function(c,a,b,f,g){(!a||!d.isHidden(c,a))&&r.apply(e,arguments)};var s=e.setSonNodeCheckBox;if(s)e.setSonNodeCheckBox=
function(c,a,b,f){(!a||!d.isHidden(c,a))&&s.apply(e,arguments)};var t=e.repairParentChkClassWithSelf;if(t)e.repairParentChkClassWithSelf=function(c,a){(!a||!d.isHidden(c,a))&&t.apply(e,arguments)}})(jQuery);
{% load i18n %} {% load i18n %}
<div class="footer fixed"> <div class="footer fixed">
<div class="pull-right"> <div class="pull-right">
Version <strong>1.4.4-{% include '_build.html' %}</strong> GPLv2. Version <strong>1.4.6-{% include '_build.html' %}</strong> GPLv2.
<!--<img style="display: none" src="http://www.jumpserver.org/img/evaluate_avatar1.jpg">--> <!--<img style="display: none" src="http://www.jumpserver.org/img/evaluate_avatar1.jpg">-->
</div> </div>
<div> <div>
......
...@@ -58,16 +58,15 @@ ...@@ -58,16 +58,15 @@
{% endif %} {% endif %}
</ul> </ul>
</li> </li>
{% if request.user.is_superuser %}
<li id="ops"> <li id="ops">
<a> <a>
<i class="fa fa-coffee" style="width: 14px"></i> <span class="nav-label">{% trans 'Job Center' %}</span><span class="fa arrow"></span> <i class="fa fa-coffee" style="width: 14px"></i> <span class="nav-label">{% trans 'Job Center' %}</span><span class="fa arrow"></span>
</a> </a>
<ul class="nav nav-second-level"> <ul class="nav nav-second-level">
<li id="task"><a href="{% url 'ops:task-list' %}">{% trans 'Task list' %}</a></li> <li id="task"><a href="{% url 'ops:task-list' %}">{% trans 'Task list' %}</a></li>
<li id="command-execution"><a href="{% url 'ops:command-execution-start' %}">{% trans 'Command execution' %}</a></li>
</ul> </ul>
</li> </li>
{% endif %}
<li id="audits"> <li id="audits">
<a> <a>
<i class="fa fa-history" style="width: 14px"></i> <span class="nav-label">{% trans 'Audits' %}</span><span class="fa arrow"></span> <i class="fa fa-history" style="width: 14px"></i> <span class="nav-label">{% trans 'Audits' %}</span><span class="fa arrow"></span>
...@@ -77,17 +76,9 @@ ...@@ -77,17 +76,9 @@
<li id="ftp-log"><a href="{% url 'audits:ftp-log-list' %}">{% trans 'FTP log' %}</a></li> <li id="ftp-log"><a href="{% url 'audits:ftp-log-list' %}">{% trans 'FTP log' %}</a></li>
<li id="operate-log"><a href="{% url 'audits:operate-log-list' %}">{% trans 'Operate log' %}</a></li> <li id="operate-log"><a href="{% url 'audits:operate-log-list' %}">{% trans 'Operate log' %}</a></li>
<li id="password-change-log"><a href="{% url 'audits:password-change-log-list' %}">{% trans 'Password change log' %}</a></li> <li id="password-change-log"><a href="{% url 'audits:password-change-log-list' %}">{% trans 'Password change log' %}</a></li>
<li id="command-execution-log"><a href="{% url 'audits:command-execution-log-list' %}">{% trans 'Command execution' %}</a></li>
</ul> </ul>
</li> </li>
{#<li id="">#}
{# <a href="#">#}
{# <i class="fa fa-download"></i> <span class="nav-label">{% trans 'File' %}</span><span class="fa arrow"></span>#}
{# </a>#}
{# <ul class="nav nav-second-level">#}
{# <li id="upload"><a href="">{% trans 'File upload' %}</a></li>#}
{# <li id="download"><a href="">{% trans 'File download' %}</a></li>#}
{# </ul>#}
{#</li>#}
{% if XPACK_PLUGINS %} {% if XPACK_PLUGINS %}
<li id="xpack"> <li id="xpack">
<a> <a>
......
...@@ -4,6 +4,11 @@ ...@@ -4,6 +4,11 @@
<i class="fa fa-files-o" style="width: 14px"></i><span class="nav-label">{% trans 'My assets' %}</span><span class="label label-info pull-right"></span> <i class="fa fa-files-o" style="width: 14px"></i><span class="nav-label">{% trans 'My assets' %}</span><span class="label label-info pull-right"></span>
</a> </a>
</li> </li>
<li id="ops">
<a href="{% url 'ops:command-execution-start' %}">
<i class="fa fa-terminal" style="width: 14px"></i> <span class="nav-label">{% trans 'Command execution' %}</span><span class="label label-info pull-right"></span>
</a>
</li>
<li id="users"> <li id="users">
<a href="{% url 'users:user-profile' %}"> <a href="{% url 'users:user-profile' %}">
<i class="fa fa-user" style="width: 14px"></i> <span class="nav-label">{% trans 'Profile' %}</span><span class="label label-info pull-right"></span> <i class="fa fa-user" style="width: 14px"></i> <span class="nav-label">{% trans 'Profile' %}</span><span class="label label-info pull-right"></span>
......
...@@ -94,44 +94,15 @@ class SessionReplayViewSet(viewsets.ViewSet): ...@@ -94,44 +94,15 @@ class SessionReplayViewSet(viewsets.ViewSet):
serializer_class = serializers.ReplaySerializer serializer_class = serializers.ReplaySerializer
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
session = None session = None
upload_to = 'replay' # 仅添加到本地存储中
def get_session_path(self, version=2):
"""
获取session日志的文件路径
:param version: 原来后缀是 .gz,为了统一新版本改为 .replay.gz
:return:
"""
suffix = '.replay.gz'
if version == 1:
suffix = '.gz'
date = self.session.date_start.strftime('%Y-%m-%d')
return os.path.join(date, str(self.session.id) + suffix)
def get_local_path(self, version=2):
session_path = self.get_session_path(version=version)
if version == 2:
local_path = os.path.join(self.upload_to, session_path)
else:
local_path = session_path
return local_path
def save_to_storage(self, f):
local_path = self.get_local_path()
try:
name = default_storage.save(local_path, f)
return name, None
except OSError as e:
return None, e
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
session_id = kwargs.get('pk') session_id = kwargs.get('pk')
self.session = get_object_or_404(Session, id=session_id) session = get_object_or_404(Session, id=session_id)
serializer = self.serializer_class(data=request.data) serializer = self.serializer_class(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
file = serializer.validated_data['file'] file = serializer.validated_data['file']
name, err = self.save_to_storage(file) name, err = session.save_to_storage(file)
if not name: if not name:
msg = "Failed save replay `{}`: {}".format(session_id, err) msg = "Failed save replay `{}`: {}".format(session_id, err)
logger.error(msg) logger.error(msg)
...@@ -145,17 +116,18 @@ class SessionReplayViewSet(viewsets.ViewSet): ...@@ -145,17 +116,18 @@ class SessionReplayViewSet(viewsets.ViewSet):
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
session_id = kwargs.get('pk') session_id = kwargs.get('pk')
self.session = get_object_or_404(Session, id=session_id) session = get_object_or_404(Session, id=session_id)
tp = 'json'
if session.protocol in ('rdp', 'vnc'):
tp = 'guacamole'
data = { data = {'type': tp, 'src': ''}
'type': 'guacamole' if self.session.protocol == 'rdp' else 'json',
'src': '',
}
# 新版本和老版本的文件后缀不同 # 新版本和老版本的文件后缀不同
session_path = self.get_session_path() # 存在外部存储上的路径 session_path = session.get_rel_replay_path() # 存在外部存储上的路径
local_path = self.get_local_path() local_path = session.get_local_path()
local_path_v1 = self.get_local_path(version=1) local_path_v1 = session.get_local_path(version=1)
# 去default storage中查找 # 去default storage中查找
for _local_path in (local_path, local_path_v1, session_path): for _local_path in (local_path, local_path_v1, session_path):
......
...@@ -83,10 +83,10 @@ class TerminalTokenApi(APIView): ...@@ -83,10 +83,10 @@ class TerminalTokenApi(APIView):
if not terminal.is_accepted: if not terminal.is_accepted:
return Response("Terminal was not accepted yet", status=400) return Response("Terminal was not accepted yet", status=400)
if not terminal.user or not terminal.user.access_key.all(): if not terminal.user or not terminal.user.access_key:
return Response("No access key generate", status=401) return Response("No access key generate", status=401)
access_key = terminal.user.access_key.first() access_key = terminal.user.access_key()
data = OrderedDict() data = OrderedDict()
data['access_key'] = {'id': access_key.id, 'secret': access_key.secret} data['access_key'] = {'id': access_key.id, 'secret': access_key.secret}
return Response(data, status=200) return Response(data, status=200)
......
...@@ -13,11 +13,11 @@ class Migration(migrations.Migration): ...@@ -13,11 +13,11 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='command', model_name='command',
name='org_id', name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
migrations.AddField( migrations.AddField(
model_name='session', model_name='session',
name='org_id', name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
] ]
...@@ -10,14 +10,4 @@ class Migration(migrations.Migration): ...@@ -10,14 +10,4 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.AlterField(
model_name='command',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='session',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
] ]
# Generated by Django 2.1 on 2018-12-26 06:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('terminal', '0013_auto_20181123_1113'),
]
operations = [
migrations.AlterField(
model_name='session',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('vnc', 'vnc')], default='ssh', max_length=8),
),
]
from __future__ import unicode_literals from __future__ import unicode_literals
import os
import uuid import uuid
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone from django.utils import timezone
from django.conf import settings from django.conf import settings
from django.core.files.storage import default_storage
from users.models import User from users.models import User
from orgs.mixins import OrgModelMixin from orgs.mixins import OrgModelMixin
...@@ -130,7 +132,8 @@ class Session(OrgModelMixin): ...@@ -130,7 +132,8 @@ class Session(OrgModelMixin):
) )
PROTOCOL_CHOICES = ( PROTOCOL_CHOICES = (
('ssh', 'ssh'), ('ssh', 'ssh'),
('rdp', 'rdp') ('rdp', 'rdp'),
('vnc', 'vnc')
) )
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
...@@ -148,6 +151,36 @@ class Session(OrgModelMixin): ...@@ -148,6 +151,36 @@ class Session(OrgModelMixin):
date_start = models.DateTimeField(verbose_name=_("Date start"), db_index=True, default=timezone.now) date_start = models.DateTimeField(verbose_name=_("Date start"), db_index=True, default=timezone.now)
date_end = models.DateTimeField(verbose_name=_("Date end"), null=True) date_end = models.DateTimeField(verbose_name=_("Date end"), null=True)
upload_to = 'replay'
def get_rel_replay_path(self, version=2):
"""
获取session日志的文件路径
:param version: 原来后缀是 .gz,为了统一新版本改为 .replay.gz
:return:
"""
suffix = '.replay.gz'
if version == 1:
suffix = '.gz'
date = self.date_start.strftime('%Y-%m-%d')
return os.path.join(date, str(self.id) + suffix)
def get_local_path(self, version=2):
rel_path = self.get_rel_replay_path(version=version)
if version == 2:
local_path = os.path.join(self.upload_to, rel_path)
else:
local_path = rel_path
return local_path
def save_to_storage(self, f):
local_path = self.get_local_path()
try:
name = default_storage.save(local_path, f)
return name, None
except OSError as e:
return None, e
class Meta: class Meta:
db_table = "terminal_session" db_table = "terminal_session"
ordering = ["-date_start"] ordering = ["-date_start"]
......
...@@ -4,15 +4,20 @@ ...@@ -4,15 +4,20 @@
import datetime import datetime
from celery import shared_task from celery import shared_task
from celery.utils.log import get_task_logger
from django.utils import timezone from django.utils import timezone
from django.conf import settings
from django.core.files.storage import default_storage
from ops.celery.utils import register_as_period_task, after_app_ready_start, \ from ops.celery.utils import register_as_period_task, after_app_ready_start, \
after_app_shutdown_clean after_app_shutdown_clean
from .models import Status, Session from .models import Status, Session, Command
CACHE_REFRESH_INTERVAL = 10 CACHE_REFRESH_INTERVAL = 10
RUNNING = False RUNNING = False
logger = get_task_logger(__name__)
@shared_task @shared_task
...@@ -34,3 +39,28 @@ def clean_orphan_session(): ...@@ -34,3 +39,28 @@ def clean_orphan_session():
if not session.terminal or not session.terminal.is_active: if not session.terminal or not session.terminal.is_active:
session.is_finished = True session.is_finished = True
session.save() session.save()
@shared_task
@register_as_period_task(interval=3600*24)
@after_app_ready_start
@after_app_shutdown_clean
def clean_expired_session_period():
logger.info("Start clean expired session record, commands and replay")
days = settings.TERMINAL_SESSION_KEEP_DURATION
dt = timezone.now() - timezone.timedelta(days=days)
expired_sessions = Session.objects.filter(date_start__lt=dt)
for session in expired_sessions:
logger.info("Clean session: {}".format(session.id))
Command.objects.filter(session=str(session.id)).delete()
# 删除录像文件
session_path = session.get_rel_replay_path()
local_path = session.get_local_path()
local_path_v1 = session.get_local_path(version=1)
# 去default storage中查找
for _local_path in (local_path, local_path_v1, session_path):
if default_storage.exists(_local_path):
default_storage.delete(_local_path)
# 删除session记录
session.delete()
...@@ -104,10 +104,10 @@ ...@@ -104,10 +104,10 @@
<a onclick="window.open('/luna/replay/{{ session.id }}','luna', 'height=600, width=800, top=400, left=400, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')" class="btn btn-xs btn-warning btn-replay" >{% trans "Replay" %}</a> <a onclick="window.open('/luna/replay/{{ session.id }}','luna', 'height=600, width=800, top=400, left=400, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')" class="btn btn-xs btn-warning btn-replay" >{% trans "Replay" %}</a>
{% else %} {% else %}
<!--<a onclick="window.open('/luna/monitor/{{ session.id }}','luna', 'height=600, width=800, top=0, left=0, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')" class="btn btn-xs btn-warning btn-monitor" >{% trans "Monitor" %}</a>--> <!--<a onclick="window.open('/luna/monitor/{{ session.id }}','luna', 'height=600, width=800, top=0, left=0, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')" class="btn btn-xs btn-warning btn-monitor" >{% trans "Monitor" %}</a>-->
{% if session.protocol == 'rdp' %} {% if session.protocol == 'ssh' %}
<a class="btn btn-xs btn-danger btn-term" disabled value="{{ session.id }}" terminal="{{ session.terminal.id }}" >{% trans "Terminate" %}</a>
{% else %}
<a class="btn btn-xs btn-danger btn-term" value="{{ session.id }}" terminal="{{ session.terminal.id }}" >{% trans "Terminate" %}</a> <a class="btn btn-xs btn-danger btn-term" value="{{ session.id }}" terminal="{{ session.terminal.id }}" >{% trans "Terminate" %}</a>
{% else %}
<a class="btn btn-xs btn-danger btn-term" disabled value="{{ session.id }}" terminal="{{ session.terminal.id }}" >{% trans "Terminate" %}</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
</td> </td>
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.core.cache import cache from django.core.cache import cache
from .models import Session
from assets.models import Asset, SystemUser
from users.models import User
from .const import USERS_CACHE_KEY, ASSETS_CACHE_KEY, SYSTEM_USER_CACHE_KEY from .const import USERS_CACHE_KEY, ASSETS_CACHE_KEY, SYSTEM_USER_CACHE_KEY
def get_session_asset_list(): def get_session_asset_list():
return set(list(Session.objects.values_list('asset', flat=True))) return Asset.objects.values_list('hostname', flat=True)
def get_session_user_list(): def get_session_user_list():
return set(list(Session.objects.values_list('user', flat=True))) return User.objects.values_list('username', flat=True)
def get_session_system_user_list(): def get_session_system_user_list():
return set(list(Session.objects.values_list('system_user', flat=True))) return SystemUser.objects.values_list('username', flat=True)
def get_user_list_from_cache(): def get_user_list_from_cache():
......
...@@ -6,7 +6,7 @@ from rest_framework_bulk import BulkModelViewSet ...@@ -6,7 +6,7 @@ from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination from rest_framework.pagination import LimitOffsetPagination
from ..serializers import UserGroupSerializer, \ from ..serializers import UserGroupSerializer, \
UserGroupUpdateMemeberSerializer UserGroupUpdateMemberSerializer
from ..models import UserGroup from ..models import UserGroup
from common.permissions import IsOrgAdmin from common.permissions import IsOrgAdmin
from common.mixins import IDInFilterMixin from common.mixins import IDInFilterMixin
...@@ -26,5 +26,5 @@ class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet): ...@@ -26,5 +26,5 @@ class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet):
class UserGroupUpdateUserApi(generics.RetrieveUpdateAPIView): class UserGroupUpdateUserApi(generics.RetrieveUpdateAPIView):
queryset = UserGroup.objects.all() queryset = UserGroup.objects.all()
serializer_class = UserGroupUpdateMemeberSerializer serializer_class = UserGroupUpdateMemberSerializer
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
...@@ -38,10 +38,7 @@ class UserViewSet(IDInFilterMixin, BulkModelViewSet): ...@@ -38,10 +38,7 @@ class UserViewSet(IDInFilterMixin, BulkModelViewSet):
pagination_class = LimitOffsetPagination pagination_class = LimitOffsetPagination
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = current_org.get_org_users()
if current_org.is_real():
org_users = current_org.get_org_users()
queryset = queryset.filter(id__in=org_users)
return queryset return queryset
def get_permissions(self): def get_permissions(self):
...@@ -49,6 +46,9 @@ class UserViewSet(IDInFilterMixin, BulkModelViewSet): ...@@ -49,6 +46,9 @@ class UserViewSet(IDInFilterMixin, BulkModelViewSet):
self.permission_classes = (IsOrgAdminOrAppUser,) self.permission_classes = (IsOrgAdminOrAppUser,)
return super().get_permissions() return super().get_permissions()
def allow_bulk_destroy(self, qs, filtered):
return qs.count() != filtered.count()
class UserChangePasswordApi(generics.RetrieveUpdateAPIView): class UserChangePasswordApi(generics.RetrieveUpdateAPIView):
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
......
...@@ -63,11 +63,6 @@ class UserCreateUpdateForm(OrgModelForm): ...@@ -63,11 +63,6 @@ class UserCreateUpdateForm(OrgModelForm):
'username', 'name', 'email', 'groups', 'wechat', 'username', 'name', 'email', 'groups', 'wechat',
'phone', 'role', 'date_expired', 'comment', 'otp_level' 'phone', 'role', 'date_expired', 'comment', 'otp_level'
] ]
help_texts = {
'username': '* required',
'name': '* required',
'email': '* required',
}
widgets = { widgets = {
'otp_level': forms.RadioSelect(), 'otp_level': forms.RadioSelect(),
'groups': forms.SelectMultiple( 'groups': forms.SelectMultiple(
...@@ -131,17 +126,16 @@ class UserCreateUpdateForm(OrgModelForm): ...@@ -131,17 +126,16 @@ class UserCreateUpdateForm(OrgModelForm):
class UserProfileForm(forms.ModelForm): class UserProfileForm(forms.ModelForm):
username = forms.CharField(disabled=True)
name = forms.CharField(disabled=True)
email = forms.CharField(disabled=True)
class Meta: class Meta:
model = User model = User
fields = [ fields = [
'username', 'name', 'email', 'username', 'name', 'email',
'wechat', 'phone', 'wechat', 'phone',
] ]
help_texts = {
'username': '* required',
'name': '* required',
'email': '* required',
}
UserProfileForm.verbose_name = _("Profile") UserProfileForm.verbose_name = _("Profile")
...@@ -259,7 +253,6 @@ UserPublicKeyForm.verbose_name = _("Public key") ...@@ -259,7 +253,6 @@ UserPublicKeyForm.verbose_name = _("Public key")
class UserBulkUpdateForm(OrgModelForm): class UserBulkUpdateForm(OrgModelForm):
users = forms.ModelMultipleChoiceField( users = forms.ModelMultipleChoiceField(
required=True, required=True,
help_text='* required',
label=_('Select users'), label=_('Select users'),
queryset=User.objects.all(), queryset=User.objects.all(),
widget=forms.SelectMultiple( widget=forms.SelectMultiple(
...@@ -342,9 +335,6 @@ class UserGroupForm(OrgModelForm): ...@@ -342,9 +335,6 @@ class UserGroupForm(OrgModelForm):
fields = [ fields = [
'name', 'users', 'comment', 'name', 'users', 'comment',
] ]
help_texts = {
'name': '* required'
}
class FileForm(forms.Form): class FileForm(forms.Form):
......
...@@ -13,6 +13,6 @@ class Migration(migrations.Migration): ...@@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='usergroup', model_name='usergroup',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
] ]
...@@ -120,7 +120,14 @@ class User(AbstractUser): ...@@ -120,7 +120,14 @@ class User(AbstractUser):
def set_password(self, raw_password): def set_password(self, raw_password):
self._set_password = True self._set_password = True
return super().set_password(raw_password) if self.can_update_password():
return super().set_password(raw_password)
else:
error = _("User auth from {}, go there change password").format(self.source)
raise PermissionError(error)
def can_update_password(self):
return self.is_local
@property @property
def otp_secret_key(self): def otp_secret_key(self):
...@@ -142,6 +149,18 @@ class User(AbstractUser): ...@@ -142,6 +149,18 @@ class User(AbstractUser):
return True return True
return False return False
@property
def groups_display(self):
return ' '.join(self.groups.all().values_list('name', flat=True))
@property
def role_display(self):
return self.get_role_display()
@property
def source_display(self):
return self.get_source_display()
@property @property
def is_expired(self): def is_expired(self):
if self.date_expired and self.date_expired < timezone.now(): if self.date_expired and self.date_expired < timezone.now():
...@@ -260,8 +279,6 @@ class User(AbstractUser): ...@@ -260,8 +279,6 @@ class User(AbstractUser):
self.role = 'Admin' self.role = 'Admin'
self.is_active = True self.is_active = True
super().save(*args, **kwargs) super().save(*args, **kwargs)
if current_org and current_org.is_real():
self.orgs.add(current_org.id)
@property @property
def private_token(self): def private_token(self):
......
...@@ -13,31 +13,18 @@ signer = get_signer() ...@@ -13,31 +13,18 @@ signer = get_signer()
class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
groups_display = serializers.SerializerMethodField()
groups = serializers.PrimaryKeyRelatedField(
many=True, queryset=UserGroup.objects.all(), required=False
)
class Meta: class Meta:
model = User model = User
list_serializer_class = BulkListSerializer list_serializer_class = BulkListSerializer
exclude = [ fields = [
'first_name', 'last_name', 'password', '_private_key', 'id', 'name', 'username', 'email', 'groups', 'groups_display',
'_public_key', '_otp_secret_key', 'user_permissions' 'role', 'role_display', 'avatar_url', 'wechat', 'phone',
'otp_level', 'comment', 'source', 'source_display',
'is_valid', 'is_expired', 'is_active',
'created_by', 'is_first_login',
'date_password_last_updated', 'date_expired',
] ]
# validators = []
def get_field_names(self, declared_fields, info):
fields = super(UserSerializer, self).get_field_names(declared_fields, info)
fields.extend([
'groups_display', 'get_role_display',
'get_source_display', 'is_valid'
])
return fields
@staticmethod
def get_groups_display(obj):
return " ".join([group.name for group in obj.groups.all()])
class UserPKUpdateSerializer(serializers.ModelSerializer): class UserPKUpdateSerializer(serializers.ModelSerializer):
...@@ -74,7 +61,7 @@ class UserGroupSerializer(BulkSerializerMixin, serializers.ModelSerializer): ...@@ -74,7 +61,7 @@ class UserGroupSerializer(BulkSerializerMixin, serializers.ModelSerializer):
return [user.name for user in obj.users.all()] return [user.name for user in obj.users.all()]
class UserGroupUpdateMemeberSerializer(serializers.ModelSerializer): class UserGroupUpdateMemberSerializer(serializers.ModelSerializer):
users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all()) users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all())
class Meta: class Meta:
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
# #
from django.dispatch import receiver from django.dispatch import receiver
from django_auth_ldap.backend import populate_user
# from django.db.models.signals import post_save # from django.db.models.signals import post_save
from common.utils import get_logger from common.utils import get_logger
...@@ -30,9 +29,3 @@ def on_user_create(sender, user=None, **kwargs): ...@@ -30,9 +29,3 @@ def on_user_create(sender, user=None, **kwargs):
if user.email: if user.email:
send_user_created_mail(user) send_user_created_mail(user)
@receiver(populate_user)
def on_ldap_create_user(sender, user, ldap_user, **kwargs):
if user:
user.source = user.SOURCE_LDAP
user.save()
...@@ -30,7 +30,11 @@ ...@@ -30,7 +30,11 @@
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group date"> <div class="input-group date">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span> <span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input id="{{ form.date_expired.id_for_label }}" name="{{ form.date_expired.html_name }}" type="text" class="form-control" value="{{ form.date_expired.value|date:'Y-m-d H:i' }}"> {% if form.errors %}
<input id="{{ form.date_expired.id_for_label }}" name="{{ form.date_expired.html_name }}" type="text" class="form-control" value="{{ form.date_expired.value }}">
{% else %}
<input id="{{ form.date_expired.id_for_label }}" name="{{ form.date_expired.html_name }}" type="text" class="form-control" value="{{ form.date_expired.value|date:'Y-m-d H:i' }}">
{% endif %}
</div> </div>
<span class="help-block ">{{ form.date_expired.errors }}</span> <span class="help-block ">{{ form.date_expired.errors }}</span>
</div> </div>
......
...@@ -177,6 +177,7 @@ ...@@ -177,6 +177,7 @@
</span> </span>
</td> </td>
</tr> </tr>
{% if user_object.can_update_password %}
<tr> <tr>
<td>{% trans 'Send reset password mail' %}:</td> <td>{% trans 'Send reset password mail' %}:</td>
<td> <td>
...@@ -185,6 +186,7 @@ ...@@ -185,6 +186,7 @@
</span> </span>
</td> </td>
</tr> </tr>
{% endif %}
<tr> <tr>
<td>{% trans 'Send reset ssh key mail' %}:</td> <td>{% trans 'Send reset ssh key mail' %}:</td>
<td> <td>
......
...@@ -57,7 +57,6 @@ ...@@ -57,7 +57,6 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
...@@ -108,7 +107,8 @@ function initTable() { ...@@ -108,7 +107,8 @@ function initTable() {
function onSelected(event, treeNode) { function onSelected(event, treeNode) {
url = '{% url "api-perms:user-node-assets" pk=object.id node_id=DEFAULT_PK %}'; url = '{% url "api-perms:user-node-assets" pk=object.id node_id=DEFAULT_PK %}';
url = url.replace("{{ DEFAULT_PK }}", treeNode.node_id); var node_id = treeNode.meta.node.id;
url = url.replace("{{ DEFAULT_PK }}", node_id);
asset_table.ajax.url(url); asset_table.ajax.url(url);
asset_table.ajax.reload(); asset_table.ajax.reload();
} }
...@@ -129,23 +129,9 @@ function initTree() { ...@@ -129,23 +129,9 @@ function initTree() {
} }
}; };
var zNodes = []; $.get("{% url 'api-perms:user-nodes-assets-as-tree' pk=object.id %}?show_assets=0", function(data, status) {
$.get("{% url 'api-perms:user-nodes' pk=object.id %}", function(data, status) { $.fn.zTree.init($("#assetTree"), setting, data);
$.each(data, function (index, value) {
value["node_id"] = value["id"];
value["id"] = value["tree_id"];
if (value["tree_id"] !== value["tree_parent"]) {
value["pId"] = value["tree_parent"];
}
value["isParent"] = value["is_node"];
value['name'] = value['value'];
value["iconSkin"] = value["is_node"] ? null : 'file';
});
zNodes = data;
$.fn.zTree.init($("#assetTree"), setting, zNodes);
zTree = $.fn.zTree.getZTreeObj("assetTree"); zTree = $.fn.zTree.getZTreeObj("assetTree");
var root = zTree.getNodes()[0];
zTree.expandNode(root);
}); });
} }
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
<th class="text-center">{% trans 'Role' %}</th> <th class="text-center">{% trans 'Role' %}</th>
<th class="text-center">{% trans 'User group' %}</th> <th class="text-center">{% trans 'User group' %}</th>
<th class="text-center">{% trans 'Source' %}</th> <th class="text-center">{% trans 'Source' %}</th>
<th class="text-center">{% trans 'Active' %}</th> <th class="text-center">{% trans 'Validity' %}</th>
<th class="text-center">{% trans 'Action' %}</th> <th class="text-center">{% trans 'Action' %}</th>
</tr> </tr>
</thead> </thead>
...@@ -66,11 +66,14 @@ function initTable() { ...@@ -66,11 +66,14 @@ function initTable() {
var innerHtml = cellData.length > 20 ? cellData.substring(0, 20) + '...': cellData; var innerHtml = cellData.length > 20 ? cellData.substring(0, 20) + '...': cellData;
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>'); $(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
}}, }},
{targets: 6, createdCell: function (td, cellData) { {targets: 6, createdCell: function (td, cellData, rowData) {
if (!cellData) { if (cellData) {
$(td).html('<i class="fa fa-times text-danger"></i>')
} else {
$(td).html('<i class="fa fa-check text-navy"></i>') $(td).html('<i class="fa fa-check text-navy"></i>')
} else if (!rowData.is_active) {
$(td).html('<i class="fa fa-times text-danger inactive"></i>')
} else if (rowData.is_expired) {
$(td).html('<i class="fa fa-times text-danger expired"></i>')
} }
}}, }},
{targets: 7, createdCell: function (td, cellData, rowData) { {targets: 7, createdCell: function (td, cellData, rowData) {
...@@ -91,9 +94,9 @@ function initTable() { ...@@ -91,9 +94,9 @@ function initTable() {
ajax_url: '{% url "api-users:user-list" %}', ajax_url: '{% url "api-users:user-list" %}',
columns: [ columns: [
{data: "id"}, {data: "name" }, {data: "username" }, {data: "id"}, {data: "name" }, {data: "username" },
{data: "get_role_display", orderable: false}, {data: "role_display", orderable: false},
{data: "groups_display", orderable: false}, {data: "groups_display", orderable: false},
{data: "get_source_display", orderable: false}, {data: "source_display", orderable: false},
{data: "is_valid", orderable: false}, {data: "is_valid", orderable: false},
{data: "id", orderable: false} {data: "id", orderable: false}
], ],
...@@ -246,6 +249,13 @@ $(document).ready(function(){ ...@@ -246,6 +249,13 @@ $(document).ready(function(){
var uid = $this.data('uid'); var uid = $this.data('uid');
var the_url = '{% url "api-users:user-detail" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", uid); var the_url = '{% url "api-users:user-detail" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", uid);
objectDelete($this, name, the_url); objectDelete($this, name, the_url);
}).on('click', '.expired', function () {
var msg = '{% trans "User is expired" %}';
toastr.error(msg)
}).on('click', '.inactive', function () {
var msg = '{% trans 'User is inactive' %}';
toastr.error(msg)
}) })
</script> </script>
{% endblock %} {% endblock %}
......
...@@ -148,14 +148,6 @@ ...@@ -148,14 +148,6 @@
<div class="panel-body"> <div class="panel-body">
<table class="table"> <table class="table">
<tbody> <tbody>
<tr class="no-borders-tr">
<td>{% trans 'Update password' %}:</td>
<td>
<span class="pull-right">
<a type="button" class="btn btn-primary btn-xs" style="width: 54px" href="{% url 'users:user-password-update' %}">{% trans 'Update' %}</a>
</span>
</td>
</tr>
<tr class="no-borders-tr"> <tr class="no-borders-tr">
<td>{% trans 'Set MFA' %}:</td> <td>{% trans 'Set MFA' %}:</td>
<td> <td>
...@@ -177,6 +169,16 @@ ...@@ -177,6 +169,16 @@
</span> </span>
</td> </td>
</tr> </tr>
{% if request.user.can_update_password %}
<tr class="no-borders">
<td>{% trans 'Update password' %}:</td>
<td>
<span class="pull-right">
<a type="button" class="btn btn-primary btn-xs" style="width: 54px" href="{% url 'users:user-password-update' %}">{% trans 'Update' %}</a>
</span>
</td>
</tr>
{% endif %}
{% if request.user.otp_enabled and request.user.otp_secret_key %} {% if request.user.otp_enabled and request.user.otp_secret_key %}
<tr> <tr>
<td>{% trans 'Update MFA' %}:</td> <td>{% trans 'Update MFA' %}:</td>
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
{% load bootstrap3 %} {% load bootstrap3 %}
{% block user_template_title %}{% trans "Update user" %}{% endblock %} {% block user_template_title %}{% trans "Update user" %}{% endblock %}
{% block password %} {% block password %}
{% if object.can_update_password %}
{% bootstrap_field form.password layout="horizontal" %} {% bootstrap_field form.password layout="horizontal" %}
{# 密码popover #} {# 密码popover #}
<div id="container"> <div id="container">
...@@ -14,13 +15,24 @@ ...@@ -14,13 +15,24 @@
<div class="popover-content"></div> <div class="popover-content"></div>
</div> </div>
</div> </div>
{% else %}
<div class="form-group">
<label class="col-sm-2 control-label">{% trans 'Password' %}</label>
<div class="col-sm-8 controls" style="margin-top: 8px;" id="password_help_text">
{% trans 'User auth from {}, go there change password' %}
</div>
</div>
{% endif %}
{% bootstrap_field form.public_key layout="horizontal" %} {% bootstrap_field form.public_key layout="horizontal" %}
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
{{ block.super }} {{ block.super }}
<script> <script>
$(document).ready(function(){ function passwordCheck() {
if ($('#id_password').length != 1) {
return
}
var el = $('#id_password_rules'), var el = $('#id_password_rules'),
idPassword = $('#id_password'), idPassword = $('#id_password'),
idPopover = $('#popover777'), idPopover = $('#popover777'),
...@@ -39,11 +51,11 @@ ...@@ -39,11 +51,11 @@
"veryStrong": "{% trans 'Very strong' %}" "veryStrong": "{% trans 'Very strong' %}"
}; };
jQuery.each(password_check_rules, function (idx, rules) { $.each(password_check_rules, function (idx, rules) {
if(rules.key === 'id_security_password_min_length'){ if(rules.key === 'id_security_password_min_length'){
minLength = rules.value minLength = rules.value
} }
}); });
// 初始化popover // 初始化popover
initPopover(container, progress, idPassword, el, password_check_rules, i18n_fallback); initPopover(container, progress, idPassword, el, password_check_rules, i18n_fallback);
...@@ -61,6 +73,13 @@ ...@@ -61,6 +73,13 @@
var password = idPassword.val(); var password = idPassword.val();
checkPasswordRules(password, minLength); checkPasswordRules(password, minLength);
}); });
}
$(document).ready(function(){
passwordCheck();
var origin_text = $("#password_help_text").text();
var new_text = origin_text.replace('{}', "{{ object.source_display }}");
$("#password_help_text").html(new_text);
}) })
</script> </script>
......
...@@ -284,14 +284,16 @@ def generate_otp_uri(request, issuer="Jumpserver"): ...@@ -284,14 +284,16 @@ def generate_otp_uri(request, issuer="Jumpserver"):
otp_secret_key = base64.b32encode(os.urandom(10)).decode('utf-8') otp_secret_key = base64.b32encode(os.urandom(10)).decode('utf-8')
cache.set(request.session.session_key+'otp_key', otp_secret_key, 600) cache.set(request.session.session_key+'otp_key', otp_secret_key, 600)
totp = pyotp.TOTP(otp_secret_key) totp = pyotp.TOTP(otp_secret_key)
return totp.provisioning_uri(name=user.username, issuer_name=issuer), otp_secret_key otp_issuer_name = settings.OTP_ISSUER_NAME or issuer
return totp.provisioning_uri(name=user.username, issuer_name=otp_issuer_name), otp_secret_key
def check_otp_code(otp_secret_key, otp_code): def check_otp_code(otp_secret_key, otp_code):
if not otp_secret_key or not otp_code: if not otp_secret_key or not otp_code:
return False return False
totp = pyotp.TOTP(otp_secret_key) totp = pyotp.TOTP(otp_secret_key)
return totp.verify(otp_code) otp_valid_window = settings.OTP_VALID_WINDOW or 0
return totp.verify(otp=otp_code, valid_window=otp_valid_window)
def get_password_check_rules(): def get_password_check_rules():
......
...@@ -239,7 +239,7 @@ class UserForgotPasswordView(TemplateView): ...@@ -239,7 +239,7 @@ class UserForgotPasswordView(TemplateView):
if not user: if not user:
error = _('Email address invalid, please input again') error = _('Email address invalid, please input again')
return self.get(request, errors=error) return self.get(request, errors=error)
elif not user.is_local: elif not user.can_update_password():
error = _('User auth from {}, go there change password'.format(user.source)) error = _('User auth from {}, go there change password'.format(user.source))
return self.get(request, errors=error) return self.get(request, errors=error)
else: else:
...@@ -298,6 +298,9 @@ class UserResetPasswordView(TemplateView): ...@@ -298,6 +298,9 @@ class UserResetPasswordView(TemplateView):
return self.get(request, errors=_('Password not same')) return self.get(request, errors=_('Password not same'))
user = User.validate_reset_token(token) user = User.validate_reset_token(token)
if not user.can_update_password():
error = _('User auth from {}, go there change password'.format(user.source))
return self.get(request, errors=error)
if not user: if not user:
return self.get(request, errors=_('Token invalid or expired')) return self.get(request, errors=_('Token invalid or expired'))
......
...@@ -87,6 +87,8 @@ class UserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): ...@@ -87,6 +87,8 @@ class UserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView):
user = form.save(commit=False) user = form.save(commit=False)
user.created_by = self.request.user.username or 'System' user.created_by = self.request.user.username or 'System'
user.save() user.save()
if current_org and current_org.is_real():
user.orgs.add(current_org.id)
post_user_create.send(self.__class__, user=user) post_user_create.send(self.__class__, user=user)
return super().form_valid(form) return super().form_valid(form)
...@@ -412,6 +414,12 @@ class UserPasswordUpdateView(LoginRequiredMixin, UpdateView): ...@@ -412,6 +414,12 @@ class UserPasswordUpdateView(LoginRequiredMixin, UpdateView):
return super().get_success_url() return super().get_success_url()
def form_valid(self, form): def form_valid(self, form):
if not self.request.user.can_update_password():
error = _("User auth from {}, go there change password").format(
self.request.source_display
)
form.add_error("password", error)
return self.form_invalid(form)
password = form.cleaned_data.get('new_password') password = form.cleaned_data.get('new_password')
is_ok = check_password_rules(password) is_ok = check_password_rules(password)
if not is_ok: if not is_ok:
......
...@@ -100,6 +100,9 @@ class Config: ...@@ -100,6 +100,9 @@ class Config:
} }
AUTH_LDAP_START_TLS = False AUTH_LDAP_START_TLS = False
#
# OTP_VALID_WINDOW = 0
def __init__(self): def __init__(self):
pass pass
...@@ -200,6 +203,10 @@ class DockerConfig(Config): ...@@ -200,6 +203,10 @@ class DockerConfig(Config):
AUTH_LDAP_START_TLS = False AUTH_LDAP_START_TLS = False
#
OTP_VALID_WINDOW = int(os.environ.get("OTP_VALID_WINDOW")) if os.environ.get("OTP_VALID_WINDOW") else 0
# Default using Config settings, you can write if/else for different env # Default using Config settings, you can write if/else for different env
config = DockerConfig() config = DockerConfig()
...@@ -44,6 +44,11 @@ class Config: ...@@ -44,6 +44,11 @@ class Config:
# LOG_LEVEL = 'DEBUG' # LOG_LEVEL = 'DEBUG'
# LOG_DIR = os.path.join(BASE_DIR, 'logs') # LOG_DIR = os.path.join(BASE_DIR, 'logs')
# Session expiration setting, Default 24 hour, Also set expired on on browser close
# 浏览器Session过期时间,默认24小时, 也可以设置浏览器关闭则过期
# SESSION_COOKIE_AGE = 3600 * 24
# SESSION_EXPIRE_AT_BROWSER_CLOSE = False
# Database setting, Support sqlite3, mysql, postgres .... # Database setting, Support sqlite3, mysql, postgres ....
# 数据库设置 # 数据库设置
# See https://docs.djangoproject.com/en/1.10/ref/settings/#databases # See https://docs.djangoproject.com/en/1.10/ref/settings/#databases
...@@ -72,7 +77,9 @@ class Config: ...@@ -72,7 +77,9 @@ class Config:
# Redis配置 # Redis配置
REDIS_HOST = '127.0.0.1' REDIS_HOST = '127.0.0.1'
REDIS_PORT = 6379 REDIS_PORT = 6379
REDIS_PASSWORD = '' # REDIS_PASSWORD = ''
# REDIS_DB_CELERY = 3
# REDIS_DB_CACHE = 4
# Use OpenID authorization # Use OpenID authorization
# 使用OpenID 来进行认证设置 # 使用OpenID 来进行认证设置
...@@ -83,6 +90,9 @@ class Config: ...@@ -83,6 +90,9 @@ class Config:
# AUTH_OPENID_CLIENT_ID = 'client-id' # AUTH_OPENID_CLIENT_ID = 'client-id'
# AUTH_OPENID_CLIENT_SECRET = 'client-secret' # AUTH_OPENID_CLIENT_SECRET = 'client-secret'
#
# OTP_VALID_WINDOW = 0
def __init__(self): def __init__(self):
pass pass
......
...@@ -26,10 +26,10 @@ LOG_DIR = os.path.join(BASE_DIR, 'logs') ...@@ -26,10 +26,10 @@ LOG_DIR = os.path.join(BASE_DIR, 'logs')
TMP_DIR = os.path.join(BASE_DIR, 'tmp') TMP_DIR = os.path.join(BASE_DIR, 'tmp')
HTTP_HOST = CONFIG.HTTP_BIND_HOST or '127.0.0.1' HTTP_HOST = CONFIG.HTTP_BIND_HOST or '127.0.0.1'
HTTP_PORT = CONFIG.HTTP_LISTEN_PORT or 8080 HTTP_PORT = CONFIG.HTTP_LISTEN_PORT or 8080
DEBUG = CONFIG.DEBUG DEBUG = CONFIG.DEBUG or False
LOG_LEVEL = CONFIG.LOG_LEVEL LOG_LEVEL = CONFIG.LOG_LEVEL or 'INFO'
START_TIMEOUT = 15 START_TIMEOUT = 40
WORKERS = 4 WORKERS = 4
DAEMON = False DAEMON = False
...@@ -58,9 +58,6 @@ def check_database_connection(): ...@@ -58,9 +58,6 @@ def check_database_connection():
def make_migrations(): def make_migrations():
print("Check database structure change ...") print("Check database structure change ...")
os.chdir(os.path.join(BASE_DIR, 'apps')) os.chdir(os.path.join(BASE_DIR, 'apps'))
if len(os.listdir('assets/migrations')) < 4:
print("Make database migrations ...")
subprocess.call('python3 manage.py makemigrations', shell=True)
print("Migrate model change to database ...") print("Migrate model change to database ...")
subprocess.call('python3 manage.py migrate', shell=True) subprocess.call('python3 manage.py migrate', shell=True)
......
tiff-dev jpeg-dev zlib-dev freetype-dev lcms-dev libwebp-dev tcl-dev tk-dev python3-dev libressl-dev openldap-dev cyrus-sasl-dev krb5-dev sshpass postgresql-dev mariadb-dev sqlite-dev libffi-dev openssh-client
libtiff5-dev libjpeg8-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev python-tk python-dev openssl libssl-dev libldap2-dev libsasl2-dev sqlite libkrb5-dev sshpass libtiff5-dev libjpeg8-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev python-tk python-dev openssl libssl-dev libldap2-dev libsasl2-dev sqlite libkrb5-dev sshpass libmysqlclient-dev
...@@ -15,7 +15,7 @@ coreschema==0.0.4 ...@@ -15,7 +15,7 @@ coreschema==0.0.4
cryptography==2.3.1 cryptography==2.3.1
decorator==4.1.2 decorator==4.1.2
Django==2.1 Django==2.1
django-auth-ldap==1.3.0 django-auth-ldap==1.7.0
django-bootstrap3==9.1.0 django-bootstrap3==9.1.0
django-celery-beat==1.1.1 django-celery-beat==1.1.1
django-filter==2.0.0 django-filter==2.0.0
...@@ -43,7 +43,7 @@ jmespath==0.9.3 ...@@ -43,7 +43,7 @@ jmespath==0.9.3
kombu==4.0.2 kombu==4.0.2
ldap3==2.4 ldap3==2.4
MarkupSafe==1.0 MarkupSafe==1.0
mysqlclient==1.3.12 mysqlclient==1.3.14
olefile==0.44 olefile==0.44
openapi-codec==1.3.2 openapi-codec==1.3.2
paramiko==2.4.1 paramiko==2.4.1
...@@ -52,7 +52,6 @@ Pillow==4.3.0 ...@@ -52,7 +52,6 @@ Pillow==4.3.0
pyasn1==0.4.2 pyasn1==0.4.2
pycparser==2.19 pycparser==2.19
pycrypto==2.6.1 pycrypto==2.6.1
pyldap==2.4.45
pyotp==2.2.6 pyotp==2.2.6
PyNaCl==1.2.1 PyNaCl==1.2.1
python-dateutil==2.6.1 python-dateutil==2.6.1
...@@ -77,3 +76,5 @@ aliyun-python-sdk-ecs==4.10.1 ...@@ -77,3 +76,5 @@ aliyun-python-sdk-ecs==4.10.1
python-keycloak==0.13.3 python-keycloak==0.13.3
python-keycloak-client==0.1.3 python-keycloak-client==0.1.3
rest_condition==1.0.3 rest_condition==1.0.3
python-ldap==3.1.0
tencentcloud-sdk-python==3.0.40
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment