mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-07-10 21:33:29 +00:00
Merge branch 'dev' of github.com:jumpserver/jumpserver into dev
This commit is contained in:
commit
2fde6cfe24
@ -34,7 +34,7 @@ Jumpserver采纳分布式架构,支持多机房跨区域部署,中心节点
|
||||
|
||||
我们提供了DEMO和截图可以让你快速了解Jumpserver
|
||||
|
||||
[DEMO](http://demo.jumpserver.org)
|
||||
[DEMO](https://demo.jumpserver.org)
|
||||
[截图](http://docs.jumpserver.org/zh/docs/snapshot.html)
|
||||
|
||||
### SDK
|
||||
|
@ -2,4 +2,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
__version__ = "1.4.4"
|
||||
__version__ = "1.4.6"
|
||||
|
@ -14,6 +14,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import generics
|
||||
from rest_framework.response import Response
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
@ -24,13 +25,14 @@ from common.utils import get_logger
|
||||
from ..hands import IsOrgAdmin
|
||||
from ..models import AdminUser, Asset
|
||||
from .. import serializers
|
||||
from ..tasks import test_admin_user_connectability_manual
|
||||
from ..tasks import test_admin_user_connectivity_manual
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'AdminUserViewSet', 'ReplaceNodesAdminUserApi',
|
||||
'AdminUserTestConnectiveApi', 'AdminUserAuthApi',
|
||||
'AdminUserAssetsListView',
|
||||
]
|
||||
|
||||
|
||||
@ -81,12 +83,29 @@ class ReplaceNodesAdminUserApi(generics.UpdateAPIView):
|
||||
|
||||
class AdminUserTestConnectiveApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Test asset admin user connectivity
|
||||
Test asset admin user assets_connectivity
|
||||
"""
|
||||
queryset = AdminUser.objects.all()
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
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})
|
||||
|
||||
|
||||
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
|
||||
from ..models import Asset, AdminUser, Node
|
||||
from .. import serializers
|
||||
from ..tasks import update_asset_hardware_info_manual, \
|
||||
test_asset_connectability_manual
|
||||
test_asset_connectivity_manual
|
||||
from ..utils import LabelFilter
|
||||
|
||||
|
||||
@ -41,40 +41,46 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet):
|
||||
pagination_class = LimitOffsetPagination
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
|
||||
def filter_node(self):
|
||||
def filter_node(self, queryset):
|
||||
node_id = self.request.query_params.get("node_id")
|
||||
if not node_id:
|
||||
return
|
||||
return queryset
|
||||
|
||||
node = get_object_or_404(Node, id=node_id)
|
||||
show_current_asset = self.request.query_params.get("show_current_asset") in ('1', 'true')
|
||||
|
||||
if node.is_root():
|
||||
if show_current_asset:
|
||||
self.queryset = self.queryset.filter(
|
||||
Q(nodes=node_id) | Q(nodes__isnull=True)
|
||||
)
|
||||
return
|
||||
if show_current_asset:
|
||||
self.queryset = self.queryset.filter(nodes=node)
|
||||
if node.is_root() and show_current_asset:
|
||||
queryset = queryset.filter(
|
||||
Q(nodes=node_id) | Q(nodes__isnull=True)
|
||||
)
|
||||
elif node.is_root() and not show_current_asset:
|
||||
pass
|
||||
elif not node.is_root() and show_current_asset:
|
||||
queryset = queryset.filter(nodes=node)
|
||||
else:
|
||||
self.queryset = self.queryset.filter(
|
||||
queryset = queryset.filter(
|
||||
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')
|
||||
if admin_user_id:
|
||||
admin_user = get_object_or_404(AdminUser, id=admin_user_id)
|
||||
self.queryset = self.queryset.filter(admin_user=admin_user)
|
||||
if not admin_user_id:
|
||||
return queryset
|
||||
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):
|
||||
self.queryset = super().get_queryset()\
|
||||
.prefetch_related('labels', 'nodes')\
|
||||
.select_related('admin_user')
|
||||
self.filter_admin_user_id()
|
||||
self.filter_node()
|
||||
return self.queryset.distinct()
|
||||
queryset = super().get_queryset().distinct()
|
||||
queryset = self.get_serializer_class().setup_eager_loading(queryset)
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView):
|
||||
@ -103,7 +109,7 @@ class AssetRefreshHardwareApi(generics.RetrieveAPIView):
|
||||
|
||||
class AssetAdminUserTestApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Test asset admin user connectivity
|
||||
Test asset admin user assets_connectivity
|
||||
"""
|
||||
queryset = Asset.objects.all()
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
@ -111,7 +117,7 @@ class AssetAdminUserTestApi(generics.RetrieveAPIView):
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
asset_id = kwargs.get('pk')
|
||||
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})
|
||||
|
||||
|
||||
|
@ -9,7 +9,6 @@ from django.views.generic.detail import SingleObjectMixin
|
||||
from common.utils import get_logger
|
||||
from common.permissions import IsOrgAdmin, IsAppUser, IsOrgAdminOrAppUser
|
||||
from ..models import Domain, Gateway
|
||||
from ..utils import test_gateway_connectability
|
||||
from .. import serializers
|
||||
|
||||
|
||||
@ -54,7 +53,7 @@ class GatewayTestConnectionApi(SingleObjectMixin, APIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object(Gateway.objects.all())
|
||||
ok, e = test_gateway_connectability(self.object)
|
||||
ok, e = self.object.test_connective()
|
||||
if ok:
|
||||
return Response("ok")
|
||||
else:
|
||||
|
@ -17,15 +17,14 @@ from rest_framework import generics, mixins, viewsets
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
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.tree import TreeNodeSerializer
|
||||
from ..hands import IsOrgAdmin
|
||||
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
|
||||
|
||||
|
||||
@ -34,7 +33,8 @@ __all__ = [
|
||||
'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi',
|
||||
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi',
|
||||
'NodeAddChildrenApi', 'RefreshNodeHardwareInfoApi',
|
||||
'TestNodeConnectiveApi'
|
||||
'TestNodeConnectiveApi', 'NodeListAsTreeApi',
|
||||
'NodeChildrenAsTreeApi', 'RefreshAssetsAmount',
|
||||
]
|
||||
|
||||
|
||||
@ -43,22 +43,89 @@ class NodeViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
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):
|
||||
node = self.get_object()
|
||||
if node.is_root():
|
||||
node_value = node.value
|
||||
post_value = request.data.get('value')
|
||||
if node_value != post_value:
|
||||
return Response(
|
||||
{"msg": _("You can't update the root node name")},
|
||||
status=400
|
||||
)
|
||||
return super().update(request, *args, **kwargs)
|
||||
class NodeListAsTreeApi(generics.ListAPIView):
|
||||
"""
|
||||
获取节点列表树
|
||||
[
|
||||
{
|
||||
"id": "",
|
||||
"name": "",
|
||||
"pId": "",
|
||||
"meta": ""
|
||||
}
|
||||
]
|
||||
"""
|
||||
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):
|
||||
@ -67,19 +134,13 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
|
||||
serializer_class = serializers.NodeSerializer
|
||||
instance = None
|
||||
|
||||
def counter(self):
|
||||
values = [
|
||||
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 get(self, request, *args, **kwargs):
|
||||
return self.list(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
if not request.data.get("value"):
|
||||
request.data["value"] = _("New node {}").format(self.counter())
|
||||
request.data["value"] = instance.get_next_child_preset_name()
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
@ -91,15 +152,12 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
|
||||
'The same level node name cannot be the same'
|
||||
)
|
||||
node = instance.create_child(value=value)
|
||||
return Response(
|
||||
{"id": node.id, "key": node.key, "value": node.value},
|
||||
status=201,
|
||||
)
|
||||
return Response(self.serializer_class(instance=node).data, status=201)
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk') or self.request.query_params.get('id')
|
||||
if not pk:
|
||||
node = None
|
||||
node = Node.root()
|
||||
else:
|
||||
node = get_object_or_404(Node, pk=pk)
|
||||
return node
|
||||
@ -107,7 +165,6 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
|
||||
def get_queryset(self):
|
||||
queryset = []
|
||||
query_all = self.request.query_params.get("all")
|
||||
query_assets = self.request.query_params.get('assets')
|
||||
node = self.get_object()
|
||||
|
||||
if node is None:
|
||||
@ -120,23 +177,8 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
|
||||
else:
|
||||
children = node.get_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
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
|
||||
class NodeAssetsApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
@ -234,5 +276,14 @@ class TestNodeConnectiveApi(APIView):
|
||||
assets = node.assets.all()
|
||||
# task_name = _("测试节点下资产是否可连接: {}".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})
|
||||
|
||||
|
||||
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
|
||||
from ..models import SystemUser, Asset
|
||||
from .. import serializers
|
||||
from ..tasks import push_system_user_to_assets_manual, \
|
||||
test_system_user_connectability_manual, push_system_user_a_asset_manual, \
|
||||
test_system_user_connectability_a_asset
|
||||
test_system_user_connectivity_manual, push_system_user_a_asset_manual, \
|
||||
test_system_user_connectivity_a_asset
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@ -33,7 +33,7 @@ __all__ = [
|
||||
'SystemUserViewSet', 'SystemUserAuthInfoApi',
|
||||
'SystemUserPushApi', 'SystemUserTestConnectiveApi',
|
||||
'SystemUserAssetsListView', 'SystemUserPushToAssetApi',
|
||||
'SystemUserTestAssetConnectabilityApi', 'SystemUserCommandFilterRuleListApi',
|
||||
'SystemUserTestAssetConnectivityApi', 'SystemUserCommandFilterRuleListApi',
|
||||
|
||||
]
|
||||
|
||||
@ -93,15 +93,16 @@ class SystemUserTestConnectiveApi(generics.RetrieveAPIView):
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
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})
|
||||
|
||||
|
||||
class SystemUserAssetsListView(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.AssetSerializer
|
||||
serializer_class = serializers.AssetSimpleSerializer
|
||||
pagination_class = LimitOffsetPagination
|
||||
filter_fields = ("hostname", "ip")
|
||||
http_method_names = ['get']
|
||||
search_fields = filter_fields
|
||||
|
||||
def get_object(self):
|
||||
@ -125,7 +126,7 @@ class SystemUserPushToAssetApi(generics.RetrieveAPIView):
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
class SystemUserTestAssetConnectabilityApi(generics.RetrieveAPIView):
|
||||
class SystemUserTestAssetConnectivityApi(generics.RetrieveAPIView):
|
||||
queryset = SystemUser.objects.all()
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
@ -133,7 +134,7 @@ class SystemUserTestAssetConnectabilityApi(generics.RetrieveAPIView):
|
||||
system_user = self.get_object()
|
||||
asset_id = self.kwargs.get('aid')
|
||||
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})
|
||||
|
||||
|
||||
|
@ -41,9 +41,6 @@ class AssetCreateForm(OrgModelForm):
|
||||
'nodes': _("Node"),
|
||||
}
|
||||
help_texts = {
|
||||
'hostname': '* required',
|
||||
'ip': '* required',
|
||||
'port': '* required',
|
||||
'admin_user': _(
|
||||
'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'
|
||||
@ -80,10 +77,6 @@ class AssetUpdateForm(OrgModelForm):
|
||||
'nodes': _("Node"),
|
||||
}
|
||||
help_texts = {
|
||||
'hostname': '* required',
|
||||
'ip': '* required',
|
||||
'port': '* required',
|
||||
'cluster': '* required',
|
||||
'admin_user': _(
|
||||
'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'
|
||||
@ -95,7 +88,7 @@ class AssetUpdateForm(OrgModelForm):
|
||||
|
||||
class AssetBulkUpdateForm(OrgModelForm):
|
||||
assets = forms.ModelMultipleChoiceField(
|
||||
required=True, help_text='* required',
|
||||
required=True,
|
||||
label=_('Select assets'), queryset=Asset.objects.all(),
|
||||
widget=forms.SelectMultiple(
|
||||
attrs={
|
||||
@ -142,14 +135,14 @@ class AssetBulkUpdateForm(OrgModelForm):
|
||||
if k in changed_fields}
|
||||
assets = cleaned_data.pop('assets')
|
||||
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.update(**cleaned_data)
|
||||
|
||||
if labels:
|
||||
for label in labels:
|
||||
label.assets.add(*tuple(assets))
|
||||
for asset in assets:
|
||||
asset.labels.set(labels)
|
||||
if nodes:
|
||||
for node in nodes:
|
||||
node.assets.add(*tuple(assets))
|
||||
for asset in assets:
|
||||
asset.nodes.set(nodes)
|
||||
return assets
|
||||
|
@ -28,6 +28,15 @@ class DomainForm(forms.ModelForm):
|
||||
initial['assets'] = kwargs['instance'].assets.all()
|
||||
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):
|
||||
instance = super().save(commit=commit)
|
||||
assets = self.cleaned_data['assets']
|
||||
@ -40,6 +49,8 @@ class GatewayForm(PasswordAndKeyAuthForm, OrgModelForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
password_field = self.fields.get('password')
|
||||
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):
|
||||
# Because we define custom field, so we need rewrite :method: `save`
|
||||
@ -59,7 +70,3 @@ class GatewayForm(PasswordAndKeyAuthForm, OrgModelForm):
|
||||
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
|
||||
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
|
||||
}
|
||||
help_texts = {
|
||||
'name': '* required',
|
||||
'username': '* required',
|
||||
}
|
||||
|
@ -26,6 +26,15 @@ class LabelForm(forms.ModelForm):
|
||||
initial['assets'] = kwargs['instance'].assets.all()
|
||||
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):
|
||||
label = super().save(commit=commit)
|
||||
assets = self.cleaned_data['assets']
|
||||
|
@ -80,10 +80,6 @@ class AdminUserForm(PasswordAndKeyAuthForm):
|
||||
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
|
||||
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
|
||||
}
|
||||
help_texts = {
|
||||
'name': '* required',
|
||||
'username': '* required',
|
||||
}
|
||||
|
||||
|
||||
class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
|
||||
@ -99,8 +95,10 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
|
||||
auto_generate_key = self.cleaned_data.get('auto_generate_key', False)
|
||||
private_key, public_key = super().gen_keys()
|
||||
|
||||
if login_mode == SystemUser.MANUAL_LOGIN or \
|
||||
protocol in [SystemUser.RDP_PROTOCOL, SystemUser.TELNET_PROTOCOL]:
|
||||
if login_mode == SystemUser.LOGIN_MANUAL or \
|
||||
protocol in [SystemUser.PROTOCOL_RDP,
|
||||
SystemUser.PROTOCOL_TELNET,
|
||||
SystemUser.PROTOCOL_VNC]:
|
||||
system_user.auto_push = 0
|
||||
auto_generate_key = False
|
||||
system_user.save()
|
||||
@ -120,17 +118,18 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
|
||||
if not self.instance and not auto_generate:
|
||||
super().validate_password_key()
|
||||
|
||||
def is_valid(self):
|
||||
validated = super().is_valid()
|
||||
username = self.cleaned_data.get('username')
|
||||
login_mode = self.cleaned_data.get('login_mode')
|
||||
if login_mode == SystemUser.AUTO_LOGIN and not username:
|
||||
self.add_error(
|
||||
"username", _('* Automatic login mode,'
|
||||
' must fill in the username.')
|
||||
)
|
||||
return False
|
||||
return validated
|
||||
def clean_username(self):
|
||||
username = self.data.get('username')
|
||||
login_mode = self.data.get('login_mode')
|
||||
protocol = self.data.get('protocol')
|
||||
|
||||
if username:
|
||||
return username
|
||||
if login_mode == SystemUser.LOGIN_AUTO and \
|
||||
protocol != SystemUser.PROTOCOL_VNC:
|
||||
msg = _('* Automatic login mode must fill in the username.')
|
||||
raise forms.ValidationError(msg)
|
||||
return username
|
||||
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
@ -147,8 +146,6 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
|
||||
}),
|
||||
}
|
||||
help_texts = {
|
||||
'name': '* required',
|
||||
'username': '* required',
|
||||
'auto_push': _('Auto push system user to asset'),
|
||||
'priority': _('1-100, High level will be using login asset as default, '
|
||||
'if user was granted more than 2 system user'),
|
||||
|
@ -13,36 +13,36 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
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(
|
||||
model_name='asset',
|
||||
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(
|
||||
model_name='domain',
|
||||
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(
|
||||
model_name='gateway',
|
||||
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(
|
||||
model_name='label',
|
||||
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(
|
||||
model_name='node',
|
||||
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(
|
||||
model_name='systemuser',
|
||||
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):
|
||||
migrations.CreateModel(
|
||||
name='CommandFilter',
|
||||
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)),
|
||||
('name', models.CharField(max_length=64, verbose_name='Name')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
||||
@ -32,7 +32,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='CommandFilterRule',
|
||||
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)),
|
||||
('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')),
|
||||
|
23
apps/assets/migrations/0024_auto_20181219_1614.py
Normal file
23
apps/assets/migrations/0024_auto_20181219_1614.py
Normal file
@ -0,0 +1,23 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .user import *
|
||||
from .label import Label
|
||||
from .cluster import *
|
||||
|
@ -13,7 +13,6 @@ from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.cache import cache
|
||||
|
||||
from ..const import ASSET_ADMIN_CONN_CACHE_KEY
|
||||
from .user import AdminUser, SystemUser
|
||||
from orgs.mixins import OrgModelMixin, OrgManager
|
||||
|
||||
@ -60,78 +59,65 @@ class Asset(OrgModelMixin):
|
||||
('Other', 'Other'),
|
||||
)
|
||||
|
||||
SSH_PROTOCOL = 'ssh'
|
||||
RDP_PROTOCOL = 'rdp'
|
||||
TELNET_PROTOCOL = 'telnet'
|
||||
PROTOCOL_SSH = 'ssh'
|
||||
PROTOCOL_RDP = 'rdp'
|
||||
PROTOCOL_TELNET = 'telnet'
|
||||
PROTOCOL_VNC = 'vnc'
|
||||
PROTOCOL_CHOICES = (
|
||||
(SSH_PROTOCOL, 'ssh'),
|
||||
(RDP_PROTOCOL, 'rdp'),
|
||||
(TELNET_PROTOCOL, 'telnet (beta)'),
|
||||
(PROTOCOL_SSH, 'ssh'),
|
||||
(PROTOCOL_RDP, 'rdp'),
|
||||
(PROTOCOL_TELNET, 'telnet (beta)'),
|
||||
(PROTOCOL_VNC, 'vnc'),
|
||||
)
|
||||
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True)
|
||||
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'))
|
||||
platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform'))
|
||||
domain = models.ForeignKey("assets.Domain", null=True, blank=True,
|
||||
related_name='assets', verbose_name=_("Domain"),
|
||||
on_delete=models.SET_NULL)
|
||||
nodes = models.ManyToManyField('assets.Node', default=default_node,
|
||||
related_name='assets',
|
||||
verbose_name=_("Nodes"))
|
||||
domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), 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'))
|
||||
|
||||
# Auth
|
||||
admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT,
|
||||
null=True, verbose_name=_("Admin user"))
|
||||
admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, null=True, verbose_name=_("Admin user"))
|
||||
|
||||
# Some information
|
||||
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'))
|
||||
|
||||
# Collect
|
||||
vendor = models.CharField(max_length=64, null=True, blank=True,
|
||||
verbose_name=_('Vendor'))
|
||||
model = models.CharField(max_length=54, null=True, blank=True,
|
||||
verbose_name=_('Model'))
|
||||
sn = models.CharField(max_length=128, null=True, blank=True,
|
||||
verbose_name=_('Serial number'))
|
||||
vendor = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Vendor'))
|
||||
model = models.CharField(max_length=54, null=True, blank=True, verbose_name=_('Model'))
|
||||
sn = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Serial number'))
|
||||
|
||||
cpu_model = models.CharField(max_length=64, null=True, blank=True,
|
||||
verbose_name=_('CPU model'))
|
||||
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_cores = models.IntegerField(null=True, verbose_name=_('CPU cores'))
|
||||
cpu_vcpus = models.IntegerField(null=True, verbose_name=_('CPU vcpus'))
|
||||
memory = models.CharField(max_length=64, null=True, blank=True,
|
||||
verbose_name=_('Memory'))
|
||||
disk_total = models.CharField(max_length=1024, null=True, blank=True,
|
||||
verbose_name=_('Disk total'))
|
||||
disk_info = models.CharField(max_length=1024, null=True, blank=True,
|
||||
verbose_name=_('Disk info'))
|
||||
memory = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Memory'))
|
||||
disk_total = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk total'))
|
||||
disk_info = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk info'))
|
||||
|
||||
os = models.CharField(max_length=128, null=True, blank=True,
|
||||
verbose_name=_('OS'))
|
||||
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'))
|
||||
hostname_raw = models.CharField(max_length=128, blank=True, null=True,
|
||||
verbose_name=_('Hostname raw'))
|
||||
os = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('OS'))
|
||||
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'))
|
||||
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'))
|
||||
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)()
|
||||
CONNECTIVITY_CACHE_KEY = '_JMS_ASSET_CONNECTIVITY_{}'
|
||||
UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3)
|
||||
CONNECTIVITY_CHOICES = (
|
||||
(UNREACHABLE, _("Unreachable")),
|
||||
(REACHABLE, _('Reachable')),
|
||||
(UNKNOWN, _("Unknown")),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return '{0.hostname}({0.ip})'.format(self)
|
||||
@ -145,6 +131,13 @@ class Asset(OrgModelMixin):
|
||||
return True, ''
|
||||
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):
|
||||
if self.platform not in ("Windows", "Windows2016"):
|
||||
return True
|
||||
@ -190,25 +183,17 @@ class Asset(OrgModelMixin):
|
||||
return ''
|
||||
|
||||
@property
|
||||
def is_connective(self):
|
||||
def connectivity(self):
|
||||
if not self.is_unixlike():
|
||||
return True
|
||||
val = cache.get(ASSET_ADMIN_CONN_CACHE_KEY.format(self.hostname))
|
||||
if val == 1:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return self.UNKNOWN
|
||||
key = self.CONNECTIVITY_CACHE_KEY.format(str(self.id))
|
||||
cached = cache.get(key, None)
|
||||
return cached if cached is not None else self.UNKNOWN
|
||||
|
||||
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
|
||||
@connectivity.setter
|
||||
def connectivity(self, value):
|
||||
key = self.CONNECTIVITY_CACHE_KEY.format(str(self.id))
|
||||
cache.set(key, value, 3600*2)
|
||||
|
||||
def get_auth_info(self):
|
||||
if self.admin_user:
|
||||
@ -229,11 +214,20 @@ class Asset(OrgModelMixin):
|
||||
fake_node.is_node = False
|
||||
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):
|
||||
"""
|
||||
Ansible use it create inventory, First using asset user,
|
||||
otherwise using cluster admin user
|
||||
|
||||
Ansible use it create inventory
|
||||
Todo: May be move to ops implements it
|
||||
"""
|
||||
data = self.to_json()
|
||||
@ -248,6 +242,36 @@ class Asset(OrgModelMixin):
|
||||
})
|
||||
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:
|
||||
unique_together = [('org_id', 'hostname')]
|
||||
verbose_name = _("Asset")
|
||||
@ -257,7 +281,8 @@ class Asset(OrgModelMixin):
|
||||
from random import seed, choice
|
||||
import forgery_py
|
||||
from django.db import IntegrityError
|
||||
|
||||
from .node import Node
|
||||
nodes = list(Node.objects.all())
|
||||
seed()
|
||||
for i in range(count):
|
||||
ip = [str(i) for i in random.sample(range(255), 4)]
|
||||
@ -268,6 +293,11 @@ class Asset(OrgModelMixin):
|
||||
created_by='Fake')
|
||||
try:
|
||||
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)]
|
||||
logger.debug('Generate fake asset : %s' % asset.ip)
|
||||
except IntegrityError:
|
||||
|
@ -29,6 +29,13 @@ class AssetUser(OrgModelMixin):
|
||||
date_updated = models.DateTimeField(auto_now=True)
|
||||
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
|
||||
def password(self):
|
||||
if self._password:
|
||||
@ -105,6 +112,9 @@ class AssetUser(OrgModelMixin):
|
||||
if update_fields:
|
||||
self.save(update_fields=update_fields)
|
||||
|
||||
def get_auth(self, asset=None):
|
||||
pass
|
||||
|
||||
def clear_auth(self):
|
||||
self._password = ''
|
||||
self._private_key = ''
|
||||
|
@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import uuid
|
||||
import re
|
||||
|
||||
from django.db import models
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
@ -35,7 +36,7 @@ class CommandFilterRule(OrgModelMixin):
|
||||
(TYPE_COMMAND, _('Command')),
|
||||
)
|
||||
|
||||
ACTION_DENY, ACTION_ALLOW = range(2)
|
||||
ACTION_DENY, ACTION_ALLOW, ACTION_UNKNOWN = range(3)
|
||||
ACTION_CHOICES = (
|
||||
(ACTION_DENY, _('Deny')),
|
||||
(ACTION_ALLOW, _('Allow')),
|
||||
@ -53,8 +54,34 @@ class CommandFilterRule(OrgModelMixin):
|
||||
date_updated = models.DateTimeField(auto_now=True)
|
||||
created_by = models.CharField(max_length=128, blank=True, default='', verbose_name=_('Created by'))
|
||||
|
||||
__pattern = None
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
return '{} % {}'.format(self.type, self.content)
|
||||
|
@ -4,6 +4,8 @@
|
||||
import uuid
|
||||
import random
|
||||
|
||||
import paramiko
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
@ -38,15 +40,15 @@ class Domain(OrgModelMixin):
|
||||
|
||||
|
||||
class Gateway(AssetUser):
|
||||
SSH_PROTOCOL = 'ssh'
|
||||
RDP_PROTOCOL = 'rdp'
|
||||
PROTOCOL_SSH = 'ssh'
|
||||
PROTOCOL_RDP = 'rdp'
|
||||
PROTOCOL_CHOICES = (
|
||||
(SSH_PROTOCOL, 'ssh'),
|
||||
(RDP_PROTOCOL, 'rdp'),
|
||||
(PROTOCOL_SSH, 'ssh'),
|
||||
(PROTOCOL_RDP, 'rdp'),
|
||||
)
|
||||
ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True)
|
||||
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"))
|
||||
comment = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Comment"))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
||||
@ -57,3 +59,37 @@ class Gateway(AssetUser):
|
||||
class Meta:
|
||||
unique_together = [('name', 'org_id')]
|
||||
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):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_("Name"))
|
||||
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"))
|
||||
comment = models.TextField(blank=True, null=True, verbose_name=_("Comment"))
|
||||
date_created = models.DateTimeField(
|
||||
|
@ -5,6 +5,7 @@ import uuid
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ugettext
|
||||
from django.core.cache import cache
|
||||
|
||||
from orgs.mixins import OrgModelMixin
|
||||
@ -22,7 +23,9 @@ class Node(OrgModelMixin):
|
||||
date_create = models.DateTimeField(auto_now_add=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:
|
||||
verbose_name = _("Node")
|
||||
@ -49,30 +52,65 @@ class Node(OrgModelMixin):
|
||||
def name(self):
|
||||
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
|
||||
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)
|
||||
if 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():
|
||||
return self.value
|
||||
parent_full_value = self.parent.full_value
|
||||
value = parent_full_value + ' / ' + self.value
|
||||
key = self._full_value_cache_key.format(self.key)
|
||||
cache.set(key, value, 3600)
|
||||
return value
|
||||
|
||||
def cache_full_value(self, value):
|
||||
key = self._full_value_cache_key_prefix.format(self.key)
|
||||
cache.set(key, value, 3600)
|
||||
|
||||
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+'*')
|
||||
|
||||
@property
|
||||
@ -85,6 +123,17 @@ class Node(OrgModelMixin):
|
||||
self.save()
|
||||
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):
|
||||
with transaction.atomic():
|
||||
child_key = self.get_next_child_key()
|
||||
@ -134,7 +183,7 @@ class Node(OrgModelMixin):
|
||||
pattern = r'^{0}$|^{0}:'.format(self.key)
|
||||
args = []
|
||||
kwargs = {}
|
||||
if self.is_default_node():
|
||||
if self.is_root():
|
||||
args.append(Q(nodes__key__regex=pattern) | Q(nodes=None))
|
||||
else:
|
||||
kwargs['nodes__key__regex'] = pattern
|
||||
@ -182,17 +231,18 @@ class Node(OrgModelMixin):
|
||||
child.save()
|
||||
self.save()
|
||||
|
||||
def get_ancestor(self, with_self=False):
|
||||
if self.is_root():
|
||||
root = self.__class__.root()
|
||||
return [root]
|
||||
_key = self.key.split(':')
|
||||
def get_ancestor_keys(self, with_self=False):
|
||||
parent_keys = []
|
||||
key_list = self.key.split(":")
|
||||
if not with_self:
|
||||
_key.pop()
|
||||
ancestor_keys = []
|
||||
for i in range(len(_key)):
|
||||
ancestor_keys.append(':'.join(_key))
|
||||
_key.pop()
|
||||
key_list.pop()
|
||||
for i in range(len(key_list)):
|
||||
parent_keys.append(":".join(key_list))
|
||||
key_list.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(
|
||||
key__in=ancestor_keys
|
||||
).order_by('key')
|
||||
@ -203,17 +253,14 @@ class Node(OrgModelMixin):
|
||||
# 如果使用current_org 在set_current_org时会死循环
|
||||
_current_org = get_current_org()
|
||||
with transaction.atomic():
|
||||
if _current_org.is_root():
|
||||
key = '0'
|
||||
elif _current_org.is_default():
|
||||
key = '1'
|
||||
else:
|
||||
set_current_org(Organization.root())
|
||||
org_nodes_roots = cls.objects.filter(key__regex=r'^[0-9]+$')
|
||||
org_nodes_roots_keys = org_nodes_roots.values_list('key', flat=True) or ['1']
|
||||
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)
|
||||
if not _current_org.is_real():
|
||||
return cls.default_node()
|
||||
set_current_org(Organization.root())
|
||||
org_nodes_roots = cls.objects.filter(key__regex=r'^[0-9]+$')
|
||||
org_nodes_roots_keys = org_nodes_roots.values_list('key', flat=True) or ['1']
|
||||
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)
|
||||
return root
|
||||
|
||||
@ -230,9 +277,25 @@ class Node(OrgModelMixin):
|
||||
defaults = {'value': 'Default'}
|
||||
return cls.objects.get_or_create(defaults=defaults, key='1')
|
||||
|
||||
@classmethod
|
||||
def get_tree_name_ref(cls):
|
||||
pass
|
||||
def as_tree_node(self):
|
||||
from common.tree import TreeNode
|
||||
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
|
||||
def generate_fake(cls, count=100):
|
||||
|
@ -14,7 +14,7 @@ from ..const import SYSTEM_USER_CONN_CACHE_KEY
|
||||
from .base import AssetUser
|
||||
|
||||
|
||||
__all__ = ['AdminUser', 'SystemUser',]
|
||||
__all__ = ['AdminUser', 'SystemUser']
|
||||
logger = logging.getLogger(__name__)
|
||||
signer = get_signer()
|
||||
|
||||
@ -31,6 +31,7 @@ class AdminUser(AssetUser):
|
||||
become_method = models.CharField(choices=BECOME_METHOD_CHOICES, default='sudo', max_length=4)
|
||||
become_user = models.CharField(default='root', max_length=64)
|
||||
_become_pass = models.CharField(default='', max_length=128)
|
||||
CONNECTIVE_CACHE_KEY = '_JMS_ADMIN_USER_CONNECTIVE_{}'
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@ -67,6 +68,23 @@ class AdminUser(AssetUser):
|
||||
def assets_amount(self):
|
||||
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:
|
||||
ordering = ['name']
|
||||
unique_together = [('name', 'org_id')]
|
||||
@ -94,34 +112,36 @@ class AdminUser(AssetUser):
|
||||
|
||||
|
||||
class SystemUser(AssetUser):
|
||||
SSH_PROTOCOL = 'ssh'
|
||||
RDP_PROTOCOL = 'rdp'
|
||||
TELNET_PROTOCOL = 'telnet'
|
||||
PROTOCOL_SSH = 'ssh'
|
||||
PROTOCOL_RDP = 'rdp'
|
||||
PROTOCOL_TELNET = 'telnet'
|
||||
PROTOCOL_VNC = 'vnc'
|
||||
PROTOCOL_CHOICES = (
|
||||
(SSH_PROTOCOL, 'ssh'),
|
||||
(RDP_PROTOCOL, 'rdp'),
|
||||
(TELNET_PROTOCOL, 'telnet (beta)'),
|
||||
(PROTOCOL_SSH, 'ssh'),
|
||||
(PROTOCOL_RDP, 'rdp'),
|
||||
(PROTOCOL_TELNET, 'telnet (beta)'),
|
||||
(PROTOCOL_VNC, 'vnc'),
|
||||
)
|
||||
|
||||
AUTO_LOGIN = 'auto'
|
||||
MANUAL_LOGIN = 'manual'
|
||||
LOGIN_AUTO = 'auto'
|
||||
LOGIN_MANUAL = 'manual'
|
||||
LOGIN_MODE_CHOICES = (
|
||||
(AUTO_LOGIN, _('Automatic login')),
|
||||
(MANUAL_LOGIN, _('Manually login'))
|
||||
(LOGIN_AUTO, _('Automatic login')),
|
||||
(LOGIN_MANUAL, _('Manually login'))
|
||||
)
|
||||
|
||||
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
|
||||
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
|
||||
priority = models.IntegerField(default=20, verbose_name=_("Priority"),
|
||||
validators=[MinValueValidator(1), MaxValueValidator(100)])
|
||||
priority = models.IntegerField(default=20, verbose_name=_("Priority"), validators=[MinValueValidator(1), MaxValueValidator(100)])
|
||||
protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol'))
|
||||
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
|
||||
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
|
||||
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)
|
||||
|
||||
cache_key = "__SYSTEM_USER_CACHED_{}"
|
||||
SYSTEM_USER_CACHE_KEY = "__SYSTEM_USER_CACHED_{}"
|
||||
CONNECTIVE_CACHE_KEY = '_JMS_SYSTEM_USER_CONNECTIVE_{}'
|
||||
|
||||
def __str__(self):
|
||||
return '{0.name}({0.username})'.format(self)
|
||||
@ -136,34 +156,61 @@ class SystemUser(AssetUser):
|
||||
'auto_push': self.auto_push,
|
||||
}
|
||||
|
||||
def get_assets(self):
|
||||
def get_related_assets(self):
|
||||
assets = set(self.assets.all())
|
||||
return assets
|
||||
|
||||
@property
|
||||
def assets_connective(self):
|
||||
_result = cache.get(SYSTEM_USER_CONN_CACHE_KEY.format(self.name), {})
|
||||
return _result
|
||||
def connectivity(self):
|
||||
cache_key = self.CONNECTIVE_CACHE_KEY.format(str(self.id))
|
||||
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 unreachable_assets(self):
|
||||
return list(self.assets_connective.get('dark', {}).keys())
|
||||
def assets_unreachable(self):
|
||||
return self.connectivity.get('unreachable')
|
||||
|
||||
@property
|
||||
def reachable_assets(self):
|
||||
return self.assets_connective.get('contacted', [])
|
||||
def assets_reachable(self):
|
||||
return self.connectivity.get('reachable')
|
||||
|
||||
@property
|
||||
def login_mode_display(self):
|
||||
return self.get_login_mode_display()
|
||||
|
||||
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
|
||||
else:
|
||||
return False
|
||||
|
||||
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):
|
||||
cache.delete(self.cache_key.format(self.id))
|
||||
cache.delete(self.SYSTEM_USER_CACHE_KEY.format(self.id))
|
||||
|
||||
@property
|
||||
def cmd_filter_rules(self):
|
||||
@ -173,9 +220,18 @@ class SystemUser(AssetUser):
|
||||
).distinct()
|
||||
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
|
||||
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:
|
||||
return cached
|
||||
try:
|
||||
|
@ -9,6 +9,7 @@ from .system_user import AssetSystemUserSerializer
|
||||
|
||||
__all__ = [
|
||||
'AssetSerializer', 'AssetGrantedSerializer', 'MyAssetGrantedSerializer',
|
||||
'AssetAsNodeSerializer', 'AssetSimpleSerializer',
|
||||
]
|
||||
|
||||
|
||||
@ -22,14 +23,27 @@ class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer):
|
||||
fields = '__all__'
|
||||
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):
|
||||
fields = super().get_field_names(declared_fields, info)
|
||||
fields.extend([
|
||||
'hardware_info', 'is_connective', 'org_name'
|
||||
'hardware_info', 'connectivity', 'org_name'
|
||||
])
|
||||
return fields
|
||||
|
||||
|
||||
class AssetAsNodeSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = ['id', 'hostname', 'ip', 'port', 'platform', 'protocol']
|
||||
|
||||
|
||||
class AssetGrantedSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
被授权资产的数据结构
|
||||
@ -64,3 +78,9 @@ class MyAssetGrantedSerializer(AssetGrantedSerializer):
|
||||
"is_active", "system_users_join", "org_name",
|
||||
"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):
|
||||
|
||||
|
||||
class GatewaySerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Gateway
|
||||
fields = [
|
||||
|
@ -8,85 +8,36 @@ from .asset import AssetGrantedSerializer
|
||||
|
||||
|
||||
__all__ = [
|
||||
'NodeSerializer', "NodeGrantedSerializer", "NodeAddChildrenSerializer",
|
||||
'NodeSerializer', "NodeAddChildrenSerializer",
|
||||
"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):
|
||||
assets_amount = serializers.SerializerMethodField()
|
||||
tree_id = serializers.SerializerMethodField()
|
||||
tree_parent = serializers.SerializerMethodField()
|
||||
assets_amount = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Node
|
||||
fields = [
|
||||
'id', 'key', 'value', 'assets_amount',
|
||||
'is_node', 'org_id', 'tree_id', 'tree_parent',
|
||||
'id', 'key', 'value', 'assets_amount', 'org_id',
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'key', 'assets_amount', 'org_id',
|
||||
]
|
||||
list_serializer_class = BulkListSerializer
|
||||
|
||||
def validate(self, data):
|
||||
value = data.get('value')
|
||||
def validate_value(self, data):
|
||||
instance = self.instance if self.instance else Node.root()
|
||||
children = instance.parent.get_children().exclude(key=instance.key)
|
||||
values = [child.value for child in children]
|
||||
if value in values:
|
||||
if data in values:
|
||||
raise serializers.ValidationError(
|
||||
'The same level node name cannot be the same'
|
||||
)
|
||||
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):
|
||||
assets = serializers.PrimaryKeyRelatedField(many=True, queryset = Asset.objects.all())
|
||||
assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all())
|
||||
|
||||
class Meta:
|
||||
model = Node
|
||||
@ -95,3 +46,4 @@ class NodeAssetsSerializer(serializers.ModelSerializer):
|
||||
|
||||
class NodeAddChildrenSerializer(serializers.Serializer):
|
||||
nodes = serializers.ListField()
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..models import SystemUser
|
||||
from ..models import SystemUser, Asset
|
||||
from .base import AuthSerializer
|
||||
|
||||
|
||||
@ -21,17 +21,17 @@ class SystemUserSerializer(serializers.ModelSerializer):
|
||||
def get_field_names(self, declared_fields, info):
|
||||
fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info)
|
||||
fields.extend([
|
||||
'get_login_mode_display',
|
||||
'login_mode_display',
|
||||
])
|
||||
return fields
|
||||
|
||||
@staticmethod
|
||||
def get_unreachable_assets(obj):
|
||||
return obj.unreachable_assets
|
||||
return obj.assets_unreachable
|
||||
|
||||
@staticmethod
|
||||
def get_reachable_assets(obj):
|
||||
return obj.reachable_assets
|
||||
return obj.assets_reachable
|
||||
|
||||
def get_unreachable_amount(self, obj):
|
||||
return len(self.get_unreachable_assets(obj))
|
||||
@ -41,7 +41,7 @@ class SystemUserSerializer(serializers.ModelSerializer):
|
||||
|
||||
@staticmethod
|
||||
def get_assets_amount(obj):
|
||||
return len(obj.get_assets())
|
||||
return len(obj.get_related_assets())
|
||||
|
||||
|
||||
class SystemUserAuthSerializer(AuthSerializer):
|
||||
@ -75,4 +75,7 @@ class SystemUserSimpleSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
fields = ('id', 'name', 'username')
|
||||
fields = ('id', 'name', 'username')
|
||||
|
||||
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
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 common.utils import get_logger
|
||||
from .models import Asset, SystemUser, Node
|
||||
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__)
|
||||
@ -19,8 +19,8 @@ def update_asset_hardware_info_on_created(asset):
|
||||
|
||||
|
||||
def test_asset_conn_on_created(asset):
|
||||
logger.debug("Test asset `{}` connectability".format(asset))
|
||||
test_asset_connectability_util.delay([asset])
|
||||
logger.debug("Test asset `{}` connectivity".format(asset))
|
||||
test_asset_connectivity_util.delay([asset])
|
||||
|
||||
|
||||
def set_asset_root_node(asset):
|
||||
@ -35,6 +35,17 @@ def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
|
||||
update_asset_hardware_info_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")
|
||||
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):
|
||||
def on_system_user_assets_change(sender, instance=None, **kwargs):
|
||||
if instance and kwargs["action"] == "post_add":
|
||||
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)
|
||||
def on_asset_node_changed(sender, instance=None, **kwargs):
|
||||
logger.debug("Asset nodes change signal received")
|
||||
if isinstance(instance, Asset):
|
||||
if kwargs['action'] == 'post_add':
|
||||
logger.debug("Asset node change signal received")
|
||||
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':
|
||||
nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
|
||||
Node.expire_nodes_assets_amount(nodes)
|
||||
system_users_assets = defaultdict(set)
|
||||
system_users = SystemUser.objects.filter(nodes__in=nodes)
|
||||
# 清理节点缓存
|
||||
@ -79,9 +94,11 @@ def on_asset_node_changed(sender, instance=None, **kwargs):
|
||||
@receiver(m2m_changed, sender=Asset.nodes.through)
|
||||
def on_node_assets_changed(sender, instance=None, **kwargs):
|
||||
if isinstance(instance, Node):
|
||||
logger.debug("Node assets change signal {} received".format(instance))
|
||||
# 当节点和资产关系发生改变时,过期资产数量缓存
|
||||
instance.expire_assets_amount()
|
||||
assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
|
||||
if kwargs['action'] == 'post_add':
|
||||
logger.debug("Node assets change signal received")
|
||||
# 重新关联系统用户和资产的关系
|
||||
system_users = SystemUser.objects.filter(nodes=instance)
|
||||
for system_user in system_users:
|
||||
|
@ -1,17 +1,16 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import os
|
||||
|
||||
from celery import shared_task
|
||||
from django.core.cache import cache
|
||||
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
|
||||
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 ops.celery.utils import register_as_period_task, after_app_shutdown_clean
|
||||
|
||||
from .models import SystemUser, AdminUser, Asset
|
||||
from . import const
|
||||
@ -20,34 +19,51 @@ from . import const
|
||||
FORKS = 10
|
||||
TIMEOUT = 60
|
||||
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')
|
||||
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
|
||||
def set_assets_hardware_info(result, **kwargs):
|
||||
def set_assets_hardware_info(assets, result, **kwargs):
|
||||
"""
|
||||
Using ops task run result, to update asset info
|
||||
|
||||
@shared_task must be exit, because we using it as a task callback, is must
|
||||
be a celery task also
|
||||
:param assets:
|
||||
:param result:
|
||||
:param kwargs: {task_name: ""}
|
||||
:return:
|
||||
"""
|
||||
result_raw = result[0]
|
||||
assets_updated = []
|
||||
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', {})
|
||||
if not info:
|
||||
logger.error("Get asset info failed: {}".format(hostname))
|
||||
logger.error(_("Get asset info failed: {}").format(hostname))
|
||||
continue
|
||||
|
||||
asset = Asset.objects.get_object_by_fullname(hostname)
|
||||
if not asset:
|
||||
continue
|
||||
|
||||
___vendor = info.get('ansible_system_vendor', 'Unknown')
|
||||
___model = info.get('ansible_product_name', 'Unknown')
|
||||
___sn = info.get('ansible_product_serial', 'Unknown')
|
||||
@ -59,9 +75,12 @@ def set_assets_hardware_info(result, **kwargs):
|
||||
___cpu_model = 'Unknown'
|
||||
___cpu_model = ___cpu_model[:64]
|
||||
___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)
|
||||
___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 = {}
|
||||
for dev, dev_info in info.get('ansible_devices', {}).items():
|
||||
if disk_pattern.match(dev) and dev_info['removable'] == '0':
|
||||
@ -94,34 +113,31 @@ def update_assets_hardware_info_util(assets, task_name=None):
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
if task_name is None:
|
||||
task_name = _("Update some assets hardware info")
|
||||
# task_name = _("更新资产硬件信息")
|
||||
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
|
||||
hostname_list = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
|
||||
if not hostname_list:
|
||||
logger.info("Not hosts get, may be asset is not active or not unixlike platform")
|
||||
hosts = clean_hosts(assets)
|
||||
if not hosts:
|
||||
return {}
|
||||
created_by = str(assets[0].org_id)
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name, hosts=hostname_list, tasks=tasks, pattern='all',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System',
|
||||
task_name, hosts=hosts, tasks=tasks, created_by=created_by,
|
||||
pattern='all', options=const.TASK_OPTIONS, run_as_admin=True,
|
||||
)
|
||||
result = task.run()
|
||||
# Todo: may be somewhere using
|
||||
# Manual run callback function
|
||||
set_assets_hardware_info(result)
|
||||
set_assets_hardware_info(assets, result)
|
||||
return result
|
||||
|
||||
|
||||
@shared_task
|
||||
def update_asset_hardware_info_manual(asset):
|
||||
task_name = _("Update asset hardware info")
|
||||
# task_name = _("更新资产硬件信息")
|
||||
return update_assets_hardware_info_util([asset], task_name=task_name)
|
||||
task_name = _("Update asset hardware info: {}").format(asset.hostname)
|
||||
return update_assets_hardware_info_util(
|
||||
[asset], task_name=task_name
|
||||
)
|
||||
|
||||
|
||||
@celery_app.task
|
||||
@register_as_period_task(interval=3600)
|
||||
@after_app_ready_start
|
||||
@after_app_shutdown_clean
|
||||
@shared_task
|
||||
def update_assets_hardware_info_period():
|
||||
"""
|
||||
Update asset hardware period task
|
||||
@ -131,126 +147,41 @@ def update_assets_hardware_info_period():
|
||||
logger.debug("Period task disabled, update assets hardware info pass")
|
||||
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 ##
|
||||
|
||||
@shared_task
|
||||
def set_admin_user_connectability_info(result, **kwargs):
|
||||
admin_user = kwargs.get("admin_user")
|
||||
task_name = kwargs.get("task_name")
|
||||
if admin_user is None and task_name is not None:
|
||||
admin_user = task_name.split(":")[-1]
|
||||
|
||||
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', []):
|
||||
asset_conn_cache_key = const.ASSET_ADMIN_CONN_CACHE_KEY.format(i)
|
||||
cache.set(asset_conn_cache_key, 1, CACHE_MAX_TIME)
|
||||
|
||||
for i, msg in summary.get('dark', {}).items():
|
||||
asset_conn_cache_key = const.ASSET_ADMIN_CONN_CACHE_KEY.format(i)
|
||||
cache.set(asset_conn_cache_key, 0, CACHE_MAX_TIME)
|
||||
logger.error(msg)
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_admin_user_connectability_util(admin_user, task_name):
|
||||
"""
|
||||
Test asset admin user can connect or not. Using ansible api do that
|
||||
:param admin_user:
|
||||
:param task_name:
|
||||
:return:
|
||||
"""
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
|
||||
assets = admin_user.get_related_assets()
|
||||
hosts = [asset.fullname for asset in assets
|
||||
if asset.is_active and asset.is_unixlike()]
|
||||
if not hosts:
|
||||
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()
|
||||
set_admin_user_connectability_info(result, admin_user=admin_user.name)
|
||||
return result
|
||||
|
||||
|
||||
@celery_app.task
|
||||
@register_as_period_task(interval=3600)
|
||||
@after_app_ready_start
|
||||
@after_app_shutdown_clean
|
||||
def test_admin_user_connectability_period():
|
||||
"""
|
||||
A period task that update the ansible task period
|
||||
"""
|
||||
if PERIOD_TASK != "on":
|
||||
logger.debug("Period task disabled, test admin user connectability pass")
|
||||
return
|
||||
|
||||
admin_users = AdminUser.objects.all()
|
||||
for admin_user in admin_users:
|
||||
task_name = _("Test admin user connectability period: {}".format(admin_user.name))
|
||||
# task_name = _("定期测试管理账号可连接性: {}".format(admin_user.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):
|
||||
def test_asset_connectivity_util(assets, task_name=None):
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
|
||||
if task_name is None:
|
||||
task_name = _("Test assets connectability")
|
||||
# task_name = _("测试资产可连接性")
|
||||
hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
|
||||
task_name = _("Test assets connectivity")
|
||||
hosts = clean_hosts(assets)
|
||||
if not hosts:
|
||||
logger.info("No hosts, passed")
|
||||
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='System',
|
||||
options=const.TASK_OPTIONS, run_as_admin=True, created_by=created_by,
|
||||
)
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_asset_connectability_manual(asset):
|
||||
summary = test_asset_connectability_util([asset])
|
||||
def test_asset_connectivity_manual(asset):
|
||||
task_name = _("Test assets connectivity: {}").format(asset)
|
||||
summary = test_asset_connectivity_util([asset], task_name=task_name)
|
||||
|
||||
if summary.get('dark'):
|
||||
return False, summary['dark']
|
||||
@ -258,21 +189,56 @@ def test_asset_connectability_manual(asset):
|
||||
return True, ""
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_admin_user_connectivity_util(admin_user, task_name):
|
||||
"""
|
||||
Test asset admin user can connect or not. Using ansible api do that
|
||||
:param admin_user:
|
||||
:param task_name:
|
||||
:return:
|
||||
"""
|
||||
assets = admin_user.get_related_assets()
|
||||
hosts = clean_hosts(assets)
|
||||
if not hosts:
|
||||
return {}
|
||||
summary = test_asset_connectivity_util(hosts, task_name)
|
||||
return summary
|
||||
|
||||
|
||||
@shared_task
|
||||
@register_as_period_task(interval=3600)
|
||||
def test_admin_user_connectivity_period():
|
||||
"""
|
||||
A period task that update the ansible task period
|
||||
"""
|
||||
key = '_JMS_TEST_ADMIN_USER_CONNECTIVITY_PERIOD'
|
||||
prev_execute_time = cache.get(key)
|
||||
if prev_execute_time:
|
||||
logger.debug("Test admin user connectivity, less than 40 minutes, skip")
|
||||
return
|
||||
cache.set(key, 1, 60*40)
|
||||
admin_users = AdminUser.objects.all()
|
||||
for admin_user in admin_users:
|
||||
task_name = _("Test admin user connectivity period: {}").format(admin_user.name)
|
||||
test_admin_user_connectivity_util(admin_user, task_name)
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_admin_user_connectivity_manual(admin_user):
|
||||
task_name = _("Test admin user connectivity: {}").format(admin_user.name)
|
||||
return test_admin_user_connectivity_util(admin_user, task_name)
|
||||
|
||||
|
||||
## System user connective ##
|
||||
|
||||
@shared_task
|
||||
def set_system_user_connectablity_info(result, **kwargs):
|
||||
def set_system_user_connectivity_info(system_user, result):
|
||||
summary = result[1]
|
||||
task_name = kwargs.get("task_name")
|
||||
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)
|
||||
system_user.connectivity = summary
|
||||
|
||||
|
||||
@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.
|
||||
:param system_user:
|
||||
@ -281,51 +247,45 @@ def test_system_user_connectability_util(system_user, assets, task_name):
|
||||
:return:
|
||||
"""
|
||||
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
|
||||
hosts = clean_hosts(assets)
|
||||
if not hosts:
|
||||
logger.info("No hosts, passed")
|
||||
return {}
|
||||
task, created = update_or_create_ansible_task(
|
||||
task_name, hosts=hosts, tasks=tasks, pattern='all',
|
||||
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()
|
||||
set_system_user_connectablity_info(result, system_user=system_user.name)
|
||||
set_system_user_connectivity_info(system_user, result)
|
||||
return result
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_system_user_connectability_manual(system_user):
|
||||
task_name = _("Test system user connectability: {}").format(system_user)
|
||||
assets = system_user.get_assets()
|
||||
return test_system_user_connectability_util(system_user, assets, task_name)
|
||||
def test_system_user_connectivity_manual(system_user):
|
||||
task_name = _("Test system user connectivity: {}").format(system_user)
|
||||
assets = system_user.get_related_assets()
|
||||
return test_system_user_connectivity_util(system_user, assets, task_name)
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_system_user_connectability_a_asset(system_user, asset):
|
||||
task_name = _("Test system user connectability: {} => {}").format(
|
||||
def test_system_user_connectivity_a_asset(system_user, asset):
|
||||
task_name = _("Test system user connectivity: {} => {}").format(
|
||||
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
|
||||
@register_as_period_task(interval=3600)
|
||||
@after_app_ready_start
|
||||
@after_app_shutdown_clean
|
||||
def test_system_user_connectability_period():
|
||||
def test_system_user_connectivity_period():
|
||||
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
|
||||
|
||||
system_users = SystemUser.objects.all()
|
||||
for system_user in system_users:
|
||||
task_name = _("Test system user connectability period: {}".format(system_user))
|
||||
# task_name = _("定期测试系统用户可连接性: {}".format(system_user))
|
||||
test_system_user_connectability_util(system_user, task_name)
|
||||
task_name = _("Test system user connectivity period: {}").format(system_user)
|
||||
assets = system_user.get_related_assets()
|
||||
test_system_user_connectivity_util(system_user, assets, task_name)
|
||||
|
||||
|
||||
#### Push system user tasks ####
|
||||
@ -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:
|
||||
tasks.append({
|
||||
'name': 'Set {} authorized key'.format(system_user.username),
|
||||
@ -374,53 +352,57 @@ def get_push_system_user_tasks(system_user):
|
||||
|
||||
|
||||
@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
|
||||
tasks = []
|
||||
for system_user in system_users:
|
||||
if not system_user.is_need_push():
|
||||
msg = "push system user `{}` passed, may be not auto push or ssh " \
|
||||
"protocol is not ssh".format(system_user.name)
|
||||
logger.info(msg)
|
||||
continue
|
||||
tasks.extend(get_push_system_user_tasks(system_user))
|
||||
if not system_user.is_need_push():
|
||||
msg = _("Push system user task skip, auto push not enable or "
|
||||
"protocol is not ssh: {}").format(system_user.name)
|
||||
logger.info(msg)
|
||||
return
|
||||
|
||||
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:
|
||||
logger.info("Not hosts, passed")
|
||||
return {}
|
||||
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'
|
||||
options=const.TASK_OPTIONS, run_as_admin=True,
|
||||
created_by=system_user.org_id,
|
||||
)
|
||||
return task.run()
|
||||
|
||||
|
||||
@shared_task
|
||||
def push_system_user_to_assets_manual(system_user):
|
||||
assets = system_user.get_assets()
|
||||
# task_name = "推送系统用户到入资产: {}".format(system_user.name)
|
||||
assets = system_user.get_related_assets()
|
||||
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
|
||||
def push_system_user_a_asset_manual(system_user, asset):
|
||||
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
|
||||
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)
|
||||
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
|
||||
|
@ -57,6 +57,10 @@
|
||||
<script>
|
||||
var zTree2, asset_table2 = 0;
|
||||
function initTable2() {
|
||||
if(asset_table2){
|
||||
return
|
||||
}
|
||||
|
||||
var options = {
|
||||
ele: $('#asset_list_modal_table'),
|
||||
ajax_url: '{% url "api-assets:asset-list" %}?show_current_asset=1',
|
||||
@ -69,16 +73,16 @@ function initTable2() {
|
||||
return asset_table2
|
||||
}
|
||||
|
||||
function onSelected2(event, treeNode) {
|
||||
function onNodeSelected2(event, treeNode) {
|
||||
var url = asset_table2.ajax.url();
|
||||
url = setUrlParam(url, "node_id", treeNode.node_id);
|
||||
setCookie('node_selected', treeNode.id);
|
||||
url = setUrlParam(url, "node_id", treeNode.meta.node.id);
|
||||
asset_table2.ajax.url(url);
|
||||
asset_table2.ajax.reload();
|
||||
}
|
||||
|
||||
|
||||
function initTree2() {
|
||||
var url = '{% url 'api-assets:node-children-tree' %}?assets=0';
|
||||
var setting = {
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
@ -89,33 +93,22 @@ function initTree2() {
|
||||
enable: true
|
||||
}
|
||||
},
|
||||
async: {
|
||||
enable: true,
|
||||
url: url,
|
||||
autoParam: ["id=key", "name=n", "level=lv"],
|
||||
type: 'get'
|
||||
},
|
||||
callback: {
|
||||
onSelected: onSelected2
|
||||
onSelected: onNodeSelected2
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
zTree2 = $.fn.zTree.init($("#assetTree2"), setting);
|
||||
}
|
||||
|
||||
|
||||
$(document).ready(function(){
|
||||
}).on('show.bs.modal', function () {
|
||||
initTable2();
|
||||
initTree2();
|
||||
})
|
||||
|
@ -103,15 +103,17 @@ var need_change_field_login_mode = [
|
||||
];
|
||||
|
||||
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');
|
||||
$('#command-filter-block').addClass('hidden');
|
||||
$.each(need_change_field, function (index, value) {
|
||||
$(value).closest('.form-group').addClass('hidden')
|
||||
});
|
||||
}
|
||||
else if ($(protocol_id + " option:selected").text() === 'telnet (beta)') {
|
||||
else if (protocol === 'telnet (beta)') {
|
||||
$('.auth-fields').removeClass('hidden');
|
||||
$('#command-filter-block').removeClass('hidden');
|
||||
$.each(need_change_field, function (index, value) {
|
||||
$(value).closest('.form-group').addClass('hidden')
|
||||
});
|
||||
@ -123,6 +125,7 @@ function protocolChange() {
|
||||
return
|
||||
}
|
||||
authFieldsDisplay();
|
||||
$('#command-filter-block').removeClass('hidden');
|
||||
$.each(need_change_field, function (index, value) {
|
||||
$(value).closest('.form-group').removeClass('hidden')
|
||||
});
|
||||
|
@ -45,13 +45,11 @@
|
||||
<table class="table table-striped table-bordered table-hover" id="asset_list_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
<input type="checkbox" id="check_all" class="ipt_check_all" >
|
||||
</th>
|
||||
<th>{% trans 'Hostname' %}</th>
|
||||
<th>{% trans 'IP' %}</th>
|
||||
<th>{% trans 'Port' %}</th>
|
||||
<th>{% trans 'Reachable' %}</th>
|
||||
<th>{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -91,26 +89,36 @@
|
||||
<script>
|
||||
|
||||
function initTable() {
|
||||
var reachable = {{ admin_user.REACHABLE }};
|
||||
var unreachable = {{ admin_user.UNREACHABLE }};
|
||||
var options = {
|
||||
ele: $('#asset_list_table'),
|
||||
buttons: [],
|
||||
order: [],
|
||||
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>';
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
if (!cellData) {
|
||||
{targets: 3, createdCell: function (td, cellData) {
|
||||
if (cellData === unreachable) {
|
||||
$(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>')
|
||||
}
|
||||
}}],
|
||||
ajax_url: '{% url "api-assets:asset-list" %}?admin_user_id={{ admin_user.id }}',
|
||||
} else {
|
||||
$(td).html('')
|
||||
}
|
||||
}},
|
||||
{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: [
|
||||
{data: function(){return ""}}, {data: "hostname" }, {data: "ip" },
|
||||
{data: "port" }, {data: "is_connective" }],
|
||||
{data: "hostname" }, {data: "ip" },
|
||||
{data: "port" }, {data: "connectivity" }, {data: "id"}],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
jumpserver.initServerSideDataTable(options);
|
||||
@ -119,6 +127,21 @@ function initTable() {
|
||||
$(document).ready(function () {
|
||||
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 () {
|
||||
var the_url = "{% url 'api-assets:admin-user-connective' pk=admin_user.id %}";
|
||||
var success = function (data) {
|
||||
|
@ -86,15 +86,21 @@ $(document).ready(function () {
|
||||
allowClear: true,
|
||||
templateSelection: format
|
||||
});
|
||||
$('#id_nodes.select2').select2({
|
||||
closeOnSelect: false
|
||||
});
|
||||
$("#id_protocol").change(function (){
|
||||
var protocol = $("#id_protocol option:selected").text();
|
||||
var port = 22;
|
||||
if(protocol === 'rdp'){
|
||||
port = 3389;
|
||||
}
|
||||
if(protocol === 'telnet (beta)'){
|
||||
else if(protocol === 'telnet (beta)'){
|
||||
port = 23;
|
||||
}
|
||||
else if(protocol === 'vnc'){
|
||||
port = 5901;
|
||||
}
|
||||
$("#id_port").val(port);
|
||||
});
|
||||
})
|
||||
|
@ -77,6 +77,10 @@
|
||||
<td>{% trans 'Admin user' %}:</td>
|
||||
<td><b>{{ asset.admin_user }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Domain' %}:</td>
|
||||
<td><b>{{ asset.domain|default:"" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Vendor' %}:</td>
|
||||
<td><b>{{ asset.vendor|default:"" }}</b></td>
|
||||
@ -159,7 +163,7 @@
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% if asset.is_unixlike %}
|
||||
{% if asset.protocol == 'ssh' %}
|
||||
<tr>
|
||||
<td>{% trans 'Refresh hardware' %}:</td>
|
||||
<td>
|
||||
|
@ -92,8 +92,7 @@
|
||||
<th class="text-center">{% trans 'Hostname' %}</th>
|
||||
<th class="text-center">{% trans 'IP' %}</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>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -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_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 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_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>
|
||||
</div>
|
||||
|
||||
@ -147,6 +149,8 @@
|
||||
<script>
|
||||
var zTree, rMenu, asset_table, show = 0;
|
||||
var update_node_action = "";
|
||||
var current_node_id = null;
|
||||
var current_node = null;
|
||||
function initTable() {
|
||||
var options = {
|
||||
ele: $('#asset_list_table'),
|
||||
@ -160,10 +164,12 @@ function initTable() {
|
||||
$(td).html(rowData.hardware_info)
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
if (!cellData) {
|
||||
$(td).html('<i class="fa fa-times text-danger"></i>')
|
||||
if (cellData === 1){
|
||||
$(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 {
|
||||
$(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() {
|
||||
ajax_url: '{% url "api-assets:asset-list" %}',
|
||||
columns: [
|
||||
{data: "id"}, {data: "hostname" }, {data: "ip" },
|
||||
{data: "cpu_cores"}, {data: "is_active", orderable: false },
|
||||
{data: "id", orderable: false }
|
||||
{data: "cpu_cores", orderable: false},
|
||||
{data: "connectivity", orderable: false}, {data: "id", orderable: false }
|
||||
],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
@ -191,18 +197,20 @@ function addTreeNode() {
|
||||
if (!parentNode){
|
||||
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){
|
||||
if (status === "success") {
|
||||
var newNode = {
|
||||
id: data["key"],
|
||||
name: data["value"],
|
||||
node_id: data["id"],
|
||||
pId: parentNode.id
|
||||
pId: parentNode.id,
|
||||
meta: {
|
||||
"node": data
|
||||
}
|
||||
};
|
||||
newNode.checked = zTree.getSelectedNodes()[0].checked;
|
||||
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);
|
||||
} else {
|
||||
alert("{% trans 'Create node failed' %}")
|
||||
@ -218,10 +226,10 @@ function removeTreeNode() {
|
||||
}
|
||||
if (current_node.children && current_node.children.length > 0) {
|
||||
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' %}");
|
||||
} 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({
|
||||
url: url,
|
||||
method: "DELETE",
|
||||
@ -238,8 +246,8 @@ function editTreeNode() {
|
||||
if (!current_node){
|
||||
return
|
||||
}
|
||||
if (current_node.value) {
|
||||
current_node.name = current_node.value;
|
||||
if (current_node) {
|
||||
current_node.name = current_node.meta.node.value;
|
||||
}
|
||||
zTree.editName(current_node);
|
||||
}
|
||||
@ -281,7 +289,7 @@ function onBodyMouseDown(event){
|
||||
|
||||
|
||||
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};
|
||||
if (isCancel){
|
||||
return
|
||||
@ -291,13 +299,20 @@ function onRename(event, treeId, treeNode, isCancel){
|
||||
body: JSON.stringify(data),
|
||||
method: "PATCH",
|
||||
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) {
|
||||
current_node = treeNode;
|
||||
current_node_id = treeNode.meta.node.id;
|
||||
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'));
|
||||
setCookie('node_selected', treeNode.node_id);
|
||||
asset_table.ajax.url(url);
|
||||
@ -305,6 +320,9 @@ function onSelected(event, treeNode) {
|
||||
}
|
||||
|
||||
function selectQueryNode() {
|
||||
// TODO: 是否应该添加
|
||||
// 暂时忽略之前选中的内容
|
||||
return
|
||||
var query_node_id = $.getUrlParam("node");
|
||||
var cookie_node_id = getCookie('node_selected');
|
||||
var node;
|
||||
@ -329,10 +347,10 @@ function beforeDrag() {
|
||||
function beforeDrop(treeId, treeNodes, targetNode, moveType) {
|
||||
var treeNodesNames = [];
|
||||
$.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);
|
||||
}
|
||||
|
||||
@ -342,10 +360,10 @@ function onDrag(event, treeId, treeNodes) {
|
||||
function onDrop(event, treeId, treeNodes, targetNode, moveType) {
|
||||
var treeNodesIds = [];
|
||||
$.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};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
@ -355,6 +373,14 @@ function onDrop(event, treeId, treeNodes, targetNode, moveType) {
|
||||
}
|
||||
|
||||
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 = {
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
@ -365,6 +391,12 @@ function initTree() {
|
||||
enable: true
|
||||
}
|
||||
},
|
||||
async: {
|
||||
enable: true,
|
||||
url: url,
|
||||
autoParam: ["id=key", "name=n", "level=lv"],
|
||||
type: 'get'
|
||||
},
|
||||
edit: {
|
||||
enable: true,
|
||||
showRemoveBtn: false,
|
||||
@ -387,26 +419,8 @@ function initTree() {
|
||||
};
|
||||
|
||||
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"];
|
||||
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();
|
||||
});
|
||||
zTree = $.fn.zTree.init($("#assetTree"), setting, zNodes);
|
||||
rMenu = $("#rMenu");
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
@ -443,20 +457,15 @@ $(document).ready(function(){
|
||||
.on('click', '.btn_export', function () {
|
||||
var $data_table = $('#asset_list_table').DataTable();
|
||||
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 = [];
|
||||
$.each(rows, function (index, obj) {
|
||||
assets.push(obj.id)
|
||||
});
|
||||
var _node_id = current_node ? current_node : null;
|
||||
$.ajax({
|
||||
url: "{% url "assets:asset-export" %}",
|
||||
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",
|
||||
success: function (data, textStatus) {
|
||||
window.open(data.redirect)
|
||||
@ -469,12 +478,8 @@ $(document).ready(function(){
|
||||
.on('click', '#btn_asset_import', function () {
|
||||
var $form = $('#fm_asset_import');
|
||||
var action = $form.attr("action");
|
||||
var nodes = zTree.getSelectedNodes();
|
||||
var current_node;
|
||||
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;#}
|
||||
if (current_node_id){
|
||||
action = setUrlParam(action, 'node_id', current_node_id);
|
||||
$form.attr("action", action)
|
||||
}
|
||||
$form.find('.help-block').remove();
|
||||
@ -496,25 +501,14 @@ $(document).ready(function(){
|
||||
})
|
||||
.on('click', '.btn-create-asset', function () {
|
||||
var url = "{% url 'assets:asset-create' %}";
|
||||
var nodes = zTree.getSelectedNodes();
|
||||
var current_node;
|
||||
if (nodes && nodes.length ===1 ){
|
||||
current_node = nodes[0];
|
||||
url += "?node_id=" + current_node.node_id;
|
||||
if (current_node_id) {
|
||||
url += "?node_id=" + current_node_id;
|
||||
}
|
||||
window.open(url, '_self');
|
||||
})
|
||||
.on('click', '.btn-refresh-hardware', function () {
|
||||
var url = "{% url 'api-assets:node-refresh-hardware-info' pk=DEFAULT_PK %}";
|
||||
var nodes = zTree.getSelectedNodes();
|
||||
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);
|
||||
var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
|
||||
function success(data) {
|
||||
rMenu.css({"visibility" : "hidden"});
|
||||
var task_id = data.task;
|
||||
@ -531,15 +525,10 @@ $(document).ready(function(){
|
||||
})
|
||||
.on('click', '.btn-test-connective', function () {
|
||||
var url = "{% url 'api-assets:node-test-connective' pk=DEFAULT_PK %}";
|
||||
var nodes = zTree.getSelectedNodes();
|
||||
var current_node;
|
||||
if (nodes && nodes.length ===1 ){
|
||||
current_node = nodes[0];
|
||||
} else {
|
||||
if (!current_node_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var the_url = url.replace("{{ DEFAULT_PK }}", current_node.node_id);
|
||||
var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
|
||||
function success(data) {
|
||||
rMenu.css({"visibility" : "hidden"});
|
||||
var task_id = data.task;
|
||||
@ -567,6 +556,19 @@ $(document).ready(function(){
|
||||
setCookie('show_current_asset', '');
|
||||
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 () {
|
||||
var $this = $(this);
|
||||
var $data_table = $("#asset_list_table").DataTable();
|
||||
@ -660,11 +662,8 @@ $(document).ready(function(){
|
||||
}
|
||||
|
||||
function doRemove() {
|
||||
var current_node;
|
||||
var nodes = zTree.getSelectedNodes();
|
||||
if (nodes && nodes.length === 1) {
|
||||
current_node = nodes[0]
|
||||
} else {
|
||||
if (!current_node_id) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -677,7 +676,7 @@ $(document).ready(function(){
|
||||
};
|
||||
|
||||
APIUpdateAttr({
|
||||
'url': '/api/assets/v1/nodes/' + current_node.node_id + '/assets/remove/',
|
||||
'url': '/api/assets/v1/nodes/' + current_node_id + '/assets/remove/',
|
||||
'method': 'PUT',
|
||||
'body': JSON.stringify(data),
|
||||
'success': success
|
||||
@ -706,11 +705,7 @@ $(document).ready(function(){
|
||||
})
|
||||
.on('click', '#btn_asset_modal_confirm', function () {
|
||||
var assets_selected = asset_table2.selected;
|
||||
var current_node;
|
||||
var nodes = zTree.getSelectedNodes();
|
||||
if (nodes && nodes.length === 1) {
|
||||
current_node = nodes[0]
|
||||
} else {
|
||||
if (!current_node_id) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -722,9 +717,9 @@ $(document).ready(function(){
|
||||
|
||||
var url = '';
|
||||
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 {
|
||||
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({
|
||||
@ -740,6 +735,7 @@ $(document).ready(function(){
|
||||
}).on('click', '#menu_asset_move', function () {
|
||||
update_node_action = "move"
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
@ -24,17 +24,27 @@
|
||||
{% block custom_foot_js %}
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function () {
|
||||
console.log($.fn.select2.defaults);
|
||||
$('.select2').select2().off("select2:open");
|
||||
}).on('click', '.select2-selection__rendered', function (e) {
|
||||
e.preventDefault();
|
||||
$("#asset_list_modal").modal();
|
||||
initSelectedAssets2Table();
|
||||
})
|
||||
.on('click', '#btn_asset_modal_confirm', function () {
|
||||
var assets = asset_table2.selected;
|
||||
$.each(assets, function (id, data) {
|
||||
$('.select2').val(assets).trigger('change');
|
||||
var options = [];
|
||||
$('#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');
|
||||
|
||||
})
|
||||
|
@ -32,15 +32,23 @@ $(document).ready(function () {
|
||||
}).on('click', '.select2-selection__rendered', function (e) {
|
||||
e.preventDefault();
|
||||
$("#asset_list_modal").modal();
|
||||
initSelectedAssets2Table();
|
||||
})
|
||||
.on('click', '#btn_asset_modal_confirm', function () {
|
||||
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) {
|
||||
$('.select2').val(assets).trigger('change');
|
||||
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');
|
||||
}
|
||||
});
|
||||
$('#id_assets').val(assets).trigger('change');
|
||||
$("#asset_list_modal").modal('hide');
|
||||
})
|
||||
</script>
|
||||
|
@ -136,7 +136,7 @@
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
function initAssetsTable() {
|
||||
var unreachable = {{ system_user.unreachable_assets|safe }};
|
||||
var connectivity = {{ system_user.connectivity | safe }};
|
||||
var options = {
|
||||
ele: $('#system_user_list'),
|
||||
buttons: [],
|
||||
@ -147,11 +147,13 @@ function initAssetsTable() {
|
||||
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
|
||||
}},
|
||||
{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>')
|
||||
} else {
|
||||
} else if (connectivity.reachable.indexOf(cellData) >= 0 ) {
|
||||
$(td).html('<i class="fa fa-check text-navy"></i>')
|
||||
}
|
||||
} else {
|
||||
$(td).html('')
|
||||
}
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
var push_btn = '';
|
||||
|
@ -95,7 +95,7 @@ function initTable() {
|
||||
}}],
|
||||
ajax_url: '{% url "api-assets:system-user-list" %}',
|
||||
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" }
|
||||
],
|
||||
op_html: $('#actions').html()
|
||||
|
@ -71,7 +71,6 @@ function initTable() {
|
||||
} else {
|
||||
inited = true;
|
||||
}
|
||||
console.log("init table")
|
||||
url = "{% url 'api-perms:my-assets' %}";
|
||||
var options = {
|
||||
ele: $('#user_assets_table'),
|
||||
@ -108,7 +107,8 @@ function initTable() {
|
||||
|
||||
function onSelected(event, treeNode) {
|
||||
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);
|
||||
asset_table.ajax.url(url);
|
||||
asset_table.ajax.reload();
|
||||
@ -131,21 +131,10 @@ function initTree() {
|
||||
};
|
||||
|
||||
var zNodes = [];
|
||||
$.get("{% url 'api-perms:my-nodes' %}", 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'];
|
||||
});
|
||||
$.get("{% url 'api-perms:my-nodes-assets-as-tree' %}?show_assets=0", function(data, status){
|
||||
zNodes = data;
|
||||
$.fn.zTree.init($("#assetTree"), setting, zNodes);
|
||||
zTree = $.fn.zTree.getZTreeObj("assetTree");
|
||||
var root = zTree.getNodes()[0];
|
||||
zTree.expandNode(root);
|
||||
});
|
||||
}
|
||||
|
||||
@ -170,7 +159,7 @@ $(document).ready(function () {
|
||||
'comment': "{% trans 'Comment' %}"
|
||||
{#'date_joined': "{% trans 'Date joined' %}",#}
|
||||
};
|
||||
$.each(data, function(index, value){
|
||||
$.each(data.results, function(index, value){
|
||||
if(value.id === asset_id){
|
||||
for(var i in desc){
|
||||
trs += "<tr class='no-borders-tr'>\n" +
|
||||
|
@ -24,35 +24,39 @@ cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-r
|
||||
|
||||
urlpatterns = [
|
||||
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/',
|
||||
api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'),
|
||||
path('assets/<uuid:pk>/alive/',
|
||||
api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'),
|
||||
path('assets/<uuid:pk>/gateway/',
|
||||
api.AssetGatewayApi.as_view(), name='asset-gateway'),
|
||||
|
||||
path('admin-user/<uuid:pk>/nodes/',
|
||||
api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'),
|
||||
path('admin-user/<uuid:pk>/auth/',
|
||||
api.AdminUserAuthApi.as_view(), name='admin-user-auth'),
|
||||
path('admin-user/<uuid:pk>/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/',
|
||||
api.SystemUserPushApi.as_view(), name='system-user-push'),
|
||||
path('system-user/<uuid:pk>/asset/<uuid:aid>/push/',
|
||||
api.SystemUserPushToAssetApi.as_view(), name='system-user-push-to-asset'),
|
||||
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/',
|
||||
api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'),
|
||||
|
||||
path('system-user/<uuid:pk>/cmd-filter-rules/',
|
||||
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/',
|
||||
api.NodeChildrenApi.as_view(), name='node-children'),
|
||||
path('nodes/children/', api.NodeChildrenApi.as_view(), name='node-children-2'),
|
||||
@ -70,6 +74,8 @@ urlpatterns = [
|
||||
api.RefreshNodeHardwareInfoApi.as_view(), name='node-refresh-hardware-info'),
|
||||
path('nodes/<uuid:pk>/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/',
|
||||
api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
|
||||
|
@ -9,7 +9,11 @@ from .models import Asset, SystemUser, Label
|
||||
|
||||
|
||||
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):
|
||||
@ -21,6 +25,11 @@ def get_system_user_by_name(name):
|
||||
return system_user
|
||||
|
||||
|
||||
def get_system_user_by_id(id):
|
||||
system_user = get_object_or_none(SystemUser, id=id)
|
||||
return system_user
|
||||
|
||||
|
||||
class LabelFilter:
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
@ -40,44 +49,3 @@ class LabelFilter:
|
||||
for kwargs in conditions:
|
||||
queryset = queryset.filter(**kwargs)
|
||||
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):
|
||||
'app': _('Assets'),
|
||||
'action': _('Admin user detail'),
|
||||
"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)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
@ -13,6 +13,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='ftplog',
|
||||
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):
|
||||
migrations.AlterField(
|
||||
model_name='ftplog',
|
||||
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):
|
||||
migrations.CreateModel(
|
||||
name='OperateLog',
|
||||
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)),
|
||||
('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')),
|
||||
|
@ -13,4 +13,5 @@ urlpatterns = [
|
||||
path('ftp-log/', views.FTPLogListView.as_view(), name='ftp-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('command-execution-log/', views.CommandExecutionListView.as_view(), name='command-execution-log-list'),
|
||||
]
|
||||
|
@ -7,6 +7,8 @@ from common.mixins import DatetimeSearchMixin
|
||||
from common.permissions import AdminUserRequiredMixin
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -122,7 +124,10 @@ class PasswordChangeLogList(AdminUserRequiredMixin, DatetimeSearchMixin, ListVie
|
||||
date_from = date_to = None
|
||||
|
||||
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')
|
||||
|
||||
filter_kwargs = dict()
|
||||
@ -184,7 +189,7 @@ class LoginLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Users'),
|
||||
'app': _('Audits'),
|
||||
'action': _('Login log'),
|
||||
'date_from': self.date_from,
|
||||
'date_to': self.date_to,
|
||||
@ -193,4 +198,35 @@ class LoginLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
|
||||
'user_list': self.get_org_users(),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
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)
|
||||
|
0
apps/authentication/ldap/__init__.py
Normal file
0
apps/authentication/ldap/__init__.py
Normal file
95
apps/authentication/ldap/backends.py
Normal file
95
apps/authentication/ldap/backends.py
Normal file
@ -0,0 +1,95 @@
|
||||
# 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)
|
||||
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
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.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 .signals import post_create_openid_user
|
||||
|
||||
@ -31,3 +32,9 @@ def on_post_create_openid_user(sender, user=None, **kwargs):
|
||||
user.source = user.SOURCE_OPENID
|
||||
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):
|
||||
data = {storage_name: 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)
|
||||
return Response({"msg": _('Create succeed')}, status=200)
|
||||
@ -136,7 +140,11 @@ class CommandStorageCreateAPI(APIView):
|
||||
storage_name = storage_data.pop('NAME')
|
||||
data = {storage_name: 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)
|
||||
return Response({"msg": _('Create succeed')}, status=200)
|
||||
|
@ -15,8 +15,6 @@ class BaseForm(forms.Form):
|
||||
super().__init__(*args, **kwargs)
|
||||
for name, field in self.fields.items():
|
||||
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:
|
||||
continue
|
||||
|
||||
@ -24,8 +22,6 @@ class BaseForm(forms.Form):
|
||||
if isinstance(value, dict):
|
||||
value = json.dumps(value)
|
||||
initial_value = value
|
||||
# elif django_value is False or django_value:
|
||||
# initial_value = django_value
|
||||
else:
|
||||
initial_value = ''
|
||||
field.initial = initial_value
|
||||
@ -134,17 +130,34 @@ class TerminalSettingForm(BaseForm):
|
||||
('hostname', _('Hostname')),
|
||||
('ip', _('IP')),
|
||||
)
|
||||
PAGE_SIZE_CHOICES = (
|
||||
('all', _('All')),
|
||||
('auto', _('Auto')),
|
||||
(10, 10),
|
||||
(15, 15),
|
||||
(25, 25),
|
||||
(50, 50),
|
||||
)
|
||||
TERMINAL_PASSWORD_AUTH = forms.BooleanField(
|
||||
initial=True, required=False, label=_("Password auth")
|
||||
required=False, label=_("Password auth")
|
||||
)
|
||||
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(
|
||||
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(
|
||||
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):
|
||||
class SecuritySettingForm(BaseForm):
|
||||
# MFA global setting
|
||||
SECURITY_MFA_AUTH = forms.BooleanField(
|
||||
initial=False, required=False,
|
||||
label=_("MFA Secondary certification"),
|
||||
required=False, label=_("MFA Secondary certification"),
|
||||
help_text=_(
|
||||
'After opening, the user login must use MFA secondary '
|
||||
'authentication (valid for all users, including administrators)'
|
||||
@ -164,13 +176,11 @@ class SecuritySettingForm(BaseForm):
|
||||
)
|
||||
# limit login count
|
||||
SECURITY_LOGIN_LIMIT_COUNT = forms.IntegerField(
|
||||
initial=7, min_value=3,
|
||||
label=_("Limit the number of login failures")
|
||||
min_value=3, label=_("Limit the number of login failures")
|
||||
)
|
||||
# limit login time
|
||||
SECURITY_LOGIN_LIMIT_TIME = forms.IntegerField(
|
||||
initial=30, min_value=5,
|
||||
label=_("No logon interval"),
|
||||
min_value=5, label=_("No logon interval"),
|
||||
help_text=_(
|
||||
"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."
|
||||
@ -178,8 +188,7 @@ class SecuritySettingForm(BaseForm):
|
||||
)
|
||||
# ssh max idle time
|
||||
SECURITY_MAX_IDLE_TIME = forms.IntegerField(
|
||||
initial=30, required=False,
|
||||
label=_("Connection max idle time"),
|
||||
required=False, label=_("Connection max idle time"),
|
||||
help_text=_(
|
||||
'If idle time more than it, disconnect connection(only ssh now) '
|
||||
'Unit: minute'
|
||||
@ -187,10 +196,10 @@ class SecuritySettingForm(BaseForm):
|
||||
)
|
||||
# password expiration time
|
||||
SECURITY_PASSWORD_EXPIRATION_TIME = forms.IntegerField(
|
||||
initial=9999, label=_("Password expiration time"),
|
||||
min_value=1,
|
||||
label=_("Password expiration time"),
|
||||
min_value=1, max_value=99999,
|
||||
help_text=_(
|
||||
"Tip: (unit/day) "
|
||||
"Tip: (unit: day) "
|
||||
"If the user does not update the password during the time, "
|
||||
"the user password will expire failure;"
|
||||
"The password expiration reminder mail will be automatic sent to the user "
|
||||
@ -199,35 +208,30 @@ class SecuritySettingForm(BaseForm):
|
||||
)
|
||||
# min length
|
||||
SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField(
|
||||
initial=6, label=_("Password minimum length"),
|
||||
min_value=6
|
||||
min_value=6, label=_("Password minimum length"),
|
||||
)
|
||||
# upper case
|
||||
SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField(
|
||||
initial=False, required=False,
|
||||
label=_("Must contain capital letters"),
|
||||
required=False, label=_("Must contain capital letters"),
|
||||
help_text=_(
|
||||
'After opening, the user password changes '
|
||||
'and resets must contain uppercase letters')
|
||||
)
|
||||
# lower case
|
||||
SECURITY_PASSWORD_LOWER_CASE = forms.BooleanField(
|
||||
initial=False, required=False,
|
||||
label=_("Must contain lowercase letters"),
|
||||
required=False, label=_("Must contain lowercase letters"),
|
||||
help_text=_('After opening, the user password changes '
|
||||
'and resets must contain lowercase letters')
|
||||
)
|
||||
# number
|
||||
SECURITY_PASSWORD_NUMBER = forms.BooleanField(
|
||||
initial=False, required=False,
|
||||
label=_("Must contain numeric characters"),
|
||||
required=False, label=_("Must contain numeric characters"),
|
||||
help_text=_('After opening, the user password changes '
|
||||
'and resets must contain numeric characters')
|
||||
)
|
||||
# special char
|
||||
SECURITY_PASSWORD_SPECIAL_CHAR = forms.BooleanField(
|
||||
initial=False, required=False,
|
||||
label=_("Must contain special characters"),
|
||||
required=False, label=_("Must contain special characters"),
|
||||
help_text=_('After opening, the user password changes '
|
||||
'and resets must contain special characters')
|
||||
)
|
||||
|
@ -47,6 +47,8 @@ class Setting(models.Model):
|
||||
value = self.value
|
||||
if self.encrypted:
|
||||
value = signer.unsign(value)
|
||||
if not value:
|
||||
return None
|
||||
value = json.loads(value)
|
||||
return value
|
||||
except json.JSONDecodeError:
|
||||
@ -64,6 +66,11 @@ class Setting(models.Model):
|
||||
|
||||
@classmethod
|
||||
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()
|
||||
if not obj:
|
||||
obj = cls()
|
||||
@ -79,6 +86,11 @@ class Setting(models.Model):
|
||||
|
||||
@classmethod
|
||||
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()
|
||||
if not obj:
|
||||
return False
|
||||
|
@ -26,21 +26,20 @@ def refresh_settings_on_changed(sender, instance=None, **kwargs):
|
||||
def refresh_all_settings_on_django_ready(sender, **kwargs):
|
||||
logger.debug("Receive django ready signal")
|
||||
logger.debug(" - fresh all settings")
|
||||
CACHE_KEY_PREFIX = '_SETTING_'
|
||||
cache_key_prefix = '_SETTING_'
|
||||
|
||||
def monkey_patch_getattr(self, name):
|
||||
key = CACHE_KEY_PREFIX + name
|
||||
key = cache_key_prefix + name
|
||||
cached = cache.get(key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
if self._wrapped is empty:
|
||||
self._setup(name)
|
||||
val = getattr(self._wrapped, name)
|
||||
# self.__dict__[name] = val # Never set it
|
||||
return val
|
||||
|
||||
def monkey_patch_setattr(self, name, value):
|
||||
key = CACHE_KEY_PREFIX + name
|
||||
key = cache_key_prefix + name
|
||||
cache.set(key, value, None)
|
||||
if name == '_wrapped':
|
||||
self.__dict__.clear()
|
||||
@ -51,7 +50,7 @@ def refresh_all_settings_on_django_ready(sender, **kwargs):
|
||||
def monkey_patch_delattr(self, name):
|
||||
super(LazySettings, self).__delattr__(name)
|
||||
self.__dict__.pop(name, None)
|
||||
key = CACHE_KEY_PREFIX + name
|
||||
key = cache_key_prefix + name
|
||||
cache.delete(key)
|
||||
|
||||
try:
|
||||
@ -65,6 +64,8 @@ def refresh_all_settings_on_django_ready(sender, **kwargs):
|
||||
|
||||
@receiver(pre_save, dispatch_uid="my_unique_identifier")
|
||||
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 current_request and current_request.user.is_authenticated:
|
||||
instance.created_by = current_request.user.name
|
||||
|
@ -41,7 +41,6 @@
|
||||
<label class="col-md-2 control-label" for="id_name">{% trans "Name" %}</label>
|
||||
<div class="col-md-9">
|
||||
<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>
|
||||
</div>
|
||||
|
@ -44,7 +44,6 @@
|
||||
<label class="col-md-2 control-label" for="id_name">{% trans "Name" %}</label>
|
||||
<div class="col-md-9">
|
||||
<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>
|
||||
</div>
|
||||
|
@ -39,9 +39,9 @@
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
|
||||
<h3>{% trans "User login settings" %}</h3>
|
||||
<h3>{% trans "Security setting" %}</h3>
|
||||
{% for field in form %}
|
||||
{% if forloop.counter == 5 %}
|
||||
{% if forloop.counter == 6 %}
|
||||
<div class="hr-line-dashed"></div>
|
||||
<h3>{% trans "Password check rule" %}</h3>
|
||||
{% endif %}
|
||||
|
@ -87,7 +87,7 @@
|
||||
<tr>
|
||||
<td>{{ name }}</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>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@ -109,7 +109,7 @@
|
||||
<tr>
|
||||
<td>{{ name }}</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>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
@ -111,3 +111,13 @@ def sort(data):
|
||||
@register.filter
|
||||
def subtract(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
|
||||
|
94
apps/common/tree.py
Normal file
94
apps/common/tree.py
Normal file
@ -0,0 +1,94 @@
|
||||
# -*- 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):
|
||||
def get_command_storage_setting():
|
||||
default = settings.DEFAULT_TERMINAL_COMMAND_STORAGE
|
||||
value = settings.TERMINAL_COMMAND_STORAGE
|
||||
if not value:
|
||||
return default
|
||||
value.update(default)
|
||||
return value
|
||||
|
||||
@ -398,6 +400,8 @@ def get_command_storage_setting():
|
||||
def get_replay_storage_setting():
|
||||
default = settings.DEFAULT_TERMINAL_REPLAY_STORAGE
|
||||
value = settings.TERMINAL_REPLAY_STORAGE
|
||||
if not value:
|
||||
return default
|
||||
value.update(default)
|
||||
return value
|
||||
|
||||
|
@ -52,7 +52,7 @@ class EmailSettingView(SuperUserRequiredMixin, TemplateView):
|
||||
form = self.form_class(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
msg = _("Update setting successfully, please restart program")
|
||||
msg = _("Update setting successfully")
|
||||
messages.success(request, msg)
|
||||
return redirect('settings:email-setting')
|
||||
else:
|
||||
|
@ -301,16 +301,35 @@ defaults = {
|
||||
'REDIS_HOST': '127.0.0.1',
|
||||
'REDIS_PORT': 6379,
|
||||
'REDIS_PASSWORD': '',
|
||||
'REDIS_DB_CELERY_BROKER': 3,
|
||||
'REDIS_DB_CELERY': 3,
|
||||
'REDIS_DB_CACHE': 4,
|
||||
'CAPTCHA_TEST_MODE': None,
|
||||
'TOKEN_EXPIRATION': 3600,
|
||||
'TOKEN_EXPIRATION': 3600 * 24,
|
||||
'DISPLAY_PER_PAGE': 25,
|
||||
'DEFAULT_EXPIRED_YEARS': 70,
|
||||
'SESSION_COOKIE_DOMAIN': None,
|
||||
'CSRF_COOKIE_DOMAIN': None,
|
||||
'SESSION_COOKIE_AGE': 3600 * 24,
|
||||
'SESSION_EXPIRE_AT_BROWSER_CLOSE': 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
|
||||
import sys
|
||||
|
||||
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 .conf import load_user_config
|
||||
@ -146,6 +146,7 @@ LOGIN_URL = reverse_lazy('users:login')
|
||||
SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN
|
||||
CSRF_COOKIE_DOMAIN = CONFIG.CSRF_COOKIE_DOMAIN
|
||||
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'
|
||||
# Database
|
||||
@ -353,6 +354,10 @@ AUTH_USER_MODEL = 'users.User'
|
||||
FILE_UPLOAD_PERMISSIONS = 0o644
|
||||
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 = False
|
||||
AUTH_LDAP_SERVER_URI = 'ldap://localhost:389'
|
||||
@ -362,22 +367,17 @@ AUTH_LDAP_SEARCH_OU = 'ou=tech,dc=jumpserver,dc=org'
|
||||
AUTH_LDAP_SEARCH_FILTER = '(cn=%(user)s)'
|
||||
AUTH_LDAP_START_TLS = False
|
||||
AUTH_LDAP_USER_ATTR_MAP = {"username": "cn", "name": "sn", "email": "mail"}
|
||||
AUTH_LDAP_USER_SEARCH_UNION = lambda: [
|
||||
LDAPSearch(USER_SEARCH, ldap.SCOPE_SUBTREE, AUTH_LDAP_SEARCH_FILTER)
|
||||
for USER_SEARCH in str(AUTH_LDAP_SEARCH_OU).split("|")
|
||||
]
|
||||
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_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 = {
|
||||
ldap.OPT_TIMEOUT: 5
|
||||
}
|
||||
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 1
|
||||
AUTH_LDAP_ALWAYS_UPDATE_USER = True
|
||||
AUTH_LDAP_BACKEND = 'django_auth_ldap.backend.LDAPBackend'
|
||||
AUTH_LDAP_BACKEND = 'authentication.ldap.backends.LDAPAuthorizationBackend'
|
||||
|
||||
if AUTH_LDAP:
|
||||
AUTHENTICATION_BACKENDS.insert(0, AUTH_LDAP_BACKEND)
|
||||
@ -402,10 +402,10 @@ if AUTH_OPENID:
|
||||
|
||||
# Celery using redis as broker
|
||||
CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % {
|
||||
'password': CONFIG.REDIS_PASSWORD if CONFIG.REDIS_PASSWORD else '',
|
||||
'host': CONFIG.REDIS_HOST or '127.0.0.1',
|
||||
'port': CONFIG.REDIS_PORT or 6379,
|
||||
'db': CONFIG.REDIS_DB_CELERY_BROKER or 3,
|
||||
'password': CONFIG.REDIS_PASSWORD,
|
||||
'host': CONFIG.REDIS_HOST,
|
||||
'port': CONFIG.REDIS_PORT,
|
||||
'db': CONFIG.REDIS_DB_CELERY,
|
||||
}
|
||||
CELERY_TASK_SERIALIZER = 'pickle'
|
||||
CELERY_RESULT_SERIALIZER = 'pickle'
|
||||
@ -467,6 +467,7 @@ DEFAULT_TERMINAL_REPLAY_STORAGE = {
|
||||
TERMINAL_REPLAY_STORAGE = {
|
||||
}
|
||||
|
||||
|
||||
SECURITY_MFA_AUTH = False
|
||||
SECURITY_LOGIN_LIMIT_COUNT = 7
|
||||
SECURITY_LOGIN_LIMIT_TIME = 30 # Unit: minute
|
||||
@ -485,14 +486,22 @@ SECURITY_PASSWORD_RULES = [
|
||||
'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
|
||||
BOOTSTRAP3 = {
|
||||
'horizontal_label_class': 'col-md-2',
|
||||
# Field class to use in horizontal forms
|
||||
'horizontal_field_class': 'col-md-9',
|
||||
# Set placeholder attributes to label if no placeholder is provided
|
||||
'set_placeholder': True,
|
||||
'set_placeholder': False,
|
||||
'success_css_class': '',
|
||||
'required_css_class': 'required',
|
||||
}
|
||||
|
||||
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
||||
@ -510,3 +519,5 @@ SWAGGER_SETTINGS = {
|
||||
},
|
||||
}
|
||||
|
||||
# Default email suffix
|
||||
EMAIL_SUFFIX = CONFIG.EMAIL_SUFFIX
|
||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -1,18 +1,17 @@
|
||||
# ~*~ 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.default import CallbackModule
|
||||
|
||||
from .display import TeeObj
|
||||
from ansible.plugins.callback.minimal import CallbackModule as CMDCallBackModule
|
||||
|
||||
|
||||
class AdHocResultCallback(CallbackModule):
|
||||
"""
|
||||
Task result Callback
|
||||
"""
|
||||
def __init__(self, display=None, options=None, file_obj=None):
|
||||
class CallbackMixin:
|
||||
def __init__(self, display=None):
|
||||
# result_raw example: {
|
||||
# "ok": {"hostname": {"task_name": {},...},..},
|
||||
# "failed": {"hostname": {"task_name": {}..}, ..},
|
||||
@ -20,71 +19,138 @@ class AdHocResultCallback(CallbackModule):
|
||||
# "skipped": {"hostname": {"task_name": {}, ..}, ..},
|
||||
# }
|
||||
# results_summary example: {
|
||||
# "contacted": {"hostname",...},
|
||||
# "contacted": {"hostname": {"task_name": {}}, "hostname": {}},
|
||||
# "dark": {"hostname": {"task_name": {}, "task_name": {}},...,},
|
||||
# "success": True
|
||||
# }
|
||||
self.results_raw = dict(ok={}, failed={}, unreachable={}, skipped={})
|
||||
self.results_summary = dict(contacted=[], dark={})
|
||||
self.results_raw = dict(
|
||||
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__()
|
||||
if file_obj is not None:
|
||||
sys.stdout = TeeObj(file_obj)
|
||||
if display:
|
||||
self._display = display
|
||||
self._display.columns = 79
|
||||
|
||||
def gather_result(self, t, res):
|
||||
self._clean_results(res._result, res._task.action)
|
||||
host = res._host.get_name()
|
||||
task_name = res.task_name
|
||||
task_result = res._result
|
||||
def display(self, msg):
|
||||
self._display.display(msg)
|
||||
|
||||
if self.results_raw[t].get(host):
|
||||
self.results_raw[t][host][task_name] = task_result
|
||||
else:
|
||||
self.results_raw[t][host] = {task_name: task_result}
|
||||
def gather_result(self, t, result):
|
||||
self._clean_results(result._result, result._task.action)
|
||||
host = result._host.get_name()
|
||||
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)
|
||||
|
||||
|
||||
class AdHocResultCallback(CallbackMixin, CallbackModule, CMDCallBackModule):
|
||||
"""
|
||||
Task result Callback
|
||||
"""
|
||||
def clean_result(self, t, host, task_name, task_result):
|
||||
contacted = self.results_summary["contacted"]
|
||||
dark = self.results_summary["dark"]
|
||||
if t in ("ok", "skipped") and host not in dark:
|
||||
if host not in contacted:
|
||||
contacted.append(host)
|
||||
else:
|
||||
if dark.get(host):
|
||||
dark[host][task_name] = task_result.values
|
||||
|
||||
if task_result.get('rc') is not None:
|
||||
cmd = task_result.get('cmd')
|
||||
if isinstance(cmd, list):
|
||||
cmd = " ".join(cmd)
|
||||
else:
|
||||
dark[host] = {task_name: task_result}
|
||||
if host in contacted:
|
||||
contacted.remove(host)
|
||||
cmd = str(cmd)
|
||||
detail = {
|
||||
'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):
|
||||
self.results_summary['success'] = False
|
||||
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):
|
||||
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):
|
||||
self.gather_result("skipped", result)
|
||||
super().v2_runner_on_skipped(result)
|
||||
|
||||
def v2_runner_on_unreachable(self, result):
|
||||
self.results_summary['success'] = False
|
||||
self.gather_result("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):
|
||||
"""
|
||||
Command result callback
|
||||
|
||||
results_command: {
|
||||
"cmd": "",
|
||||
"stderr": "",
|
||||
"stdout": "",
|
||||
"rc": 0,
|
||||
"delta": 0:0:0.123
|
||||
}
|
||||
"""
|
||||
def __init__(self, display=None):
|
||||
# results_command: {
|
||||
# "cmd": "",
|
||||
# "stderr": "",
|
||||
# "stdout": "",
|
||||
# "rc": 0,
|
||||
# "delta": 0:0:0.123
|
||||
# }
|
||||
#
|
||||
def __init__(self, display=None, **kwargs):
|
||||
|
||||
self.results_command = dict()
|
||||
super().__init__(display)
|
||||
|
||||
@ -92,6 +158,43 @@ class CommandResultCallback(AdHocResultCallback):
|
||||
super().gather_result(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):
|
||||
host = res._host.get_name()
|
||||
cmd = {}
|
||||
|
@ -17,7 +17,7 @@ from common.utils import get_logger
|
||||
from .exceptions import AnsibleError
|
||||
|
||||
|
||||
__all__ = ["AdHocRunner", "PlayBookRunner"]
|
||||
__all__ = ["AdHocRunner", "PlayBookRunner", "CommandRunner"]
|
||||
C.HOST_KEY_CHECKING = False
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@ -45,7 +45,7 @@ def get_default_options():
|
||||
listtasks=False,
|
||||
listhosts=False,
|
||||
syntax=False,
|
||||
timeout=60,
|
||||
timeout=30,
|
||||
connection='ssh',
|
||||
module_path='',
|
||||
forks=10,
|
||||
@ -135,6 +135,7 @@ class AdHocRunner:
|
||||
loader_class = DataLoader
|
||||
variable_manager_class = VariableManager
|
||||
default_options = get_default_options()
|
||||
command_modules_choices = ('shell', 'raw', 'command', 'script', 'win_shell')
|
||||
|
||||
def __init__(self, inventory, options=None):
|
||||
self.options = self.update_options(options)
|
||||
@ -145,7 +146,7 @@ class AdHocRunner:
|
||||
)
|
||||
|
||||
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
|
||||
def check_module_args(module_name, module_args=''):
|
||||
@ -163,10 +164,30 @@ class AdHocRunner:
|
||||
"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):
|
||||
cleaned_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)
|
||||
return cleaned_tasks
|
||||
|
||||
@ -177,17 +198,16 @@ class AdHocRunner:
|
||||
options = self.__class__.default_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 pattern: all, *, or others
|
||||
:param play_name: The play name
|
||||
:param gather_facts:
|
||||
:param file_obj: logging to file_obj
|
||||
:return:
|
||||
"""
|
||||
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)
|
||||
|
||||
play_source = dict(
|
||||
@ -211,10 +231,6 @@ class AdHocRunner:
|
||||
stdout_callback=self.results_callback,
|
||||
passwords=self.options.passwords,
|
||||
)
|
||||
print("Get matched hosts: {}".format(
|
||||
self.inventory.get_matched_hosts(pattern)
|
||||
))
|
||||
|
||||
try:
|
||||
tqm.run(play)
|
||||
return self.results_callback
|
||||
@ -229,16 +245,12 @@ class CommandRunner(AdHocRunner):
|
||||
results_callback_class = CommandResultCallback
|
||||
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:
|
||||
raise AnsibleError("Module should in {}".format(self.modules_choices))
|
||||
else:
|
||||
module = "shell"
|
||||
|
||||
tasks = [
|
||||
{"action": {"module": module, "args": cmd}}
|
||||
]
|
||||
hosts = self.inventory.get_hosts(pattern=pattern)
|
||||
name = "Run command {} on {}".format(cmd, ", ".join([host.name for host in hosts]))
|
||||
return self.run(tasks, pattern, play_name=name)
|
||||
return self.run(tasks, pattern, play_name=cmd)
|
||||
|
||||
|
5
apps/ops/api/__init__.py
Normal file
5
apps/ops/api/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .adhoc import *
|
||||
from .celery import *
|
||||
from .command import *
|
@ -1,26 +1,34 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
import uuid
|
||||
import os
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import viewsets, generics
|
||||
from rest_framework.views import Response
|
||||
|
||||
from common.permissions import IsOrgAdmin
|
||||
from .models import Task, AdHoc, AdHocRunHistory, CeleryTask
|
||||
from .serializers import TaskSerializer, AdHocSerializer, \
|
||||
from orgs.utils import current_org
|
||||
from ..models import Task, AdHoc, AdHocRunHistory
|
||||
from ..serializers import TaskSerializer, AdHocSerializer, \
|
||||
AdHocRunHistorySerializer
|
||||
from .tasks import run_ansible_task
|
||||
from ..tasks import run_ansible_task
|
||||
|
||||
__all__ = [
|
||||
'TaskViewSet', 'TaskRun', 'AdHocViewSet', 'AdHocRunHistoryViewSet'
|
||||
]
|
||||
|
||||
|
||||
class TaskViewSet(viewsets.ModelViewSet):
|
||||
queryset = Task.objects.all()
|
||||
serializer_class = TaskSerializer
|
||||
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):
|
||||
@ -47,7 +55,7 @@ class AdHocViewSet(viewsets.ModelViewSet):
|
||||
return self.queryset
|
||||
|
||||
|
||||
class AdHocRunHistorySet(viewsets.ModelViewSet):
|
||||
class AdHocRunHistoryViewSet(viewsets.ModelViewSet):
|
||||
queryset = AdHocRunHistory.objects.all()
|
||||
serializer_class = AdHocRunHistorySerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
@ -66,28 +74,6 @@ class AdHocRunHistorySet(viewsets.ModelViewSet):
|
||||
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})
|
||||
|
53
apps/ops/api/celery.py
Normal file
53
apps/ops/api/celery.py
Normal file
@ -0,0 +1,53 @@
|
||||
# -*- 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)
|
||||
|
27
apps/ops/api/command.py
Normal file
27
apps/ops/api/command.py
Normal file
@ -0,0 +1,27 @@
|
||||
# -*- 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):
|
||||
if cache.get("CELERY_APP_READY", 0) == 1:
|
||||
return
|
||||
cache.set("CELERY_APP_READY", 1, 10)
|
||||
logger.debug("App ready signal recv")
|
||||
tasks = get_after_app_ready_tasks()
|
||||
logger.debug("Start need start task: [{}]".format(
|
||||
", ".join(tasks))
|
||||
|
17
apps/ops/forms.py
Normal file
17
apps/ops/forms.py
Normal file
@ -0,0 +1,17 @@
|
||||
# -*- 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 @@
|
||||
#
|
||||
|
||||
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__ = [
|
||||
'JMSInventory'
|
||||
@ -14,19 +14,18 @@ class JMSInventory(BaseInventory):
|
||||
JMS Inventory is the manager with jumpserver assets, so you can
|
||||
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: 是否统一使用某个系统用户去执行
|
||||
:param become_info: 是否become成某个用户去执行
|
||||
"""
|
||||
self.hostname_list = hostname_list
|
||||
self.assets = assets
|
||||
self.using_admin = run_as_admin
|
||||
self.run_as = run_as
|
||||
self.become_info = become_info
|
||||
|
||||
assets = self.get_jms_assets()
|
||||
host_list = []
|
||||
|
||||
for asset in assets:
|
||||
@ -43,14 +42,10 @@ class JMSInventory(BaseInventory):
|
||||
host.update(become_info)
|
||||
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):
|
||||
info = {
|
||||
'id': asset.id,
|
||||
'hostname': asset.fullname,
|
||||
'hostname': asset.hostname,
|
||||
'ip': asset.ip,
|
||||
'port': asset.port,
|
||||
'vars': dict(),
|
||||
@ -75,7 +70,7 @@ class JMSInventory(BaseInventory):
|
||||
return info
|
||||
|
||||
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:
|
||||
return {}
|
||||
else:
|
||||
|
56
apps/ops/migrations/0003_auto_20181207_1744.py
Normal file
56
apps/ops/migrations/0003_auto_20181207_1744.py
Normal file
@ -0,0 +1,56 @@
|
||||
# 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')},
|
||||
),
|
||||
]
|
20
apps/ops/migrations/0004_adhoc_run_as.py
Normal file
20
apps/ops/migrations/0004_adhoc_run_as.py
Normal file
@ -0,0 +1,20 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
31
apps/ops/migrations/0005_auto_20181219_1807.py
Normal file
31
apps/ops/migrations/0005_auto_20181219_1807.py
Normal file
@ -0,0 +1,31 @@
|
||||
# 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 @@
|
||||
#
|
||||
|
||||
from .adhoc import *
|
||||
from .celery import *
|
||||
from .celery import *
|
||||
from .command import *
|
||||
|
@ -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
|
||||
"""
|
||||
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"))
|
||||
crontab = models.CharField(verbose_name=_("Crontab"), null=True, blank=True, max_length=128, help_text=_("5 * * * *"))
|
||||
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
|
||||
is_deleted = models.BooleanField(default=False)
|
||||
comment = models.TextField(blank=True, verbose_name=_("Comment"))
|
||||
created_by = models.CharField(max_length=128, blank=True, null=True, default='')
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
created_by = models.CharField(max_length=128, blank=True, default='')
|
||||
date_created = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
__latest_adhoc = None
|
||||
_ignore_auto_created_by = True
|
||||
|
||||
@property
|
||||
def short_id(self):
|
||||
@ -94,7 +95,7 @@ class Task(models.Model):
|
||||
update_fields=None):
|
||||
from ..tasks import run_ansible_task
|
||||
super().save(
|
||||
force_insert=force_insert, force_update=force_update,
|
||||
force_insert=force_insert, force_update=force_update,
|
||||
using=using, update_fields=update_fields,
|
||||
)
|
||||
|
||||
@ -108,7 +109,7 @@ class Task(models.Model):
|
||||
crontab = self.crontab
|
||||
|
||||
tasks = {
|
||||
self.name: {
|
||||
self.__str__(): {
|
||||
"task": run_ansible_task.name,
|
||||
"interval": interval,
|
||||
"crontab": crontab,
|
||||
@ -119,11 +120,11 @@ class Task(models.Model):
|
||||
}
|
||||
create_or_update_celery_periodic_tasks(tasks)
|
||||
else:
|
||||
disable_celery_periodic_task(self.name)
|
||||
disable_celery_periodic_task(self.__str__())
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
super().delete(using=using, keep_parents=keep_parents)
|
||||
delete_celery_periodic_task(self.name)
|
||||
delete_celery_periodic_task(self.__str__())
|
||||
|
||||
@property
|
||||
def schedule(self):
|
||||
@ -133,10 +134,11 @@ class Task(models.Model):
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
return self.name + '@' + str(self.created_by)
|
||||
|
||||
class Meta:
|
||||
db_table = 'ops_task'
|
||||
unique_together = ('name', 'created_by')
|
||||
get_latest_by = 'date_created'
|
||||
|
||||
|
||||
@ -157,15 +159,19 @@ class AdHoc(models.Model):
|
||||
pattern = models.CharField(max_length=64, default='{}', verbose_name=_('Pattern'))
|
||||
_options = models.CharField(max_length=1024, default='', verbose_name=_('Options'))
|
||||
_hosts = models.TextField(blank=True, verbose_name=_('Hosts')) # ['hostname1', 'hostname2']
|
||||
hosts = models.ManyToManyField('assets.Asset', verbose_name=_("Host"))
|
||||
run_as_admin = models.BooleanField(default=False, verbose_name=_('Run as admin'))
|
||||
run_as = models.CharField(max_length=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"))
|
||||
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
|
||||
def tasks(self):
|
||||
return json.loads(self._tasks)
|
||||
try:
|
||||
return json.loads(self._tasks)
|
||||
except:
|
||||
return []
|
||||
|
||||
@tasks.setter
|
||||
def tasks(self, item):
|
||||
@ -174,14 +180,6 @@ class AdHoc(models.Model):
|
||||
else:
|
||||
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
|
||||
def inventory(self):
|
||||
if self.become:
|
||||
@ -194,7 +192,7 @@ class AdHoc(models.Model):
|
||||
become_info = None
|
||||
|
||||
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
|
||||
)
|
||||
return inventory
|
||||
@ -242,14 +240,13 @@ class AdHoc(models.Model):
|
||||
history.timedelta = time.time() - time_start
|
||||
history.save()
|
||||
|
||||
def _run_only(self, file_obj=None):
|
||||
def _run_only(self):
|
||||
runner = AdHocRunner(self.inventory, options=self.options)
|
||||
try:
|
||||
result = runner.run(
|
||||
self.tasks,
|
||||
self.pattern,
|
||||
self.task.name,
|
||||
file_obj=file_obj,
|
||||
)
|
||||
return result.results_raw, result.results_summary
|
||||
except AnsibleError as e:
|
||||
|
@ -19,7 +19,7 @@ class CeleryTask(models.Model):
|
||||
)
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
|
||||
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)
|
||||
date_published = models.DateTimeField(auto_now_add=True)
|
||||
date_start = models.DateTimeField(null=True)
|
||||
|
74
apps/ops/models/command.py
Normal file
74
apps/ops/models/command.py
Normal file
@ -0,0 +1,74 @@
|
||||
# -*- 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
|
@ -1,8 +1,19 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
from __future__ import unicode_literals
|
||||
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):
|
||||
@ -42,7 +53,7 @@ class AdHocRunHistorySerializer(serializers.ModelSerializer):
|
||||
@staticmethod
|
||||
def get_stat(obj):
|
||||
return {
|
||||
"total": len(obj.adhoc.hosts),
|
||||
"total": obj.adhoc.hosts.count(),
|
||||
"success": len(obj.summary.get("contacted", [])),
|
||||
"failed": len(obj.summary.get("dark", [])),
|
||||
}
|
||||
@ -51,3 +62,23 @@ class AdHocRunHistorySerializer(serializers.ModelSerializer):
|
||||
fields = super().get_field_names(declared_fields, info)
|
||||
fields.extend(['summary', 'short_id'])
|
||||
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 @@
|
||||
from celery import shared_task, subtask
|
||||
|
||||
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__)
|
||||
|
||||
@ -28,6 +29,25 @@ def run_ansible_task(tid, callback=None, **kwargs):
|
||||
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
|
||||
def hello(name, callback=None):
|
||||
print("Hello {}".format(name))
|
||||
|
@ -186,6 +186,19 @@
|
||||
</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 %}
|
||||
|
||||
|
@ -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>
|
||||
</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>
|
||||
</ul>
|
||||
</div>
|
||||
@ -141,4 +141,14 @@
|
||||
</div>
|
||||
{% include 'users/_user_update_pk_modal.html' %}
|
||||
{% 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 @@
|
||||
var end = false;
|
||||
var error = false;
|
||||
var interval = 200;
|
||||
var success = true;
|
||||
|
||||
function calWinSize() {
|
||||
var t = $('#marker');
|
||||
@ -34,20 +35,19 @@
|
||||
{#colWidth = 1.00 * t.width() / 6;#}
|
||||
}
|
||||
function resize() {
|
||||
{#console.log(rowHeight, window.innerHeight);#}
|
||||
{#console.log(colWidth, window.innerWidth);#}
|
||||
var rows = Math.floor(window.innerHeight / rowHeight) - 1;
|
||||
var cols = Math.floor(window.innerWidth / colWidth) - 2;
|
||||
console.log(rows, cols);
|
||||
term.resize(cols, rows);
|
||||
}
|
||||
function requestAndWrite() {
|
||||
if (!end) {
|
||||
if (!end && success) {
|
||||
success = false;
|
||||
$.ajax({
|
||||
url: url + '?mark=' + mark,
|
||||
method: "GET",
|
||||
contentType: "application/json; charset=utf-8"
|
||||
}).done(function(data, textStatue, jqXHR) {
|
||||
success = true;
|
||||
if (jqXHR.status === 203) {
|
||||
error = true;
|
||||
term.write('.');
|
||||
@ -64,7 +64,14 @@
|
||||
}
|
||||
}
|
||||
$(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.resize(80, 24);
|
||||
resize();
|
||||
|
292
apps/ops/templates/ops/command_execution_create.html
Normal file
292
apps/ops/templates/ops/command_execution_create.html
Normal file
@ -0,0 +1,292 @@
|
||||
{% 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 %}
|
122
apps/ops/templates/ops/command_execution_list.html
Normal file
122
apps/ops/templates/ops/command_execution_list.html
Normal file
@ -0,0 +1,122 @@
|
||||
{% 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 @@
|
||||
<a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a>
|
||||
</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>
|
||||
</ul>
|
||||
</div>
|
||||
@ -78,52 +78,60 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
var options = {
|
||||
ele: $('#task-version-list-table'),
|
||||
buttons: [],
|
||||
order: [],
|
||||
select: [],
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
var options = {
|
||||
ele: $('#task-version-list-table'),
|
||||
buttons: [],
|
||||
order: [],
|
||||
select: [],
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
{# var detail_btn = '<a href="' + cellData + '</a>';#}
|
||||
$(td).html(cellData);
|
||||
}},
|
||||
{targets: 2, createdCell: function (td, cellData, rowData) {
|
||||
var dataLength = cellData.length;
|
||||
$(td).html(dataLength);
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
if (!cellData) {
|
||||
$(td).html("Admin")
|
||||
} else {
|
||||
$(td).html(cellData)
|
||||
}
|
||||
}},
|
||||
{targets: 5, createdCell: function (td, cellData, rowData) {
|
||||
if (!cellData) {
|
||||
$(td).html("")
|
||||
} else {
|
||||
$(td).html(cellData.user)
|
||||
}
|
||||
}},
|
||||
{targets: 6, createdCell: function (td, cellData) {
|
||||
var d = new Date(cellData);
|
||||
$(td).html(d.toLocaleString())
|
||||
}},
|
||||
{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);
|
||||
if (cellData) {
|
||||
$(td).html(detail_btn);
|
||||
}
|
||||
}}
|
||||
],
|
||||
ajax_url: '{% url "api-ops:adhoc-list" %}?task={{ object.pk }}',
|
||||
columns: [{data: function(){return ""}}, {data: "short_id" }, {data: "hosts"}, {data: "pattern"},
|
||||
{data: "run_as"}, {data: "become"}, {data: "date_created"}, {data: "id"}]
|
||||
};
|
||||
jumpserver.initDataTable(options);
|
||||
})
|
||||
</script>
|
||||
$(td).html(cellData);
|
||||
}},
|
||||
{targets: 2, createdCell: function (td, cellData, rowData) {
|
||||
var dataLength = cellData.length;
|
||||
$(td).html(dataLength);
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
if (!cellData) {
|
||||
$(td).html("Admin")
|
||||
} else {
|
||||
$(td).html(cellData)
|
||||
}
|
||||
}},
|
||||
{targets: 5, createdCell: function (td, cellData, rowData) {
|
||||
if (!cellData) {
|
||||
$(td).html("")
|
||||
} else {
|
||||
$(td).html(cellData.user)
|
||||
}
|
||||
}},
|
||||
{targets: 6, createdCell: function (td, cellData) {
|
||||
var d = new Date(cellData);
|
||||
$(td).html(d.toLocaleString())
|
||||
}},
|
||||
{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);
|
||||
if (cellData) {
|
||||
$(td).html(detail_btn);
|
||||
}
|
||||
}}
|
||||
],
|
||||
ajax_url: '{% url "api-ops:adhoc-list" %}?task={{ object.pk }}',
|
||||
columns: [{data: function(){return ""}}, {data: "short_id" }, {data: "hosts"}, {data: "pattern"},
|
||||
{data: "run_as"}, {data: "become"}, {data: "date_created"}, {data: "id"}]
|
||||
};
|
||||
jumpserver.initDataTable(options);
|
||||
}).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 %}
|
||||
|
@ -4,10 +4,11 @@
|
||||
|
||||
{% block custom_head_css_js %}
|
||||
<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/sweetalert/sweetalert.min.js" %}"></script>
|
||||
<script src="{% static 'js/plugins/sweetalert/sweetalert.min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
@ -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>
|
||||
</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>
|
||||
</ul>
|
||||
</div>
|
||||
@ -164,4 +165,19 @@
|
||||
</div>
|
||||
</div>
|
||||
{% 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 @@
|
||||
<a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a>
|
||||
</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>
|
||||
</ul>
|
||||
</div>
|
||||
@ -148,6 +148,14 @@ function initTable() {
|
||||
|
||||
$(document).ready(function () {
|
||||
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>
|
||||
|
@ -11,11 +11,13 @@ app_name = "ops"
|
||||
router = DefaultRouter()
|
||||
router.register(r'tasks', api.TaskViewSet, 'task')
|
||||
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 = [
|
||||
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>/result/', api.CeleryResultApi.as_view(), name='celery-result'),
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
|
@ -18,4 +18,7 @@ urlpatterns = [
|
||||
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('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'),
|
||||
]
|
||||
|
@ -1,5 +1,7 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from orgs.utils import set_to_root_org
|
||||
from .models import Task, AdHoc
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@ -10,15 +12,14 @@ def get_task_by_id(task_id):
|
||||
|
||||
|
||||
def update_or_create_ansible_task(
|
||||
task_name, hosts, tasks,
|
||||
task_name, hosts, tasks, created_by,
|
||||
interval=None, crontab=None, is_periodic=False,
|
||||
callback=None, pattern='all', options=None,
|
||||
run_as_admin=False, run_as="", become_info=None,
|
||||
created_by=None,
|
||||
run_as_admin=False, run_as=None, become_info=None,
|
||||
):
|
||||
if not hosts or not tasks or not task_name:
|
||||
return
|
||||
|
||||
set_to_root_org()
|
||||
defaults = {
|
||||
'name': task_name,
|
||||
'interval': interval,
|
||||
@ -29,22 +30,27 @@ def update_or_create_ansible_task(
|
||||
}
|
||||
|
||||
created = False
|
||||
task, _ = Task.objects.update_or_create(
|
||||
defaults=defaults, name=task_name,
|
||||
task, ok = Task.objects.update_or_create(
|
||||
defaults=defaults, name=task_name, created_by=created_by
|
||||
)
|
||||
|
||||
adhoc = task.latest_adhoc
|
||||
adhoc = task.get_latest_adhoc()
|
||||
new_adhoc = AdHoc(task=task, pattern=pattern,
|
||||
run_as_admin=run_as_admin,
|
||||
run_as=run_as)
|
||||
new_adhoc.hosts = hosts
|
||||
new_adhoc.tasks = tasks
|
||||
new_adhoc.options = options
|
||||
new_adhoc.become = become_info
|
||||
|
||||
if not adhoc or adhoc != new_adhoc:
|
||||
print("Task create new adhoc: {}".format(task_name))
|
||||
hosts_same = True
|
||||
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.hosts.set(hosts)
|
||||
task.latest_adhoc = new_adhoc
|
||||
created = True
|
||||
return task, created
|
||||
|
3
apps/ops/views/__init__.py
Normal file
3
apps/ops/views/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .adhoc import *
|
||||
from .celery import *
|
||||
from .command import *
|
@ -2,14 +2,22 @@
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
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 .models import Task, AdHoc, AdHocRunHistory, CeleryTask
|
||||
from common.permissions import SuperUserRequiredMixin, AdminUserRequiredMixin
|
||||
from common.permissions import 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
|
||||
model = Task
|
||||
ordering = ('-date_created',)
|
||||
@ -18,18 +26,23 @@ class TaskListView(SuperUserRequiredMixin, DatetimeSearchMixin, ListView):
|
||||
keyword = ''
|
||||
|
||||
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.queryset = self.queryset.filter(
|
||||
queryset = queryset.filter(
|
||||
date_created__gt=self.date_from,
|
||||
date_created__lt=self.date_to
|
||||
)
|
||||
|
||||
if self.keyword:
|
||||
self.queryset = self.queryset.filter(
|
||||
queryset = queryset.filter(
|
||||
name__icontains=self.keyword,
|
||||
)
|
||||
return self.queryset
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
@ -43,10 +56,19 @@ class TaskListView(SuperUserRequiredMixin, DatetimeSearchMixin, ListView):
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class TaskDetailView(SuperUserRequiredMixin, DetailView):
|
||||
class TaskDetailView(AdminUserRequiredMixin, DetailView):
|
||||
model = Task
|
||||
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):
|
||||
context = {
|
||||
'app': _('Ops'),
|
||||
@ -56,7 +78,7 @@ class TaskDetailView(SuperUserRequiredMixin, DetailView):
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class TaskAdhocView(SuperUserRequiredMixin, DetailView):
|
||||
class TaskAdhocView(AdminUserRequiredMixin, DetailView):
|
||||
model = Task
|
||||
template_name = 'ops/task_adhoc.html'
|
||||
|
||||
@ -69,7 +91,7 @@ class TaskAdhocView(SuperUserRequiredMixin, DetailView):
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class TaskHistoryView(SuperUserRequiredMixin, DetailView):
|
||||
class TaskHistoryView(AdminUserRequiredMixin, DetailView):
|
||||
model = Task
|
||||
template_name = 'ops/task_history.html'
|
||||
|
||||
@ -82,7 +104,7 @@ class TaskHistoryView(SuperUserRequiredMixin, DetailView):
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AdHocDetailView(SuperUserRequiredMixin, DetailView):
|
||||
class AdHocDetailView(AdminUserRequiredMixin, DetailView):
|
||||
model = AdHoc
|
||||
template_name = 'ops/adhoc_detail.html'
|
||||
|
||||
@ -95,7 +117,7 @@ class AdHocDetailView(SuperUserRequiredMixin, DetailView):
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AdHocHistoryView(SuperUserRequiredMixin, DetailView):
|
||||
class AdHocHistoryView(AdminUserRequiredMixin, DetailView):
|
||||
model = AdHoc
|
||||
template_name = 'ops/adhoc_history.html'
|
||||
|
||||
@ -108,7 +130,7 @@ class AdHocHistoryView(SuperUserRequiredMixin, DetailView):
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AdHocHistoryDetailView(SuperUserRequiredMixin, DetailView):
|
||||
class AdHocHistoryDetailView(AdminUserRequiredMixin, DetailView):
|
||||
model = AdHocRunHistory
|
||||
template_name = 'ops/adhoc_history_detail.html'
|
||||
|
||||
@ -121,6 +143,5 @@ class AdHocHistoryDetailView(SuperUserRequiredMixin, DetailView):
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class CeleryTaskLogView(AdminUserRequiredMixin, DetailView):
|
||||
template_name = 'ops/celery_task_log.html'
|
||||
model = CeleryTask
|
||||
|
||||
|
14
apps/ops/views/celery.py
Normal file
14
apps/ops/views/celery.py
Normal file
@ -0,0 +1,14 @@
|
||||
# -*- 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
|
76
apps/ops/views/command.py
Normal file
76
apps/ops/views/command.py
Normal file
@ -0,0 +1,76 @@
|
||||
# -*- 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):
|
||||
|
||||
|
||||
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()
|
||||
|
||||
sep = '@'
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user