Merge pull request #2206 from jumpserver/dev

Dev
pull/2328/head
老广 2018-12-19 10:53:00 +08:00 committed by GitHub
commit a5b9b4e1d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 1271 additions and 1001 deletions

View File

@ -14,6 +14,7 @@
# limitations under the License. # limitations under the License.
from django.db import transaction from django.db import transaction
from django.shortcuts import get_object_or_404
from rest_framework import generics from rest_framework import generics
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
@ -24,13 +25,14 @@ from common.utils import get_logger
from ..hands import IsOrgAdmin from ..hands import IsOrgAdmin
from ..models import AdminUser, Asset from ..models import AdminUser, Asset
from .. import serializers from .. import serializers
from ..tasks import test_admin_user_connectability_manual from ..tasks import test_admin_user_connectivity_manual
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = [
'AdminUserViewSet', 'ReplaceNodesAdminUserApi', 'AdminUserViewSet', 'ReplaceNodesAdminUserApi',
'AdminUserTestConnectiveApi', 'AdminUserAuthApi', 'AdminUserTestConnectiveApi', 'AdminUserAuthApi',
'AdminUserAssetsListView',
] ]
@ -81,12 +83,29 @@ class ReplaceNodesAdminUserApi(generics.UpdateAPIView):
class AdminUserTestConnectiveApi(generics.RetrieveAPIView): class AdminUserTestConnectiveApi(generics.RetrieveAPIView):
""" """
Test asset admin user connectivity Test asset admin user assets_connectivity
""" """
queryset = AdminUser.objects.all() queryset = AdminUser.objects.all()
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
admin_user = self.get_object() admin_user = self.get_object()
task = test_admin_user_connectability_manual.delay(admin_user) task = test_admin_user_connectivity_manual.delay(admin_user)
return Response({"task": task.id}) return Response({"task": task.id})
class AdminUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSimpleSerializer
pagination_class = LimitOffsetPagination
filter_fields = ("hostname", "ip")
http_method_names = ['get']
search_fields = filter_fields
def get_object(self):
pk = self.kwargs.get('pk')
return get_object_or_404(AdminUser, pk=pk)
def get_queryset(self):
admin_user = self.get_object()
return admin_user.get_related_assets()

View File

@ -17,7 +17,7 @@ from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from ..models import Asset, AdminUser, Node from ..models import Asset, AdminUser, Node
from .. import serializers from .. import serializers
from ..tasks import update_asset_hardware_info_manual, \ from ..tasks import update_asset_hardware_info_manual, \
test_asset_connectability_manual test_asset_connectivity_manual
from ..utils import LabelFilter from ..utils import LabelFilter
@ -41,40 +41,46 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet):
pagination_class = LimitOffsetPagination pagination_class = LimitOffsetPagination
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
def filter_node(self): def filter_node(self, queryset):
node_id = self.request.query_params.get("node_id") node_id = self.request.query_params.get("node_id")
if not node_id: if not node_id:
return return queryset
node = get_object_or_404(Node, id=node_id) node = get_object_or_404(Node, id=node_id)
show_current_asset = self.request.query_params.get("show_current_asset") in ('1', 'true') show_current_asset = self.request.query_params.get("show_current_asset") in ('1', 'true')
if node.is_root(): if node.is_root() and show_current_asset:
if show_current_asset: queryset = queryset.filter(
self.queryset = self.queryset.filter( Q(nodes=node_id) | Q(nodes__isnull=True)
Q(nodes=node_id) | Q(nodes__isnull=True) )
) elif node.is_root() and not show_current_asset:
return pass
if show_current_asset: elif not node.is_root() and show_current_asset:
self.queryset = self.queryset.filter(nodes=node) queryset = queryset.filter(nodes=node)
else: else:
self.queryset = self.queryset.filter( queryset = queryset.filter(
nodes__key__regex='^{}(:[0-9]+)*$'.format(node.key), nodes__key__regex='^{}(:[0-9]+)*$'.format(node.key),
) )
return queryset
def filter_admin_user_id(self): def filter_admin_user_id(self, queryset):
admin_user_id = self.request.query_params.get('admin_user_id') admin_user_id = self.request.query_params.get('admin_user_id')
if admin_user_id: if not admin_user_id:
admin_user = get_object_or_404(AdminUser, id=admin_user_id) return queryset
self.queryset = self.queryset.filter(admin_user=admin_user) admin_user = get_object_or_404(AdminUser, id=admin_user_id)
queryset = queryset.filter(admin_user=admin_user)
return queryset
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
queryset = self.filter_node(queryset)
queryset = self.filter_admin_user_id(queryset)
return queryset
def get_queryset(self): def get_queryset(self):
self.queryset = super().get_queryset()\ queryset = super().get_queryset().distinct()
.prefetch_related('labels', 'nodes')\ queryset = self.get_serializer_class().setup_eager_loading(queryset)
.select_related('admin_user') return queryset
self.filter_admin_user_id()
self.filter_node()
return self.queryset.distinct()
class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView): class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView):
@ -103,7 +109,7 @@ class AssetRefreshHardwareApi(generics.RetrieveAPIView):
class AssetAdminUserTestApi(generics.RetrieveAPIView): class AssetAdminUserTestApi(generics.RetrieveAPIView):
""" """
Test asset admin user connectivity Test asset admin user assets_connectivity
""" """
queryset = Asset.objects.all() queryset = Asset.objects.all()
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
@ -111,7 +117,7 @@ class AssetAdminUserTestApi(generics.RetrieveAPIView):
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
asset_id = kwargs.get('pk') asset_id = kwargs.get('pk')
asset = get_object_or_404(Asset, pk=asset_id) asset = get_object_or_404(Asset, pk=asset_id)
task = test_asset_connectability_manual.delay(asset) task = test_asset_connectivity_manual.delay(asset)
return Response({"task": task.id}) return Response({"task": task.id})

View File

@ -17,15 +17,14 @@ from rest_framework import generics, mixins, viewsets
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.db.models import Count
from common.utils import get_logger, get_object_or_none from common.utils import get_logger, get_object_or_none
from common.tree import TreeNodeSerializer
from ..hands import IsOrgAdmin from ..hands import IsOrgAdmin
from ..models import Node from ..models import Node
from ..tasks import update_assets_hardware_info_util, test_asset_connectability_util from ..tasks import update_assets_hardware_info_util, test_asset_connectivity_util
from .. import serializers from .. import serializers
@ -34,7 +33,8 @@ __all__ = [
'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi', 'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi',
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi', 'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi',
'NodeAddChildrenApi', 'RefreshNodeHardwareInfoApi', 'NodeAddChildrenApi', 'RefreshNodeHardwareInfoApi',
'TestNodeConnectiveApi' 'TestNodeConnectiveApi', 'NodeListAsTreeApi',
'NodeChildrenAsTreeApi',
] ]
@ -43,22 +43,89 @@ class NodeViewSet(viewsets.ModelViewSet):
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeSerializer serializer_class = serializers.NodeSerializer
def perform_create(self, serializer):
child_key = Node.root().get_next_child_key()
serializer.validated_data["key"] = child_key
serializer.save()
def update(self, request, *args, **kwargs): class NodeListAsTreeApi(generics.ListAPIView):
node = self.get_object() """
if node.is_root(): 获取节点列表树
node_value = node.value [
post_value = request.data.get('value') {
if node_value != post_value: "id": "",
return Response( "name": "",
{"msg": _("You can't update the root node name")}, "pId": "",
status=400 "meta": ""
) }
return super().update(request, *args, **kwargs) ]
"""
permission_classes = (IsOrgAdmin,)
serializer_class = TreeNodeSerializer
def get_queryset(self):
queryset = [node.as_tree_node() for node in Node.objects.all()]
return queryset
def filter_queryset(self, queryset):
if self.request.query_params.get('refresh', '0') == '1':
queryset = self.refresh_nodes(queryset)
return queryset
@staticmethod
def refresh_nodes(queryset):
Node.expire_nodes_assets_amount()
Node.expire_nodes_full_value()
return queryset
class NodeChildrenAsTreeApi(generics.ListAPIView):
"""
节点子节点作为树返回
[
{
"id": "",
"name": "",
"pId": "",
"meta": ""
}
]
"""
permission_classes = (IsOrgAdmin,)
serializer_class = TreeNodeSerializer
node = None
is_root = False
def get_queryset(self):
node_key = self.request.query_params.get('key')
if node_key:
self.node = Node.objects.get(key=node_key)
queryset = self.node.get_children(with_self=False)
else:
self.is_root = True
self.node = Node.root()
queryset = list(self.node.get_children(with_self=True))
nodes_invalid = Node.objects.exclude(key__startswith=self.node.key)
queryset.extend(list(nodes_invalid))
queryset = [node.as_tree_node() for node in queryset]
return queryset
def filter_assets(self, queryset):
include_assets = self.request.query_params.get('assets', '0') == '1'
if not include_assets:
return queryset
assets = self.node.get_assets()
for asset in assets:
queryset.append(asset.as_tree_node(self.node))
return queryset
def filter_queryset(self, queryset):
queryset = self.filter_assets(queryset)
queryset = self.filter_refresh_nodes(queryset)
return queryset
def filter_refresh_nodes(self, queryset):
if self.request.query_params.get('refresh', '0') == '1':
Node.expire_nodes_assets_amount()
Node.expire_nodes_full_value()
return queryset
class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView): class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
@ -67,19 +134,10 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
serializer_class = serializers.NodeSerializer serializer_class = serializers.NodeSerializer
instance = None 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 post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
instance = self.get_object()
if not request.data.get("value"): if not request.data.get("value"):
request.data["value"] = _("New node {}").format(self.counter()) request.data["value"] = instance.get_next_child_preset_name()
return super().post(request, *args, **kwargs) return super().post(request, *args, **kwargs)
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
@ -91,10 +149,7 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
'The same level node name cannot be the same' 'The same level node name cannot be the same'
) )
node = instance.create_child(value=value) node = instance.create_child(value=value)
return Response( return Response(self.serializer_class(instance=node).data, status=201)
{"id": node.id, "key": node.key, "value": node.value},
status=201,
)
def get_object(self): def get_object(self):
pk = self.kwargs.get('pk') or self.request.query_params.get('id') pk = self.kwargs.get('pk') or self.request.query_params.get('id')
@ -107,7 +162,6 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
def get_queryset(self): def get_queryset(self):
queryset = [] queryset = []
query_all = self.request.query_params.get("all") query_all = self.request.query_params.get("all")
query_assets = self.request.query_params.get('assets')
node = self.get_object() node = self.get_object()
if node is None: if node is None:
@ -120,23 +174,8 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
else: else:
children = node.get_children() children = node.get_children()
queryset.extend(list(children)) queryset.extend(list(children))
if query_assets:
assets = node.get_assets()
for asset in assets:
node_fake = Node()
node_fake.assets__count = 0
node_fake.id = asset.id
node_fake.is_node = False
node_fake.key = node.key + ':0'
node_fake.value = asset.hostname
queryset.append(node_fake)
queryset = sorted(queryset, key=lambda x: x.is_node, reverse=True)
return queryset return queryset
def get(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
class NodeAssetsApi(generics.ListAPIView): class NodeAssetsApi(generics.ListAPIView):
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
@ -234,5 +273,5 @@ class TestNodeConnectiveApi(APIView):
assets = node.assets.all() assets = node.assets.all()
# task_name = _("测试节点下资产是否可连接: {}".format(node.name)) # task_name = _("测试节点下资产是否可连接: {}".format(node.name))
task_name = _("Test if the assets under the node are connectable: {}".format(node.name)) task_name = _("Test if the assets under the node are connectable: {}".format(node.name))
task = test_asset_connectability_util.delay(assets, task_name=task_name) task = test_asset_connectivity_util.delay(assets, task_name=task_name)
return Response({"task": task.id}) return Response({"task": task.id})

View File

@ -24,8 +24,8 @@ from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from ..models import SystemUser, Asset from ..models import SystemUser, Asset
from .. import serializers from .. import serializers
from ..tasks import push_system_user_to_assets_manual, \ from ..tasks import push_system_user_to_assets_manual, \
test_system_user_connectability_manual, push_system_user_a_asset_manual, \ test_system_user_connectivity_manual, push_system_user_a_asset_manual, \
test_system_user_connectability_a_asset test_system_user_connectivity_a_asset
logger = get_logger(__file__) logger = get_logger(__file__)
@ -33,7 +33,7 @@ __all__ = [
'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserViewSet', 'SystemUserAuthInfoApi',
'SystemUserPushApi', 'SystemUserTestConnectiveApi', 'SystemUserPushApi', 'SystemUserTestConnectiveApi',
'SystemUserAssetsListView', 'SystemUserPushToAssetApi', 'SystemUserAssetsListView', 'SystemUserPushToAssetApi',
'SystemUserTestAssetConnectabilityApi', 'SystemUserCommandFilterRuleListApi', 'SystemUserTestAssetConnectivityApi', 'SystemUserCommandFilterRuleListApi',
] ]
@ -93,15 +93,16 @@ class SystemUserTestConnectiveApi(generics.RetrieveAPIView):
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
system_user = self.get_object() system_user = self.get_object()
task = test_system_user_connectability_manual.delay(system_user) task = test_system_user_connectivity_manual.delay(system_user)
return Response({"task": task.id}) return Response({"task": task.id})
class SystemUserAssetsListView(generics.ListAPIView): class SystemUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSerializer serializer_class = serializers.AssetSimpleSerializer
pagination_class = LimitOffsetPagination pagination_class = LimitOffsetPagination
filter_fields = ("hostname", "ip") filter_fields = ("hostname", "ip")
http_method_names = ['get']
search_fields = filter_fields search_fields = filter_fields
def get_object(self): def get_object(self):
@ -125,7 +126,7 @@ class SystemUserPushToAssetApi(generics.RetrieveAPIView):
return Response({"task": task.id}) return Response({"task": task.id})
class SystemUserTestAssetConnectabilityApi(generics.RetrieveAPIView): class SystemUserTestAssetConnectivityApi(generics.RetrieveAPIView):
queryset = SystemUser.objects.all() queryset = SystemUser.objects.all()
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
@ -133,7 +134,7 @@ class SystemUserTestAssetConnectabilityApi(generics.RetrieveAPIView):
system_user = self.get_object() system_user = self.get_object()
asset_id = self.kwargs.get('aid') asset_id = self.kwargs.get('aid')
asset = get_object_or_404(Asset, id=asset_id) asset = get_object_or_404(Asset, id=asset_id)
task = test_system_user_connectability_a_asset.delay(system_user, asset) task = test_system_user_connectivity_a_asset.delay(system_user, asset)
return Response({"task": task.id}) return Response({"task": task.id})

View File

@ -99,8 +99,8 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
auto_generate_key = self.cleaned_data.get('auto_generate_key', False) auto_generate_key = self.cleaned_data.get('auto_generate_key', False)
private_key, public_key = super().gen_keys() private_key, public_key = super().gen_keys()
if login_mode == SystemUser.MANUAL_LOGIN or \ if login_mode == SystemUser.LOGIN_MANUAL or \
protocol in [SystemUser.RDP_PROTOCOL, SystemUser.TELNET_PROTOCOL]: protocol in [SystemUser.PROTOCOL_RDP, SystemUser.PROTOCOL_TELNET]:
system_user.auto_push = 0 system_user.auto_push = 0
auto_generate_key = False auto_generate_key = False
system_user.save() system_user.save()
@ -124,7 +124,7 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
validated = super().is_valid() validated = super().is_valid()
username = self.cleaned_data.get('username') username = self.cleaned_data.get('username')
login_mode = self.cleaned_data.get('login_mode') login_mode = self.cleaned_data.get('login_mode')
if login_mode == SystemUser.AUTO_LOGIN and not username: if login_mode == SystemUser.LOGIN_AUTO and not username:
self.add_error( self.add_error(
"username", _('* Automatic login mode,' "username", _('* Automatic login mode,'
' must fill in the username.') ' must fill in the username.')

View File

@ -13,36 +13,36 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='adminuser', model_name='adminuser',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='asset', model_name='asset',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='domain', model_name='domain',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='gateway', model_name='gateway',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='label', model_name='label',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='node', model_name='node',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='systemuser', model_name='systemuser',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
] ]

View File

@ -16,7 +16,7 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='CommandFilter', name='CommandFilter',
fields=[ fields=[
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')), ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=64, verbose_name='Name')), ('name', models.CharField(max_length=64, verbose_name='Name')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')), ('is_active', models.BooleanField(default=True, verbose_name='Is active')),
@ -32,7 +32,7 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='CommandFilterRule', name='CommandFilterRule',
fields=[ fields=[
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')), ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('type', models.CharField(choices=[('regex', 'Regex'), ('command', 'Command')], default='command', max_length=16, verbose_name='Type')), ('type', models.CharField(choices=[('regex', 'Regex'), ('command', 'Command')], default='command', max_length=16, verbose_name='Type')),
('priority', models.IntegerField(default=50, help_text='1-100, the lower will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')), ('priority', models.IntegerField(default=50, help_text='1-100, the lower will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')),

View File

@ -13,7 +13,6 @@ from django.db.models import Q
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.cache import cache from django.core.cache import cache
from ..const import ASSET_ADMIN_CONN_CACHE_KEY
from .user import AdminUser, SystemUser from .user import AdminUser, SystemUser
from orgs.mixins import OrgModelMixin, OrgManager from orgs.mixins import OrgModelMixin, OrgManager
@ -75,63 +74,48 @@ class Asset(OrgModelMixin):
protocol = models.CharField(max_length=128, default=SSH_PROTOCOL, choices=PROTOCOL_CHOICES, verbose_name=_('Protocol')) protocol = models.CharField(max_length=128, default=SSH_PROTOCOL, choices=PROTOCOL_CHOICES, verbose_name=_('Protocol'))
port = models.IntegerField(default=22, verbose_name=_('Port')) port = models.IntegerField(default=22, verbose_name=_('Port'))
platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform')) platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform'))
domain = models.ForeignKey("assets.Domain", null=True, blank=True, domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), on_delete=models.SET_NULL)
related_name='assets', verbose_name=_("Domain"), nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes"))
on_delete=models.SET_NULL)
nodes = models.ManyToManyField('assets.Node', default=default_node,
related_name='assets',
verbose_name=_("Nodes"))
is_active = models.BooleanField(default=True, verbose_name=_('Is active')) is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
# Auth # Auth
admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, null=True, verbose_name=_("Admin user"))
null=True, verbose_name=_("Admin user"))
# Some information # Some information
public_ip = models.GenericIPAddressField(max_length=32, blank=True, null=True, verbose_name=_('Public IP')) public_ip = models.GenericIPAddressField(max_length=32, blank=True, null=True, verbose_name=_('Public IP'))
number = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Asset number')) number = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Asset number'))
# Collect # Collect
vendor = models.CharField(max_length=64, null=True, blank=True, vendor = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Vendor'))
verbose_name=_('Vendor')) model = models.CharField(max_length=54, null=True, blank=True, verbose_name=_('Model'))
model = models.CharField(max_length=54, null=True, blank=True, sn = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Serial number'))
verbose_name=_('Model'))
sn = models.CharField(max_length=128, null=True, blank=True,
verbose_name=_('Serial number'))
cpu_model = models.CharField(max_length=64, null=True, blank=True, cpu_model = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('CPU model'))
verbose_name=_('CPU model'))
cpu_count = models.IntegerField(null=True, verbose_name=_('CPU count')) cpu_count = models.IntegerField(null=True, verbose_name=_('CPU count'))
cpu_cores = models.IntegerField(null=True, verbose_name=_('CPU cores')) cpu_cores = models.IntegerField(null=True, verbose_name=_('CPU cores'))
cpu_vcpus = models.IntegerField(null=True, verbose_name=_('CPU vcpus')) cpu_vcpus = models.IntegerField(null=True, verbose_name=_('CPU vcpus'))
memory = models.CharField(max_length=64, null=True, blank=True, memory = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Memory'))
verbose_name=_('Memory')) disk_total = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk total'))
disk_total = models.CharField(max_length=1024, null=True, blank=True, disk_info = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk info'))
verbose_name=_('Disk total'))
disk_info = models.CharField(max_length=1024, null=True, blank=True,
verbose_name=_('Disk info'))
os = models.CharField(max_length=128, null=True, blank=True, os = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('OS'))
verbose_name=_('OS')) os_version = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('OS version'))
os_version = models.CharField(max_length=16, null=True, blank=True, os_arch = models.CharField(max_length=16, blank=True, null=True, verbose_name=_('OS arch'))
verbose_name=_('OS version')) hostname_raw = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Hostname raw'))
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, labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
related_name='assets', created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
verbose_name=_("Labels")) date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created'))
created_by = models.CharField(max_length=32, null=True, blank=True, comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment'))
verbose_name=_('Created by'))
date_created = models.DateTimeField(auto_now_add=True, null=True,
blank=True,
verbose_name=_('Date created'))
comment = models.TextField(max_length=128, default='', blank=True,
verbose_name=_('Comment'))
objects = OrgManager.from_queryset(AssetQuerySet)() objects = OrgManager.from_queryset(AssetQuerySet)()
CONNECTIVITY_CACHE_KEY = '_JMS_ASSET_CONNECTIVITY_{}'
UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3)
CONNECTIVITY_CHOICES = (
(UNREACHABLE, _("Unreachable")),
(REACHABLE, _('Reachable')),
(UNKNOWN, _("Unknown")),
)
def __str__(self): def __str__(self):
return '{0.hostname}({0.ip})'.format(self) return '{0.hostname}({0.ip})'.format(self)
@ -197,25 +181,17 @@ class Asset(OrgModelMixin):
return '' return ''
@property @property
def is_connective(self): def connectivity(self):
if not self.is_unixlike(): if not self.is_unixlike():
return True return self.UNKNOWN
val = cache.get(ASSET_ADMIN_CONN_CACHE_KEY.format(self.hostname)) key = self.CONNECTIVITY_CACHE_KEY.format(str(self.id))
if val == 1: cached = cache.get(key, None)
return True return cached if cached is not None else self.UNKNOWN
else:
return False
def to_json(self): @connectivity.setter
info = { def connectivity(self, value):
'id': self.id, key = self.CONNECTIVITY_CACHE_KEY.format(str(self.id))
'hostname': self.hostname, cache.set(key, value, 3600*2)
'ip': self.ip,
'port': self.port,
}
if self.domain and self.domain.gateway_set.all():
info["gateways"] = [d.id for d in self.domain.gateway_set.all()]
return info
def get_auth_info(self): def get_auth_info(self):
if self.admin_user: if self.admin_user:
@ -236,11 +212,20 @@ class Asset(OrgModelMixin):
fake_node.is_node = False fake_node.is_node = False
return fake_node return fake_node
def to_json(self):
info = {
'id': self.id,
'hostname': self.hostname,
'ip': self.ip,
'port': self.port,
}
if self.domain and self.domain.gateway_set.all():
info["gateways"] = [d.id for d in self.domain.gateway_set.all()]
return info
def _to_secret_json(self): def _to_secret_json(self):
""" """
Ansible use it create inventory, First using asset user, Ansible use it create inventory
otherwise using cluster admin user
Todo: May be move to ops implements it Todo: May be move to ops implements it
""" """
data = self.to_json() data = self.to_json()
@ -255,6 +240,36 @@ class Asset(OrgModelMixin):
}) })
return data return data
def as_tree_node(self, parent_node):
from common.tree import TreeNode
icon_skin = 'file'
if self.platform.lower() == 'windows':
icon_skin = 'windows'
elif self.platform.lower() == 'linux':
icon_skin = 'linux'
data = {
'id': str(self.id),
'name': self.hostname,
'title': self.ip,
'pId': parent_node.key,
'isParent': False,
'open': False,
'iconSkin': icon_skin,
'meta': {
'type': 'asset',
'asset': {
'id': self.id,
'hostname': self.hostname,
'ip': self.ip,
'port': self.port,
'platform': self.platform,
'protocol': self.protocol,
}
}
}
tree_node = TreeNode(**data)
return tree_node
class Meta: class Meta:
unique_together = [('org_id', 'hostname')] unique_together = [('org_id', 'hostname')]
verbose_name = _("Asset") verbose_name = _("Asset")

View File

@ -29,6 +29,13 @@ class AssetUser(OrgModelMixin):
date_updated = models.DateTimeField(auto_now=True) date_updated = models.DateTimeField(auto_now=True)
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by')) created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3)
CONNECTIVITY_CHOICES = (
(UNREACHABLE, _("Unreachable")),
(REACHABLE, _('Reachable')),
(UNKNOWN, _("Unknown")),
)
@property @property
def password(self): def password(self):
if self._password: if self._password:

View File

@ -5,6 +5,7 @@ import uuid
from django.db import models, transaction from django.db import models, transaction
from django.db.models import Q from django.db.models import Q
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.core.cache import cache from django.core.cache import cache
from orgs.mixins import OrgModelMixin from orgs.mixins import OrgModelMixin
@ -22,7 +23,9 @@ class Node(OrgModelMixin):
date_create = models.DateTimeField(auto_now_add=True) date_create = models.DateTimeField(auto_now_add=True)
is_node = True is_node = True
_full_value_cache_key_prefix = '_NODE_VALUE_{}' _assets_amount = None
_full_value_cache_key = '_NODE_VALUE_{}'
_assets_amount_cache_key = '_NODE_ASSETS_AMOUNT_{}'
class Meta: class Meta:
verbose_name = _("Node") verbose_name = _("Node")
@ -49,30 +52,65 @@ class Node(OrgModelMixin):
def name(self): def name(self):
return self.value return self.value
@property
def assets_amount(self):
"""
获取节点下所有资产数量速度太慢所以需要重写使用cache等方案
:return:
"""
if self._assets_amount is not None:
return self._assets_amount
cache_key = self._assets_amount_cache_key.format(self.key)
cached = cache.get(cache_key)
if cached is not None:
return cached
assets_amount = self.get_all_assets().count()
cache.set(cache_key, assets_amount, 3600)
return assets_amount
@assets_amount.setter
def assets_amount(self, value):
self._assets_amount = value
def expire_assets_amount(self):
ancestor_keys = self.get_ancestor_keys(with_self=True)
cache_keys = [self._assets_amount_cache_key.format(k) for k in ancestor_keys]
cache.delete_many(cache_keys)
@classmethod
def expire_nodes_assets_amount(cls, nodes=None):
if nodes:
for node in nodes:
node.expire_assets_amount()
return
key = cls._assets_amount_cache_key.format('*')
cache.delete_pattern(key)
@property @property
def full_value(self): def full_value(self):
key = self._full_value_cache_key_prefix.format(self.key) key = self._full_value_cache_key.format(self.key)
cached = cache.get(key) cached = cache.get(key)
if cached: if cached:
return cached return cached
value = self.get_full_value()
self.cache_full_value(value)
return value
def get_full_value(self):
# ancestor = [a.value for a in self.get_ancestor(with_self=True)]
if self.is_root(): if self.is_root():
return self.value return self.value
parent_full_value = self.parent.full_value parent_full_value = self.parent.full_value
value = parent_full_value + ' / ' + self.value value = parent_full_value + ' / ' + self.value
key = self._full_value_cache_key.format(self.key)
cache.set(key, value, 3600)
return value 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): def expire_full_value(self):
key = self._full_value_cache_key_prefix.format(self.key) key = self._full_value_cache_key.format(self.key)
cache.delete_pattern(key+'*')
@classmethod
def expire_nodes_full_value(cls, nodes=None):
if nodes:
for node in nodes:
node.expire_full_value()
return
key = cls._full_value_cache_key.format('*')
cache.delete_pattern(key+'*') cache.delete_pattern(key+'*')
@property @property
@ -85,6 +123,17 @@ class Node(OrgModelMixin):
self.save() self.save()
return "{}:{}".format(self.key, mark) return "{}:{}".format(self.key, mark)
def get_next_child_preset_name(self):
name = ugettext("New node")
values = [
child.value[child.value.rfind(' '):]
for child in self.get_children()
if child.value.startswith(name)
]
values = [int(value) for value in values if value.strip().isdigit()]
count = max(values) + 1 if values else 1
return '{} {}'.format(name, count)
def create_child(self, value): def create_child(self, value):
with transaction.atomic(): with transaction.atomic():
child_key = self.get_next_child_key() child_key = self.get_next_child_key()
@ -134,7 +183,7 @@ class Node(OrgModelMixin):
pattern = r'^{0}$|^{0}:'.format(self.key) pattern = r'^{0}$|^{0}:'.format(self.key)
args = [] args = []
kwargs = {} kwargs = {}
if self.is_default_node(): if self.is_root():
args.append(Q(nodes__key__regex=pattern) | Q(nodes=None)) args.append(Q(nodes__key__regex=pattern) | Q(nodes=None))
else: else:
kwargs['nodes__key__regex'] = pattern kwargs['nodes__key__regex'] = pattern
@ -182,17 +231,18 @@ class Node(OrgModelMixin):
child.save() child.save()
self.save() self.save()
def get_ancestor(self, with_self=False): def get_ancestor_keys(self, with_self=False):
if self.is_root(): parent_keys = []
root = self.__class__.root() key_list = self.key.split(":")
return [root]
_key = self.key.split(':')
if not with_self: if not with_self:
_key.pop() key_list.pop()
ancestor_keys = [] for i in range(len(key_list)):
for i in range(len(_key)): parent_keys.append(":".join(key_list))
ancestor_keys.append(':'.join(_key)) key_list.pop()
_key.pop() return parent_keys
def get_ancestor(self, with_self=False):
ancestor_keys = self.get_ancestor_keys(with_self=with_self)
ancestor = self.__class__.objects.filter( ancestor = self.__class__.objects.filter(
key__in=ancestor_keys key__in=ancestor_keys
).order_by('key') ).order_by('key')
@ -227,9 +277,25 @@ class Node(OrgModelMixin):
defaults = {'value': 'Default'} defaults = {'value': 'Default'}
return cls.objects.get_or_create(defaults=defaults, key='1') return cls.objects.get_or_create(defaults=defaults, key='1')
@classmethod def as_tree_node(self):
def get_tree_name_ref(cls): from common.tree import TreeNode
pass from ..serializers import NodeSerializer
name = '{} ({})'.format(self.value, self.assets_amount)
node_serializer = NodeSerializer(instance=self)
data = {
'id': self.key,
'name': name,
'title': name,
'pId': self.parent_key,
'isParent': True,
'open': self.is_root(),
'meta': {
'node': node_serializer.data,
'type': 'node'
}
}
tree_node = TreeNode(**data)
return tree_node
@classmethod @classmethod
def generate_fake(cls, count=100): def generate_fake(cls, count=100):

View File

@ -14,7 +14,7 @@ from ..const import SYSTEM_USER_CONN_CACHE_KEY
from .base import AssetUser from .base import AssetUser
__all__ = ['AdminUser', 'SystemUser',] __all__ = ['AdminUser', 'SystemUser']
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
signer = get_signer() signer = get_signer()
@ -31,6 +31,7 @@ class AdminUser(AssetUser):
become_method = models.CharField(choices=BECOME_METHOD_CHOICES, default='sudo', max_length=4) become_method = models.CharField(choices=BECOME_METHOD_CHOICES, default='sudo', max_length=4)
become_user = models.CharField(default='root', max_length=64) become_user = models.CharField(default='root', max_length=64)
_become_pass = models.CharField(default='', max_length=128) _become_pass = models.CharField(default='', max_length=128)
CONNECTIVE_CACHE_KEY = '_JMS_ADMIN_USER_CONNECTIVE_{}'
def __str__(self): def __str__(self):
return self.name return self.name
@ -67,6 +68,23 @@ class AdminUser(AssetUser):
def assets_amount(self): def assets_amount(self):
return self.get_related_assets().count() return self.get_related_assets().count()
@property
def connectivity(self):
from .asset import Asset
assets = self.get_related_assets().values_list('id', 'hostname', flat=True)
data = {
'unreachable': [],
'reachable': [],
}
for asset_id, hostname in assets:
key = Asset.CONNECTIVITY_CACHE_KEY.format(str(self.id))
value = cache.get(key, Asset.UNKNOWN)
if value == Asset.REACHABLE:
data['reachable'].append(hostname)
elif value == Asset.UNREACHABLE:
data['unreachable'].append(hostname)
return data
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
unique_together = [('name', 'org_id')] unique_together = [('name', 'org_id')]
@ -94,34 +112,34 @@ class AdminUser(AssetUser):
class SystemUser(AssetUser): class SystemUser(AssetUser):
SSH_PROTOCOL = 'ssh' PROTOCOL_SSH = 'ssh'
RDP_PROTOCOL = 'rdp' PROTOCOL_RDP = 'rdp'
TELNET_PROTOCOL = 'telnet' PROTOCOL_TELNET = 'telnet'
PROTOCOL_CHOICES = ( PROTOCOL_CHOICES = (
(SSH_PROTOCOL, 'ssh'), (PROTOCOL_SSH, 'ssh'),
(RDP_PROTOCOL, 'rdp'), (PROTOCOL_RDP, 'rdp'),
(TELNET_PROTOCOL, 'telnet (beta)'), (PROTOCOL_TELNET, 'telnet (beta)'),
) )
AUTO_LOGIN = 'auto' LOGIN_AUTO = 'auto'
MANUAL_LOGIN = 'manual' LOGIN_MANUAL = 'manual'
LOGIN_MODE_CHOICES = ( LOGIN_MODE_CHOICES = (
(AUTO_LOGIN, _('Automatic login')), (LOGIN_AUTO, _('Automatic login')),
(MANUAL_LOGIN, _('Manually login')) (LOGIN_MANUAL, _('Manually login'))
) )
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes")) nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets")) assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
priority = models.IntegerField(default=20, verbose_name=_("Priority"), priority = models.IntegerField(default=20, verbose_name=_("Priority"), validators=[MinValueValidator(1), MaxValueValidator(100)])
validators=[MinValueValidator(1), MaxValueValidator(100)])
protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol')) protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol'))
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push')) auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo')) sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell')) shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=AUTO_LOGIN, max_length=10, verbose_name=_('Login mode')) login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode'))
cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True) cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True)
cache_key = "__SYSTEM_USER_CACHED_{}" SYSTEM_USER_CACHE_KEY = "__SYSTEM_USER_CACHED_{}"
CONNECTIVE_CACHE_KEY = '_JMS_SYSTEM_USER_CONNECTIVE_{}'
def __str__(self): def __str__(self):
return '{0.name}({0.username})'.format(self) return '{0.name}({0.username})'.format(self)
@ -136,34 +154,61 @@ class SystemUser(AssetUser):
'auto_push': self.auto_push, 'auto_push': self.auto_push,
} }
def get_assets(self): def get_related_assets(self):
assets = set(self.assets.all()) assets = set(self.assets.all())
return assets return assets
@property @property
def assets_connective(self): def connectivity(self):
_result = cache.get(SYSTEM_USER_CONN_CACHE_KEY.format(self.name), {}) cache_key = self.CONNECTIVE_CACHE_KEY.format(str(self.id))
return _result value = cache.get(cache_key, None)
if not value or 'unreachable' not in value:
return {'unreachable': [], 'reachable': []}
else:
return value
@connectivity.setter
def connectivity(self, value):
data = self.connectivity
unreachable = data['unreachable']
reachable = data['reachable']
for host in value.get('dark', {}).keys():
if host not in unreachable:
unreachable.append(host)
if host in reachable:
reachable.remove(host)
for host in value.get('contacted'):
if host not in reachable:
reachable.append(host)
if host in unreachable:
unreachable.remove(host)
cache_key = self.CONNECTIVE_CACHE_KEY.format(str(self.id))
cache.set(cache_key, data, 3600)
@property @property
def unreachable_assets(self): def assets_unreachable(self):
return list(self.assets_connective.get('dark', {}).keys()) return self.connectivity.get('unreachable')
@property @property
def reachable_assets(self): def assets_reachable(self):
return self.assets_connective.get('contacted', []) return self.connectivity.get('reachable')
@property
def login_mode_display(self):
return self.get_login_mode_display()
def is_need_push(self): def is_need_push(self):
if self.auto_push and self.protocol == self.__class__.SSH_PROTOCOL: if self.auto_push and self.protocol == self.PROTOCOL_SSH:
return True return True
else: else:
return False return False
def set_cache(self): def set_cache(self):
cache.set(self.cache_key.format(self.id), self, 3600) cache.set(self.SYSTEM_USER_CACHE_KEY.format(self.id), self, 3600)
def expire_cache(self): def expire_cache(self):
cache.delete(self.cache_key.format(self.id)) cache.delete(self.SYSTEM_USER_CACHE_KEY.format(self.id))
@property @property
def cmd_filter_rules(self): def cmd_filter_rules(self):
@ -184,7 +229,7 @@ class SystemUser(AssetUser):
@classmethod @classmethod
def get_system_user_by_id_or_cached(cls, sid): def get_system_user_by_id_or_cached(cls, sid):
cached = cache.get(cls.cache_key.format(sid)) cached = cache.get(cls.SYSTEM_USER_CACHE_KEY.format(sid))
if cached: if cached:
return cached return cached
try: try:

View File

@ -9,6 +9,7 @@ from .system_user import AssetSystemUserSerializer
__all__ = [ __all__ = [
'AssetSerializer', 'AssetGrantedSerializer', 'MyAssetGrantedSerializer', 'AssetSerializer', 'AssetGrantedSerializer', 'MyAssetGrantedSerializer',
'AssetAsNodeSerializer', 'AssetSimpleSerializer',
] ]
@ -22,14 +23,27 @@ class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer):
fields = '__all__' fields = '__all__'
validators = [] validators = []
@classmethod
def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """
queryset = queryset.prefetch_related('labels', 'nodes')\
.select_related('admin_user')
return queryset
def get_field_names(self, declared_fields, info): def get_field_names(self, declared_fields, info):
fields = super().get_field_names(declared_fields, info) fields = super().get_field_names(declared_fields, info)
fields.extend([ fields.extend([
'hardware_info', 'is_connective', 'org_name' 'hardware_info', 'connectivity', 'org_name'
]) ])
return fields return fields
class AssetAsNodeSerializer(serializers.ModelSerializer):
class Meta:
model = Asset
fields = ['id', 'hostname', 'ip', 'port', 'platform', 'protocol']
class AssetGrantedSerializer(serializers.ModelSerializer): class AssetGrantedSerializer(serializers.ModelSerializer):
""" """
被授权资产的数据结构 被授权资产的数据结构
@ -64,3 +78,9 @@ class MyAssetGrantedSerializer(AssetGrantedSerializer):
"is_active", "system_users_join", "org_name", "is_active", "system_users_join", "org_name",
"os", "platform", "comment", "org_id", "protocol" "os", "platform", "comment", "org_id", "protocol"
) )
class AssetSimpleSerializer(serializers.ModelSerializer):
class Meta:
model = Asset
fields = ['id', 'hostname', 'port', 'ip', 'connectivity']

View File

@ -8,84 +8,33 @@ from .asset import AssetGrantedSerializer
__all__ = [ __all__ = [
'NodeSerializer', "NodeGrantedSerializer", "NodeAddChildrenSerializer", 'NodeSerializer', "NodeAddChildrenSerializer",
"NodeAssetsSerializer", "NodeAssetsSerializer",
] ]
class NodeGrantedSerializer(BulkSerializerMixin, serializers.ModelSerializer):
"""
授权资产组
"""
assets_granted = AssetGrantedSerializer(many=True, read_only=True)
assets_amount = serializers.SerializerMethodField()
parent = serializers.SerializerMethodField()
name = serializers.SerializerMethodField()
class Meta:
model = Node
fields = [
'id', 'key', 'name', 'value', 'parent',
'assets_granted', 'assets_amount', 'org_id',
]
@staticmethod
def get_assets_amount(obj):
return len(obj.assets_granted)
@staticmethod
def get_name(obj):
return obj.name
@staticmethod
def get_parent(obj):
return obj.parent.id
class NodeSerializer(serializers.ModelSerializer): class NodeSerializer(serializers.ModelSerializer):
assets_amount = serializers.SerializerMethodField() assets_amount = serializers.IntegerField(read_only=True)
tree_id = serializers.SerializerMethodField()
tree_parent = serializers.SerializerMethodField()
class Meta: class Meta:
model = Node model = Node
fields = [ fields = [
'id', 'key', 'value', 'assets_amount', 'id', 'key', 'value', 'assets_amount', 'org_id',
'is_node', 'org_id', 'tree_id', 'tree_parent', ]
read_only_fields = [
'id', 'key', 'assets_amount', 'org_id',
] ]
list_serializer_class = BulkListSerializer
def validate(self, data): def validate_value(self, data):
value = data.get('value')
instance = self.instance if self.instance else Node.root() instance = self.instance if self.instance else Node.root()
children = instance.parent.get_children().exclude(key=instance.key) children = instance.parent.get_children().exclude(key=instance.key)
values = [child.value for child in children] values = [child.value for child in children]
if value in values: if data in values:
raise serializers.ValidationError( raise serializers.ValidationError(
'The same level node name cannot be the same' 'The same level node name cannot be the same'
) )
return data return data
@staticmethod
def get_assets_amount(obj):
if hasattr(obj, 'assets_amount'):
return obj.assets_amount
return obj.get_all_assets().count()
@staticmethod
def get_tree_id(obj):
return obj.key
@staticmethod
def get_tree_parent(obj):
return obj.parent_key
def get_fields(self):
fields = super().get_fields()
field = fields["key"]
field.required = False
return fields
class NodeAssetsSerializer(serializers.ModelSerializer): class NodeAssetsSerializer(serializers.ModelSerializer):
assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all()) assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all())
@ -97,3 +46,4 @@ class NodeAssetsSerializer(serializers.ModelSerializer):
class NodeAddChildrenSerializer(serializers.Serializer): class NodeAddChildrenSerializer(serializers.Serializer):
nodes = serializers.ListField() nodes = serializers.ListField()

View File

@ -1,6 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from ..models import SystemUser from ..models import SystemUser, Asset
from .base import AuthSerializer from .base import AuthSerializer
@ -21,17 +21,17 @@ class SystemUserSerializer(serializers.ModelSerializer):
def get_field_names(self, declared_fields, info): def get_field_names(self, declared_fields, info):
fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info) fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info)
fields.extend([ fields.extend([
'get_login_mode_display', 'login_mode_display',
]) ])
return fields return fields
@staticmethod @staticmethod
def get_unreachable_assets(obj): def get_unreachable_assets(obj):
return obj.unreachable_assets return obj.assets_unreachable
@staticmethod @staticmethod
def get_reachable_assets(obj): def get_reachable_assets(obj):
return obj.reachable_assets return obj.assets_reachable
def get_unreachable_amount(self, obj): def get_unreachable_amount(self, obj):
return len(self.get_unreachable_assets(obj)) return len(self.get_unreachable_assets(obj))
@ -41,7 +41,7 @@ class SystemUserSerializer(serializers.ModelSerializer):
@staticmethod @staticmethod
def get_assets_amount(obj): def get_assets_amount(obj):
return len(obj.get_assets()) return len(obj.get_related_assets())
class SystemUserAuthSerializer(AuthSerializer): class SystemUserAuthSerializer(AuthSerializer):
@ -75,4 +75,7 @@ class SystemUserSimpleSerializer(serializers.ModelSerializer):
""" """
class Meta: class Meta:
model = SystemUser model = SystemUser
fields = ('id', 'name', 'username') fields = ('id', 'name', 'username')

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from collections import defaultdict from collections import defaultdict
from django.db.models.signals import post_save, m2m_changed from django.db.models.signals import post_save, m2m_changed, post_delete
from django.dispatch import receiver from django.dispatch import receiver
from common.utils import get_logger from common.utils import get_logger
from .models import Asset, SystemUser, Node from .models import Asset, SystemUser, Node
from .tasks import update_assets_hardware_info_util, \ from .tasks import update_assets_hardware_info_util, \
test_asset_connectability_util, push_system_user_to_assets test_asset_connectivity_util, push_system_user_to_assets
logger = get_logger(__file__) logger = get_logger(__file__)
@ -19,8 +19,8 @@ def update_asset_hardware_info_on_created(asset):
def test_asset_conn_on_created(asset): def test_asset_conn_on_created(asset):
logger.debug("Test asset `{}` connectability".format(asset)) logger.debug("Test asset `{}` connectivity".format(asset))
test_asset_connectability_util.delay([asset]) test_asset_connectivity_util.delay([asset])
def set_asset_root_node(asset): def set_asset_root_node(asset):
@ -35,6 +35,17 @@ def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
update_asset_hardware_info_on_created(instance) update_asset_hardware_info_on_created(instance)
test_asset_conn_on_created(instance) test_asset_conn_on_created(instance)
# 过期节点资产数量
nodes = instance.nodes.all()
Node.expire_nodes_assets_amount(nodes)
@receiver(post_delete, sender=Asset, dispatch_uid="my_unique_identifier")
def on_asset_delete(sender, instance=None, **kwargs):
# 过期节点资产数量
nodes = instance.nodes.all()
Node.expire_nodes_assets_amount(nodes)
@receiver(post_save, sender=SystemUser, dispatch_uid="my_unique_identifier") @receiver(post_save, sender=SystemUser, dispatch_uid="my_unique_identifier")
def on_system_user_update(sender, instance=None, created=True, **kwargs): def on_system_user_update(sender, instance=None, created=True, **kwargs):
@ -63,10 +74,11 @@ def on_system_user_assets_change(sender, instance=None, **kwargs):
@receiver(m2m_changed, sender=Asset.nodes.through) @receiver(m2m_changed, sender=Asset.nodes.through)
def on_asset_node_changed(sender, instance=None, **kwargs): def on_asset_node_changed(sender, instance=None, **kwargs):
logger.debug("Asset node change signal received")
if isinstance(instance, Asset): if isinstance(instance, Asset):
if kwargs['action'] == 'post_add': if kwargs['action'] == 'post_add':
logger.debug("Asset node change signal received")
nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
Node.expire_nodes_assets_amount(nodes)
system_users_assets = defaultdict(set) system_users_assets = defaultdict(set)
system_users = SystemUser.objects.filter(nodes__in=nodes) system_users = SystemUser.objects.filter(nodes__in=nodes)
# 清理节点缓存 # 清理节点缓存
@ -79,9 +91,11 @@ def on_asset_node_changed(sender, instance=None, **kwargs):
@receiver(m2m_changed, sender=Asset.nodes.through) @receiver(m2m_changed, sender=Asset.nodes.through)
def on_node_assets_changed(sender, instance=None, **kwargs): def on_node_assets_changed(sender, instance=None, **kwargs):
if isinstance(instance, Node): if isinstance(instance, Node):
logger.debug("Node assets change signal received")
# 当节点和资产关系发生改变时,过期资产数量缓存
instance.expire_assets_amount()
assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
if kwargs['action'] == 'post_add': if kwargs['action'] == 'post_add':
logger.debug("Node assets change signal received")
# 重新关联系统用户和资产的关系 # 重新关联系统用户和资产的关系
system_users = SystemUser.objects.filter(nodes=instance) system_users = SystemUser.objects.filter(nodes=instance)
for system_user in system_users: for system_user in system_users:

View File

@ -26,6 +26,23 @@ disk_pattern = re.compile(r'^hd|sd|xvd|vd')
PERIOD_TASK = os.environ.get("PERIOD_TASK", "off") PERIOD_TASK = os.environ.get("PERIOD_TASK", "off")
def clean_hosts(assets):
clean_assets = []
for asset in assets:
if not asset.is_active:
msg = _("Asset has been disabled, skipped: {}").format(asset)
logger.info(msg)
continue
if not asset.support_ansible():
msg = _("Asset may not be support ansible, skipped: {}").format(asset)
logger.info(msg)
continue
clean_assets.append(asset)
if not clean_assets:
logger.info(_("No assets matched, stop task"))
return clean_assets
@shared_task @shared_task
def set_assets_hardware_info(assets, result, **kwargs): def set_assets_hardware_info(assets, result, **kwargs):
""" """
@ -60,9 +77,12 @@ def set_assets_hardware_info(assets, result, **kwargs):
___cpu_model = 'Unknown' ___cpu_model = 'Unknown'
___cpu_model = ___cpu_model[:64] ___cpu_model = ___cpu_model[:64]
___cpu_count = info.get('ansible_processor_count', 0) ___cpu_count = info.get('ansible_processor_count', 0)
___cpu_cores = info.get('ansible_processor_cores', None) or len(info.get('ansible_processor', [])) ___cpu_cores = info.get('ansible_processor_cores', None) or \
len(info.get('ansible_processor', []))
___cpu_vcpus = info.get('ansible_processor_vcpus', 0) ___cpu_vcpus = info.get('ansible_processor_vcpus', 0)
___memory = '%s %s' % capacity_convert('{} MB'.format(info.get('ansible_memtotal_mb'))) ___memory = '%s %s' % capacity_convert(
'{} MB'.format(info.get('ansible_memtotal_mb'))
)
disk_info = {} disk_info = {}
for dev, dev_info in info.get('ansible_devices', {}).items(): for dev, dev_info in info.get('ansible_devices', {}).items():
if disk_pattern.match(dev) and dev_info['removable'] == '0': if disk_pattern.match(dev) and dev_info['removable'] == '0':
@ -96,19 +116,8 @@ def update_assets_hardware_info_util(assets, task_name=None):
if task_name is None: if task_name is None:
task_name = _("Update some assets hardware info") task_name = _("Update some assets hardware info")
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
hosts = [] hosts = clean_hosts(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
hosts.append(asset)
if not hosts: if not hosts:
logger.info(_("No assets matched, stop task"))
return {} return {}
created_by = str(assets[0].org_id) created_by = str(assets[0].org_id)
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
@ -125,7 +134,6 @@ def update_assets_hardware_info_util(assets, task_name=None):
@shared_task @shared_task
def update_asset_hardware_info_manual(asset): def update_asset_hardware_info_manual(asset):
task_name = _("Update asset hardware info: {}").format(asset.hostname) task_name = _("Update asset hardware info: {}").format(asset.hostname)
# task_name = _("更新资产硬件信息")
return update_assets_hardware_info_util( return update_assets_hardware_info_util(
[asset], task_name=task_name [asset], task_name=task_name
) )
@ -141,123 +149,18 @@ def update_assets_hardware_info_period():
logger.debug("Period task disabled, update assets hardware info pass") logger.debug("Period task disabled, update assets hardware info pass")
return return
from ops.utils import update_or_create_ansible_task
from orgs.models import Organization
orgs = Organization.objects.all().values_list('id', flat=True)
orgs.append('')
task_name = _("Update assets hardware info period")
# for org_id in orgs:
# org_id = str(org_id)
# hostname_list = [
# asset for asset in Asset.objects.all()
# if asset.is_active and asset.is_unixlike()
# ]
# tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
#
# # Only create, schedule by celery beat
# update_or_create_ansible_task(
# task_name, hosts=hostname_list, tasks=tasks, pattern='all',
# options=const.TASK_OPTIONS, run_as_admin=True, created_by='System',
# interval=60*60*24, is_periodic=True, callback=set_assets_hardware_info.name,
# )
## ADMIN USER CONNECTIVE ## ## ADMIN USER CONNECTIVE ##
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 @shared_task
def test_admin_user_connectability_util(admin_user, task_name): def test_asset_connectivity_util(assets, task_name=None):
"""
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 = []
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
hosts.append(asset)
if not hosts:
logger.info(_("No assets matched, stop task"))
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=admin_user.org_id,
)
result = task.run()
set_admin_user_connectability_info(result, admin_user=admin_user.name)
return result
@shared_task
@register_as_period_task(interval=3600)
def test_admin_user_connectability_period():
"""
A period task that update the ansible task period
"""
admin_users = AdminUser.objects.all()
for admin_user in admin_users:
task_name = _("Test admin user connectability period: {}").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):
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
if task_name is None: if task_name is None:
task_name = _("Test assets connectability") task_name = _("Test assets connectivity")
# task_name = _("测试资产可连接性") hosts = clean_hosts(assets)
hosts = []
for asset in assets:
if not asset.is_active:
msg = _("Asset has been disabled, skip: {}").format(asset)
logger.info(msg)
continue
if not asset.support_ansible():
msg = _("Asset may not be support ansible, skip: {}").format(asset)
logger.info(msg)
continue
hosts.append(asset)
if not hosts: if not hosts:
logger.info(_("No assets, task stop"))
return {} return {}
tasks = const.TEST_ADMIN_USER_CONN_TASKS tasks = const.TEST_ADMIN_USER_CONN_TASKS
created_by = assets[0].org_id created_by = assets[0].org_id
@ -267,18 +170,20 @@ def test_asset_connectability_util(assets, task_name=None):
) )
result = task.run() result = task.run()
summary = result[1] summary = result[1]
for k in summary.get('dark'): for asset in assets:
cache.set(const.ASSET_ADMIN_CONN_CACHE_KEY.format(k), 0, CACHE_MAX_TIME) if asset.hostname in summary.get('dark', {}):
asset.connectivity = asset.UNREACHABLE
for k in summary.get('contacted'): elif asset.hostname in summary.get('contacted', []):
cache.set(const.ASSET_ADMIN_CONN_CACHE_KEY.format(k), 1, CACHE_MAX_TIME) asset.connectivity = asset.REACHABLE
else:
asset.connectivity = asset.UNKNOWN
return summary return summary
@shared_task @shared_task
def test_asset_connectability_manual(asset): def test_asset_connectivity_manual(asset):
task_name = _("Test assets connectability: {}").format(asset) task_name = _("Test assets connectivity: {}").format(asset)
summary = test_asset_connectability_util([asset], task_name=task_name) summary = test_asset_connectivity_util([asset], task_name=task_name)
if summary.get('dark'): if summary.get('dark'):
return False, summary['dark'] return False, summary['dark']
@ -286,21 +191,50 @@ def test_asset_connectability_manual(asset):
return True, "" 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
"""
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 ## ## System user connective ##
@shared_task @shared_task
def set_system_user_connectablity_info(result, **kwargs): def set_system_user_connectivity_info(system_user, result):
summary = result[1] summary = result[1]
task_name = kwargs.get("task_name") system_user.connectivity = summary
system_user = kwargs.get("system_user")
if system_user is None:
system_user = task_name.split(":")[-1]
cache_key = const.SYSTEM_USER_CONN_CACHE_KEY.format(str(system_user.id))
cache.set(cache_key, summary, CACHE_MAX_TIME)
@shared_task @shared_task
def test_system_user_connectability_util(system_user, assets, task_name): def test_system_user_connectivity_util(system_user, assets, task_name):
""" """
Test system cant connect his assets or not. Test system cant connect his assets or not.
:param system_user: :param system_user:
@ -309,20 +243,9 @@ def test_system_user_connectability_util(system_user, assets, task_name):
:return: :return:
""" """
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
hosts = []
tasks = const.TEST_SYSTEM_USER_CONN_TASKS tasks = const.TEST_SYSTEM_USER_CONN_TASKS
for asset in assets: hosts = clean_hosts(assets)
if not asset.is_active:
msg = _("Asset has been disabled, skip: {}").format(asset)
logger.info(msg)
continue
if not asset.support_ansible():
msg = _("Asset may not be support ansible, skip: {}").format(asset)
logger.info(msg)
continue
hosts.append(asset)
if not hosts: if not hosts:
logger.info(_("No assets matched, stop task"))
return {} return {}
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name, hosts=hosts, tasks=tasks, pattern='all', task_name, hosts=hosts, tasks=tasks, pattern='all',
@ -330,35 +253,35 @@ def test_system_user_connectability_util(system_user, assets, task_name):
run_as=system_user, created_by=system_user.org_id, run_as=system_user, created_by=system_user.org_id,
) )
result = task.run() result = task.run()
set_system_user_connectablity_info(result, system_user=system_user) set_system_user_connectivity_info(system_user, result)
return result return result
@shared_task @shared_task
def test_system_user_connectability_manual(system_user): def test_system_user_connectivity_manual(system_user):
task_name = _("Test system user connectability: {}").format(system_user) task_name = _("Test system user connectivity: {}").format(system_user)
assets = system_user.get_assets() assets = system_user.get_related_assets()
return test_system_user_connectability_util(system_user, assets, task_name) return test_system_user_connectivity_util(system_user, assets, task_name)
@shared_task @shared_task
def test_system_user_connectability_a_asset(system_user, asset): def test_system_user_connectivity_a_asset(system_user, asset):
task_name = _("Test system user connectability: {} => {}").format( task_name = _("Test system user connectivity: {} => {}").format(
system_user, asset system_user, asset
) )
return test_system_user_connectability_util(system_user, [asset], task_name) return test_system_user_connectivity_util(system_user, [asset], task_name)
@shared_task @shared_task
def test_system_user_connectability_period(): def test_system_user_connectivity_period():
if PERIOD_TASK != "on": if PERIOD_TASK != "on":
logger.debug("Period task disabled, test system user connectability pass") logger.debug("Period task disabled, test system user connectivity pass")
return return
system_users = SystemUser.objects.all() system_users = SystemUser.objects.all()
for system_user in system_users: for system_user in system_users:
task_name = _("Test system user connectability period: {}").format(system_user) task_name = _("Test system user connectivity period: {}").format(system_user)
# task_name = _("定期测试系统用户可连接性: {}".format(system_user)) assets = system_user.get_related_assets()
test_system_user_connectability_util(system_user, task_name) test_system_user_connectivity_util(system_user, assets, task_name)
#### Push system user tasks #### #### Push system user tasks ####
@ -380,6 +303,24 @@ def get_push_system_user_tasks(system_user):
), ),
} }
}) })
tasks.extend([
{
'name': 'Check home dir exists',
'action': {
'module': 'stat',
'args': 'path=/home/{}'.format(system_user.username)
},
'register': 'home_existed'
},
{
'name': "Set home dir permission",
'action': {
'module': 'file',
'args': "path=/home/{0} owner={0} group={0} mode=700".format(system_user.username)
},
'when': 'home_existed.stat.exists == true'
}
])
if system_user.public_key: if system_user.public_key:
tasks.append({ tasks.append({
'name': 'Set {} authorized key'.format(system_user.username), 'name': 'Set {} authorized key'.format(system_user.username),
@ -416,19 +357,8 @@ def push_system_user_util(system_user, assets, task_name):
return return
tasks = get_push_system_user_tasks(system_user) tasks = get_push_system_user_tasks(system_user)
hosts = [] hosts = clean_hosts(assets)
for asset in assets:
if not asset.is_active:
msg = _("Asset has been disabled, skip: {}").format(asset)
logger.info(msg)
continue
if not asset.support_ansible():
msg = _("Asset may not be support ansible, skip: {}").format(asset)
logger.info(msg)
continue
hosts.append(asset)
if not hosts: if not hosts:
logger.info(_("No assets matched, stop task"))
return {} return {}
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all', task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
@ -440,7 +370,7 @@ def push_system_user_util(system_user, assets, task_name):
@shared_task @shared_task
def push_system_user_to_assets_manual(system_user): def push_system_user_to_assets_manual(system_user):
assets = system_user.get_assets() assets = system_user.get_related_assets()
task_name = _("Push system users to assets: {}").format(system_user.name) task_name = _("Push system users to assets: {}").format(system_user.name)
return push_system_user_util(system_user, assets, task_name=task_name) return push_system_user_util(system_user, assets, task_name=task_name)
@ -459,6 +389,18 @@ def push_system_user_to_assets(system_user, assets):
return push_system_user_util(system_user, assets, task_name) return push_system_user_util(system_user, assets, task_name)
@shared_task
@after_app_shutdown_clean
def test_system_user_connectability_period():
pass
@shared_task
@after_app_shutdown_clean
def test_admin_user_connectability_period():
pass
# @shared_task # @shared_task
# @register_as_period_task(interval=3600) # @register_as_period_task(interval=3600)
# @after_app_ready_start # @after_app_ready_start

View File

@ -57,6 +57,10 @@
<script> <script>
var zTree2, asset_table2 = 0; var zTree2, asset_table2 = 0;
function initTable2() { function initTable2() {
if(asset_table2){
return
}
var options = { var options = {
ele: $('#asset_list_modal_table'), ele: $('#asset_list_modal_table'),
ajax_url: '{% url "api-assets:asset-list" %}?show_current_asset=1', ajax_url: '{% url "api-assets:asset-list" %}?show_current_asset=1',
@ -71,14 +75,14 @@ function initTable2() {
function onSelected2(event, treeNode) { function onSelected2(event, treeNode) {
var url = asset_table2.ajax.url(); var url = asset_table2.ajax.url();
url = setUrlParam(url, "node_id", treeNode.node_id); url = setUrlParam(url, "node_id", treeNode.meta.node.id);
setCookie('node_selected', treeNode.id);
asset_table2.ajax.url(url); asset_table2.ajax.url(url);
asset_table2.ajax.reload(); asset_table2.ajax.reload();
} }
function initTree2() { function initTree2() {
var url = '{% url 'api-assets:node-children-tree' %}?assets=0';
var setting = { var setting = {
view: { view: {
dblClickExpand: false, dblClickExpand: false,
@ -89,33 +93,22 @@ function initTree2() {
enable: true enable: true
} }
}, },
async: {
enable: true,
url: url,
autoParam: ["id=key", "name=n", "level=lv"],
type: 'get'
},
callback: { callback: {
onSelected: onSelected2 onSelected: onSelected2
} }
}; };
zTree2 = $.fn.zTree.init($("#assetTree2"), setting);
var zNodes = [];
$.get("{% url 'api-assets:node-list' %}", function(data, status){
$.each(data, function (index, value) {
value["node_id"] = value["id"];
value["id"] = value["tree_id"];
value["pId"] = value["tree_parent"];
{#value["open"] = true;#}
if (value["key"] === "0") {
value["open"] = true;
}
value["name"] = value["value"] + ' (' + value['assets_amount'] + ')';
});
zNodes = data;
$.fn.zTree.init($("#assetTree2"), setting, zNodes);
zTree2 = $.fn.zTree.getZTreeObj("assetTree2");
var root = zTree2.getNodes()[0];
zTree2.expandNode(root);
});
} }
$(document).ready(function(){ $(document).ready(function(){
}).on('show.bs.modal', function () {
initTable2(); initTable2();
initTree2(); initTree2();
}) })

View File

@ -45,13 +45,11 @@
<table class="table table-striped table-bordered table-hover" id="asset_list_table"> <table class="table table-striped table-bordered table-hover" id="asset_list_table">
<thead> <thead>
<tr> <tr>
<th class="text-center">
<input type="checkbox" id="check_all" class="ipt_check_all" >
</th>
<th>{% trans 'Hostname' %}</th> <th>{% trans 'Hostname' %}</th>
<th>{% trans 'IP' %}</th> <th>{% trans 'IP' %}</th>
<th>{% trans 'Port' %}</th> <th>{% trans 'Port' %}</th>
<th>{% trans 'Reachable' %}</th> <th>{% trans 'Reachable' %}</th>
<th>{% trans 'Action' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -91,26 +89,36 @@
<script> <script>
function initTable() { function initTable() {
var reachable = {{ admin_user.REACHABLE }};
var unreachable = {{ admin_user.UNREACHABLE }};
var options = { var options = {
ele: $('#asset_list_table'), ele: $('#asset_list_table'),
buttons: [], buttons: [],
order: [], order: [],
columnDefs: [ columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) { {targets: 0, createdCell: function (td, cellData, rowData) {
var detail_btn = '<a href="{% url "assets:asset-detail" pk=DEFAULT_PK %}" data-aid="'+rowData.id+'">' + cellData + '</a>'; var detail_btn = '<a href="{% url "assets:asset-detail" pk=DEFAULT_PK %}" data-aid="'+rowData.id+'">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id)); $(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}}, }},
{targets: 4, createdCell: function (td, cellData) { {targets: 3, createdCell: function (td, cellData) {
if (!cellData) { if (cellData === unreachable) {
$(td).html('<i class="fa fa-times text-danger"></i>') $(td).html('<i class="fa fa-times text-danger"></i>')
} else { } else if (cellData === reachable) {
$(td).html('<i class="fa fa-check text-navy"></i>') $(td).html('<i class="fa fa-check text-navy"></i>')
} } else {
}}], $(td).html('')
ajax_url: '{% url "api-assets:asset-list" %}?admin_user_id={{ admin_user.id }}', }
}},
{targets: 4, createdCell: function (td, cellData) {
var test_btn = ' <a class="btn btn-xs btn-info btn-test-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Test" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
$(td).html(test_btn);
}}
],
ajax_url: '{% url "api-assets:admin-user-assets" pk=admin_user.id %}',
columns: [ columns: [
{data: function(){return ""}}, {data: "hostname" }, {data: "ip" }, {data: "hostname" }, {data: "ip" },
{data: "port" }, {data: "is_connective" }], {data: "port" }, {data: "connectivity" }, {data: "id"}],
op_html: $('#actions').html() op_html: $('#actions').html()
}; };
jumpserver.initServerSideDataTable(options); jumpserver.initServerSideDataTable(options);
@ -119,6 +127,21 @@ function initTable() {
$(document).ready(function () { $(document).ready(function () {
initTable(); initTable();
}) })
.on('click', '.btn-test-asset', function () {
var asset_id = $(this).data('uid');
var the_url = "{% url 'api-assets:asset-alive-test' pk=DEFAULT_PK %}".replace('{{ DEFAULT_PK }}', asset_id);
var success = function (data) {
var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
};
APIUpdateAttr({
url: the_url,
method: 'GET',
success: success,
flash_message: false
});
})
.on('click', '.btn-test-connective', function () { .on('click', '.btn-test-connective', function () {
var the_url = "{% url 'api-assets:admin-user-connective' pk=admin_user.id %}"; var the_url = "{% url 'api-assets:admin-user-connective' pk=admin_user.id %}";
var success = function (data) { var success = function (data) {

View File

@ -136,6 +136,7 @@
<li class="divider"></li> <li class="divider"></li>
<li id="show_current_asset" class="btn-show-current-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-hand-o-up"></i> {% trans 'Display only current node assets' %}</a></li> <li id="show_current_asset" class="btn-show-current-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-hand-o-up"></i> {% trans 'Display only current node assets' %}</a></li>
<li id="show_all_asset" class="btn-show-all-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-th"></i> {% trans 'Displays all child node assets' %}</a></li> <li id="show_all_asset" class="btn-show-all-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-th"></i> {% trans 'Displays all child node assets' %}</a></li>
<li id="fresh_tree" class="btn-refresh-tree" tabindex="-1"><a><i class="fa fa-refresh"></i> {% trans 'Refresh' %}</a></li>
</ul> </ul>
</div> </div>
@ -147,6 +148,8 @@
<script> <script>
var zTree, rMenu, asset_table, show = 0; var zTree, rMenu, asset_table, show = 0;
var update_node_action = ""; var update_node_action = "";
var current_node_id = null;
var current_node = null;
function initTable() { function initTable() {
var options = { var options = {
ele: $('#asset_list_table'), ele: $('#asset_list_table'),
@ -191,18 +194,20 @@ function addTreeNode() {
if (!parentNode){ if (!parentNode){
return return
} }
var url = "{% url 'api-assets:node-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", parentNode.node_id ); var url = "{% url 'api-assets:node-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", parentNode.meta.node.id);
$.post(url, {}, function (data, status){ $.post(url, {}, function (data, status){
if (status === "success") { if (status === "success") {
var newNode = { var newNode = {
id: data["key"], id: data["key"],
name: data["value"], name: data["value"],
node_id: data["id"], pId: parentNode.id,
pId: parentNode.id meta: {
"node": data
}
}; };
newNode.checked = zTree.getSelectedNodes()[0].checked; newNode.checked = zTree.getSelectedNodes()[0].checked;
zTree.addNodes(parentNode, 0, newNode); zTree.addNodes(parentNode, 0, newNode);
var node = zTree.getNodeByParam('id', newNode.node_id, parentNode); var node = zTree.getNodeByParam('id', newNode.id, parentNode);
zTree.editName(node); zTree.editName(node);
} else { } else {
alert("{% trans 'Create node failed' %}") alert("{% trans 'Create node failed' %}")
@ -218,10 +223,10 @@ function removeTreeNode() {
} }
if (current_node.children && current_node.children.length > 0) { if (current_node.children && current_node.children.length > 0) {
toastr.error("{% trans 'Have child node, cancel' %}"); toastr.error("{% trans 'Have child node, cancel' %}");
} else if (current_node.assets_amount !== 0) { } else if (current_node.meta.node.assets_amount !== 0) {
toastr.error("{% trans 'Have assets, cancel' %}"); toastr.error("{% trans 'Have assets, cancel' %}");
} else { } else {
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.node_id ); var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
$.ajax({ $.ajax({
url: url, url: url,
method: "DELETE", method: "DELETE",
@ -238,8 +243,8 @@ function editTreeNode() {
if (!current_node){ if (!current_node){
return return
} }
if (current_node.value) { if (current_node) {
current_node.name = current_node.value; current_node.name = current_node.meta.node.value;
} }
zTree.editName(current_node); zTree.editName(current_node);
} }
@ -281,7 +286,7 @@ function onBodyMouseDown(event){
function onRename(event, treeId, treeNode, isCancel){ function onRename(event, treeId, treeNode, isCancel){
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", treeNode.node_id); var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
var data = {"value": treeNode.name}; var data = {"value": treeNode.name};
if (isCancel){ if (isCancel){
return return
@ -291,13 +296,20 @@ function onRename(event, treeId, treeNode, isCancel){
body: JSON.stringify(data), body: JSON.stringify(data),
method: "PATCH", method: "PATCH",
success_message: "{% trans 'Rename success' %}", success_message: "{% trans 'Rename success' %}",
fail_message: "{% trans 'Rename failed, do not change the root node name' %}" fail_message: "{% trans 'Rename failed, do not change the root node name' %}",
success: function () {
treeNode.name = treeNode.name + ' (' + treeNode.meta.node.assets_amount + ')'
zTree.updateNode(treeNode);
console.log("Success: " + treeNode.name)
}
}) })
} }
function onSelected(event, treeNode) { function onSelected(event, treeNode) {
current_node = treeNode;
current_node_id = treeNode.meta.node.id;
var url = asset_table.ajax.url(); var url = asset_table.ajax.url();
url = setUrlParam(url, "node_id", treeNode.node_id); url = setUrlParam(url, "node_id", current_node_id);
url = setUrlParam(url, "show_current_asset", getCookie('show_current_asset')); url = setUrlParam(url, "show_current_asset", getCookie('show_current_asset'));
setCookie('node_selected', treeNode.node_id); setCookie('node_selected', treeNode.node_id);
asset_table.ajax.url(url); asset_table.ajax.url(url);
@ -305,6 +317,9 @@ function onSelected(event, treeNode) {
} }
function selectQueryNode() { function selectQueryNode() {
// TODO: 是否应该添加
// 暂时忽略之前选中的内容
return
var query_node_id = $.getUrlParam("node"); var query_node_id = $.getUrlParam("node");
var cookie_node_id = getCookie('node_selected'); var cookie_node_id = getCookie('node_selected');
var node; var node;
@ -355,6 +370,14 @@ function onDrop(event, treeId, treeNodes, targetNode, moveType) {
} }
function initTree() { function initTree() {
if (zTree) {
return
}
var url = '{% url 'api-assets:node-children-tree' %}?assets=0&all=';
var showCurrentAsset = getCookie('show_current_asset');
if (!showCurrentAsset) {
url += '1'
}
var setting = { var setting = {
view: { view: {
dblClickExpand: false, dblClickExpand: false,
@ -365,6 +388,12 @@ function initTree() {
enable: true enable: true
} }
}, },
async: {
enable: true,
url: url,
autoParam: ["id=key", "name=n", "level=lv"],
type: 'get'
},
edit: { edit: {
enable: true, enable: true,
showRemoveBtn: false, showRemoveBtn: false,
@ -387,26 +416,8 @@ function initTree() {
}; };
var zNodes = []; var zNodes = [];
$.get("{% url 'api-assets:node-list' %}", function(data, status){ zTree = $.fn.zTree.init($("#assetTree"), setting, zNodes);
$.each(data, function (index, value) { rMenu = $("#rMenu");
value["node_id"] = value["id"];
value["id"] = value["tree_id"];
if (value["tree_id"] !== value["tree_parent"]){
value["pId"] = value["tree_parent"];
} else {
value["isParent"] = true;
}
value["name"] = value["value"] + ' (' + value['assets_amount'] + ')';
value['value'] = value['value'];
});
zNodes = data;
$.fn.zTree.init($("#assetTree"), setting, zNodes);
zTree = $.fn.zTree.getZTreeObj("assetTree");
var root = zTree.getNodes()[0];
zTree.expandNode(root);
rMenu = $("#rMenu");
selectQueryNode();
});
} }
function toggle() { function toggle() {
@ -443,20 +454,15 @@ $(document).ready(function(){
.on('click', '.btn_export', function () { .on('click', '.btn_export', function () {
var $data_table = $('#asset_list_table').DataTable(); var $data_table = $('#asset_list_table').DataTable();
var rows = $data_table.rows('.selected').data(); var rows = $data_table.rows('.selected').data();
var nodes = zTree.getSelectedNodes();
var current_node;
if (nodes && nodes.length === 1) {
current_node = nodes[0];
}
var assets = []; var assets = [];
$.each(rows, function (index, obj) { $.each(rows, function (index, obj) {
assets.push(obj.id) assets.push(obj.id)
}); });
var _node_id = current_node ? current_node.node_id : null;
$.ajax({ $.ajax({
url: "{% url "assets:asset-export" %}", url: "{% url "assets:asset-export" %}",
method: 'POST', method: 'POST',
data: JSON.stringify({assets_id: assets, node_id: _node_id}), data: JSON.stringify({assets_id: assets, node_id: current_node_id}),
dataType: "json", dataType: "json",
success: function (data, textStatus) { success: function (data, textStatus) {
window.open(data.redirect) window.open(data.redirect)
@ -469,12 +475,8 @@ $(document).ready(function(){
.on('click', '#btn_asset_import', function () { .on('click', '#btn_asset_import', function () {
var $form = $('#fm_asset_import'); var $form = $('#fm_asset_import');
var action = $form.attr("action"); var action = $form.attr("action");
var nodes = zTree.getSelectedNodes(); if (current_node_id){
var current_node; action = setUrlParam(action, 'node_id', current_node_id);
if (nodes && nodes.length ===1 ){
current_node = nodes[0];
action = setUrlParam(action, 'node_id', current_node.node_id);
{#action += "?node_id=" + current_node.node_id;#}
$form.attr("action", action) $form.attr("action", action)
} }
$form.find('.help-block').remove(); $form.find('.help-block').remove();
@ -496,25 +498,14 @@ $(document).ready(function(){
}) })
.on('click', '.btn-create-asset', function () { .on('click', '.btn-create-asset', function () {
var url = "{% url 'assets:asset-create' %}"; var url = "{% url 'assets:asset-create' %}";
var nodes = zTree.getSelectedNodes(); if (current_node_id) {
var current_node; url += "?node_id=" + current_node_id;
if (nodes && nodes.length ===1 ){
current_node = nodes[0];
url += "?node_id=" + current_node.node_id;
} }
window.open(url, '_self'); window.open(url, '_self');
}) })
.on('click', '.btn-refresh-hardware', function () { .on('click', '.btn-refresh-hardware', function () {
var url = "{% url 'api-assets:node-refresh-hardware-info' pk=DEFAULT_PK %}"; var url = "{% url 'api-assets:node-refresh-hardware-info' pk=DEFAULT_PK %}";
var nodes = zTree.getSelectedNodes(); var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
var current_node;
if (nodes && nodes.length ===1 ){
current_node = nodes[0];
} else {
return null;
}
var the_url = url.replace("{{ DEFAULT_PK }}", current_node.node_id);
function success(data) { function success(data) {
rMenu.css({"visibility" : "hidden"}); rMenu.css({"visibility" : "hidden"});
var task_id = data.task; var task_id = data.task;
@ -531,15 +522,10 @@ $(document).ready(function(){
}) })
.on('click', '.btn-test-connective', function () { .on('click', '.btn-test-connective', function () {
var url = "{% url 'api-assets:node-test-connective' pk=DEFAULT_PK %}"; var url = "{% url 'api-assets:node-test-connective' pk=DEFAULT_PK %}";
var nodes = zTree.getSelectedNodes(); if (!current_node_id) {
var current_node;
if (nodes && nodes.length ===1 ){
current_node = nodes[0];
} else {
return null; return null;
} }
var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
var the_url = url.replace("{{ DEFAULT_PK }}", current_node.node_id);
function success(data) { function success(data) {
rMenu.css({"visibility" : "hidden"}); rMenu.css({"visibility" : "hidden"});
var task_id = data.task; var task_id = data.task;
@ -567,6 +553,10 @@ $(document).ready(function(){
setCookie('show_current_asset', ''); setCookie('show_current_asset', '');
location.reload(); location.reload();
}) })
.on('click', '.btn-test-connective', function () {
hideRMenu();
})
.on('click', '.btn_asset_delete', function () { .on('click', '.btn_asset_delete', function () {
var $this = $(this); var $this = $(this);
var $data_table = $("#asset_list_table").DataTable(); var $data_table = $("#asset_list_table").DataTable();
@ -660,11 +650,8 @@ $(document).ready(function(){
} }
function doRemove() { function doRemove() {
var current_node;
var nodes = zTree.getSelectedNodes(); var nodes = zTree.getSelectedNodes();
if (nodes && nodes.length === 1) { if (!current_node_id) {
current_node = nodes[0]
} else {
return return
} }
@ -677,7 +664,7 @@ $(document).ready(function(){
}; };
APIUpdateAttr({ APIUpdateAttr({
'url': '/api/assets/v1/nodes/' + current_node.node_id + '/assets/remove/', 'url': '/api/assets/v1/nodes/' + current_node_id + '/assets/remove/',
'method': 'PUT', 'method': 'PUT',
'body': JSON.stringify(data), 'body': JSON.stringify(data),
'success': success 'success': success
@ -706,11 +693,7 @@ $(document).ready(function(){
}) })
.on('click', '#btn_asset_modal_confirm', function () { .on('click', '#btn_asset_modal_confirm', function () {
var assets_selected = asset_table2.selected; var assets_selected = asset_table2.selected;
var current_node; if (!current_node_id) {
var nodes = zTree.getSelectedNodes();
if (nodes && nodes.length === 1) {
current_node = nodes[0]
} else {
return return
} }
@ -722,9 +705,9 @@ $(document).ready(function(){
var url = ''; var url = '';
if (update_node_action === "move") { if (update_node_action === "move") {
url = "{% url 'api-assets:node-replace-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.node_id); url = "{% url 'api-assets:node-replace-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
} else { } else {
url = "{% url 'api-assets:node-add-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.node_id); url = "{% url 'api-assets:node-add-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
} }
APIUpdateAttr({ APIUpdateAttr({

View File

@ -32,6 +32,7 @@ $(document).ready(function () {
}).on('click', '.select2-selection__rendered', function (e) { }).on('click', '.select2-selection__rendered', function (e) {
e.preventDefault(); e.preventDefault();
$("#asset_list_modal").modal(); $("#asset_list_modal").modal();
initSelectedAssets2Table();
}) })
.on('click', '#btn_asset_modal_confirm', function () { .on('click', '#btn_asset_modal_confirm', function () {
var assets = asset_table2.selected; var assets = asset_table2.selected;

View File

@ -136,7 +136,7 @@
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
function initAssetsTable() { function initAssetsTable() {
var unreachable = {{ system_user.unreachable_assets|safe }}; var connectivity = {{ system_user.connectivity | safe }};
var options = { var options = {
ele: $('#system_user_list'), ele: $('#system_user_list'),
buttons: [], buttons: [],
@ -147,11 +147,13 @@ function initAssetsTable() {
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id)); $(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}}, }},
{targets: 3, createdCell: function (td, cellData) { {targets: 3, createdCell: function (td, cellData) {
if (unreachable.indexOf(cellData) >= 0) { if (connectivity.unreachable.indexOf(cellData) >= 0) {
$(td).html('<i class="fa fa-times text-danger"></i>') $(td).html('<i class="fa fa-times text-danger"></i>')
} else { } else if (connectivity.reachable.indexOf(cellData) >= 0 ) {
$(td).html('<i class="fa fa-check text-navy"></i>') $(td).html('<i class="fa fa-check text-navy"></i>')
} } else {
$(td).html('')
}
}}, }},
{targets: 4, createdCell: function (td, cellData) { {targets: 4, createdCell: function (td, cellData) {
var push_btn = ''; var push_btn = '';

View File

@ -95,7 +95,7 @@ function initTable() {
}}], }}],
ajax_url: '{% url "api-assets:system-user-list" %}', ajax_url: '{% url "api-assets:system-user-list" %}',
columns: [ columns: [
{data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "get_login_mode_display"}, {data: "assets_amount" }, {data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "login_mode_display"}, {data: "assets_amount" },
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" } {data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" }
], ],
op_html: $('#actions').html() op_html: $('#actions').html()

View File

@ -24,35 +24,39 @@ cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-r
urlpatterns = [ urlpatterns = [
path('assets-bulk/', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'), path('assets-bulk/', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'),
path('system-user/<uuid:pk>/auth-info/',
api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
path('system-user/<uuid:pk>/assets/',
api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
path('assets/<uuid:pk>/refresh/', path('assets/<uuid:pk>/refresh/',
api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'), api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'),
path('assets/<uuid:pk>/alive/', path('assets/<uuid:pk>/alive/',
api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'), api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'),
path('assets/<uuid:pk>/gateway/', path('assets/<uuid:pk>/gateway/',
api.AssetGatewayApi.as_view(), name='asset-gateway'), api.AssetGatewayApi.as_view(), name='asset-gateway'),
path('admin-user/<uuid:pk>/nodes/', path('admin-user/<uuid:pk>/nodes/',
api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'), api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'),
path('admin-user/<uuid:pk>/auth/', path('admin-user/<uuid:pk>/auth/',
api.AdminUserAuthApi.as_view(), name='admin-user-auth'), api.AdminUserAuthApi.as_view(), name='admin-user-auth'),
path('admin-user/<uuid:pk>/connective/', path('admin-user/<uuid:pk>/connective/',
api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'), api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'),
path('admin-user/<uuid:pk>/assets/',
api.AdminUserAssetsListView.as_view(), name='admin-user-assets'),
path('system-user/<uuid:pk>/auth-info/',
api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
path('system-user/<uuid:pk>/assets/',
api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
path('system-user/<uuid:pk>/push/', path('system-user/<uuid:pk>/push/',
api.SystemUserPushApi.as_view(), name='system-user-push'), api.SystemUserPushApi.as_view(), name='system-user-push'),
path('system-user/<uuid:pk>/asset/<uuid:aid>/push/', path('system-user/<uuid:pk>/asset/<uuid:aid>/push/',
api.SystemUserPushToAssetApi.as_view(), name='system-user-push-to-asset'), api.SystemUserPushToAssetApi.as_view(), name='system-user-push-to-asset'),
path('system-user/<uuid:pk>/asset/<uuid:aid>/test/', path('system-user/<uuid:pk>/asset/<uuid:aid>/test/',
api.SystemUserTestAssetConnectabilityApi.as_view(), name='system-user-test-to-asset'), api.SystemUserTestAssetConnectivityApi.as_view(), name='system-user-test-to-asset'),
path('system-user/<uuid:pk>/connective/', path('system-user/<uuid:pk>/connective/',
api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'), api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'),
path('system-user/<uuid:pk>/cmd-filter-rules/', path('system-user/<uuid:pk>/cmd-filter-rules/',
api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'), api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'),
path('nodes/tree/', api.NodeListAsTreeApi.as_view(), name='node-tree'),
path('nodes/children/tree/', api.NodeChildrenAsTreeApi.as_view(), name='node-children-tree'),
path('nodes/<uuid:pk>/children/', path('nodes/<uuid:pk>/children/',
api.NodeChildrenApi.as_view(), name='node-children'), api.NodeChildrenApi.as_view(), name='node-children'),
path('nodes/children/', api.NodeChildrenApi.as_view(), name='node-children-2'), path('nodes/children/', api.NodeChildrenApi.as_view(), name='node-children-2'),

View File

@ -102,7 +102,7 @@ class AdminUserAssetsView(AdminUserRequiredMixin, SingleObjectMixin, ListView):
'app': _('Assets'), 'app': _('Assets'),
'action': _('Admin user detail'), 'action': _('Admin user detail'),
"total_amount": len(self.queryset), "total_amount": len(self.queryset),
'unreachable_amount': len([asset for asset in self.queryset if asset.is_connective is False]) 'unreachable_amount': len([asset for asset in self.queryset if asset.connectivity is False])
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)

View File

@ -216,7 +216,6 @@ class AssetExportView(LoginRequiredMixin, View):
return HttpResponse('Json object not valid', status=400) return HttpResponse('Json object not valid', status=400)
if not assets_id: if not assets_id:
print(node_id)
node = get_object_or_none(Node, id=node_id) if node_id else Node.root() node = get_object_or_none(Node, id=node_id) if node_id else Node.root()
assets = node.get_all_assets() assets = node.get_all_assets()
for asset in assets: for asset in assets:

View File

@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='ftplog', model_name='ftplog',
name='org_id', name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True), field=models.CharField(blank=True, db_index=True, default=None, max_length=36, null=True),
), ),
] ]

View File

@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='ftplog', model_name='ftplog',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
] ]

View File

@ -15,7 +15,7 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='OperateLog', name='OperateLog',
fields=[ fields=[
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')), ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('user', models.CharField(max_length=128, verbose_name='User')), ('user', models.CharField(max_length=128, verbose_name='User')),
('action', models.CharField(choices=[('create', 'Create'), ('update', 'Update'), ('delete', 'Delete')], max_length=16, verbose_name='Action')), ('action', models.CharField(choices=[('create', 'Create'), ('update', 'Update'), ('delete', 'Delete')], max_length=16, verbose_name='Action')),

View File

@ -15,8 +15,6 @@ class BaseForm(forms.Form):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
for name, field in self.fields.items(): for name, field in self.fields.items():
value = getattr(settings, name, None) value = getattr(settings, name, None)
# django_value = getattr(settings, name) if hasattr(settings, name) else None
if value is None: # and django_value is None: if value is None: # and django_value is None:
continue continue
@ -24,8 +22,6 @@ class BaseForm(forms.Form):
if isinstance(value, dict): if isinstance(value, dict):
value = json.dumps(value) value = json.dumps(value)
initial_value = value initial_value = value
# elif django_value is False or django_value:
# initial_value = django_value
else: else:
initial_value = '' initial_value = ''
field.initial = initial_value field.initial = initial_value
@ -134,6 +130,14 @@ class TerminalSettingForm(BaseForm):
('hostname', _('Hostname')), ('hostname', _('Hostname')),
('ip', _('IP')), ('ip', _('IP')),
) )
PAGE_SIZE_CHOICES = (
('all', _('All')),
('auto', _('Auto')),
(10, 10),
(15, 15),
(25, 25),
(50, 50),
)
TERMINAL_PASSWORD_AUTH = forms.BooleanField( TERMINAL_PASSWORD_AUTH = forms.BooleanField(
initial=True, required=False, label=_("Password auth") initial=True, required=False, label=_("Password auth")
) )
@ -146,6 +150,14 @@ class TerminalSettingForm(BaseForm):
TERMINAL_ASSET_LIST_SORT_BY = forms.ChoiceField( TERMINAL_ASSET_LIST_SORT_BY = forms.ChoiceField(
choices=SORT_BY_CHOICES, initial='hostname', label=_("List sort by") choices=SORT_BY_CHOICES, initial='hostname', label=_("List sort by")
) )
TERMINAL_ASSET_LIST_PAGE_SIZE = forms.ChoiceField(
choices=PAGE_SIZE_CHOICES, initial='auto', label=_("List page size"),
)
TERMINAL_SESSION_KEEP_DURATION = forms.IntegerField(
label=_("Session keep duration"),
help_text=_("Units: days, Session, record, command will be delete "
"if more than duration, only in database")
)
class TerminalCommandStorage(BaseForm): class TerminalCommandStorage(BaseForm):

View File

@ -45,6 +45,8 @@ class Setting(models.Model):
def cleaned_value(self): def cleaned_value(self):
try: try:
value = self.value value = self.value
if not isinstance(value, (str, bytes)):
return value
if self.encrypted: if self.encrypted:
value = signer.unsign(value) value = signer.unsign(value)
value = json.loads(value) value = json.loads(value)

View File

@ -26,21 +26,20 @@ def refresh_settings_on_changed(sender, instance=None, **kwargs):
def refresh_all_settings_on_django_ready(sender, **kwargs): def refresh_all_settings_on_django_ready(sender, **kwargs):
logger.debug("Receive django ready signal") logger.debug("Receive django ready signal")
logger.debug(" - fresh all settings") logger.debug(" - fresh all settings")
CACHE_KEY_PREFIX = '_SETTING_' cache_key_prefix = '_SETTING_'
def monkey_patch_getattr(self, name): def monkey_patch_getattr(self, name):
key = CACHE_KEY_PREFIX + name key = cache_key_prefix + name
cached = cache.get(key) cached = cache.get(key)
if cached is not None: if cached is not None:
return cached return cached
if self._wrapped is empty: if self._wrapped is empty:
self._setup(name) self._setup(name)
val = getattr(self._wrapped, name) val = getattr(self._wrapped, name)
# self.__dict__[name] = val # Never set it
return val return val
def monkey_patch_setattr(self, name, value): def monkey_patch_setattr(self, name, value):
key = CACHE_KEY_PREFIX + name key = cache_key_prefix + name
cache.set(key, value, None) cache.set(key, value, None)
if name == '_wrapped': if name == '_wrapped':
self.__dict__.clear() self.__dict__.clear()
@ -51,7 +50,7 @@ def refresh_all_settings_on_django_ready(sender, **kwargs):
def monkey_patch_delattr(self, name): def monkey_patch_delattr(self, name):
super(LazySettings, self).__delattr__(name) super(LazySettings, self).__delattr__(name)
self.__dict__.pop(name, None) self.__dict__.pop(name, None)
key = CACHE_KEY_PREFIX + name key = cache_key_prefix + name
cache.delete(key) cache.delete(key)
try: try:

View File

@ -47,16 +47,13 @@ class TreeNode:
def __gt__(self, other): def __gt__(self, other):
if self.isParent and not other.isParent: if self.isParent and not other.isParent:
return False return False
elif not self.isParent and other.isParent:
return True
return self.id > other.id return self.id > other.id
def __eq__(self, other): def __eq__(self, other):
return self.id == other.id return self.id == other.id
def __lt__(self, other):
if self.isParent and not other.isParent:
return True
return self.id < other.id
class Tree: class Tree:
def __init__(self): def __init__(self):

View File

@ -304,7 +304,7 @@ defaults = {
'REDIS_DB_CELERY': 3, 'REDIS_DB_CELERY': 3,
'REDIS_DB_CACHE': 4, 'REDIS_DB_CACHE': 4,
'CAPTCHA_TEST_MODE': None, 'CAPTCHA_TEST_MODE': None,
'TOKEN_EXPIRATION': 3600, 'TOKEN_EXPIRATION': 3600 * 24,
'DISPLAY_PER_PAGE': 25, 'DISPLAY_PER_PAGE': 25,
'DEFAULT_EXPIRED_YEARS': 70, 'DEFAULT_EXPIRED_YEARS': 70,
'SESSION_COOKIE_DOMAIN': None, 'SESSION_COOKIE_DOMAIN': None,
@ -312,7 +312,14 @@ defaults = {
'SESSION_COOKIE_AGE': 3600 * 24, 'SESSION_COOKIE_AGE': 3600 * 24,
'SESSION_EXPIRE_AT_BROWSER_CLOSE': False, 'SESSION_EXPIRE_AT_BROWSER_CLOSE': False,
'AUTH_OPENID': False, 'AUTH_OPENID': False,
'EMAIL_SUFFIX': 'jumpserver.org' 'OTP_ISSUER_NAME': 'Jumpserver',
'EMAIL_SUFFIX': 'jumpserver.org',
'TERMINAL_PASSWORD_AUTH': True,
'TERMINAL_PUBLIC_KEY_AUTH': True,
'TERMINAL_HEARTBEAT_INTERVAL': 5,
'TERMINAL_ASSET_LIST_SORT_BY': 'hostname',
'TERMINAL_ASSET_LIST_PAGE_SIZE': 'auto',
'TERMINAL_SESSION_KEEP_DURATION': 9999,
} }

View File

@ -356,6 +356,7 @@ FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755
# OTP settings # OTP settings
OTP_ISSUER_NAME = CONFIG.OTP_ISSUER_NAME OTP_ISSUER_NAME = CONFIG.OTP_ISSUER_NAME
OTP_VALID_WINDOW = CONFIG.OTP_VALID_WINDOW
# Auth LDAP settings # Auth LDAP settings
AUTH_LDAP = False AUTH_LDAP = False
@ -466,6 +467,7 @@ DEFAULT_TERMINAL_REPLAY_STORAGE = {
TERMINAL_REPLAY_STORAGE = { TERMINAL_REPLAY_STORAGE = {
} }
SECURITY_MFA_AUTH = False SECURITY_MFA_AUTH = False
SECURITY_LOGIN_LIMIT_COUNT = 7 SECURITY_LOGIN_LIMIT_COUNT = 7
SECURITY_LOGIN_LIMIT_TIME = 30 # Unit: minute SECURITY_LOGIN_LIMIT_TIME = 30 # Unit: minute
@ -484,6 +486,13 @@ SECURITY_PASSWORD_RULES = [
'SECURITY_PASSWORD_SPECIAL_CHAR' 'SECURITY_PASSWORD_SPECIAL_CHAR'
] ]
TERMINAL_PASSWORD_AUTH = CONFIG.TERMINAL_PASSWORD_AUTH
TERMINAL_PUBLIC_KEY_AUTH = CONFIG.TERMINAL_PUBLIC_KEY_AUTH
TERMINAL_HEARTBEAT_INTERVAL = CONFIG.TERMINAL_HEARTBEAT_INTERVAL
TERMINAL_ASSET_LIST_SORT_BY = CONFIG.TERMINAL_ASSET_LIST_SORT_BY
TERMINAL_ASSET_LIST_PAGE_SIZE = CONFIG.TERMINAL_ASSET_LIST_PAGE_SIZE
TERMINAL_SESSION_KEEP_DURATION = CONFIG.TERMINAL_SESSION_KEEP_DURATION
# Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html # Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html
BOOTSTRAP3 = { BOOTSTRAP3 = {
'horizontal_label_class': 'col-md-2', 'horizontal_label_class': 'col-md-2',

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -24,8 +24,10 @@ class TaskViewSet(viewsets.ModelViewSet):
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()
if current_org: if current_org.is_real():
queryset = queryset.filter(created_by=current_org.id) queryset = queryset.filter(created_by=current_org.id)
else:
queryset = queryset.filter(created_by='')
return queryset return queryset

View File

@ -53,7 +53,7 @@ class AdHocRunHistorySerializer(serializers.ModelSerializer):
@staticmethod @staticmethod
def get_stat(obj): def get_stat(obj):
return { return {
"total": len(obj.adhoc.hosts), "total": obj.adhoc.hosts.count(),
"success": len(obj.summary.get("contacted", [])), "success": len(obj.summary.get("contacted", [])),
"failed": len(obj.summary.get("dark", [])), "failed": len(obj.summary.get("dark", [])),
} }

View File

@ -11,6 +11,7 @@ app_name = "ops"
router = DefaultRouter() router = DefaultRouter()
router.register(r'tasks', api.TaskViewSet, 'task') router.register(r'tasks', api.TaskViewSet, 'task')
router.register(r'adhoc', api.AdHocViewSet, 'adhoc') router.register(r'adhoc', api.AdHocViewSet, 'adhoc')
router.register(r'history', api.AdHocRunHistoryViewSet, 'history')
router.register(r'command-executions', api.CommandExecutionViewSet, 'command-execution') router.register(r'command-executions', api.CommandExecutionViewSet, 'command-execution')
urlpatterns = [ urlpatterns = [

View File

@ -62,8 +62,11 @@ class TaskDetailView(AdminUserRequiredMixin, DetailView):
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()
if current_org: # Todo: 需要整理默认组织等东西
if current_org.is_real():
queryset = queryset.filter(created_by=current_org.id) queryset = queryset.filter(created_by=current_org.id)
else:
queryset = queryset.filter(created_by='')
return queryset return queryset
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):

View File

@ -74,7 +74,7 @@ class OrgManager(models.Manager):
class OrgModelMixin(models.Model): class OrgModelMixin(models.Model):
org_id = models.CharField(max_length=36, blank=True, default='', verbose_name=_("Organization")) org_id = models.CharField(max_length=36, blank=True, default='', verbose_name=_("Organization"), db_index=True)
objects = OrgManager() objects = OrgManager()
sep = '@' sep = '@'

View File

@ -16,7 +16,7 @@ from orgs.utils import set_to_root_org
from .utils import AssetPermissionUtil from .utils import AssetPermissionUtil
from .models import AssetPermission from .models import AssetPermission
from .hands import AssetGrantedSerializer, User, UserGroup, Asset, Node, \ from .hands import AssetGrantedSerializer, User, UserGroup, Asset, Node, \
NodeGrantedSerializer, SystemUser, NodeSerializer SystemUser, NodeSerializer
from . import serializers from . import serializers
from .mixins import AssetsFilterMixin from .mixins import AssetsFilterMixin
@ -150,7 +150,7 @@ class UserGrantedNodesWithAssetsApi(AssetsFilterMixin, ListAPIView):
用户授权的节点并带着节点下资产的api 用户授权的节点并带着节点下资产的api
""" """
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = NodeGrantedSerializer serializer_class = serializers.NodeGrantedSerializer
def change_org_if_need(self): def change_org_if_need(self):
if self.request.user.is_superuser or \ if self.request.user.is_superuser or \
@ -228,15 +228,22 @@ class UserGrantedNodesWithAssetsAsTreeApi(ListAPIView):
@staticmethod @staticmethod
def parse_asset_to_tree_node(node, asset, system_users): def parse_asset_to_tree_node(node, asset, system_users):
system_users_protocol_matched = [s for s in system_users if s.protocol == asset.protocol] system_users_protocol_matched = [s for s in system_users if s.protocol == asset.protocol]
system_user_serializer = serializers.GrantedSystemUserSerializer(
system_users_protocol_matched, many=True
)
asset_serializer = serializers.GrantedAssetSerializer(asset)
icon_skin = 'file' icon_skin = 'file'
if asset.platform.lower() == 'windows': if asset.platform.lower() == 'windows':
icon_skin = 'windows' icon_skin = 'windows'
elif asset.platform.lower() == 'linux': elif asset.platform.lower() == 'linux':
icon_skin = 'linux' icon_skin = 'linux'
system_users = []
for system_user in system_users_protocol_matched:
system_users.append({
'id': system_user.id,
'name': system_user.name,
'username': system_user.username,
'protocol': system_user.protocol,
'priority': system_user.priority,
'login_mode': system_user.login_mode,
'comment': system_user.comment,
})
data = { data = {
'id': str(asset.id), 'id': str(asset.id),
'name': asset.hostname, 'name': asset.hostname,
@ -246,9 +253,19 @@ class UserGrantedNodesWithAssetsAsTreeApi(ListAPIView):
'open': False, 'open': False,
'iconSkin': icon_skin, 'iconSkin': icon_skin,
'meta': { 'meta': {
'system_users': system_user_serializer.data, 'system_users': system_users,
'type': 'asset', 'type': 'asset',
'asset': asset_serializer.data 'asset': {
'id': asset.id,
'hostname': asset.hostname,
'ip': asset.ip,
'port': asset.port,
'protocol': asset.protocol,
'platform': asset.platform,
'domain': None if not asset.domain else asset.domain.id,
'is_active': asset.is_active,
'comment': asset.comment
},
} }
} }
tree_node = TreeNode(**data) tree_node = TreeNode(**data)
@ -360,7 +377,7 @@ class UserGroupGrantedNodesApi(ListAPIView):
class UserGroupGrantedNodesWithAssetsApi(ListAPIView): class UserGroupGrantedNodesWithAssetsApi(ListAPIView):
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
serializer_class = NodeGrantedSerializer serializer_class = serializers.NodeGrantedSerializer
def get_queryset(self): def get_queryset(self):
user_group_id = self.kwargs.get('pk', '') user_group_id = self.kwargs.get('pk', '')

View File

@ -4,7 +4,7 @@
from common.permissions import AdminUserRequiredMixin from common.permissions import AdminUserRequiredMixin
from users.models import User, UserGroup from users.models import User, UserGroup
from assets.models import Asset, SystemUser, Node from assets.models import Asset, SystemUser, Node
from assets.serializers import AssetGrantedSerializer, NodeGrantedSerializer, NodeSerializer from assets.serializers import AssetGrantedSerializer, NodeSerializer

View File

@ -13,11 +13,11 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='assetpermission', model_name='assetpermission',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='nodepermission', model_name='nodepermission',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
] ]

View File

@ -83,6 +83,35 @@ class AssetPermissionNodeSerializer(serializers.ModelSerializer):
return obj.parent_key return obj.parent_key
class NodeGrantedSerializer(serializers.ModelSerializer):
"""
授权资产组
"""
assets_granted = AssetGrantedSerializer(many=True, read_only=True)
assets_amount = serializers.SerializerMethodField()
parent = serializers.SerializerMethodField()
name = serializers.SerializerMethodField()
class Meta:
model = Node
fields = [
'id', 'key', 'name', 'value', 'parent',
'assets_granted', 'assets_amount', 'org_id',
]
@staticmethod
def get_assets_amount(obj):
return len(obj.assets_granted)
@staticmethod
def get_name(obj):
return obj.name
@staticmethod
def get_parent(obj):
return obj.parent.id
class GrantedNodeSerializer(serializers.ModelSerializer): class GrantedNodeSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Node model = Node

View File

@ -113,6 +113,7 @@ $(document).ready(function () {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
$("#asset_list_modal").modal(); $("#asset_list_modal").modal();
initSelectedAssets2Table();
} }
}) })
}) })

View File

@ -78,58 +78,24 @@ var zTree, table, show = 0;
function onSelected(event, treeNode) { function onSelected(event, treeNode) {
setCookie('node_selected', treeNode.id); setCookie('node_selected', treeNode.id);
var url = table.ajax.url(); var url = table.ajax.url();
if (treeNode.is_node) { if (treeNode.meta.type === 'node') {
url = setUrlParam(url, 'asset', ""); url = setUrlParam(url, 'asset', "");
url = setUrlParam(url, 'node', treeNode.node_id) url = setUrlParam(url, 'node', treeNode.meta.node.id)
} else { } else {
url = setUrlParam(url, 'node', ""); url = setUrlParam(url, 'node', "");
url = setUrlParam(url, 'asset', treeNode.node_id) url = setUrlParam(url, 'asset', treeNode.meta.asset.id)
} }
setCookie('node_selected', treeNode.node_id); setCookie('node_selected', treeNode.node_id);
table.ajax.url(url); table.ajax.url(url);
table.ajax.reload(); table.ajax.reload();
} }
function selectQueryNode() {
var query_node_id = $.getUrlParam("node");
var cookie_node_id = getCookie('node_selected');
var node;
var node_id;
if (query_node_id !== null) {
node_id = query_node_id
} else if (cookie_node_id !== null) {
node_id = cookie_node_id;
}
node = zTree.getNodesByParam("id", node_id, null);
if (node){
zTree.selectNode(node[0]);
node.open = true;
}
}
function filter(treeId, parentNode, childNodes) {
$.each(childNodes, function (index, value) {
value["node_id"] = value["id"];
value["id"] = value["tree_id"];
if (value["tree_id"] !== value["tree_parent"]) {
value["pId"] = value["tree_parent"];
} else {
value["isParent"] = true;
}
value['name'] = value['value'];
value["iconSkin"] = value["is_node"] ? null : 'file';
{#value["pId"] = value["parent"];#}
{#value["name"] = value["value"];#}
value["isParent"] = value["is_node"];
});
return childNodes;
}
function beforeAsync(treeId, treeNode) { function beforeAsync(treeId, treeNode) {
return treeNode.is_node if (treeNode) {
return treeNode.meta.type === 'node'
}
return true
} }
function makeLabel(data) { function makeLabel(data) {
@ -235,9 +201,8 @@ function initTree() {
}, },
async: { async: {
enable: true, enable: true,
url: "{% url 'api-assets:node-children-2' %}?assets=1&all=", url: "{% url 'api-assets:node-children-tree' %}?assets=1",
autoParam:["node_id=id", "name=n", "level=lv"], autoParam:["id=key", "name=n", "level=lv"],
dataFilter: filter,
type: 'get' type: 'get'
}, },
callback: { callback: {
@ -245,25 +210,7 @@ function initTree() {
beforeAsync: beforeAsync beforeAsync: beforeAsync
} }
}; };
zTree = $.fn.zTree.init($("#assetTree"), setting);
var zNodes = [];
$.get("{% url 'api-assets:node-children-2' %}?assets=1", function(data, status){
$.each(data, function (index, value) {
value["node_id"] = value["id"];
value["id"] = value["tree_id"];
if (value["tree_id"] !== value["tree_parent"]) {
value["pId"] = value["tree_parent"];
}
value["isParent"] = value["is_node"];
value['name'] = value['value'];
value["iconSkin"] = value["is_node"] ? null : 'file';
});
zNodes = data;
$.fn.zTree.init($("#assetTree"), setting, zNodes);
zTree = $.fn.zTree.getZTreeObj("assetTree");
var root = zTree.getNodes()[0];
zTree.expandNode(root);
});
} }
function toggle() { function toggle() {
@ -299,10 +246,10 @@ $(document).ready(function(){
var _nodes = []; var _nodes = [];
var _assets = []; var _assets = [];
$.each(nodes, function (id, node) { $.each(nodes, function (id, node) {
if (node.is_node) { if (node.meta.type === 'node') {
_nodes.push(node.node_id) _nodes.push(node.meta.node.id)
} else { } else {
_assets.push(node.node_id) _assets.push(node.meta.asset.id)
} }
}); });
url += "?assets=" + _assets.join(",") + "&nodes=" + _nodes.join(","); url += "?assets=" + _assets.join(",") + "&nodes=" + _nodes.join(",");

View File

@ -812,3 +812,32 @@ function initPopover($container, $progress, $idPassword, $el, password_check_rul
$idPassword.pwstrength(options); $idPassword.pwstrength(options);
popoverPasswordRules(password_check_rules, $el); popoverPasswordRules(password_check_rules, $el);
} }
// 解决input框中的资产和弹出表格中资产的显示不一致
function initSelectedAssets2Table(){
var inputAssets = $('#id_assets').val();
var selectedAssets = asset_table2.selected.concat();
// input assets无table assets选中则取消勾选(再次click)
if (selectedAssets.length !== 0){
$.each(selectedAssets, function (index, assetId){
if ($.inArray(assetId, inputAssets) === -1){
$('#'+assetId).trigger('click'); // 取消勾选
}
});
}
// input assets有table assets没选则选中(click)
if (inputAssets !== null){
asset_table2.selected = inputAssets;
$.each(inputAssets, function(index, assetId){
var dom = document.getElementById(assetId);
if (dom !== null){
var selected = dom.parentElement.parentElement.className.indexOf('selected')
}
if (selected === -1){
$('#'+assetId).trigger('click');
}
});
}
}

View File

@ -94,44 +94,15 @@ class SessionReplayViewSet(viewsets.ViewSet):
serializer_class = serializers.ReplaySerializer serializer_class = serializers.ReplaySerializer
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
session = None session = None
upload_to = 'replay' # 仅添加到本地存储中
def get_session_path(self, version=2):
"""
获取session日志的文件路径
:param version: 原来后缀是 .gz为了统一新版本改为 .replay.gz
:return:
"""
suffix = '.replay.gz'
if version == 1:
suffix = '.gz'
date = self.session.date_start.strftime('%Y-%m-%d')
return os.path.join(date, str(self.session.id) + suffix)
def get_local_path(self, version=2):
session_path = self.get_session_path(version=version)
if version == 2:
local_path = os.path.join(self.upload_to, session_path)
else:
local_path = session_path
return local_path
def save_to_storage(self, f):
local_path = self.get_local_path()
try:
name = default_storage.save(local_path, f)
return name, None
except OSError as e:
return None, e
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
session_id = kwargs.get('pk') session_id = kwargs.get('pk')
self.session = get_object_or_404(Session, id=session_id) session = get_object_or_404(Session, id=session_id)
serializer = self.serializer_class(data=request.data) serializer = self.serializer_class(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
file = serializer.validated_data['file'] file = serializer.validated_data['file']
name, err = self.save_to_storage(file) name, err = session.save_to_storage(file)
if not name: if not name:
msg = "Failed save replay `{}`: {}".format(session_id, err) msg = "Failed save replay `{}`: {}".format(session_id, err)
logger.error(msg) logger.error(msg)
@ -145,7 +116,7 @@ class SessionReplayViewSet(viewsets.ViewSet):
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
session_id = kwargs.get('pk') session_id = kwargs.get('pk')
self.session = get_object_or_404(Session, id=session_id) session = get_object_or_404(Session, id=session_id)
data = { data = {
'type': 'guacamole' if self.session.protocol == 'rdp' else 'json', 'type': 'guacamole' if self.session.protocol == 'rdp' else 'json',
@ -153,9 +124,9 @@ class SessionReplayViewSet(viewsets.ViewSet):
} }
# 新版本和老版本的文件后缀不同 # 新版本和老版本的文件后缀不同
session_path = self.get_session_path() # 存在外部存储上的路径 session_path = session.get_rel_replay_path() # 存在外部存储上的路径
local_path = self.get_local_path() local_path = session.get_local_path()
local_path_v1 = self.get_local_path(version=1) local_path_v1 = session.get_local_path(version=1)
# 去default storage中查找 # 去default storage中查找
for _local_path in (local_path, local_path_v1, session_path): for _local_path in (local_path, local_path_v1, session_path):

View File

@ -13,11 +13,11 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='command', model_name='command',
name='org_id', name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
migrations.AddField( migrations.AddField(
model_name='session', model_name='session',
name='org_id', name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
] ]

View File

@ -10,14 +10,4 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.AlterField(
model_name='command',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='session',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
] ]

View File

@ -1,11 +1,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import os
import uuid import uuid
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone from django.utils import timezone
from django.conf import settings from django.conf import settings
from django.core.files.storage import default_storage
from users.models import User from users.models import User
from orgs.mixins import OrgModelMixin from orgs.mixins import OrgModelMixin
@ -148,6 +150,36 @@ class Session(OrgModelMixin):
date_start = models.DateTimeField(verbose_name=_("Date start"), db_index=True, default=timezone.now) date_start = models.DateTimeField(verbose_name=_("Date start"), db_index=True, default=timezone.now)
date_end = models.DateTimeField(verbose_name=_("Date end"), null=True) date_end = models.DateTimeField(verbose_name=_("Date end"), null=True)
upload_to = 'replay'
def get_rel_replay_path(self, version=2):
"""
获取session日志的文件路径
:param version: 原来后缀是 .gz为了统一新版本改为 .replay.gz
:return:
"""
suffix = '.replay.gz'
if version == 1:
suffix = '.gz'
date = self.date_start.strftime('%Y-%m-%d')
return os.path.join(date, str(self.id) + suffix)
def get_local_path(self, version=2):
rel_path = self.get_rel_replay_path(version=version)
if version == 2:
local_path = os.path.join(self.upload_to, rel_path)
else:
local_path = rel_path
return local_path
def save_to_storage(self, f):
local_path = self.get_local_path()
try:
name = default_storage.save(local_path, f)
return name, None
except OSError as e:
return None, e
class Meta: class Meta:
db_table = "terminal_session" db_table = "terminal_session"
ordering = ["-date_start"] ordering = ["-date_start"]

View File

@ -4,15 +4,20 @@
import datetime import datetime
from celery import shared_task from celery import shared_task
from celery.utils.log import get_task_logger
from django.utils import timezone from django.utils import timezone
from django.conf import settings
from django.core.files.storage import default_storage
from ops.celery.utils import register_as_period_task, after_app_ready_start, \ from ops.celery.utils import register_as_period_task, after_app_ready_start, \
after_app_shutdown_clean after_app_shutdown_clean
from .models import Status, Session from .models import Status, Session, Command
CACHE_REFRESH_INTERVAL = 10 CACHE_REFRESH_INTERVAL = 10
RUNNING = False RUNNING = False
logger = get_task_logger(__name__)
@shared_task @shared_task
@ -34,3 +39,28 @@ def clean_orphan_session():
if not session.terminal or not session.terminal.is_active: if not session.terminal or not session.terminal.is_active:
session.is_finished = True session.is_finished = True
session.save() session.save()
@shared_task
@register_as_period_task(interval=3600*24)
@after_app_ready_start
@after_app_shutdown_clean
def clean_expired_session_period():
logger.info("Start clean expired session record, commands and replay")
days = settings.TERMINAL_SESSION_KEEP_DURATION
dt = timezone.now() - timezone.timedelta(days=days)
expired_sessions = Session.objects.filter(date_start__lt=dt)
for session in expired_sessions:
logger.info("Clean session: {}".format(session.id))
Command.objects.filter(session=str(session.id)).delete()
# 删除录像文件
session_path = session.get_rel_replay_path()
local_path = session.get_local_path()
local_path_v1 = session.get_local_path(version=1)
# 去default storage中查找
for _local_path in (local_path, local_path_v1, session_path):
if default_storage.exists(_local_path):
default_storage.delete(_local_path)
# 删除session记录
session.delete()

View File

@ -1,21 +1,23 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.core.cache import cache from django.core.cache import cache
from .models import Session
from assets.models import Asset, SystemUser
from users.models import User
from .const import USERS_CACHE_KEY, ASSETS_CACHE_KEY, SYSTEM_USER_CACHE_KEY from .const import USERS_CACHE_KEY, ASSETS_CACHE_KEY, SYSTEM_USER_CACHE_KEY
def get_session_asset_list(): def get_session_asset_list():
return set(list(Session.objects.values_list('asset', flat=True))) return Asset.objects.values_list('hostname', flat=True)
def get_session_user_list(): def get_session_user_list():
return set(list(Session.objects.values_list('user', flat=True))) return User.objects.values_list('username', flat=True)
def get_session_system_user_list(): def get_session_system_user_list():
return set(list(Session.objects.values_list('system_user', flat=True))) return SystemUser.objects.values_list('username', flat=True)
def get_user_list_from_cache(): def get_user_list_from_cache():

View File

@ -6,7 +6,7 @@ from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination from rest_framework.pagination import LimitOffsetPagination
from ..serializers import UserGroupSerializer, \ from ..serializers import UserGroupSerializer, \
UserGroupUpdateMemeberSerializer UserGroupUpdateMemberSerializer
from ..models import UserGroup from ..models import UserGroup
from common.permissions import IsOrgAdmin from common.permissions import IsOrgAdmin
from common.mixins import IDInFilterMixin from common.mixins import IDInFilterMixin
@ -26,5 +26,5 @@ class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet):
class UserGroupUpdateUserApi(generics.RetrieveUpdateAPIView): class UserGroupUpdateUserApi(generics.RetrieveUpdateAPIView):
queryset = UserGroup.objects.all() queryset = UserGroup.objects.all()
serializer_class = UserGroupUpdateMemeberSerializer serializer_class = UserGroupUpdateMemberSerializer
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)

View File

@ -46,6 +46,9 @@ class UserViewSet(IDInFilterMixin, BulkModelViewSet):
self.permission_classes = (IsOrgAdminOrAppUser,) self.permission_classes = (IsOrgAdminOrAppUser,)
return super().get_permissions() return super().get_permissions()
def allow_bulk_destroy(self, qs, filtered):
return qs.count() == filtered.count()
class UserChangePasswordApi(generics.RetrieveUpdateAPIView): class UserChangePasswordApi(generics.RetrieveUpdateAPIView):
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)

View File

@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='usergroup', model_name='usergroup',
name='org_id', name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'), field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
), ),
] ]

View File

@ -142,6 +142,18 @@ class User(AbstractUser):
return True return True
return False return False
@property
def groups_display(self):
return ' '.join(self.groups.all().values_list('name', flat=True))
@property
def role_display(self):
return self.get_role_display()
@property
def source_display(self):
return self.get_source_display()
@property @property
def is_expired(self): def is_expired(self):
if self.date_expired and self.date_expired < timezone.now(): if self.date_expired and self.date_expired < timezone.now():

View File

@ -13,31 +13,18 @@ signer = get_signer()
class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
groups_display = serializers.SerializerMethodField()
groups = serializers.PrimaryKeyRelatedField(
many=True, queryset=UserGroup.objects.all(), required=False
)
class Meta: class Meta:
model = User model = User
list_serializer_class = BulkListSerializer list_serializer_class = BulkListSerializer
exclude = [ fields = [
'first_name', 'last_name', 'password', '_private_key', 'id', 'name', 'username', 'email', 'groups', 'groups_display',
'_public_key', '_otp_secret_key', 'user_permissions' 'role', 'role_display', 'avatar_url', 'wechat', 'phone',
'otp_level', 'comment', 'source', 'source_display',
'is_valid', 'is_expired', 'is_active',
'created_by', 'is_first_login',
'date_password_last_updated', 'date_expired',
] ]
# validators = []
def get_field_names(self, declared_fields, info):
fields = super(UserSerializer, self).get_field_names(declared_fields, info)
fields.extend([
'groups_display', 'get_role_display',
'get_source_display', 'is_valid'
])
return fields
@staticmethod
def get_groups_display(obj):
return " ".join([group.name for group in obj.groups.all()])
class UserPKUpdateSerializer(serializers.ModelSerializer): class UserPKUpdateSerializer(serializers.ModelSerializer):
@ -74,7 +61,7 @@ class UserGroupSerializer(BulkSerializerMixin, serializers.ModelSerializer):
return [user.name for user in obj.users.all()] return [user.name for user in obj.users.all()]
class UserGroupUpdateMemeberSerializer(serializers.ModelSerializer): class UserGroupUpdateMemberSerializer(serializers.ModelSerializer):
users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all()) users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all())
class Meta: class Meta:

View File

@ -25,7 +25,7 @@
<th class="text-center">{% trans 'Role' %}</th> <th class="text-center">{% trans 'Role' %}</th>
<th class="text-center">{% trans 'User group' %}</th> <th class="text-center">{% trans 'User group' %}</th>
<th class="text-center">{% trans 'Source' %}</th> <th class="text-center">{% trans 'Source' %}</th>
<th class="text-center">{% trans 'Active' %}</th> <th class="text-center">{% trans 'Validity' %}</th>
<th class="text-center">{% trans 'Action' %}</th> <th class="text-center">{% trans 'Action' %}</th>
</tr> </tr>
</thead> </thead>
@ -66,11 +66,14 @@ function initTable() {
var innerHtml = cellData.length > 20 ? cellData.substring(0, 20) + '...': cellData; var innerHtml = cellData.length > 20 ? cellData.substring(0, 20) + '...': cellData;
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>'); $(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
}}, }},
{targets: 6, createdCell: function (td, cellData) { {targets: 6, createdCell: function (td, cellData, rowData) {
if (!cellData) { if (cellData) {
$(td).html('<i class="fa fa-times text-danger"></i>')
} else {
$(td).html('<i class="fa fa-check text-navy"></i>') $(td).html('<i class="fa fa-check text-navy"></i>')
} else if (!rowData.is_active) {
$(td).html('<i class="fa fa-times text-danger inactive"></i>')
} else if (rowData.is_expired) {
$(td).html('<i class="fa fa-times text-danger expired"></i>')
} }
}}, }},
{targets: 7, createdCell: function (td, cellData, rowData) { {targets: 7, createdCell: function (td, cellData, rowData) {
@ -91,9 +94,9 @@ function initTable() {
ajax_url: '{% url "api-users:user-list" %}', ajax_url: '{% url "api-users:user-list" %}',
columns: [ columns: [
{data: "id"}, {data: "name" }, {data: "username" }, {data: "id"}, {data: "name" }, {data: "username" },
{data: "get_role_display", orderable: false}, {data: "role_display", orderable: false},
{data: "groups_display", orderable: false}, {data: "groups_display", orderable: false},
{data: "get_source_display", orderable: false}, {data: "source_display", orderable: false},
{data: "is_valid", orderable: false}, {data: "is_valid", orderable: false},
{data: "id", orderable: false} {data: "id", orderable: false}
], ],
@ -246,6 +249,13 @@ $(document).ready(function(){
var uid = $this.data('uid'); var uid = $this.data('uid');
var the_url = '{% url "api-users:user-detail" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", uid); var the_url = '{% url "api-users:user-detail" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", uid);
objectDelete($this, name, the_url); objectDelete($this, name, the_url);
}).on('click', '.expired', function () {
var msg = '{% trans "User is expired" %}';
toastr.error(msg)
}).on('click', '.inactive', function () {
var msg = '{% trans 'User is inactive' %}';
toastr.error(msg)
}) })
</script> </script>
{% endblock %} {% endblock %}

View File

@ -292,7 +292,8 @@ def check_otp_code(otp_secret_key, otp_code):
if not otp_secret_key or not otp_code: if not otp_secret_key or not otp_code:
return False return False
totp = pyotp.TOTP(otp_secret_key) totp = pyotp.TOTP(otp_secret_key)
return totp.verify(otp_code) otp_valid_window = settings.OTP_VALID_WINDOW or 0
return totp.verify(otp=otp_code, valid_window=otp_valid_window)
def get_password_check_rules(): def get_password_check_rules():

View File

@ -100,6 +100,9 @@ class Config:
} }
AUTH_LDAP_START_TLS = False AUTH_LDAP_START_TLS = False
#
# OTP_VALID_WINDOW = 0
def __init__(self): def __init__(self):
pass pass
@ -200,6 +203,10 @@ class DockerConfig(Config):
AUTH_LDAP_START_TLS = False AUTH_LDAP_START_TLS = False
#
OTP_VALID_WINDOW = int(os.environ.get("OTP_VALID_WINDOW")) if os.environ.get("OTP_VALID_WINDOW") else 0
# Default using Config settings, you can write if/else for different env # Default using Config settings, you can write if/else for different env
config = DockerConfig() config = DockerConfig()

View File

@ -90,6 +90,9 @@ class Config:
# AUTH_OPENID_CLIENT_ID = 'client-id' # AUTH_OPENID_CLIENT_ID = 'client-id'
# AUTH_OPENID_CLIENT_SECRET = 'client-secret' # AUTH_OPENID_CLIENT_SECRET = 'client-secret'
#
# OTP_VALID_WINDOW = 0
def __init__(self): def __init__(self):
pass pass

4
jms
View File

@ -26,8 +26,8 @@ LOG_DIR = os.path.join(BASE_DIR, 'logs')
TMP_DIR = os.path.join(BASE_DIR, 'tmp') TMP_DIR = os.path.join(BASE_DIR, 'tmp')
HTTP_HOST = CONFIG.HTTP_BIND_HOST or '127.0.0.1' HTTP_HOST = CONFIG.HTTP_BIND_HOST or '127.0.0.1'
HTTP_PORT = CONFIG.HTTP_LISTEN_PORT or 8080 HTTP_PORT = CONFIG.HTTP_LISTEN_PORT or 8080
DEBUG = CONFIG.DEBUG DEBUG = CONFIG.DEBUG or False
LOG_LEVEL = CONFIG.LOG_LEVEL LOG_LEVEL = CONFIG.LOG_LEVEL or 'INFO'
START_TIMEOUT = 40 START_TIMEOUT = 40
WORKERS = 4 WORKERS = 4