Unverified Commit a5b9b4e1 authored by 老广's avatar 老广 Committed by GitHub

Merge pull request #2206 from jumpserver/dev

Dev
parents ac7e3e7f b3079a4a
......@@ -14,6 +14,7 @@
# limitations under the License.
from django.db import transaction
from django.shortcuts import get_object_or_404
from rest_framework import generics
from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
......@@ -24,13 +25,14 @@ from common.utils import get_logger
from ..hands import IsOrgAdmin
from ..models import AdminUser, Asset
from .. import serializers
from ..tasks import test_admin_user_connectability_manual
from ..tasks import test_admin_user_connectivity_manual
logger = get_logger(__file__)
__all__ = [
'AdminUserViewSet', 'ReplaceNodesAdminUserApi',
'AdminUserTestConnectiveApi', 'AdminUserAuthApi',
'AdminUserAssetsListView',
]
......@@ -81,12 +83,29 @@ class ReplaceNodesAdminUserApi(generics.UpdateAPIView):
class AdminUserTestConnectiveApi(generics.RetrieveAPIView):
"""
Test asset admin user connectivity
Test asset admin user assets_connectivity
"""
queryset = AdminUser.objects.all()
permission_classes = (IsOrgAdmin,)
def retrieve(self, request, *args, **kwargs):
admin_user = self.get_object()
task = test_admin_user_connectability_manual.delay(admin_user)
task = test_admin_user_connectivity_manual.delay(admin_user)
return Response({"task": task.id})
class AdminUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSimpleSerializer
pagination_class = LimitOffsetPagination
filter_fields = ("hostname", "ip")
http_method_names = ['get']
search_fields = filter_fields
def get_object(self):
pk = self.kwargs.get('pk')
return get_object_or_404(AdminUser, pk=pk)
def get_queryset(self):
admin_user = self.get_object()
return admin_user.get_related_assets()
......@@ -17,7 +17,7 @@ from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from ..models import Asset, AdminUser, Node
from .. import serializers
from ..tasks import update_asset_hardware_info_manual, \
test_asset_connectability_manual
test_asset_connectivity_manual
from ..utils import LabelFilter
......@@ -41,40 +41,46 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet):
pagination_class = LimitOffsetPagination
permission_classes = (IsOrgAdminOrAppUser,)
def filter_node(self):
def filter_node(self, queryset):
node_id = self.request.query_params.get("node_id")
if not node_id:
return
return queryset
node = get_object_or_404(Node, id=node_id)
show_current_asset = self.request.query_params.get("show_current_asset") in ('1', 'true')
if node.is_root():
if show_current_asset:
self.queryset = self.queryset.filter(
Q(nodes=node_id) | Q(nodes__isnull=True)
)
return
if show_current_asset:
self.queryset = self.queryset.filter(nodes=node)
if node.is_root() and show_current_asset:
queryset = queryset.filter(
Q(nodes=node_id) | Q(nodes__isnull=True)
)
elif node.is_root() and not show_current_asset:
pass
elif not node.is_root() and show_current_asset:
queryset = queryset.filter(nodes=node)
else:
self.queryset = self.queryset.filter(
queryset = queryset.filter(
nodes__key__regex='^{}(:[0-9]+)*$'.format(node.key),
)
return queryset
def filter_admin_user_id(self):
def filter_admin_user_id(self, queryset):
admin_user_id = self.request.query_params.get('admin_user_id')
if admin_user_id:
admin_user = get_object_or_404(AdminUser, id=admin_user_id)
self.queryset = self.queryset.filter(admin_user=admin_user)
if not admin_user_id:
return queryset
admin_user = get_object_or_404(AdminUser, id=admin_user_id)
queryset = queryset.filter(admin_user=admin_user)
return queryset
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
queryset = self.filter_node(queryset)
queryset = self.filter_admin_user_id(queryset)
return queryset
def get_queryset(self):
self.queryset = super().get_queryset()\
.prefetch_related('labels', 'nodes')\
.select_related('admin_user')
self.filter_admin_user_id()
self.filter_node()
return self.queryset.distinct()
queryset = super().get_queryset().distinct()
queryset = self.get_serializer_class().setup_eager_loading(queryset)
return queryset
class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView):
......@@ -103,7 +109,7 @@ class AssetRefreshHardwareApi(generics.RetrieveAPIView):
class AssetAdminUserTestApi(generics.RetrieveAPIView):
"""
Test asset admin user connectivity
Test asset admin user assets_connectivity
"""
queryset = Asset.objects.all()
permission_classes = (IsOrgAdmin,)
......@@ -111,7 +117,7 @@ class AssetAdminUserTestApi(generics.RetrieveAPIView):
def retrieve(self, request, *args, **kwargs):
asset_id = kwargs.get('pk')
asset = get_object_or_404(Asset, pk=asset_id)
task = test_asset_connectability_manual.delay(asset)
task = test_asset_connectivity_manual.delay(asset)
return Response({"task": task.id})
......
......@@ -17,15 +17,14 @@ from rest_framework import generics, mixins, viewsets
from rest_framework.serializers import ValidationError
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
from django.utils.translation import ugettext_lazy as _
from django.shortcuts import get_object_or_404
from django.db.models import Count
from common.utils import get_logger, get_object_or_none
from common.tree import TreeNodeSerializer
from ..hands import IsOrgAdmin
from ..models import Node
from ..tasks import update_assets_hardware_info_util, test_asset_connectability_util
from ..tasks import update_assets_hardware_info_util, test_asset_connectivity_util
from .. import serializers
......@@ -34,7 +33,8 @@ __all__ = [
'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi',
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi',
'NodeAddChildrenApi', 'RefreshNodeHardwareInfoApi',
'TestNodeConnectiveApi'
'TestNodeConnectiveApi', 'NodeListAsTreeApi',
'NodeChildrenAsTreeApi',
]
......@@ -43,22 +43,89 @@ class NodeViewSet(viewsets.ModelViewSet):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeSerializer
def perform_create(self, serializer):
child_key = Node.root().get_next_child_key()
serializer.validated_data["key"] = child_key
serializer.save()
def update(self, request, *args, **kwargs):
node = self.get_object()
if node.is_root():
node_value = node.value
post_value = request.data.get('value')
if node_value != post_value:
return Response(
{"msg": _("You can't update the root node name")},
status=400
)
return super().update(request, *args, **kwargs)
class NodeListAsTreeApi(generics.ListAPIView):
"""
获取节点列表树
[
{
"id": "",
"name": "",
"pId": "",
"meta": ""
}
]
"""
permission_classes = (IsOrgAdmin,)
serializer_class = TreeNodeSerializer
def get_queryset(self):
queryset = [node.as_tree_node() for node in Node.objects.all()]
return queryset
def filter_queryset(self, queryset):
if self.request.query_params.get('refresh', '0') == '1':
queryset = self.refresh_nodes(queryset)
return queryset
@staticmethod
def refresh_nodes(queryset):
Node.expire_nodes_assets_amount()
Node.expire_nodes_full_value()
return queryset
class NodeChildrenAsTreeApi(generics.ListAPIView):
"""
节点子节点作为树返回,
[
{
"id": "",
"name": "",
"pId": "",
"meta": ""
}
]
"""
permission_classes = (IsOrgAdmin,)
serializer_class = TreeNodeSerializer
node = None
is_root = False
def get_queryset(self):
node_key = self.request.query_params.get('key')
if node_key:
self.node = Node.objects.get(key=node_key)
queryset = self.node.get_children(with_self=False)
else:
self.is_root = True
self.node = Node.root()
queryset = list(self.node.get_children(with_self=True))
nodes_invalid = Node.objects.exclude(key__startswith=self.node.key)
queryset.extend(list(nodes_invalid))
queryset = [node.as_tree_node() for node in queryset]
return queryset
def filter_assets(self, queryset):
include_assets = self.request.query_params.get('assets', '0') == '1'
if not include_assets:
return queryset
assets = self.node.get_assets()
for asset in assets:
queryset.append(asset.as_tree_node(self.node))
return queryset
def filter_queryset(self, queryset):
queryset = self.filter_assets(queryset)
queryset = self.filter_refresh_nodes(queryset)
return queryset
def filter_refresh_nodes(self, queryset):
if self.request.query_params.get('refresh', '0') == '1':
Node.expire_nodes_assets_amount()
Node.expire_nodes_full_value()
return queryset
class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
......@@ -67,19 +134,10 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
serializer_class = serializers.NodeSerializer
instance = None
def counter(self):
values = [
child.value[child.value.rfind(' '):]
for child in self.get_object().get_children()
if child.value.startswith("新节点 ")
]
values = [int(value) for value in values if value.strip().isdigit()]
count = max(values)+1 if values else 1
return count
def post(self, request, *args, **kwargs):
instance = self.get_object()
if not request.data.get("value"):
request.data["value"] = _("New node {}").format(self.counter())
request.data["value"] = instance.get_next_child_preset_name()
return super().post(request, *args, **kwargs)
def create(self, request, *args, **kwargs):
......@@ -91,10 +149,7 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
'The same level node name cannot be the same'
)
node = instance.create_child(value=value)
return Response(
{"id": node.id, "key": node.key, "value": node.value},
status=201,
)
return Response(self.serializer_class(instance=node).data, status=201)
def get_object(self):
pk = self.kwargs.get('pk') or self.request.query_params.get('id')
......@@ -107,7 +162,6 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
def get_queryset(self):
queryset = []
query_all = self.request.query_params.get("all")
query_assets = self.request.query_params.get('assets')
node = self.get_object()
if node is None:
......@@ -120,23 +174,8 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
else:
children = node.get_children()
queryset.extend(list(children))
if query_assets:
assets = node.get_assets()
for asset in assets:
node_fake = Node()
node_fake.assets__count = 0
node_fake.id = asset.id
node_fake.is_node = False
node_fake.key = node.key + ':0'
node_fake.value = asset.hostname
queryset.append(node_fake)
queryset = sorted(queryset, key=lambda x: x.is_node, reverse=True)
return queryset
def get(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
class NodeAssetsApi(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
......@@ -234,5 +273,5 @@ class TestNodeConnectiveApi(APIView):
assets = node.assets.all()
# task_name = _("测试节点下资产是否可连接: {}".format(node.name))
task_name = _("Test if the assets under the node are connectable: {}".format(node.name))
task = test_asset_connectability_util.delay(assets, task_name=task_name)
task = test_asset_connectivity_util.delay(assets, task_name=task_name)
return Response({"task": task.id})
......@@ -24,8 +24,8 @@ from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from ..models import SystemUser, Asset
from .. import serializers
from ..tasks import push_system_user_to_assets_manual, \
test_system_user_connectability_manual, push_system_user_a_asset_manual, \
test_system_user_connectability_a_asset
test_system_user_connectivity_manual, push_system_user_a_asset_manual, \
test_system_user_connectivity_a_asset
logger = get_logger(__file__)
......@@ -33,7 +33,7 @@ __all__ = [
'SystemUserViewSet', 'SystemUserAuthInfoApi',
'SystemUserPushApi', 'SystemUserTestConnectiveApi',
'SystemUserAssetsListView', 'SystemUserPushToAssetApi',
'SystemUserTestAssetConnectabilityApi', 'SystemUserCommandFilterRuleListApi',
'SystemUserTestAssetConnectivityApi', 'SystemUserCommandFilterRuleListApi',
]
......@@ -93,15 +93,16 @@ class SystemUserTestConnectiveApi(generics.RetrieveAPIView):
def retrieve(self, request, *args, **kwargs):
system_user = self.get_object()
task = test_system_user_connectability_manual.delay(system_user)
task = test_system_user_connectivity_manual.delay(system_user)
return Response({"task": task.id})
class SystemUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSerializer
serializer_class = serializers.AssetSimpleSerializer
pagination_class = LimitOffsetPagination
filter_fields = ("hostname", "ip")
http_method_names = ['get']
search_fields = filter_fields
def get_object(self):
......@@ -125,7 +126,7 @@ class SystemUserPushToAssetApi(generics.RetrieveAPIView):
return Response({"task": task.id})
class SystemUserTestAssetConnectabilityApi(generics.RetrieveAPIView):
class SystemUserTestAssetConnectivityApi(generics.RetrieveAPIView):
queryset = SystemUser.objects.all()
permission_classes = (IsOrgAdmin,)
......@@ -133,7 +134,7 @@ class SystemUserTestAssetConnectabilityApi(generics.RetrieveAPIView):
system_user = self.get_object()
asset_id = self.kwargs.get('aid')
asset = get_object_or_404(Asset, id=asset_id)
task = test_system_user_connectability_a_asset.delay(system_user, asset)
task = test_system_user_connectivity_a_asset.delay(system_user, asset)
return Response({"task": task.id})
......
......@@ -99,8 +99,8 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
auto_generate_key = self.cleaned_data.get('auto_generate_key', False)
private_key, public_key = super().gen_keys()
if login_mode == SystemUser.MANUAL_LOGIN or \
protocol in [SystemUser.RDP_PROTOCOL, SystemUser.TELNET_PROTOCOL]:
if login_mode == SystemUser.LOGIN_MANUAL or \
protocol in [SystemUser.PROTOCOL_RDP, SystemUser.PROTOCOL_TELNET]:
system_user.auto_push = 0
auto_generate_key = False
system_user.save()
......@@ -124,7 +124,7 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
validated = super().is_valid()
username = self.cleaned_data.get('username')
login_mode = self.cleaned_data.get('login_mode')
if login_mode == SystemUser.AUTO_LOGIN and not username:
if login_mode == SystemUser.LOGIN_AUTO and not username:
self.add_error(
"username", _('* Automatic login mode,'
' must fill in the username.')
......
......@@ -13,36 +13,36 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='adminuser',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='asset',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='domain',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='gateway',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='label',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='node',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='systemuser',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
]
......@@ -16,7 +16,7 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='CommandFilter',
fields=[
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')),
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=64, verbose_name='Name')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
......@@ -32,7 +32,7 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='CommandFilterRule',
fields=[
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')),
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('type', models.CharField(choices=[('regex', 'Regex'), ('command', 'Command')], default='command', max_length=16, verbose_name='Type')),
('priority', models.IntegerField(default=50, help_text='1-100, the lower will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')),
......
......@@ -13,7 +13,6 @@ from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from django.core.cache import cache
from ..const import ASSET_ADMIN_CONN_CACHE_KEY
from .user import AdminUser, SystemUser
from orgs.mixins import OrgModelMixin, OrgManager
......@@ -75,63 +74,48 @@ class Asset(OrgModelMixin):
protocol = models.CharField(max_length=128, default=SSH_PROTOCOL, choices=PROTOCOL_CHOICES, verbose_name=_('Protocol'))
port = models.IntegerField(default=22, verbose_name=_('Port'))
platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform'))
domain = models.ForeignKey("assets.Domain", null=True, blank=True,
related_name='assets', verbose_name=_("Domain"),
on_delete=models.SET_NULL)
nodes = models.ManyToManyField('assets.Node', default=default_node,
related_name='assets',
verbose_name=_("Nodes"))
domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), on_delete=models.SET_NULL)
nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes"))
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
# Auth
admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT,
null=True, verbose_name=_("Admin user"))
admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, null=True, verbose_name=_("Admin user"))
# Some information
public_ip = models.GenericIPAddressField(max_length=32, blank=True, null=True, verbose_name=_('Public IP'))
number = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Asset number'))
# Collect
vendor = models.CharField(max_length=64, null=True, blank=True,
verbose_name=_('Vendor'))
model = models.CharField(max_length=54, null=True, blank=True,
verbose_name=_('Model'))
sn = models.CharField(max_length=128, null=True, blank=True,
verbose_name=_('Serial number'))
cpu_model = models.CharField(max_length=64, null=True, blank=True,
verbose_name=_('CPU model'))
vendor = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Vendor'))
model = models.CharField(max_length=54, null=True, blank=True, verbose_name=_('Model'))
sn = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Serial number'))
cpu_model = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('CPU model'))
cpu_count = models.IntegerField(null=True, verbose_name=_('CPU count'))
cpu_cores = models.IntegerField(null=True, verbose_name=_('CPU cores'))
cpu_vcpus = models.IntegerField(null=True, verbose_name=_('CPU vcpus'))
memory = models.CharField(max_length=64, null=True, blank=True,
verbose_name=_('Memory'))
disk_total = models.CharField(max_length=1024, null=True, blank=True,
verbose_name=_('Disk total'))
disk_info = models.CharField(max_length=1024, null=True, blank=True,
verbose_name=_('Disk info'))
os = models.CharField(max_length=128, null=True, blank=True,
verbose_name=_('OS'))
os_version = models.CharField(max_length=16, null=True, blank=True,
verbose_name=_('OS version'))
os_arch = models.CharField(max_length=16, blank=True, null=True,
verbose_name=_('OS arch'))
hostname_raw = models.CharField(max_length=128, blank=True, null=True,
verbose_name=_('Hostname raw'))
labels = models.ManyToManyField('assets.Label', blank=True,
related_name='assets',
verbose_name=_("Labels"))
created_by = models.CharField(max_length=32, null=True, blank=True,
verbose_name=_('Created by'))
date_created = models.DateTimeField(auto_now_add=True, null=True,
blank=True,
verbose_name=_('Date created'))
comment = models.TextField(max_length=128, default='', blank=True,
verbose_name=_('Comment'))
memory = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Memory'))
disk_total = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk total'))
disk_info = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk info'))
os = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('OS'))
os_version = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('OS version'))
os_arch = models.CharField(max_length=16, blank=True, null=True, verbose_name=_('OS arch'))
hostname_raw = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Hostname raw'))
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created'))
comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment'))
objects = OrgManager.from_queryset(AssetQuerySet)()
CONNECTIVITY_CACHE_KEY = '_JMS_ASSET_CONNECTIVITY_{}'
UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3)
CONNECTIVITY_CHOICES = (
(UNREACHABLE, _("Unreachable")),
(REACHABLE, _('Reachable')),
(UNKNOWN, _("Unknown")),
)
def __str__(self):
return '{0.hostname}({0.ip})'.format(self)
......@@ -197,25 +181,17 @@ class Asset(OrgModelMixin):
return ''
@property
def is_connective(self):
def connectivity(self):
if not self.is_unixlike():
return True
val = cache.get(ASSET_ADMIN_CONN_CACHE_KEY.format(self.hostname))
if val == 1:
return True
else:
return False
return self.UNKNOWN
key = self.CONNECTIVITY_CACHE_KEY.format(str(self.id))
cached = cache.get(key, None)
return cached if cached is not None else self.UNKNOWN
def to_json(self):
info = {
'id': self.id,
'hostname': self.hostname,
'ip': self.ip,
'port': self.port,
}
if self.domain and self.domain.gateway_set.all():
info["gateways"] = [d.id for d in self.domain.gateway_set.all()]
return info
@connectivity.setter
def connectivity(self, value):
key = self.CONNECTIVITY_CACHE_KEY.format(str(self.id))
cache.set(key, value, 3600*2)
def get_auth_info(self):
if self.admin_user:
......@@ -236,11 +212,20 @@ class Asset(OrgModelMixin):
fake_node.is_node = False
return fake_node
def to_json(self):
info = {
'id': self.id,
'hostname': self.hostname,
'ip': self.ip,
'port': self.port,
}
if self.domain and self.domain.gateway_set.all():
info["gateways"] = [d.id for d in self.domain.gateway_set.all()]
return info
def _to_secret_json(self):
"""
Ansible use it create inventory, First using asset user,
otherwise using cluster admin user
Ansible use it create inventory
Todo: May be move to ops implements it
"""
data = self.to_json()
......@@ -255,6 +240,36 @@ class Asset(OrgModelMixin):
})
return data
def as_tree_node(self, parent_node):
from common.tree import TreeNode
icon_skin = 'file'
if self.platform.lower() == 'windows':
icon_skin = 'windows'
elif self.platform.lower() == 'linux':
icon_skin = 'linux'
data = {
'id': str(self.id),
'name': self.hostname,
'title': self.ip,
'pId': parent_node.key,
'isParent': False,
'open': False,
'iconSkin': icon_skin,
'meta': {
'type': 'asset',
'asset': {
'id': self.id,
'hostname': self.hostname,
'ip': self.ip,
'port': self.port,
'platform': self.platform,
'protocol': self.protocol,
}
}
}
tree_node = TreeNode(**data)
return tree_node
class Meta:
unique_together = [('org_id', 'hostname')]
verbose_name = _("Asset")
......
......@@ -29,6 +29,13 @@ class AssetUser(OrgModelMixin):
date_updated = models.DateTimeField(auto_now=True)
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3)
CONNECTIVITY_CHOICES = (
(UNREACHABLE, _("Unreachable")),
(REACHABLE, _('Reachable')),
(UNKNOWN, _("Unknown")),
)
@property
def password(self):
if self._password:
......
......@@ -5,6 +5,7 @@ import uuid
from django.db import models, transaction
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.core.cache import cache
from orgs.mixins import OrgModelMixin
......@@ -22,7 +23,9 @@ class Node(OrgModelMixin):
date_create = models.DateTimeField(auto_now_add=True)
is_node = True
_full_value_cache_key_prefix = '_NODE_VALUE_{}'
_assets_amount = None
_full_value_cache_key = '_NODE_VALUE_{}'
_assets_amount_cache_key = '_NODE_ASSETS_AMOUNT_{}'
class Meta:
verbose_name = _("Node")
......@@ -49,30 +52,65 @@ class Node(OrgModelMixin):
def name(self):
return self.value
@property
def assets_amount(self):
"""
获取节点下所有资产数量速度太慢,所以需要重写,使用cache等方案
:return:
"""
if self._assets_amount is not None:
return self._assets_amount
cache_key = self._assets_amount_cache_key.format(self.key)
cached = cache.get(cache_key)
if cached is not None:
return cached
assets_amount = self.get_all_assets().count()
cache.set(cache_key, assets_amount, 3600)
return assets_amount
@assets_amount.setter
def assets_amount(self, value):
self._assets_amount = value
def expire_assets_amount(self):
ancestor_keys = self.get_ancestor_keys(with_self=True)
cache_keys = [self._assets_amount_cache_key.format(k) for k in ancestor_keys]
cache.delete_many(cache_keys)
@classmethod
def expire_nodes_assets_amount(cls, nodes=None):
if nodes:
for node in nodes:
node.expire_assets_amount()
return
key = cls._assets_amount_cache_key.format('*')
cache.delete_pattern(key)
@property
def full_value(self):
key = self._full_value_cache_key_prefix.format(self.key)
key = self._full_value_cache_key.format(self.key)
cached = cache.get(key)
if cached:
return cached
value = self.get_full_value()
self.cache_full_value(value)
return value
def get_full_value(self):
# ancestor = [a.value for a in self.get_ancestor(with_self=True)]
if self.is_root():
return self.value
parent_full_value = self.parent.full_value
value = parent_full_value + ' / ' + self.value
return value
def cache_full_value(self, value):
key = self._full_value_cache_key_prefix.format(self.key)
key = self._full_value_cache_key.format(self.key)
cache.set(key, value, 3600)
return value
def expire_full_value(self):
key = self._full_value_cache_key_prefix.format(self.key)
key = self._full_value_cache_key.format(self.key)
cache.delete_pattern(key+'*')
@classmethod
def expire_nodes_full_value(cls, nodes=None):
if nodes:
for node in nodes:
node.expire_full_value()
return
key = cls._full_value_cache_key.format('*')
cache.delete_pattern(key+'*')
@property
......@@ -85,6 +123,17 @@ class Node(OrgModelMixin):
self.save()
return "{}:{}".format(self.key, mark)
def get_next_child_preset_name(self):
name = ugettext("New node")
values = [
child.value[child.value.rfind(' '):]
for child in self.get_children()
if child.value.startswith(name)
]
values = [int(value) for value in values if value.strip().isdigit()]
count = max(values) + 1 if values else 1
return '{} {}'.format(name, count)
def create_child(self, value):
with transaction.atomic():
child_key = self.get_next_child_key()
......@@ -134,7 +183,7 @@ class Node(OrgModelMixin):
pattern = r'^{0}$|^{0}:'.format(self.key)
args = []
kwargs = {}
if self.is_default_node():
if self.is_root():
args.append(Q(nodes__key__regex=pattern) | Q(nodes=None))
else:
kwargs['nodes__key__regex'] = pattern
......@@ -182,17 +231,18 @@ class Node(OrgModelMixin):
child.save()
self.save()
def get_ancestor(self, with_self=False):
if self.is_root():
root = self.__class__.root()
return [root]
_key = self.key.split(':')
def get_ancestor_keys(self, with_self=False):
parent_keys = []
key_list = self.key.split(":")
if not with_self:
_key.pop()
ancestor_keys = []
for i in range(len(_key)):
ancestor_keys.append(':'.join(_key))
_key.pop()
key_list.pop()
for i in range(len(key_list)):
parent_keys.append(":".join(key_list))
key_list.pop()
return parent_keys
def get_ancestor(self, with_self=False):
ancestor_keys = self.get_ancestor_keys(with_self=with_self)
ancestor = self.__class__.objects.filter(
key__in=ancestor_keys
).order_by('key')
......@@ -227,9 +277,25 @@ class Node(OrgModelMixin):
defaults = {'value': 'Default'}
return cls.objects.get_or_create(defaults=defaults, key='1')
@classmethod
def get_tree_name_ref(cls):
pass
def as_tree_node(self):
from common.tree import TreeNode
from ..serializers import NodeSerializer
name = '{} ({})'.format(self.value, self.assets_amount)
node_serializer = NodeSerializer(instance=self)
data = {
'id': self.key,
'name': name,
'title': name,
'pId': self.parent_key,
'isParent': True,
'open': self.is_root(),
'meta': {
'node': node_serializer.data,
'type': 'node'
}
}
tree_node = TreeNode(**data)
return tree_node
@classmethod
def generate_fake(cls, count=100):
......
......@@ -14,7 +14,7 @@ from ..const import SYSTEM_USER_CONN_CACHE_KEY
from .base import AssetUser
__all__ = ['AdminUser', 'SystemUser',]
__all__ = ['AdminUser', 'SystemUser']
logger = logging.getLogger(__name__)
signer = get_signer()
......@@ -31,6 +31,7 @@ class AdminUser(AssetUser):
become_method = models.CharField(choices=BECOME_METHOD_CHOICES, default='sudo', max_length=4)
become_user = models.CharField(default='root', max_length=64)
_become_pass = models.CharField(default='', max_length=128)
CONNECTIVE_CACHE_KEY = '_JMS_ADMIN_USER_CONNECTIVE_{}'
def __str__(self):
return self.name
......@@ -67,6 +68,23 @@ class AdminUser(AssetUser):
def assets_amount(self):
return self.get_related_assets().count()
@property
def connectivity(self):
from .asset import Asset
assets = self.get_related_assets().values_list('id', 'hostname', flat=True)
data = {
'unreachable': [],
'reachable': [],
}
for asset_id, hostname in assets:
key = Asset.CONNECTIVITY_CACHE_KEY.format(str(self.id))
value = cache.get(key, Asset.UNKNOWN)
if value == Asset.REACHABLE:
data['reachable'].append(hostname)
elif value == Asset.UNREACHABLE:
data['unreachable'].append(hostname)
return data
class Meta:
ordering = ['name']
unique_together = [('name', 'org_id')]
......@@ -94,34 +112,34 @@ class AdminUser(AssetUser):
class SystemUser(AssetUser):
SSH_PROTOCOL = 'ssh'
RDP_PROTOCOL = 'rdp'
TELNET_PROTOCOL = 'telnet'
PROTOCOL_SSH = 'ssh'
PROTOCOL_RDP = 'rdp'
PROTOCOL_TELNET = 'telnet'
PROTOCOL_CHOICES = (
(SSH_PROTOCOL, 'ssh'),
(RDP_PROTOCOL, 'rdp'),
(TELNET_PROTOCOL, 'telnet (beta)'),
(PROTOCOL_SSH, 'ssh'),
(PROTOCOL_RDP, 'rdp'),
(PROTOCOL_TELNET, 'telnet (beta)'),
)
AUTO_LOGIN = 'auto'
MANUAL_LOGIN = 'manual'
LOGIN_AUTO = 'auto'
LOGIN_MANUAL = 'manual'
LOGIN_MODE_CHOICES = (
(AUTO_LOGIN, _('Automatic login')),
(MANUAL_LOGIN, _('Manually login'))
(LOGIN_AUTO, _('Automatic login')),
(LOGIN_MANUAL, _('Manually login'))
)
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
priority = models.IntegerField(default=20, verbose_name=_("Priority"),
validators=[MinValueValidator(1), MaxValueValidator(100)])
priority = models.IntegerField(default=20, verbose_name=_("Priority"), validators=[MinValueValidator(1), MaxValueValidator(100)])
protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol'))
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=AUTO_LOGIN, max_length=10, verbose_name=_('Login mode'))
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode'))
cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True)
cache_key = "__SYSTEM_USER_CACHED_{}"
SYSTEM_USER_CACHE_KEY = "__SYSTEM_USER_CACHED_{}"
CONNECTIVE_CACHE_KEY = '_JMS_SYSTEM_USER_CONNECTIVE_{}'
def __str__(self):
return '{0.name}({0.username})'.format(self)
......@@ -136,34 +154,61 @@ class SystemUser(AssetUser):
'auto_push': self.auto_push,
}
def get_assets(self):
def get_related_assets(self):
assets = set(self.assets.all())
return assets
@property
def assets_connective(self):
_result = cache.get(SYSTEM_USER_CONN_CACHE_KEY.format(self.name), {})
return _result
def connectivity(self):
cache_key = self.CONNECTIVE_CACHE_KEY.format(str(self.id))
value = cache.get(cache_key, None)
if not value or 'unreachable' not in value:
return {'unreachable': [], 'reachable': []}
else:
return value
@connectivity.setter
def connectivity(self, value):
data = self.connectivity
unreachable = data['unreachable']
reachable = data['reachable']
for host in value.get('dark', {}).keys():
if host not in unreachable:
unreachable.append(host)
if host in reachable:
reachable.remove(host)
for host in value.get('contacted'):
if host not in reachable:
reachable.append(host)
if host in unreachable:
unreachable.remove(host)
cache_key = self.CONNECTIVE_CACHE_KEY.format(str(self.id))
cache.set(cache_key, data, 3600)
@property
def assets_unreachable(self):
return self.connectivity.get('unreachable')
@property
def unreachable_assets(self):
return list(self.assets_connective.get('dark', {}).keys())
def assets_reachable(self):
return self.connectivity.get('reachable')
@property
def reachable_assets(self):
return self.assets_connective.get('contacted', [])
def login_mode_display(self):
return self.get_login_mode_display()
def is_need_push(self):
if self.auto_push and self.protocol == self.__class__.SSH_PROTOCOL:
if self.auto_push and self.protocol == self.PROTOCOL_SSH:
return True
else:
return False
def set_cache(self):
cache.set(self.cache_key.format(self.id), self, 3600)
cache.set(self.SYSTEM_USER_CACHE_KEY.format(self.id), self, 3600)
def expire_cache(self):
cache.delete(self.cache_key.format(self.id))
cache.delete(self.SYSTEM_USER_CACHE_KEY.format(self.id))
@property
def cmd_filter_rules(self):
......@@ -184,7 +229,7 @@ class SystemUser(AssetUser):
@classmethod
def get_system_user_by_id_or_cached(cls, sid):
cached = cache.get(cls.cache_key.format(sid))
cached = cache.get(cls.SYSTEM_USER_CACHE_KEY.format(sid))
if cached:
return cached
try:
......
......@@ -9,6 +9,7 @@ from .system_user import AssetSystemUserSerializer
__all__ = [
'AssetSerializer', 'AssetGrantedSerializer', 'MyAssetGrantedSerializer',
'AssetAsNodeSerializer', 'AssetSimpleSerializer',
]
......@@ -22,14 +23,27 @@ class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer):
fields = '__all__'
validators = []
@classmethod
def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """
queryset = queryset.prefetch_related('labels', 'nodes')\
.select_related('admin_user')
return queryset
def get_field_names(self, declared_fields, info):
fields = super().get_field_names(declared_fields, info)
fields.extend([
'hardware_info', 'is_connective', 'org_name'
'hardware_info', 'connectivity', 'org_name'
])
return fields
class AssetAsNodeSerializer(serializers.ModelSerializer):
class Meta:
model = Asset
fields = ['id', 'hostname', 'ip', 'port', 'platform', 'protocol']
class AssetGrantedSerializer(serializers.ModelSerializer):
"""
被授权资产的数据结构
......@@ -64,3 +78,9 @@ class MyAssetGrantedSerializer(AssetGrantedSerializer):
"is_active", "system_users_join", "org_name",
"os", "platform", "comment", "org_id", "protocol"
)
class AssetSimpleSerializer(serializers.ModelSerializer):
class Meta:
model = Asset
fields = ['id', 'hostname', 'port', 'ip', 'connectivity']
......@@ -8,84 +8,33 @@ from .asset import AssetGrantedSerializer
__all__ = [
'NodeSerializer', "NodeGrantedSerializer", "NodeAddChildrenSerializer",
'NodeSerializer', "NodeAddChildrenSerializer",
"NodeAssetsSerializer",
]
class NodeGrantedSerializer(BulkSerializerMixin, serializers.ModelSerializer):
"""
授权资产组
"""
assets_granted = AssetGrantedSerializer(many=True, read_only=True)
assets_amount = serializers.SerializerMethodField()
parent = serializers.SerializerMethodField()
name = serializers.SerializerMethodField()
class Meta:
model = Node
fields = [
'id', 'key', 'name', 'value', 'parent',
'assets_granted', 'assets_amount', 'org_id',
]
@staticmethod
def get_assets_amount(obj):
return len(obj.assets_granted)
@staticmethod
def get_name(obj):
return obj.name
@staticmethod
def get_parent(obj):
return obj.parent.id
class NodeSerializer(serializers.ModelSerializer):
assets_amount = serializers.SerializerMethodField()
tree_id = serializers.SerializerMethodField()
tree_parent = serializers.SerializerMethodField()
assets_amount = serializers.IntegerField(read_only=True)
class Meta:
model = Node
fields = [
'id', 'key', 'value', 'assets_amount',
'is_node', 'org_id', 'tree_id', 'tree_parent',
'id', 'key', 'value', 'assets_amount', 'org_id',
]
read_only_fields = [
'id', 'key', 'assets_amount', 'org_id',
]
list_serializer_class = BulkListSerializer
def validate(self, data):
value = data.get('value')
def validate_value(self, data):
instance = self.instance if self.instance else Node.root()
children = instance.parent.get_children().exclude(key=instance.key)
values = [child.value for child in children]
if value in values:
if data in values:
raise serializers.ValidationError(
'The same level node name cannot be the same'
)
return data
@staticmethod
def get_assets_amount(obj):
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):
assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all())
......@@ -97,3 +46,4 @@ class NodeAssetsSerializer(serializers.ModelSerializer):
class NodeAddChildrenSerializer(serializers.Serializer):
nodes = serializers.ListField()
from rest_framework import serializers
from ..models import SystemUser
from ..models import SystemUser, Asset
from .base import AuthSerializer
......@@ -21,17 +21,17 @@ class SystemUserSerializer(serializers.ModelSerializer):
def get_field_names(self, declared_fields, info):
fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info)
fields.extend([
'get_login_mode_display',
'login_mode_display',
])
return fields
@staticmethod
def get_unreachable_assets(obj):
return obj.unreachable_assets
return obj.assets_unreachable
@staticmethod
def get_reachable_assets(obj):
return obj.reachable_assets
return obj.assets_reachable
def get_unreachable_amount(self, obj):
return len(self.get_unreachable_assets(obj))
......@@ -41,7 +41,7 @@ class SystemUserSerializer(serializers.ModelSerializer):
@staticmethod
def get_assets_amount(obj):
return len(obj.get_assets())
return len(obj.get_related_assets())
class SystemUserAuthSerializer(AuthSerializer):
......@@ -75,4 +75,7 @@ class SystemUserSimpleSerializer(serializers.ModelSerializer):
"""
class Meta:
model = SystemUser
fields = ('id', 'name', 'username')
\ No newline at end of file
fields = ('id', 'name', 'username')
# -*- coding: utf-8 -*-
#
from collections import defaultdict
from django.db.models.signals import post_save, m2m_changed
from django.db.models.signals import post_save, m2m_changed, post_delete
from django.dispatch import receiver
from common.utils import get_logger
from .models import Asset, SystemUser, Node
from .tasks import update_assets_hardware_info_util, \
test_asset_connectability_util, push_system_user_to_assets
test_asset_connectivity_util, push_system_user_to_assets
logger = get_logger(__file__)
......@@ -19,8 +19,8 @@ def update_asset_hardware_info_on_created(asset):
def test_asset_conn_on_created(asset):
logger.debug("Test asset `{}` connectability".format(asset))
test_asset_connectability_util.delay([asset])
logger.debug("Test asset `{}` connectivity".format(asset))
test_asset_connectivity_util.delay([asset])
def set_asset_root_node(asset):
......@@ -35,6 +35,17 @@ def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
update_asset_hardware_info_on_created(instance)
test_asset_conn_on_created(instance)
# 过期节点资产数量
nodes = instance.nodes.all()
Node.expire_nodes_assets_amount(nodes)
@receiver(post_delete, sender=Asset, dispatch_uid="my_unique_identifier")
def on_asset_delete(sender, instance=None, **kwargs):
# 过期节点资产数量
nodes = instance.nodes.all()
Node.expire_nodes_assets_amount(nodes)
@receiver(post_save, sender=SystemUser, dispatch_uid="my_unique_identifier")
def on_system_user_update(sender, instance=None, created=True, **kwargs):
......@@ -63,10 +74,11 @@ def on_system_user_assets_change(sender, instance=None, **kwargs):
@receiver(m2m_changed, sender=Asset.nodes.through)
def on_asset_node_changed(sender, instance=None, **kwargs):
logger.debug("Asset node change signal received")
if isinstance(instance, Asset):
if kwargs['action'] == 'post_add':
logger.debug("Asset node change signal received")
nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
Node.expire_nodes_assets_amount(nodes)
system_users_assets = defaultdict(set)
system_users = SystemUser.objects.filter(nodes__in=nodes)
# 清理节点缓存
......@@ -79,9 +91,11 @@ def on_asset_node_changed(sender, instance=None, **kwargs):
@receiver(m2m_changed, sender=Asset.nodes.through)
def on_node_assets_changed(sender, instance=None, **kwargs):
if isinstance(instance, Node):
logger.debug("Node assets change signal received")
# 当节点和资产关系发生改变时,过期资产数量缓存
instance.expire_assets_amount()
assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
if kwargs['action'] == 'post_add':
logger.debug("Node assets change signal received")
# 重新关联系统用户和资产的关系
system_users = SystemUser.objects.filter(nodes=instance)
for system_user in system_users:
......
......@@ -26,6 +26,23 @@ disk_pattern = re.compile(r'^hd|sd|xvd|vd')
PERIOD_TASK = os.environ.get("PERIOD_TASK", "off")
def clean_hosts(assets):
clean_assets = []
for asset in assets:
if not asset.is_active:
msg = _("Asset has been disabled, skipped: {}").format(asset)
logger.info(msg)
continue
if not asset.support_ansible():
msg = _("Asset may not be support ansible, skipped: {}").format(asset)
logger.info(msg)
continue
clean_assets.append(asset)
if not clean_assets:
logger.info(_("No assets matched, stop task"))
return clean_assets
@shared_task
def set_assets_hardware_info(assets, result, **kwargs):
"""
......@@ -60,9 +77,12 @@ def set_assets_hardware_info(assets, result, **kwargs):
___cpu_model = 'Unknown'
___cpu_model = ___cpu_model[:64]
___cpu_count = info.get('ansible_processor_count', 0)
___cpu_cores = info.get('ansible_processor_cores', None) or len(info.get('ansible_processor', []))
___cpu_cores = info.get('ansible_processor_cores', None) or \
len(info.get('ansible_processor', []))
___cpu_vcpus = info.get('ansible_processor_vcpus', 0)
___memory = '%s %s' % capacity_convert('{} MB'.format(info.get('ansible_memtotal_mb')))
___memory = '%s %s' % capacity_convert(
'{} MB'.format(info.get('ansible_memtotal_mb'))
)
disk_info = {}
for dev, dev_info in info.get('ansible_devices', {}).items():
if disk_pattern.match(dev) and dev_info['removable'] == '0':
......@@ -96,19 +116,8 @@ def update_assets_hardware_info_util(assets, task_name=None):
if task_name is None:
task_name = _("Update some assets hardware info")
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
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)
hosts = clean_hosts(assets)
if not hosts:
logger.info(_("No assets matched, stop task"))
return {}
created_by = str(assets[0].org_id)
task, created = update_or_create_ansible_task(
......@@ -125,7 +134,6 @@ def update_assets_hardware_info_util(assets, task_name=None):
@shared_task
def update_asset_hardware_info_manual(asset):
task_name = _("Update asset hardware info: {}").format(asset.hostname)
# task_name = _("更新资产硬件信息")
return update_assets_hardware_info_util(
[asset], task_name=task_name
)
......@@ -141,166 +149,92 @@ def update_assets_hardware_info_period():
logger.debug("Period task disabled, update assets hardware info pass")
return
from ops.utils import update_or_create_ansible_task
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 ##
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)
@shared_task
def test_asset_connectivity_util(assets, task_name=None):
from ops.utils import update_or_create_ansible_task
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)
if task_name is None:
task_name = _("Test assets connectivity")
hosts = clean_hosts(assets)
if not hosts:
return {}
tasks = const.TEST_ADMIN_USER_CONN_TASKS
created_by = assets[0].org_id
task, created = update_or_create_ansible_task(
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True, created_by=created_by,
)
result = task.run()
summary = result[1]
for asset in assets:
if asset.hostname in summary.get('dark', {}):
asset.connectivity = asset.UNREACHABLE
elif asset.hostname in summary.get('contacted', []):
asset.connectivity = asset.REACHABLE
else:
asset.connectivity = asset.UNKNOWN
return summary
for i, msg in summary.get('dark', {}).items():
asset_conn_cache_key = const.ASSET_ADMIN_CONN_CACHE_KEY.format(i)
cache.set(asset_conn_cache_key, 0, CACHE_MAX_TIME)
logger.error(msg)
@shared_task
def test_asset_connectivity_manual(asset):
task_name = _("Test assets connectivity: {}").format(asset)
summary = test_asset_connectivity_util([asset], task_name=task_name)
if summary.get('dark'):
return False, summary['dark']
else:
return True, ""
@shared_task
def test_admin_user_connectability_util(admin_user, task_name):
def test_admin_user_connectivity_util(admin_user, task_name):
"""
Test asset admin user can connect or not. Using ansible api do that
: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)
hosts = clean_hosts(assets)
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
summary = test_asset_connectivity_util(hosts, task_name)
return summary
@shared_task
@register_as_period_task(interval=3600)
def test_admin_user_connectability_period():
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 connectability period: {}").format(admin_user.name)
test_admin_user_connectability_util(admin_user, task_name)
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_connectability_manual(admin_user):
task_name = _("Test admin user connectability: {}").format(admin_user.name)
# task_name = _("测试管理行号可连接性: {}").format(admin_user.name)
return test_admin_user_connectability_util(admin_user, task_name)
@shared_task
def test_asset_connectability_util(assets, task_name=None):
from ops.utils import update_or_create_ansible_task
if task_name is None:
task_name = _("Test assets connectability")
# task_name = _("测试资产可连接性")
hosts = []
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:
logger.info(_("No assets, task stop"))
return {}
tasks = const.TEST_ADMIN_USER_CONN_TASKS
created_by = assets[0].org_id
task, created = update_or_create_ansible_task(
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True, created_by=created_by,
)
result = task.run()
summary = result[1]
for k in summary.get('dark'):
cache.set(const.ASSET_ADMIN_CONN_CACHE_KEY.format(k), 0, CACHE_MAX_TIME)
for k in summary.get('contacted'):
cache.set(const.ASSET_ADMIN_CONN_CACHE_KEY.format(k), 1, CACHE_MAX_TIME)
return summary
@shared_task
def test_asset_connectability_manual(asset):
task_name = _("Test assets connectability: {}").format(asset)
summary = test_asset_connectability_util([asset], task_name=task_name)
if summary.get('dark'):
return False, summary['dark']
else:
return True, ""
def test_admin_user_connectivity_manual(admin_user):
task_name = _("Test admin user connectivity: {}").format(admin_user.name)
return test_admin_user_connectivity_util(admin_user, task_name)
## System user connective ##
@shared_task
def set_system_user_connectablity_info(result, **kwargs):
def set_system_user_connectivity_info(system_user, result):
summary = result[1]
task_name = kwargs.get("task_name")
system_user = kwargs.get("system_user")
if system_user is None:
system_user = task_name.split(":")[-1]
cache_key = const.SYSTEM_USER_CONN_CACHE_KEY.format(str(system_user.id))
cache.set(cache_key, summary, CACHE_MAX_TIME)
system_user.connectivity = summary
@shared_task
def test_system_user_connectability_util(system_user, assets, task_name):
def test_system_user_connectivity_util(system_user, assets, task_name):
"""
Test system cant connect his assets or not.
:param system_user:
......@@ -309,20 +243,9 @@ def test_system_user_connectability_util(system_user, assets, task_name):
:return:
"""
from ops.utils import update_or_create_ansible_task
hosts = []
tasks = const.TEST_SYSTEM_USER_CONN_TASKS
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)
hosts = clean_hosts(assets)
if not hosts:
logger.info(_("No assets matched, stop task"))
return {}
task, created = update_or_create_ansible_task(
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,
)
result = task.run()
set_system_user_connectablity_info(result, system_user=system_user)
set_system_user_connectivity_info(system_user, result)
return result
@shared_task
def test_system_user_connectability_manual(system_user):
task_name = _("Test system user connectability: {}").format(system_user)
assets = system_user.get_assets()
return test_system_user_connectability_util(system_user, assets, task_name)
def test_system_user_connectivity_manual(system_user):
task_name = _("Test system user connectivity: {}").format(system_user)
assets = system_user.get_related_assets()
return test_system_user_connectivity_util(system_user, assets, task_name)
@shared_task
def test_system_user_connectability_a_asset(system_user, asset):
task_name = _("Test system user connectability: {} => {}").format(
def test_system_user_connectivity_a_asset(system_user, asset):
task_name = _("Test system user connectivity: {} => {}").format(
system_user, asset
)
return test_system_user_connectability_util(system_user, [asset], task_name)
return test_system_user_connectivity_util(system_user, [asset], task_name)
@shared_task
def test_system_user_connectability_period():
def test_system_user_connectivity_period():
if PERIOD_TASK != "on":
logger.debug("Period task disabled, test system user connectability pass")
logger.debug("Period task disabled, test system user connectivity pass")
return
system_users = SystemUser.objects.all()
for system_user in system_users:
task_name = _("Test system user connectability period: {}").format(system_user)
# task_name = _("定期测试系统用户可连接性: {}".format(system_user))
test_system_user_connectability_util(system_user, task_name)
task_name = _("Test system user connectivity period: {}").format(system_user)
assets = system_user.get_related_assets()
test_system_user_connectivity_util(system_user, assets, task_name)
#### Push system user tasks ####
......@@ -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:
tasks.append({
'name': 'Set {} authorized key'.format(system_user.username),
......@@ -416,19 +357,8 @@ def push_system_user_util(system_user, assets, task_name):
return
tasks = get_push_system_user_tasks(system_user)
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)
hosts = clean_hosts(assets)
if not hosts:
logger.info(_("No assets matched, stop task"))
return {}
task, created = update_or_create_ansible_task(
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
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)
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)
@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
# @register_as_period_task(interval=3600)
# @after_app_ready_start
......
......@@ -57,6 +57,10 @@
<script>
var zTree2, asset_table2 = 0;
function initTable2() {
if(asset_table2){
return
}
var options = {
ele: $('#asset_list_modal_table'),
ajax_url: '{% url "api-assets:asset-list" %}?show_current_asset=1',
......@@ -71,14 +75,14 @@ function initTable2() {
function onSelected2(event, treeNode) {
var url = asset_table2.ajax.url();
url = setUrlParam(url, "node_id", treeNode.node_id);
setCookie('node_selected', treeNode.id);
url = setUrlParam(url, "node_id", treeNode.meta.node.id);
asset_table2.ajax.url(url);
asset_table2.ajax.reload();
}
function initTree2() {
var url = '{% url 'api-assets:node-children-tree' %}?assets=0';
var setting = {
view: {
dblClickExpand: false,
......@@ -89,33 +93,22 @@ function initTree2() {
enable: true
}
},
async: {
enable: true,
url: url,
autoParam: ["id=key", "name=n", "level=lv"],
type: 'get'
},
callback: {
onSelected: onSelected2
}
};
var zNodes = [];
$.get("{% url 'api-assets:node-list' %}", function(data, status){
$.each(data, function (index, value) {
value["node_id"] = value["id"];
value["id"] = value["tree_id"];
value["pId"] = value["tree_parent"];
{#value["open"] = true;#}
if (value["key"] === "0") {
value["open"] = true;
}
value["name"] = value["value"] + ' (' + value['assets_amount'] + ')';
});
zNodes = data;
$.fn.zTree.init($("#assetTree2"), setting, zNodes);
zTree2 = $.fn.zTree.getZTreeObj("assetTree2");
var root = zTree2.getNodes()[0];
zTree2.expandNode(root);
});
zTree2 = $.fn.zTree.init($("#assetTree2"), setting);
}
$(document).ready(function(){
}).on('show.bs.modal', function () {
initTable2();
initTree2();
})
......
......@@ -45,13 +45,11 @@
<table class="table table-striped table-bordered table-hover" id="asset_list_table">
<thead>
<tr>
<th class="text-center">
<input type="checkbox" id="check_all" class="ipt_check_all" >
</th>
<th>{% trans 'Hostname' %}</th>
<th>{% trans 'IP' %}</th>
<th>{% trans 'Port' %}</th>
<th>{% trans 'Reachable' %}</th>
<th>{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
......@@ -91,26 +89,36 @@
<script>
function initTable() {
var reachable = {{ admin_user.REACHABLE }};
var unreachable = {{ admin_user.UNREACHABLE }};
var options = {
ele: $('#asset_list_table'),
buttons: [],
order: [],
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
{targets: 0, createdCell: function (td, cellData, rowData) {
var detail_btn = '<a href="{% url "assets:asset-detail" pk=DEFAULT_PK %}" data-aid="'+rowData.id+'">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},
{targets: 4, createdCell: function (td, cellData) {
if (!cellData) {
{targets: 3, createdCell: function (td, cellData) {
if (cellData === unreachable) {
$(td).html('<i class="fa fa-times text-danger"></i>')
} else {
} else if (cellData === reachable) {
$(td).html('<i class="fa fa-check text-navy"></i>')
}
}}],
ajax_url: '{% url "api-assets:asset-list" %}?admin_user_id={{ admin_user.id }}',
} else {
$(td).html('')
}
}},
{targets: 4, createdCell: function (td, cellData) {
var test_btn = ' <a class="btn btn-xs btn-info btn-test-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Test" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
$(td).html(test_btn);
}}
],
ajax_url: '{% url "api-assets:admin-user-assets" pk=admin_user.id %}',
columns: [
{data: function(){return ""}}, {data: "hostname" }, {data: "ip" },
{data: "port" }, {data: "is_connective" }],
{data: "hostname" }, {data: "ip" },
{data: "port" }, {data: "connectivity" }, {data: "id"}],
op_html: $('#actions').html()
};
jumpserver.initServerSideDataTable(options);
......@@ -119,6 +127,21 @@ function initTable() {
$(document).ready(function () {
initTable();
})
.on('click', '.btn-test-asset', function () {
var asset_id = $(this).data('uid');
var the_url = "{% url 'api-assets:asset-alive-test' pk=DEFAULT_PK %}".replace('{{ DEFAULT_PK }}', asset_id);
var success = function (data) {
var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
};
APIUpdateAttr({
url: the_url,
method: 'GET',
success: success,
flash_message: false
});
})
.on('click', '.btn-test-connective', function () {
var the_url = "{% url 'api-assets:admin-user-connective' pk=admin_user.id %}";
var success = function (data) {
......
......@@ -136,6 +136,7 @@
<li class="divider"></li>
<li id="show_current_asset" class="btn-show-current-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-hand-o-up"></i> {% trans 'Display only current node assets' %}</a></li>
<li id="show_all_asset" class="btn-show-all-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-th"></i> {% trans 'Displays all child node assets' %}</a></li>
<li id="fresh_tree" class="btn-refresh-tree" tabindex="-1"><a><i class="fa fa-refresh"></i> {% trans 'Refresh' %}</a></li>
</ul>
</div>
......@@ -147,6 +148,8 @@
<script>
var zTree, rMenu, asset_table, show = 0;
var update_node_action = "";
var current_node_id = null;
var current_node = null;
function initTable() {
var options = {
ele: $('#asset_list_table'),
......@@ -191,18 +194,20 @@ function addTreeNode() {
if (!parentNode){
return
}
var url = "{% url 'api-assets:node-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", parentNode.node_id );
var url = "{% url 'api-assets:node-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", parentNode.meta.node.id);
$.post(url, {}, function (data, status){
if (status === "success") {
var newNode = {
id: data["key"],
name: data["value"],
node_id: data["id"],
pId: parentNode.id
pId: parentNode.id,
meta: {
"node": data
}
};
newNode.checked = zTree.getSelectedNodes()[0].checked;
zTree.addNodes(parentNode, 0, newNode);
var node = zTree.getNodeByParam('id', newNode.node_id, parentNode);
var node = zTree.getNodeByParam('id', newNode.id, parentNode);
zTree.editName(node);
} else {
alert("{% trans 'Create node failed' %}")
......@@ -218,10 +223,10 @@ function removeTreeNode() {
}
if (current_node.children && current_node.children.length > 0) {
toastr.error("{% trans 'Have child node, cancel' %}");
} else if (current_node.assets_amount !== 0) {
} else if (current_node.meta.node.assets_amount !== 0) {
toastr.error("{% trans 'Have assets, cancel' %}");
} else {
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.node_id );
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
$.ajax({
url: url,
method: "DELETE",
......@@ -238,8 +243,8 @@ function editTreeNode() {
if (!current_node){
return
}
if (current_node.value) {
current_node.name = current_node.value;
if (current_node) {
current_node.name = current_node.meta.node.value;
}
zTree.editName(current_node);
}
......@@ -281,7 +286,7 @@ function onBodyMouseDown(event){
function onRename(event, treeId, treeNode, isCancel){
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", treeNode.node_id);
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
var data = {"value": treeNode.name};
if (isCancel){
return
......@@ -291,13 +296,20 @@ function onRename(event, treeId, treeNode, isCancel){
body: JSON.stringify(data),
method: "PATCH",
success_message: "{% trans 'Rename success' %}",
fail_message: "{% trans 'Rename failed, do not change the root node name' %}"
fail_message: "{% trans 'Rename failed, do not change the root node name' %}",
success: function () {
treeNode.name = treeNode.name + ' (' + treeNode.meta.node.assets_amount + ')'
zTree.updateNode(treeNode);
console.log("Success: " + treeNode.name)
}
})
}
function onSelected(event, treeNode) {
current_node = treeNode;
current_node_id = treeNode.meta.node.id;
var url = asset_table.ajax.url();
url = setUrlParam(url, "node_id", treeNode.node_id);
url = setUrlParam(url, "node_id", current_node_id);
url = setUrlParam(url, "show_current_asset", getCookie('show_current_asset'));
setCookie('node_selected', treeNode.node_id);
asset_table.ajax.url(url);
......@@ -305,6 +317,9 @@ function onSelected(event, treeNode) {
}
function selectQueryNode() {
// TODO: 是否应该添加
// 暂时忽略之前选中的内容
return
var query_node_id = $.getUrlParam("node");
var cookie_node_id = getCookie('node_selected');
var node;
......@@ -355,6 +370,14 @@ function onDrop(event, treeId, treeNodes, targetNode, moveType) {
}
function initTree() {
if (zTree) {
return
}
var url = '{% url 'api-assets:node-children-tree' %}?assets=0&all=';
var showCurrentAsset = getCookie('show_current_asset');
if (!showCurrentAsset) {
url += '1'
}
var setting = {
view: {
dblClickExpand: false,
......@@ -365,6 +388,12 @@ function initTree() {
enable: true
}
},
async: {
enable: true,
url: url,
autoParam: ["id=key", "name=n", "level=lv"],
type: 'get'
},
edit: {
enable: true,
showRemoveBtn: false,
......@@ -387,26 +416,8 @@ function initTree() {
};
var zNodes = [];
$.get("{% url 'api-assets:node-list' %}", function(data, status){
$.each(data, function (index, value) {
value["node_id"] = value["id"];
value["id"] = value["tree_id"];
if (value["tree_id"] !== value["tree_parent"]){
value["pId"] = value["tree_parent"];
} else {
value["isParent"] = true;
}
value["name"] = value["value"] + ' (' + value['assets_amount'] + ')';
value['value'] = value['value'];
});
zNodes = data;
$.fn.zTree.init($("#assetTree"), setting, zNodes);
zTree = $.fn.zTree.getZTreeObj("assetTree");
var root = zTree.getNodes()[0];
zTree.expandNode(root);
rMenu = $("#rMenu");
selectQueryNode();
});
zTree = $.fn.zTree.init($("#assetTree"), setting, zNodes);
rMenu = $("#rMenu");
}
function toggle() {
......@@ -443,20 +454,15 @@ $(document).ready(function(){
.on('click', '.btn_export', function () {
var $data_table = $('#asset_list_table').DataTable();
var rows = $data_table.rows('.selected').data();
var nodes = zTree.getSelectedNodes();
var current_node;
if (nodes && nodes.length === 1) {
current_node = nodes[0];
}
var assets = [];
$.each(rows, function (index, obj) {
assets.push(obj.id)
});
var _node_id = current_node ? current_node.node_id : null;
$.ajax({
url: "{% url "assets:asset-export" %}",
method: 'POST',
data: JSON.stringify({assets_id: assets, node_id: _node_id}),
data: JSON.stringify({assets_id: assets, node_id: current_node_id}),
dataType: "json",
success: function (data, textStatus) {
window.open(data.redirect)
......@@ -469,12 +475,8 @@ $(document).ready(function(){
.on('click', '#btn_asset_import', function () {
var $form = $('#fm_asset_import');
var action = $form.attr("action");
var nodes = zTree.getSelectedNodes();
var current_node;
if (nodes && nodes.length ===1 ){
current_node = nodes[0];
action = setUrlParam(action, 'node_id', current_node.node_id);
{#action += "?node_id=" + current_node.node_id;#}
if (current_node_id){
action = setUrlParam(action, 'node_id', current_node_id);
$form.attr("action", action)
}
$form.find('.help-block').remove();
......@@ -496,25 +498,14 @@ $(document).ready(function(){
})
.on('click', '.btn-create-asset', function () {
var url = "{% url 'assets:asset-create' %}";
var nodes = zTree.getSelectedNodes();
var current_node;
if (nodes && nodes.length ===1 ){
current_node = nodes[0];
url += "?node_id=" + current_node.node_id;
if (current_node_id) {
url += "?node_id=" + current_node_id;
}
window.open(url, '_self');
})
.on('click', '.btn-refresh-hardware', function () {
var url = "{% url 'api-assets:node-refresh-hardware-info' pk=DEFAULT_PK %}";
var nodes = zTree.getSelectedNodes();
var current_node;
if (nodes && nodes.length ===1 ){
current_node = nodes[0];
} else {
return null;
}
var the_url = url.replace("{{ DEFAULT_PK }}", current_node.node_id);
var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
function success(data) {
rMenu.css({"visibility" : "hidden"});
var task_id = data.task;
......@@ -531,15 +522,10 @@ $(document).ready(function(){
})
.on('click', '.btn-test-connective', function () {
var url = "{% url 'api-assets:node-test-connective' pk=DEFAULT_PK %}";
var nodes = zTree.getSelectedNodes();
var current_node;
if (nodes && nodes.length ===1 ){
current_node = nodes[0];
} else {
if (!current_node_id) {
return null;
}
var the_url = url.replace("{{ DEFAULT_PK }}", current_node.node_id);
var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
function success(data) {
rMenu.css({"visibility" : "hidden"});
var task_id = data.task;
......@@ -567,6 +553,10 @@ $(document).ready(function(){
setCookie('show_current_asset', '');
location.reload();
})
.on('click', '.btn-test-connective', function () {
hideRMenu();
})
.on('click', '.btn_asset_delete', function () {
var $this = $(this);
var $data_table = $("#asset_list_table").DataTable();
......@@ -660,11 +650,8 @@ $(document).ready(function(){
}
function doRemove() {
var current_node;
var nodes = zTree.getSelectedNodes();
if (nodes && nodes.length === 1) {
current_node = nodes[0]
} else {
if (!current_node_id) {
return
}
......@@ -677,7 +664,7 @@ $(document).ready(function(){
};
APIUpdateAttr({
'url': '/api/assets/v1/nodes/' + current_node.node_id + '/assets/remove/',
'url': '/api/assets/v1/nodes/' + current_node_id + '/assets/remove/',
'method': 'PUT',
'body': JSON.stringify(data),
'success': success
......@@ -706,11 +693,7 @@ $(document).ready(function(){
})
.on('click', '#btn_asset_modal_confirm', function () {
var assets_selected = asset_table2.selected;
var current_node;
var nodes = zTree.getSelectedNodes();
if (nodes && nodes.length === 1) {
current_node = nodes[0]
} else {
if (!current_node_id) {
return
}
......@@ -722,9 +705,9 @@ $(document).ready(function(){
var url = '';
if (update_node_action === "move") {
url = "{% url 'api-assets:node-replace-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.node_id);
url = "{% url 'api-assets:node-replace-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
} else {
url = "{% url 'api-assets:node-add-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.node_id);
url = "{% url 'api-assets:node-add-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
}
APIUpdateAttr({
......
......@@ -32,6 +32,7 @@ $(document).ready(function () {
}).on('click', '.select2-selection__rendered', function (e) {
e.preventDefault();
$("#asset_list_modal").modal();
initSelectedAssets2Table();
})
.on('click', '#btn_asset_modal_confirm', function () {
var assets = asset_table2.selected;
......
......@@ -136,7 +136,7 @@
{% block custom_foot_js %}
<script>
function initAssetsTable() {
var unreachable = {{ system_user.unreachable_assets|safe }};
var connectivity = {{ system_user.connectivity | safe }};
var options = {
ele: $('#system_user_list'),
buttons: [],
......@@ -147,11 +147,13 @@ function initAssetsTable() {
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},
{targets: 3, createdCell: function (td, cellData) {
if (unreachable.indexOf(cellData) >= 0) {
if (connectivity.unreachable.indexOf(cellData) >= 0) {
$(td).html('<i class="fa fa-times text-danger"></i>')
} else {
} else if (connectivity.reachable.indexOf(cellData) >= 0 ) {
$(td).html('<i class="fa fa-check text-navy"></i>')
}
} else {
$(td).html('')
}
}},
{targets: 4, createdCell: function (td, cellData) {
var push_btn = '';
......
......@@ -95,7 +95,7 @@ function initTable() {
}}],
ajax_url: '{% url "api-assets:system-user-list" %}',
columns: [
{data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "get_login_mode_display"}, {data: "assets_amount" },
{data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "login_mode_display"}, {data: "assets_amount" },
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" }
],
op_html: $('#actions').html()
......
......@@ -24,35 +24,39 @@ cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-r
urlpatterns = [
path('assets-bulk/', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'),
path('system-user/<uuid:pk>/auth-info/',
api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
path('system-user/<uuid:pk>/assets/',
api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
path('assets/<uuid:pk>/refresh/',
api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'),
path('assets/<uuid:pk>/alive/',
api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'),
path('assets/<uuid:pk>/gateway/',
api.AssetGatewayApi.as_view(), name='asset-gateway'),
path('admin-user/<uuid:pk>/nodes/',
api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'),
path('admin-user/<uuid:pk>/auth/',
api.AdminUserAuthApi.as_view(), name='admin-user-auth'),
path('admin-user/<uuid:pk>/connective/',
api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'),
path('admin-user/<uuid:pk>/assets/',
api.AdminUserAssetsListView.as_view(), name='admin-user-assets'),
path('system-user/<uuid:pk>/auth-info/',
api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
path('system-user/<uuid:pk>/assets/',
api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
path('system-user/<uuid:pk>/push/',
api.SystemUserPushApi.as_view(), name='system-user-push'),
path('system-user/<uuid:pk>/asset/<uuid:aid>/push/',
api.SystemUserPushToAssetApi.as_view(), name='system-user-push-to-asset'),
path('system-user/<uuid:pk>/asset/<uuid:aid>/test/',
api.SystemUserTestAssetConnectabilityApi.as_view(), name='system-user-test-to-asset'),
api.SystemUserTestAssetConnectivityApi.as_view(), name='system-user-test-to-asset'),
path('system-user/<uuid:pk>/connective/',
api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'),
path('system-user/<uuid:pk>/cmd-filter-rules/',
api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'),
path('nodes/tree/', api.NodeListAsTreeApi.as_view(), name='node-tree'),
path('nodes/children/tree/', api.NodeChildrenAsTreeApi.as_view(), name='node-children-tree'),
path('nodes/<uuid:pk>/children/',
api.NodeChildrenApi.as_view(), name='node-children'),
path('nodes/children/', api.NodeChildrenApi.as_view(), name='node-children-2'),
......
......@@ -102,7 +102,7 @@ class AdminUserAssetsView(AdminUserRequiredMixin, SingleObjectMixin, ListView):
'app': _('Assets'),
'action': _('Admin user detail'),
"total_amount": len(self.queryset),
'unreachable_amount': len([asset for asset in self.queryset if asset.is_connective is False])
'unreachable_amount': len([asset for asset in self.queryset if asset.connectivity is False])
}
kwargs.update(context)
return super().get_context_data(**kwargs)
......
......@@ -216,7 +216,6 @@ class AssetExportView(LoginRequiredMixin, View):
return HttpResponse('Json object not valid', status=400)
if not assets_id:
print(node_id)
node = get_object_or_none(Node, id=node_id) if node_id else Node.root()
assets = node.get_all_assets()
for asset in assets:
......
......@@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='ftplog',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
field=models.CharField(blank=True, db_index=True, default=None, max_length=36, null=True),
),
]
......@@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='ftplog',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
]
......@@ -15,7 +15,7 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='OperateLog',
fields=[
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')),
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('user', models.CharField(max_length=128, verbose_name='User')),
('action', models.CharField(choices=[('create', 'Create'), ('update', 'Update'), ('delete', 'Delete')], max_length=16, verbose_name='Action')),
......
......@@ -15,8 +15,6 @@ class BaseForm(forms.Form):
super().__init__(*args, **kwargs)
for name, field in self.fields.items():
value = getattr(settings, name, None)
# django_value = getattr(settings, name) if hasattr(settings, name) else None
if value is None: # and django_value is None:
continue
......@@ -24,8 +22,6 @@ class BaseForm(forms.Form):
if isinstance(value, dict):
value = json.dumps(value)
initial_value = value
# elif django_value is False or django_value:
# initial_value = django_value
else:
initial_value = ''
field.initial = initial_value
......@@ -134,6 +130,14 @@ class TerminalSettingForm(BaseForm):
('hostname', _('Hostname')),
('ip', _('IP')),
)
PAGE_SIZE_CHOICES = (
('all', _('All')),
('auto', _('Auto')),
(10, 10),
(15, 15),
(25, 25),
(50, 50),
)
TERMINAL_PASSWORD_AUTH = forms.BooleanField(
initial=True, required=False, label=_("Password auth")
)
......@@ -146,6 +150,14 @@ class TerminalSettingForm(BaseForm):
TERMINAL_ASSET_LIST_SORT_BY = forms.ChoiceField(
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):
......
......@@ -45,6 +45,8 @@ class Setting(models.Model):
def cleaned_value(self):
try:
value = self.value
if not isinstance(value, (str, bytes)):
return value
if self.encrypted:
value = signer.unsign(value)
value = json.loads(value)
......
......@@ -26,21 +26,20 @@ def refresh_settings_on_changed(sender, instance=None, **kwargs):
def refresh_all_settings_on_django_ready(sender, **kwargs):
logger.debug("Receive django ready signal")
logger.debug(" - fresh all settings")
CACHE_KEY_PREFIX = '_SETTING_'
cache_key_prefix = '_SETTING_'
def monkey_patch_getattr(self, name):
key = CACHE_KEY_PREFIX + name
key = cache_key_prefix + name
cached = cache.get(key)
if cached is not None:
return cached
if self._wrapped is empty:
self._setup(name)
val = getattr(self._wrapped, name)
# self.__dict__[name] = val # Never set it
return val
def monkey_patch_setattr(self, name, value):
key = CACHE_KEY_PREFIX + name
key = cache_key_prefix + name
cache.set(key, value, None)
if name == '_wrapped':
self.__dict__.clear()
......@@ -51,7 +50,7 @@ def refresh_all_settings_on_django_ready(sender, **kwargs):
def monkey_patch_delattr(self, name):
super(LazySettings, self).__delattr__(name)
self.__dict__.pop(name, None)
key = CACHE_KEY_PREFIX + name
key = cache_key_prefix + name
cache.delete(key)
try:
......
......@@ -47,16 +47,13 @@ class TreeNode:
def __gt__(self, other):
if self.isParent and not other.isParent:
return False
elif not self.isParent and other.isParent:
return True
return self.id > other.id
def __eq__(self, other):
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:
def __init__(self):
......
......@@ -304,7 +304,7 @@ defaults = {
'REDIS_DB_CELERY': 3,
'REDIS_DB_CACHE': 4,
'CAPTCHA_TEST_MODE': None,
'TOKEN_EXPIRATION': 3600,
'TOKEN_EXPIRATION': 3600 * 24,
'DISPLAY_PER_PAGE': 25,
'DEFAULT_EXPIRED_YEARS': 70,
'SESSION_COOKIE_DOMAIN': None,
......@@ -312,7 +312,14 @@ defaults = {
'SESSION_COOKIE_AGE': 3600 * 24,
'SESSION_EXPIRE_AT_BROWSER_CLOSE': 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,
}
......
......@@ -356,6 +356,7 @@ FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755
# OTP settings
OTP_ISSUER_NAME = CONFIG.OTP_ISSUER_NAME
OTP_VALID_WINDOW = CONFIG.OTP_VALID_WINDOW
# Auth LDAP settings
AUTH_LDAP = False
......@@ -466,6 +467,7 @@ DEFAULT_TERMINAL_REPLAY_STORAGE = {
TERMINAL_REPLAY_STORAGE = {
}
SECURITY_MFA_AUTH = False
SECURITY_LOGIN_LIMIT_COUNT = 7
SECURITY_LOGIN_LIMIT_TIME = 30 # Unit: minute
......@@ -484,6 +486,13 @@ SECURITY_PASSWORD_RULES = [
'SECURITY_PASSWORD_SPECIAL_CHAR'
]
TERMINAL_PASSWORD_AUTH = CONFIG.TERMINAL_PASSWORD_AUTH
TERMINAL_PUBLIC_KEY_AUTH = CONFIG.TERMINAL_PUBLIC_KEY_AUTH
TERMINAL_HEARTBEAT_INTERVAL = CONFIG.TERMINAL_HEARTBEAT_INTERVAL
TERMINAL_ASSET_LIST_SORT_BY = CONFIG.TERMINAL_ASSET_LIST_SORT_BY
TERMINAL_ASSET_LIST_PAGE_SIZE = CONFIG.TERMINAL_ASSET_LIST_PAGE_SIZE
TERMINAL_SESSION_KEEP_DURATION = CONFIG.TERMINAL_SESSION_KEEP_DURATION
# Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html
BOOTSTRAP3 = {
'horizontal_label_class': 'col-md-2',
......
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Jumpserver 0.3.3\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-12-07 18:11+0800\n"
"POT-Creation-Date: 2018-12-18 10:13+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ibuler <ibuler@qq.com>\n"
"Language-Team: Jumpserver team<ibuler@qq.com>\n"
......@@ -17,25 +17,17 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: assets/api/node.py:58
msgid "You can't update the root node name"
msgstr "不能修改根节点名称"
#: assets/api/node.py:82
msgid "New node {}"
msgstr "新节点 {}"
#: assets/api/node.py:222
#: assets/api/node.py:261
msgid "Update node asset hardware information: {}"
msgstr "更新节点资产硬件信息: {}"
#: assets/api/node.py:236
#: assets/api/node.py:275
msgid "Test if the assets under the node are connectable: {}"
msgstr "测试节点下资产是否可连接: {}"
#: assets/forms/asset.py:27 assets/models/asset.py:83 assets/models/user.py:113
#: assets/templates/assets/asset_detail.html:187
#: assets/templates/assets/asset_detail.html:195
#: assets/templates/assets/asset_detail.html:191
#: assets/templates/assets/asset_detail.html:199
#: assets/templates/assets/system_user_asset.html:95 perms/models.py:32
msgid "Nodes"
msgstr "节点管理"
......@@ -62,19 +54,20 @@ msgid "Label"
msgstr "标签"
#: assets/forms/asset.py:37 assets/forms/asset.py:76 assets/models/asset.py:79
#: assets/models/domain.py:24 assets/models/domain.py:50
#: assets/models/domain.py:26 assets/models/domain.py:52
#: assets/templates/assets/asset_detail.html:81
#: assets/templates/assets/user_asset_list.html:157
#: xpack/plugins/orgs/templates/orgs/org_list.html:17
msgid "Domain"
msgstr "网域"
#: assets/forms/asset.py:41 assets/forms/asset.py:66 assets/forms/asset.py:80
#: assets/forms/asset.py:131 assets/models/node.py:28
#: assets/forms/asset.py:131 assets/models/node.py:31
#: assets/templates/assets/asset_create.html:30
#: assets/templates/assets/asset_update.html:35 perms/forms.py:37
#: perms/forms.py:44 perms/models.py:79
#: perms/templates/perms/asset_permission_list.html:57
#: perms/templates/perms/asset_permission_list.html:151
#: perms/templates/perms/asset_permission_list.html:117
#: xpack/plugins/cloud/models.py:122
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:63
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:66
......@@ -105,7 +98,7 @@ msgid "Select assets"
msgstr "选择资产"
#: assets/forms/asset.py:108 assets/models/asset.py:76
#: assets/models/domain.py:48 assets/templates/assets/admin_user_assets.html:53
#: assets/models/domain.py:50 assets/templates/assets/admin_user_assets.html:53
#: assets/templates/assets/asset_detail.html:69
#: assets/templates/assets/domain_gateway_list.html:58
#: assets/templates/assets/system_user_asset.html:52
......@@ -115,7 +108,7 @@ msgid "Port"
msgstr "端口"
#: assets/forms/domain.py:15 assets/forms/label.py:13
#: assets/models/asset.py:260 assets/templates/assets/admin_user_list.html:28
#: assets/models/asset.py:290 assets/templates/assets/admin_user_list.html:28
#: assets/templates/assets/domain_detail.html:60
#: assets/templates/assets/domain_list.html:26
#: assets/templates/assets/label_list.html:16
......@@ -125,7 +118,7 @@ msgstr "端口"
#: perms/models.py:31
#: perms/templates/perms/asset_permission_create_update.html:40
#: perms/templates/perms/asset_permission_list.html:56
#: perms/templates/perms/asset_permission_list.html:148
#: perms/templates/perms/asset_permission_list.html:114
#: terminal/backends/command/models.py:13 terminal/models.py:138
#: terminal/templates/terminal/command_list.html:40
#: terminal/templates/terminal/command_list.html:73
......@@ -137,13 +130,13 @@ msgstr "端口"
msgid "Asset"
msgstr "资产"
#: assets/forms/domain.py:42
#: assets/forms/domain.py:46
msgid "Password should not contain special characters"
msgstr "不能包含特殊字符"
#: assets/forms/domain.py:59 assets/forms/user.py:80 assets/forms/user.py:143
#: assets/forms/domain.py:63 assets/forms/user.py:80 assets/forms/user.py:143
#: assets/models/base.py:22 assets/models/cluster.py:18
#: assets/models/cmd_filter.py:20 assets/models/domain.py:18
#: assets/models/cmd_filter.py:20 assets/models/domain.py:20
#: assets/models/group.py:20 assets/models/label.py:18
#: assets/templates/assets/admin_user_detail.html:56
#: assets/templates/assets/admin_user_list.html:26
......@@ -183,7 +176,7 @@ msgstr "不能包含特殊字符"
msgid "Name"
msgstr "名称"
#: assets/forms/domain.py:60 assets/forms/user.py:81 assets/forms/user.py:144
#: assets/forms/domain.py:64 assets/forms/user.py:81 assets/forms/user.py:144
#: assets/models/base.py:23 assets/templates/assets/admin_user_detail.html:60
#: assets/templates/assets/admin_user_list.html:27
#: assets/templates/assets/domain_gateway_list.html:60
......@@ -256,7 +249,7 @@ msgid ""
"password."
msgstr "如果选择手动登录模式,用户名和密码可以不填写"
#: assets/models/asset.py:73 assets/models/domain.py:47
#: assets/models/asset.py:73 assets/models/domain.py:49
#: assets/templates/assets/_asset_list_modal.html:46
#: assets/templates/assets/admin_user_assets.html:52
#: assets/templates/assets/asset_detail.html:61
......@@ -285,7 +278,7 @@ msgstr "IP"
msgid "Hostname"
msgstr "主机名"
#: assets/models/asset.py:75 assets/models/domain.py:49
#: assets/models/asset.py:75 assets/models/domain.py:51
#: assets/models/user.py:117 assets/templates/assets/asset_detail.html:73
#: assets/templates/assets/domain_gateway_list.html:59
#: assets/templates/assets/system_user_detail.html:70
......@@ -295,14 +288,14 @@ msgstr "主机名"
msgid "Protocol"
msgstr "协议"
#: assets/models/asset.py:77 assets/templates/assets/asset_detail.html:101
#: assets/models/asset.py:77 assets/templates/assets/asset_detail.html:105
#: assets/templates/assets/user_asset_list.html:154
msgid "Platform"
msgstr "系统平台"
#: assets/models/asset.py:84 assets/models/cmd_filter.py:21
#: assets/models/domain.py:52 assets/models/label.py:21
#: assets/templates/assets/asset_detail.html:109
#: assets/models/domain.py:54 assets/models/label.py:22
#: assets/templates/assets/asset_detail.html:113
#: assets/templates/assets/user_asset_list.html:158
msgid "Is active"
msgstr "激活"
......@@ -311,19 +304,19 @@ msgstr "激活"
msgid "Public IP"
msgstr "公网IP"
#: assets/models/asset.py:92 assets/templates/assets/asset_detail.html:117
#: assets/models/asset.py:92 assets/templates/assets/asset_detail.html:121
msgid "Asset number"
msgstr "资产编号"
#: assets/models/asset.py:96 assets/templates/assets/asset_detail.html:81
#: assets/models/asset.py:96 assets/templates/assets/asset_detail.html:85
msgid "Vendor"
msgstr "制造商"
#: assets/models/asset.py:98 assets/templates/assets/asset_detail.html:85
#: assets/models/asset.py:98 assets/templates/assets/asset_detail.html:89
msgid "Model"
msgstr "型号"
#: assets/models/asset.py:100 assets/templates/assets/asset_detail.html:113
#: assets/models/asset.py:100 assets/templates/assets/asset_detail.html:117
msgid "Serial number"
msgstr "序列号"
......@@ -343,7 +336,7 @@ msgstr "CPU核数"
msgid "CPU vcpus"
msgstr "CPU总数"
#: assets/models/asset.py:108 assets/templates/assets/asset_detail.html:93
#: assets/models/asset.py:108 assets/templates/assets/asset_detail.html:97
msgid "Memory"
msgstr "内存"
......@@ -355,7 +348,7 @@ msgstr "硬盘大小"
msgid "Disk info"
msgstr "硬盘信息"
#: assets/models/asset.py:115 assets/templates/assets/asset_detail.html:105
#: assets/models/asset.py:115 assets/templates/assets/asset_detail.html:109
#: assets/templates/assets/user_asset_list.html:155
msgid "OS"
msgstr "操作系统"
......@@ -373,7 +366,7 @@ msgid "Hostname raw"
msgstr "主机名原始"
#: assets/models/asset.py:125 assets/templates/assets/asset_create.html:34
#: assets/templates/assets/asset_detail.html:224
#: assets/templates/assets/asset_detail.html:228
#: assets/templates/assets/asset_update.html:39 templates/_nav.html:26
msgid "Labels"
msgstr "标签管理"
......@@ -382,7 +375,7 @@ msgstr "标签管理"
#: assets/models/cluster.py:28 assets/models/cmd_filter.py:25
#: assets/models/cmd_filter.py:55 assets/models/group.py:21
#: assets/templates/assets/admin_user_detail.html:68
#: assets/templates/assets/asset_detail.html:121
#: assets/templates/assets/asset_detail.html:125
#: assets/templates/assets/cmd_filter_detail.html:77
#: assets/templates/assets/domain_detail.html:72
#: assets/templates/assets/system_user_detail.html:100
......@@ -394,8 +387,8 @@ msgid "Created by"
msgstr "创建者"
#: assets/models/asset.py:130 assets/models/cluster.py:26
#: assets/models/domain.py:21 assets/models/group.py:22
#: assets/models/label.py:24 assets/templates/assets/admin_user_detail.html:64
#: assets/models/domain.py:23 assets/models/group.py:22
#: assets/models/label.py:25 assets/templates/assets/admin_user_detail.html:64
#: assets/templates/assets/cmd_filter_detail.html:69
#: assets/templates/assets/domain_detail.html:68
#: assets/templates/assets/system_user_detail.html:96
......@@ -413,11 +406,11 @@ msgstr "创建日期"
#: assets/models/asset.py:132 assets/models/base.py:27
#: assets/models/cluster.py:29 assets/models/cmd_filter.py:22
#: assets/models/cmd_filter.py:52 assets/models/domain.py:19
#: assets/models/domain.py:51 assets/models/group.py:23
#: assets/models/label.py:22 assets/templates/assets/admin_user_detail.html:72
#: assets/models/cmd_filter.py:52 assets/models/domain.py:21
#: assets/models/domain.py:53 assets/models/group.py:23
#: assets/models/label.py:23 assets/templates/assets/admin_user_detail.html:72
#: assets/templates/assets/admin_user_list.html:32
#: assets/templates/assets/asset_detail.html:129
#: assets/templates/assets/asset_detail.html:133
#: assets/templates/assets/cmd_filter_detail.html:65
#: assets/templates/assets/cmd_filter_list.html:27
#: assets/templates/assets/cmd_filter_rule_list.html:62
......@@ -487,7 +480,7 @@ msgid "Default"
msgstr "默认"
#: assets/models/cluster.py:36 assets/models/label.py:14
#: users/models/user.py:422
#: users/models/user.py:432
msgid "System"
msgstr "系统"
......@@ -516,7 +509,7 @@ msgid "Regex"
msgstr "正则表达式"
#: assets/models/cmd_filter.py:36 ops/models/command.py:19
#: ops/templates/ops/command_execution_list.html:46 terminal/models.py:144
#: ops/templates/ops/command_execution_list.html:60 terminal/models.py:144
#: terminal/templates/terminal/command_list.html:55
#: terminal/templates/terminal/command_list.html:71
#: terminal/templates/terminal/session_detail.html:48
......@@ -591,7 +584,7 @@ msgstr "每行一个命令"
msgid "Action"
msgstr "动作"
#: assets/models/domain.py:59 assets/templates/assets/domain_detail.html:21
#: assets/models/domain.py:61 assets/templates/assets/domain_detail.html:21
#: assets/templates/assets/domain_detail.html:64
#: assets/templates/assets/domain_gateway_list.html:21
#: assets/templates/assets/domain_list.html:27
......@@ -613,38 +606,42 @@ msgstr "默认资产组"
#: audits/templates/audits/operate_log_list.html:66
#: audits/templates/audits/password_change_log_list.html:33
#: audits/templates/audits/password_change_log_list.html:50
#: ops/templates/ops/command_execution_list.html:22
#: ops/templates/ops/command_execution_list.html:45 perms/forms.py:28
#: ops/templates/ops/command_execution_list.html:34
#: ops/templates/ops/command_execution_list.html:59 perms/forms.py:28
#: perms/models.py:29
#: perms/templates/perms/asset_permission_create_update.html:36
#: perms/templates/perms/asset_permission_list.html:54
#: perms/templates/perms/asset_permission_list.html:142 templates/index.html:87
#: perms/templates/perms/asset_permission_list.html:108 templates/index.html:87
#: terminal/backends/command/models.py:12 terminal/models.py:137
#: terminal/templates/terminal/command_list.html:32
#: terminal/templates/terminal/command_list.html:72
#: terminal/templates/terminal/session_list.html:33
#: terminal/templates/terminal/session_list.html:71 users/forms.py:310
#: users/models/user.py:33 users/models/user.py:410
#: terminal/templates/terminal/session_list.html:71 users/forms.py:314
#: users/models/user.py:33 users/models/user.py:420
#: users/templates/users/user_group_detail.html:78
#: users/templates/users/user_group_list.html:13 users/views/user.py:384
#: users/templates/users/user_group_list.html:13 users/views/user.py:386
#: xpack/plugins/orgs/forms.py:26
#: xpack/plugins/orgs/templates/orgs/org_detail.html:113
#: xpack/plugins/orgs/templates/orgs/org_list.html:14
msgid "User"
msgstr "用户"
#: assets/models/label.py:19 assets/models/node.py:20
#: assets/models/label.py:19 assets/models/node.py:21
#: assets/templates/assets/label_list.html:15 common/models.py:30
msgid "Value"
msgstr "值"
#: assets/models/label.py:20
#: assets/models/label.py:21
msgid "Category"
msgstr "分类"
#: assets/models/node.py:19
#: assets/models/node.py:20
msgid "Key"
msgstr ""
msgstr "键"
#: assets/models/node.py:127
msgid "New node"
msgstr "新节点"
#: assets/models/user.py:109
msgid "Automatic login"
......@@ -701,7 +698,7 @@ msgstr "登录模式"
#: perms/models.py:33 perms/models.py:81
#: perms/templates/perms/asset_permission_detail.html:140
#: perms/templates/perms/asset_permission_list.html:58
#: perms/templates/perms/asset_permission_list.html:154 templates/_nav.html:25
#: perms/templates/perms/asset_permission_list.html:120 templates/_nav.html:25
#: terminal/backends/command/models.py:14 terminal/models.py:139
#: terminal/templates/terminal/command_list.html:48
#: terminal/templates/terminal/command_list.html:74
......@@ -733,7 +730,7 @@ msgid "Asset may not be support ansible, skipped: {}"
msgstr "资产或许不支持ansible, 跳过: {}"
#: assets/tasks.py:111 assets/tasks.py:210 assets/tasks.py:325
#: assets/tasks.py:431
#: assets/tasks.py:432
msgid "No assets matched, stop task"
msgstr "没有匹配到资产,结束任务"
......@@ -741,27 +738,23 @@ msgstr "没有匹配到资产,结束任务"
msgid "Update asset hardware info: {}"
msgstr "更新资产硬件信息: {}"
#: assets/tasks.py:148
msgid "Update assets hardware info period"
msgstr "定期更新资产硬件信息"
#: assets/tasks.py:230
msgid "Test admin user connectability period: {}"
msgid "Test admin user connectivity period: {}"
msgstr "定期测试管理账号可连接性: {}"
#: assets/tasks.py:236
msgid "Test admin user connectability: {}"
msgid "Test admin user connectivity: {}"
msgstr "测试管理行号可连接性: {}"
#: assets/tasks.py:246
msgid "Test assets connectability"
msgid "Test assets connectivity"
msgstr "测试资产可连接性"
#: assets/tasks.py:251 assets/tasks.py:316 assets/tasks.py:422
#: assets/tasks.py:251 assets/tasks.py:316 assets/tasks.py:423
msgid "Asset has been disabled, skip: {}"
msgstr "资产被禁用,跳过:{}"
#: assets/tasks.py:255 assets/tasks.py:320 assets/tasks.py:426
#: assets/tasks.py:255 assets/tasks.py:320 assets/tasks.py:427
msgid "Asset may not be support ansible, skip: {}"
msgstr "资产或许不支持ansible, 跳过: {}"
......@@ -770,31 +763,27 @@ msgid "No assets, task stop"
msgstr "没有匹配到资产,结束任务"
#: assets/tasks.py:280
msgid "Test assets connectability: {}"
msgid "Test assets connectivity: {}"
msgstr "测试资产可连接性: {}"
#: assets/tasks.py:339
msgid "Test system user connectability: {}"
msgid "Test system user connectivity: {}"
msgstr "测试系统用户可连接性: {}"
#: assets/tasks.py:346
msgid "Test system user connectability: {} => {}"
msgid "Test system user connectivity: {} => {}"
msgstr "测试系统用户可连接性: {} => {}"
#: assets/tasks.py:359
msgid "Test system user connectability period: {}"
msgstr "定期测试系统用户可连接性: {}"
#: assets/tasks.py:413
#: assets/tasks.py:414
msgid ""
"Push system user task skip, auto push not enable or protocol is not ssh: {}"
msgstr "推送系统用户任务跳过,自动推送没有打开,或协议不是ssh: {}"
#: assets/tasks.py:444 assets/tasks.py:458
#: assets/tasks.py:445 assets/tasks.py:459
msgid "Push system users to assets: {}"
msgstr "推送系统用户到入资产: {}"
#: assets/tasks.py:450
#: assets/tasks.py:451
msgid "Push system users to asset: {} => {}"
msgstr "推送系统用户到入资产: {} => {}"
......@@ -899,9 +888,9 @@ msgstr "其它"
#: common/templates/common/replay_storage_create.html:139
#: common/templates/common/security_setting.html:70
#: common/templates/common/terminal_setting.html:68
#: perms/templates/perms/asset_permission_create_update.html:69
#: perms/templates/perms/asset_permission_create_update.html:75
#: terminal/templates/terminal/terminal_update.html:47
#: users/templates/users/_user.html:46
#: users/templates/users/_user.html:50
#: users/templates/users/user_bulk_update.html:23
#: users/templates/users/user_detail.html:176
#: users/templates/users/user_password_update.html:71
......@@ -932,11 +921,11 @@ msgstr "重置"
#: common/templates/common/replay_storage_create.html:140
#: common/templates/common/security_setting.html:71
#: common/templates/common/terminal_setting.html:70
#: perms/templates/perms/asset_permission_create_update.html:70
#: perms/templates/perms/asset_permission_create_update.html:76
#: terminal/templates/terminal/command_list.html:103
#: terminal/templates/terminal/session_list.html:127
#: terminal/templates/terminal/terminal_update.html:48
#: users/templates/users/_user.html:47
#: users/templates/users/_user.html:51
#: users/templates/users/forgot_password.html:45
#: users/templates/users/user_bulk_update.html:24
#: users/templates/users/user_list.html:45
......@@ -999,12 +988,12 @@ msgid "Quick update"
msgstr "快速更新"
#: assets/templates/assets/admin_user_assets.html:72
#: assets/templates/assets/asset_detail.html:172
#: assets/templates/assets/asset_detail.html:176
msgid "Test connective"
msgstr "测试可连接性"
#: assets/templates/assets/admin_user_assets.html:75
#: assets/templates/assets/asset_detail.html:175
#: assets/templates/assets/asset_detail.html:179
#: assets/templates/assets/system_user_asset.html:75
#: assets/templates/assets/system_user_asset.html:161
#: assets/templates/assets/system_user_detail.html:151
......@@ -1014,7 +1003,7 @@ msgstr "测试"
#: assets/templates/assets/admin_user_detail.html:24
#: assets/templates/assets/admin_user_list.html:88
#: assets/templates/assets/asset_detail.html:24
#: assets/templates/assets/asset_list.html:171
#: assets/templates/assets/asset_list.html:174
#: assets/templates/assets/cmd_filter_detail.html:29
#: assets/templates/assets/cmd_filter_list.html:57
#: assets/templates/assets/cmd_filter_rule_list.html:86
......@@ -1026,13 +1015,13 @@ msgstr "测试"
#: assets/templates/assets/system_user_detail.html:26
#: assets/templates/assets/system_user_list.html:92 audits/models.py:32
#: perms/templates/perms/asset_permission_detail.html:30
#: perms/templates/perms/asset_permission_list.html:200
#: perms/templates/perms/asset_permission_list.html:166
#: terminal/templates/terminal/terminal_detail.html:16
#: terminal/templates/terminal/terminal_list.html:71
#: users/templates/users/user_detail.html:25
#: users/templates/users/user_group_detail.html:28
#: users/templates/users/user_group_list.html:43
#: users/templates/users/user_list.html:77
#: users/templates/users/user_list.html:80
#: users/templates/users/user_profile.html:155
#: users/templates/users/user_profile.html:185
#: users/templates/users/user_profile.html:194
......@@ -1046,7 +1035,7 @@ msgstr "更新"
#: assets/templates/assets/admin_user_detail.html:28
#: assets/templates/assets/admin_user_list.html:89
#: assets/templates/assets/asset_detail.html:28
#: assets/templates/assets/asset_list.html:172
#: assets/templates/assets/asset_list.html:175
#: assets/templates/assets/cmd_filter_detail.html:33
#: assets/templates/assets/cmd_filter_list.html:58
#: assets/templates/assets/cmd_filter_rule_list.html:87
......@@ -1061,13 +1050,13 @@ msgstr "更新"
#: common/templates/common/terminal_setting.html:112
#: ops/templates/ops/task_list.html:72
#: perms/templates/perms/asset_permission_detail.html:34
#: perms/templates/perms/asset_permission_list.html:201
#: perms/templates/perms/asset_permission_list.html:167
#: terminal/templates/terminal/terminal_list.html:73
#: users/templates/users/user_detail.html:30
#: users/templates/users/user_group_detail.html:32
#: users/templates/users/user_group_list.html:45
#: users/templates/users/user_list.html:81
#: users/templates/users/user_list.html:85
#: users/templates/users/user_list.html:84
#: users/templates/users/user_list.html:88
#: xpack/plugins/cloud/templates/cloud/account_detail.html:29
#: xpack/plugins/cloud/templates/cloud/account_list.html:40
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:32
......@@ -1087,8 +1076,8 @@ msgid "Select nodes"
msgstr "选择节点"
#: assets/templates/assets/admin_user_detail.html:100
#: assets/templates/assets/asset_detail.html:204
#: assets/templates/assets/asset_list.html:633
#: assets/templates/assets/asset_detail.html:208
#: assets/templates/assets/asset_list.html:623
#: assets/templates/assets/cmd_filter_detail.html:106
#: assets/templates/assets/system_user_asset.html:112
#: assets/templates/assets/system_user_detail.html:182
......@@ -1101,7 +1090,7 @@ msgstr "选择节点"
#: users/templates/users/user_detail.html:480
#: users/templates/users/user_group_create_update.html:32
#: users/templates/users/user_group_list.html:88
#: users/templates/users/user_list.html:205
#: users/templates/users/user_list.html:208
#: users/templates/users/user_profile.html:236
#: xpack/plugins/cloud/templates/cloud/account_create_update.html:34
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create.html:36
......@@ -1153,28 +1142,28 @@ msgstr "选择需要修改属性"
msgid "Select all"
msgstr "全选"
#: assets/templates/assets/asset_detail.html:89
#: assets/templates/assets/asset_detail.html:93
msgid "CPU"
msgstr "CPU"
#: assets/templates/assets/asset_detail.html:97
#: assets/templates/assets/asset_detail.html:101
msgid "Disk"
msgstr "硬盘"
#: assets/templates/assets/asset_detail.html:125
#: assets/templates/assets/asset_detail.html:129
#: users/templates/users/user_detail.html:115
#: users/templates/users/user_profile.html:104
msgid "Date joined"
msgstr "创建日期"
#: assets/templates/assets/asset_detail.html:141
#: assets/templates/assets/asset_detail.html:145
#: terminal/templates/terminal/session_detail.html:81
#: users/templates/users/user_detail.html:138
#: users/templates/users/user_profile.html:146
msgid "Quick modify"
msgstr "快速修改"
#: assets/templates/assets/asset_detail.html:147
#: assets/templates/assets/asset_detail.html:151
#: assets/templates/assets/asset_list.html:95
#: assets/templates/assets/user_asset_list.html:47 perms/models.py:34
#: perms/models.py:82
......@@ -1186,20 +1175,20 @@ msgstr "快速修改"
#: users/templates/users/user_detail.html:144
#: users/templates/users/user_granted_asset.html:46
#: users/templates/users/user_group_granted_asset.html:46
#: users/templates/users/user_list.html:28
#: users/templates/users/user_profile.html:63
msgid "Active"
msgstr "激活中"
#: assets/templates/assets/asset_detail.html:164
#: assets/templates/assets/asset_detail.html:168
msgid "Refresh hardware"
msgstr "更新硬件信息"
#: assets/templates/assets/asset_detail.html:167
#: assets/templates/assets/asset_detail.html:171
#: assets/templates/assets/asset_list.html:139
msgid "Refresh"
msgstr "刷新"
#: assets/templates/assets/asset_detail.html:304
#: assets/templates/assets/asset_detail.html:308
#: users/templates/users/user_detail.html:305
#: users/templates/users/user_detail.html:332
msgid "Update successfully!"
......@@ -1292,41 +1281,41 @@ msgstr "仅显示当前节点资产"
msgid "Displays all child node assets"
msgstr "显示所有子节点资产"
#: assets/templates/assets/asset_list.html:208
#: assets/templates/assets/asset_list.html:213
msgid "Create node failed"
msgstr "创建节点失败"
#: assets/templates/assets/asset_list.html:220
#: assets/templates/assets/asset_list.html:225
msgid "Have child node, cancel"
msgstr "存在子节点,不能删除"
#: assets/templates/assets/asset_list.html:222
#: assets/templates/assets/asset_list.html:227
msgid "Have assets, cancel"
msgstr "存在资产,不能删除"
#: assets/templates/assets/asset_list.html:293
#: assets/templates/assets/asset_list.html:298
msgid "Rename success"
msgstr "重命名成功"
#: assets/templates/assets/asset_list.html:294
#: assets/templates/assets/asset_list.html:299
msgid "Rename failed, do not change the root node name"
msgstr "重命名失败,不能更改root节点的名称"
#: assets/templates/assets/asset_list.html:627
#: assets/templates/assets/asset_list.html:617
#: assets/templates/assets/system_user_list.html:137
#: users/templates/users/user_detail.html:380
#: users/templates/users/user_detail.html:406
#: users/templates/users/user_detail.html:474
#: users/templates/users/user_group_list.html:82
#: users/templates/users/user_list.html:199
#: users/templates/users/user_list.html:202
msgid "Are you sure?"
msgstr "你确认吗?"
#: assets/templates/assets/asset_list.html:628
#: assets/templates/assets/asset_list.html:618
msgid "This will delete the selected assets !!!"
msgstr "删除选择资产"
#: assets/templates/assets/asset_list.html:631
#: assets/templates/assets/asset_list.html:621
#: assets/templates/assets/system_user_list.html:141
#: common/templates/common/terminal_setting.html:163
#: users/templates/users/user_detail.html:384
......@@ -1334,21 +1323,21 @@ msgstr "删除选择资产"
#: users/templates/users/user_detail.html:478
#: users/templates/users/user_group_create_update.html:31
#: users/templates/users/user_group_list.html:86
#: users/templates/users/user_list.html:203
#: users/templates/users/user_list.html:206
#: xpack/plugins/orgs/templates/orgs/org_create_update.html:32
msgid "Cancel"
msgstr "取消"
#: assets/templates/assets/asset_list.html:637
#: assets/templates/assets/asset_list.html:627
msgid "Asset Deleted."
msgstr "已被删除"
#: assets/templates/assets/asset_list.html:638
#: assets/templates/assets/asset_list.html:643
#: assets/templates/assets/asset_list.html:628
#: assets/templates/assets/asset_list.html:633
msgid "Asset Delete"
msgstr "删除"
#: assets/templates/assets/asset_list.html:642
#: assets/templates/assets/asset_list.html:632
msgid "Asset Deleting failed."
msgstr "删除失败"
......@@ -1585,7 +1574,7 @@ msgstr "批量更新资产"
msgid "Update asset"
msgstr "更新资产"
#: assets/views/asset.py:293
#: assets/views/asset.py:292
msgid "already exists"
msgstr "已经存在"
......@@ -1680,7 +1669,7 @@ msgid "Filename"
msgstr "文件名"
#: audits/models.py:22 audits/templates/audits/ftp_log_list.html:76
#: ops/templates/ops/command_execution_list.html:49
#: ops/templates/ops/command_execution_list.html:64
#: ops/templates/ops/task_list.html:39 users/models/authentication.py:73
#: users/templates/users/user_detail.html:456 xpack/plugins/cloud/api.py:61
msgid "Success"
......@@ -1706,7 +1695,7 @@ msgstr "修改者"
#: audits/templates/audits/ftp_log_list.html:77
#: ops/templates/ops/adhoc_history.html:52
#: ops/templates/ops/adhoc_history_detail.html:61
#: ops/templates/ops/command_execution_list.html:50
#: ops/templates/ops/command_execution_list.html:65
#: ops/templates/ops/task_history.html:58 perms/models.py:35
#: perms/templates/perms/asset_permission_detail.html:86 terminal/models.py:148
#: terminal/templates/terminal/session_list.html:78
......@@ -1722,8 +1711,8 @@ msgstr "选择用户"
#: audits/templates/audits/login_log_list.html:40
#: audits/templates/audits/operate_log_list.html:58
#: audits/templates/audits/password_change_log_list.html:42
#: ops/templates/ops/command_execution_list.html:30
#: ops/templates/ops/command_execution_list.html:35
#: ops/templates/ops/command_execution_list.html:42
#: ops/templates/ops/command_execution_list.html:47
#: ops/templates/ops/task_list.html:21 ops/templates/ops/task_list.html:26
#: templates/_base_list.html:43 templates/_header_bar.html:8
#: terminal/templates/terminal/command_list.html:60
......@@ -1751,7 +1740,7 @@ msgstr "Agent"
msgid "City"
msgstr "城市"
#: audits/templates/audits/login_log_list.html:54 users/forms.py:168
#: audits/templates/audits/login_log_list.html:54 users/forms.py:172
#: users/models/authentication.py:82 users/models/user.py:75
#: users/templates/users/first_login.html:45
msgid "MFA"
......@@ -1785,23 +1774,23 @@ msgid "Datetime"
msgstr "日期"
#: audits/views.py:68 audits/views.py:112 audits/views.py:148
#: audits/views.py:192 audits/views.py:223 templates/_nav.html:71
#: audits/views.py:192 audits/views.py:223 templates/_nav.html:72
msgid "Audits"
msgstr "日志审计"
#: audits/views.py:69 templates/_nav.html:75
#: audits/views.py:69 templates/_nav.html:76
msgid "FTP log"
msgstr "FTP日志"
#: audits/views.py:113 templates/_nav.html:76
#: audits/views.py:113 templates/_nav.html:77
msgid "Operate log"
msgstr "操作日志"
#: audits/views.py:149 templates/_nav.html:77
#: audits/views.py:149 templates/_nav.html:78
msgid "Password change log"
msgstr "改密日志"
#: audits/views.py:193 templates/_nav.html:74
#: audits/views.py:193 templates/_nav.html:75
msgid "Login log"
msgstr "登录日志"
......@@ -1958,64 +1947,86 @@ msgid "Enable LDAP auth"
msgstr "启用LDAP认证"
#: common/forms.py:138
msgid "All"
msgstr "全部"
#: common/forms.py:139
msgid "Auto"
msgstr "自动"
#: common/forms.py:146
msgid "Password auth"
msgstr "密码认证"
#: common/forms.py:141
#: common/forms.py:149
msgid "Public key auth"
msgstr "密钥认证"
#: common/forms.py:144
#: common/forms.py:152
msgid "Heartbeat interval"
msgstr "心跳间隔"
#: common/forms.py:144 ops/models/adhoc.py:38
#: common/forms.py:152 ops/models/adhoc.py:38
msgid "Units: seconds"
msgstr "单位: 秒"
#: common/forms.py:147
#: common/forms.py:155
msgid "List sort by"
msgstr "资产列表排序"
#: common/forms.py:159
#: common/forms.py:158
msgid "List page size"
msgstr "资产分页每页数量"
#: common/forms.py:161
msgid "Session keep duration"
msgstr "会话保留时长"
#: common/forms.py:162
msgid ""
"Units: days, Session, record, command will be delete if more than duration, "
"only in database"
msgstr "单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss等不受影响)"
#: common/forms.py:175
msgid "MFA Secondary certification"
msgstr "MFA 二次认证"
#: common/forms.py:161
#: common/forms.py:177
msgid ""
"After opening, the user login must use MFA secondary authentication (valid "
"for all users, including administrators)"
msgstr "开启后,用户登录必须使用MFA二次认证(对所有用户有效,包括管理员)"
#: common/forms.py:168
#: common/forms.py:184
msgid "Limit the number of login failures"
msgstr "限制登录失败次数"
#: common/forms.py:173
#: common/forms.py:189
msgid "No logon interval"
msgstr "禁止登录时间间隔"
#: common/forms.py:175
#: common/forms.py:191
msgid ""
"Tip: (unit/minute) if the user has failed to log in for a limited number of "
"times, no login is allowed during this time interval."
msgstr ""
"提示:(单位:分)当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录"
#: common/forms.py:182
#: common/forms.py:198
msgid "Connection max idle time"
msgstr "SSH最大空闲时间"
#: common/forms.py:184
#: common/forms.py:200
msgid ""
"If idle time more than it, disconnect connection(only ssh now) Unit: minute"
msgstr "提示:(单位:分)如果超过该配置没有操作,连接会被断开(仅ssh)"
#: common/forms.py:190
#: common/forms.py:206
msgid "Password expiration time"
msgstr "密码过期时间"
#: common/forms.py:193
#: common/forms.py:209
msgid ""
"Tip: (unit: day) If the user does not update the password during the time, "
"the user password will expire failure;The password expiration reminder mail "
......@@ -2025,45 +2036,45 @@ msgstr ""
"提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期"
"提醒邮件将在密码过期前5天内由系统(每天)自动发送给用户"
#: common/forms.py:202
#: common/forms.py:218
msgid "Password minimum length"
msgstr "密码最小长度 "
#: common/forms.py:208
#: common/forms.py:224
msgid "Must contain capital letters"
msgstr "必须包含大写字母"
#: common/forms.py:210
#: common/forms.py:226
msgid ""
"After opening, the user password changes and resets must contain uppercase "
"letters"
msgstr "开启后,用户密码修改、重置必须包含大写字母"
#: common/forms.py:216
#: common/forms.py:232
msgid "Must contain lowercase letters"
msgstr "必须包含小写字母"
#: common/forms.py:217
#: common/forms.py:233
msgid ""
"After opening, the user password changes and resets must contain lowercase "
"letters"
msgstr "开启后,用户密码修改、重置必须包含小写字母"
#: common/forms.py:223
#: common/forms.py:239
msgid "Must contain numeric characters"
msgstr "必须包含数字字符"
#: common/forms.py:224
#: common/forms.py:240
msgid ""
"After opening, the user password changes and resets must contain numeric "
"characters"
msgstr "开启后,用户密码修改、重置必须包含数字字符"
#: common/forms.py:230
#: common/forms.py:246
msgid "Must contain special characters"
msgstr "必须包含特殊字符"
#: common/forms.py:231
#: common/forms.py:247
msgid ""
"After opening, the user password changes and resets must contain special "
"characters"
......@@ -2126,7 +2137,7 @@ msgstr "安全设置"
#: common/templates/common/command_storage_create.html:50
#: ops/models/adhoc.py:161 ops/templates/ops/adhoc_detail.html:53
#: ops/templates/ops/command_execution_list.html:44
#: ops/templates/ops/command_execution_list.html:58
#: ops/templates/ops/task_adhoc.html:59 ops/templates/ops/task_list.html:38
msgid "Hosts"
msgstr "主机"
......@@ -2227,7 +2238,7 @@ msgstr "不能包含特殊字符"
#: common/views.py:18 common/views.py:44 common/views.py:70 common/views.py:99
#: common/views.py:126 common/views.py:138 common/views.py:151
#: templates/_nav.html:106
#: templates/_nav.html:107
msgid "Settings"
msgstr "系统设置"
......@@ -2339,15 +2350,15 @@ msgstr "汇总"
msgid "Result"
msgstr "结果"
#: ops/models/command.py:52
#: ops/models/command.py:55
msgid "Task start"
msgstr "任务开始"
#: ops/models/command.py:64
#: ops/models/command.py:67
msgid "Command `{}` is forbidden ........"
msgstr "命令 `{}` 不允许被执行 ......."
#: ops/models/command.py:70
#: ops/models/command.py:73
msgid "Task end"
msgstr "任务结束"
......@@ -2357,12 +2368,14 @@ msgid "Version detail"
msgstr "版本详情"
#: ops/templates/ops/adhoc_detail.html:22
#: ops/templates/ops/adhoc_history.html:22 ops/views/adhoc.py:124
#: ops/templates/ops/adhoc_history.html:22 ops/views/adhoc.py:127
msgid "Version run history"
msgstr "执行历史"
#: ops/templates/ops/adhoc_detail.html:72
#: ops/templates/ops/adhoc_detail.html:77 ops/templates/ops/task_adhoc.html:61
#: ops/templates/ops/adhoc_detail.html:77
#: ops/templates/ops/command_execution_list.html:61
#: ops/templates/ops/task_adhoc.html:61
msgid "Run as"
msgstr "运行用户"
......@@ -2419,12 +2432,12 @@ msgstr "失败/成功/总"
msgid "Version"
msgstr "版本"
#: ops/templates/ops/adhoc_history_detail.html:19 ops/views/adhoc.py:137
#: ops/templates/ops/adhoc_history_detail.html:19 ops/views/adhoc.py:140
msgid "Run history detail"
msgstr "执行历史详情"
#: ops/templates/ops/adhoc_history_detail.html:22
#: ops/templates/ops/command_execution_list.html:47
#: ops/templates/ops/command_execution_list.html:62
#: terminal/backends/command/models.py:16
msgid "Output"
msgstr "输出"
......@@ -2450,31 +2463,27 @@ msgstr "没有资产"
msgid "Success assets"
msgstr "成功资产"
#: ops/templates/ops/command_execution_create.html:67
#: ops/templates/ops/command_execution_create.html:71
#: terminal/templates/terminal/session_detail.html:91
#: terminal/templates/terminal/session_detail.html:100
msgid "Go"
msgstr ""
#: ops/templates/ops/command_execution_create.html:244
#: ops/templates/ops/command_execution_create.html:253
msgid "Pending"
msgstr ""
#: ops/templates/ops/command_execution_list.html:48
#: ops/templates/ops/command_execution_list.html:63
msgid "Finished"
msgstr "结束"
#: ops/templates/ops/command_execution_list.html:51
msgid "Date finished"
msgstr "结束日期"
#: ops/templates/ops/task_adhoc.html:19 ops/templates/ops/task_detail.html:20
#: ops/templates/ops/task_history.html:19 ops/views/adhoc.py:72
#: ops/templates/ops/task_history.html:19 ops/views/adhoc.py:75
msgid "Task detail"
msgstr "任务详情"
#: ops/templates/ops/task_adhoc.html:22 ops/templates/ops/task_detail.html:23
#: ops/templates/ops/task_history.html:22 ops/views/adhoc.py:85
#: ops/templates/ops/task_history.html:22 ops/views/adhoc.py:88
msgid "Task versions"
msgstr "任务各版本"
......@@ -2521,9 +2530,9 @@ msgstr "任务开始: "
msgid "Update task content: {}"
msgstr "更新任务内容: {}"
#: ops/views/adhoc.py:49 ops/views/adhoc.py:71 ops/views/adhoc.py:84
#: ops/views/adhoc.py:97 ops/views/adhoc.py:110 ops/views/adhoc.py:123
#: ops/views/adhoc.py:136 ops/views/command.py:43 ops/views/command.py:67
#: ops/views/adhoc.py:49 ops/views/adhoc.py:74 ops/views/adhoc.py:87
#: ops/views/adhoc.py:100 ops/views/adhoc.py:113 ops/views/adhoc.py:126
#: ops/views/adhoc.py:139 ops/views/command.py:43 ops/views/command.py:67
msgid "Ops"
msgstr "作业中心"
......@@ -2531,11 +2540,12 @@ msgstr "作业中心"
msgid "Task list"
msgstr "任务列表"
#: ops/views/adhoc.py:98
#: ops/views/adhoc.py:101
msgid "Task run history"
msgstr "执行历史"
#: ops/views/command.py:68 templates/_nav.html:78 templates/_nav_user.html:9
#: ops/views/command.py:68 templates/_nav.html:67 templates/_nav.html:79
#: templates/_nav_user.html:9
msgid "Command execution"
msgstr "命令执行"
......@@ -2545,8 +2555,8 @@ msgstr "组织管理"
#: perms/forms.py:31 perms/models.py:30 perms/models.py:80
#: perms/templates/perms/asset_permission_list.html:55
#: perms/templates/perms/asset_permission_list.html:145 templates/_nav.html:14
#: users/forms.py:280 users/models/group.py:26 users/models/user.py:59
#: perms/templates/perms/asset_permission_list.html:111 templates/_nav.html:14
#: users/forms.py:284 users/models/group.py:26 users/models/user.py:59
#: users/templates/users/_select_user_modal.html:16
#: users/templates/users/user_detail.html:211
#: users/templates/users/user_list.html:26
......@@ -2688,14 +2698,14 @@ msgstr "文档"
msgid "Commercial support"
msgstr "商业支持"
#: templates/_header_bar.html:89 templates/_nav_user.html:14 users/forms.py:147
#: users/templates/users/_user.html:39
#: templates/_header_bar.html:89 templates/_nav_user.html:14 users/forms.py:151
#: users/templates/users/_user.html:43
#: users/templates/users/first_login.html:39
#: users/templates/users/user_password_update.html:40
#: users/templates/users/user_profile.html:17
#: users/templates/users/user_profile_update.html:37
#: users/templates/users/user_profile_update.html:57
#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:366
#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:368
msgid "Profile"
msgstr "个人信息"
......@@ -2783,8 +2793,8 @@ msgstr ""
#: templates/_nav.html:10 users/views/group.py:28 users/views/group.py:44
#: users/views/group.py:60 users/views/group.py:76 users/views/group.py:92
#: users/views/login.py:346 users/views/user.py:68 users/views/user.py:83
#: users/views/user.py:111 users/views/user.py:192 users/views/user.py:353
#: users/views/user.py:403 users/views/user.py:437
#: users/views/user.py:113 users/views/user.py:194 users/views/user.py:355
#: users/views/user.py:405 users/views/user.py:439
msgid "Users"
msgstr "用户管理"
......@@ -2831,15 +2841,15 @@ msgstr "终端管理"
msgid "Job Center"
msgstr "作业中心"
#: templates/_nav.html:84
#: templates/_nav.html:85
msgid "XPack"
msgstr ""
#: templates/_nav.html:92 xpack/plugins/cloud/views.py:26
#: templates/_nav.html:93 xpack/plugins/cloud/views.py:26
msgid "Account list"
msgstr "账户列表"
#: templates/_nav.html:93
#: templates/_nav.html:94
msgid "Sync instance"
msgstr "同步实例"
......@@ -3309,11 +3319,11 @@ msgstr "MFA 验证码"
msgid "Role"
msgstr "角色"
#: users/forms.py:55 users/forms.py:226
#: users/forms.py:55 users/forms.py:230
msgid "ssh public key"
msgstr "ssh公钥"
#: users/forms.py:56 users/forms.py:227
#: users/forms.py:56 users/forms.py:231
msgid "ssh-rsa AAAA..."
msgstr ""
......@@ -3325,15 +3335,15 @@ msgstr "复制用户公钥到这里"
msgid "Join user groups"
msgstr "添加到用户组"
#: users/forms.py:110 users/forms.py:241
#: users/forms.py:110 users/forms.py:245
msgid "Public key should not be the same as your old one."
msgstr "不能和原来的密钥相同"
#: users/forms.py:114 users/forms.py:245 users/serializers/v1.py:51
#: users/forms.py:114 users/forms.py:249 users/serializers/v1.py:38
msgid "Not a valid ssh public key"
msgstr "ssh密钥不合法"
#: users/forms.py:153
#: users/forms.py:157
msgid ""
"Tip: when enabled, you will enter the MFA binding process the next time you "
"log in. you can also directly bind in \"personal information -> quick "
......@@ -3342,11 +3352,11 @@ msgstr ""
"提示:启用之后您将会在下次登录时进入MFA绑定流程;您也可以在(个人信息->快速修"
"改->更改MFA设置)中直接绑定!"
#: users/forms.py:163
#: users/forms.py:167
msgid "* Enable MFA authentication to make the account more secure."
msgstr "* 启用MFA认证,使账号更加安全."
#: users/forms.py:173
#: users/forms.py:177
msgid ""
"In order to protect you and your company, please keep your account, password "
"and key sensitive information properly. (for example: setting complex "
......@@ -3355,41 +3365,41 @@ msgstr ""
"为了保护您和公司的安全,请妥善保管您的账户、密码和密钥等重要敏感信息;(如:"
"设置复杂密码,启用MFA认证)"
#: users/forms.py:180 users/templates/users/first_login.html:48
#: users/forms.py:184 users/templates/users/first_login.html:48
#: users/templates/users/first_login.html:107
#: users/templates/users/first_login.html:130
msgid "Finish"
msgstr "完成"
#: users/forms.py:186
#: users/forms.py:190
msgid "Old password"
msgstr "原来密码"
#: users/forms.py:191
#: users/forms.py:195
msgid "New password"
msgstr "新密码"
#: users/forms.py:196
#: users/forms.py:200
msgid "Confirm password"
msgstr "确认密码"
#: users/forms.py:206
#: users/forms.py:210
msgid "Old password error"
msgstr "原来密码错误"
#: users/forms.py:214
#: users/forms.py:218
msgid "Password does not match"
msgstr "密码不一致"
#: users/forms.py:224
#: users/forms.py:228
msgid "Automatically configure and download the SSH key"
msgstr "自动配置并下载SSH密钥"
#: users/forms.py:228
#: users/forms.py:232
msgid "Paste your id_rsa.pub here."
msgstr "复制你的公钥到这里"
#: users/forms.py:256 users/models/user.py:83
#: users/forms.py:260 users/models/user.py:83
#: users/templates/users/first_login.html:42
#: users/templates/users/user_password_update.html:46
#: users/templates/users/user_profile.html:68
......@@ -3398,7 +3408,7 @@ msgstr "复制你的公钥到这里"
msgid "Public key"
msgstr "ssh公钥"
#: users/forms.py:263 users/forms.py:268 users/forms.py:314
#: users/forms.py:267 users/forms.py:272 users/forms.py:318
#: xpack/plugins/orgs/forms.py:30
msgid "Select users"
msgstr "选择用户"
......@@ -3456,7 +3466,7 @@ msgstr "Agent"
msgid "Date login"
msgstr "登录日期"
#: users/models/user.py:32 users/models/user.py:418
#: users/models/user.py:32 users/models/user.py:428
msgid "Administrator"
msgstr "管理员"
......@@ -3502,7 +3512,7 @@ msgstr "用户来源"
msgid "Date password last updated"
msgstr "最后更新密码日期"
#: users/models/user.py:421
#: users/models/user.py:431
msgid "Administrator is the super user of system"
msgstr "Administrator是初始的超级管理员"
......@@ -3790,7 +3800,7 @@ msgid "Reset link will be generated and sent to the user. "
msgstr "生成重置密码连接,通过邮件发送给用户"
#: users/templates/users/user_detail.html:19
#: users/templates/users/user_granted_asset.html:18 users/views/user.py:193
#: users/templates/users/user_granted_asset.html:18 users/views/user.py:195
msgid "User detail"
msgstr "用户详情"
......@@ -3923,23 +3933,37 @@ msgstr "用户组删除"
msgid "UserGroup Deleting failed."
msgstr "用户组删除失败"
#: users/templates/users/user_list.html:200
#: users/templates/users/user_list.html:28 xpack/plugins/cloud/models.py:52
#: xpack/plugins/cloud/templates/cloud/account_detail.html:60
#: xpack/plugins/cloud/templates/cloud/account_list.html:14
msgid "Validity"
msgstr "账户状态"
#: users/templates/users/user_list.html:203
msgid "This will delete the selected users !!!"
msgstr "删除选中用户 !!!"
#: users/templates/users/user_list.html:209
#: users/templates/users/user_list.html:212
msgid "User Deleted."
msgstr "已被删除"
#: users/templates/users/user_list.html:210
#: users/templates/users/user_list.html:215
#: users/templates/users/user_list.html:213
#: users/templates/users/user_list.html:218
msgid "User Delete"
msgstr "删除"
#: users/templates/users/user_list.html:214
#: users/templates/users/user_list.html:217
msgid "User Deleting failed."
msgstr "用户删除失败"
#: users/templates/users/user_list.html:253
msgid "User is expired"
msgstr "用户已失效"
#: users/templates/users/user_list.html:256
msgid "User is inactive"
msgstr "用户已禁用"
#: users/templates/users/user_otp_authentication.html:6
#: users/templates/users/user_password_authentication.html:6
msgid "Authenticate"
......@@ -3985,8 +4009,8 @@ msgstr "安装完成后点击下一步进入绑定页面(如已安装,直接
msgid "Administrator Settings force MFA login"
msgstr "管理员设置强制使用MFA登录"
#: users/templates/users/user_profile.html:120 users/views/user.py:229
#: users/views/user.py:283
#: users/templates/users/user_profile.html:120 users/views/user.py:231
#: users/views/user.py:285
msgid "User groups"
msgstr "用户组"
......@@ -4036,7 +4060,7 @@ msgid ""
"corresponding private key."
msgstr "新的公钥已设置成功,请下载对应的私钥"
#: users/templates/users/user_update.html:4 users/views/user.py:112
#: users/templates/users/user_update.html:4 users/views/user.py:114
msgid "Update user"
msgstr "更新用户"
......@@ -4243,7 +4267,7 @@ msgstr "用户组授权资产"
msgid "Please enable cookies and try again."
msgstr "设置你的浏览器支持cookie"
#: users/views/login.py:191 users/views/user.py:524 users/views/user.py:549
#: users/views/login.py:191 users/views/user.py:526 users/views/user.py:551
msgid "MFA code invalid, or ntp sync server time"
msgstr "MFA验证码不正确,或者服务器端时间不对"
......@@ -4288,7 +4312,7 @@ msgstr "Token错误或失效"
msgid "Password not same"
msgstr "密码不一致"
#: users/views/login.py:308 users/views/user.py:126 users/views/user.py:420
#: users/views/login.py:308 users/views/user.py:128 users/views/user.py:422
msgid "* Your password does not meet the requirements"
msgstr "* 您的密码不符合要求"
......@@ -4296,51 +4320,51 @@ msgstr "* 您的密码不符合要求"
msgid "First login"
msgstr "首次登陆"
#: users/views/user.py:143
#: users/views/user.py:145
msgid "Bulk update user success"
msgstr "批量更新用户成功"
#: users/views/user.py:173
#: users/views/user.py:175
msgid "Bulk update user"
msgstr "批量更新用户"
#: users/views/user.py:258
#: users/views/user.py:260
msgid "Invalid file."
msgstr "文件不合法"
#: users/views/user.py:354
#: users/views/user.py:356
msgid "User granted assets"
msgstr "用户授权资产"
#: users/views/user.py:385
#: users/views/user.py:387
msgid "Profile setting"
msgstr "个人信息设置"
#: users/views/user.py:404
#: users/views/user.py:406
msgid "Password update"
msgstr "密码更新"
#: users/views/user.py:438
#: users/views/user.py:440
msgid "Public key update"
msgstr "密钥更新"
#: users/views/user.py:479
#: users/views/user.py:481
msgid "Password invalid"
msgstr "用户名或密码无效"
#: users/views/user.py:579
#: users/views/user.py:581
msgid "MFA enable success"
msgstr "MFA 绑定成功"
#: users/views/user.py:580
#: users/views/user.py:582
msgid "MFA enable success, return login page"
msgstr "MFA 绑定成功,返回到登录页面"
#: users/views/user.py:582
#: users/views/user.py:584
msgid "MFA disable success"
msgstr "MFA 解绑成功"
#: users/views/user.py:583
#: users/views/user.py:585
msgid "MFA disable success, return login page"
msgstr "MFA 解绑成功,返回登录页面"
......@@ -4406,12 +4430,6 @@ msgstr ""
msgid "Access key secret"
msgstr ""
#: xpack/plugins/cloud/models.py:52
#: xpack/plugins/cloud/templates/cloud/account_detail.html:60
#: xpack/plugins/cloud/templates/cloud/account_list.html:14
msgid "Validity"
msgstr "账户状态"
#: xpack/plugins/cloud/models.py:120
msgid "Regions"
msgstr "地域"
......@@ -4490,7 +4508,7 @@ msgstr "创建账户"
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create.html:91
msgid "Loading..."
msgstr ""
msgstr "加载中..."
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create.html:106
msgid "Load failed"
......@@ -4605,6 +4623,23 @@ msgstr "创建组织"
msgid "Update org"
msgstr "更新组织"
#, fuzzy
#~| msgid "Validity"
#~ msgid "Valid"
#~ msgstr "账户状态"
#~ msgid "You can't update the root node name"
#~ msgstr "不能修改根节点名称"
#~ msgid "Update assets hardware info period"
#~ msgstr "定期更新资产硬件信息"
#~ msgid "Test system user connectivity period: {}"
#~ msgstr "定期测试系统用户可连接性: {}"
#~ msgid "Date finished"
#~ msgstr "结束日期"
#, fuzzy
#~| msgid "Audits"
#~ msgid "Audit"
......
......@@ -24,8 +24,10 @@ class TaskViewSet(viewsets.ModelViewSet):
def get_queryset(self):
queryset = super().get_queryset()
if current_org:
if current_org.is_real():
queryset = queryset.filter(created_by=current_org.id)
else:
queryset = queryset.filter(created_by='')
return queryset
......
......@@ -53,7 +53,7 @@ class AdHocRunHistorySerializer(serializers.ModelSerializer):
@staticmethod
def get_stat(obj):
return {
"total": len(obj.adhoc.hosts),
"total": obj.adhoc.hosts.count(),
"success": len(obj.summary.get("contacted", [])),
"failed": len(obj.summary.get("dark", [])),
}
......
......@@ -11,6 +11,7 @@ app_name = "ops"
router = DefaultRouter()
router.register(r'tasks', api.TaskViewSet, 'task')
router.register(r'adhoc', api.AdHocViewSet, 'adhoc')
router.register(r'history', api.AdHocRunHistoryViewSet, 'history')
router.register(r'command-executions', api.CommandExecutionViewSet, 'command-execution')
urlpatterns = [
......
......@@ -62,8 +62,11 @@ class TaskDetailView(AdminUserRequiredMixin, DetailView):
def get_queryset(self):
queryset = super().get_queryset()
if current_org:
# Todo: 需要整理默认组织等东西
if current_org.is_real():
queryset = queryset.filter(created_by=current_org.id)
else:
queryset = queryset.filter(created_by='')
return queryset
def get_context_data(self, **kwargs):
......
......@@ -74,7 +74,7 @@ class OrgManager(models.Manager):
class OrgModelMixin(models.Model):
org_id = models.CharField(max_length=36, blank=True, default='', verbose_name=_("Organization"))
org_id = models.CharField(max_length=36, blank=True, default='', verbose_name=_("Organization"), db_index=True)
objects = OrgManager()
sep = '@'
......
......@@ -16,7 +16,7 @@ from orgs.utils import set_to_root_org
from .utils import AssetPermissionUtil
from .models import AssetPermission
from .hands import AssetGrantedSerializer, User, UserGroup, Asset, Node, \
NodeGrantedSerializer, SystemUser, NodeSerializer
SystemUser, NodeSerializer
from . import serializers
from .mixins import AssetsFilterMixin
......@@ -150,7 +150,7 @@ class UserGrantedNodesWithAssetsApi(AssetsFilterMixin, ListAPIView):
用户授权的节点并带着节点下资产的api
"""
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = NodeGrantedSerializer
serializer_class = serializers.NodeGrantedSerializer
def change_org_if_need(self):
if self.request.user.is_superuser or \
......@@ -228,15 +228,22 @@ class UserGrantedNodesWithAssetsAsTreeApi(ListAPIView):
@staticmethod
def parse_asset_to_tree_node(node, asset, system_users):
system_users_protocol_matched = [s for s in system_users if s.protocol == asset.protocol]
system_user_serializer = serializers.GrantedSystemUserSerializer(
system_users_protocol_matched, many=True
)
asset_serializer = serializers.GrantedAssetSerializer(asset)
icon_skin = 'file'
if asset.platform.lower() == 'windows':
icon_skin = 'windows'
elif asset.platform.lower() == 'linux':
icon_skin = 'linux'
system_users = []
for system_user in system_users_protocol_matched:
system_users.append({
'id': system_user.id,
'name': system_user.name,
'username': system_user.username,
'protocol': system_user.protocol,
'priority': system_user.priority,
'login_mode': system_user.login_mode,
'comment': system_user.comment,
})
data = {
'id': str(asset.id),
'name': asset.hostname,
......@@ -246,9 +253,19 @@ class UserGrantedNodesWithAssetsAsTreeApi(ListAPIView):
'open': False,
'iconSkin': icon_skin,
'meta': {
'system_users': system_user_serializer.data,
'system_users': system_users,
'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)
......@@ -360,7 +377,7 @@ class UserGroupGrantedNodesApi(ListAPIView):
class UserGroupGrantedNodesWithAssetsApi(ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = NodeGrantedSerializer
serializer_class = serializers.NodeGrantedSerializer
def get_queryset(self):
user_group_id = self.kwargs.get('pk', '')
......
......@@ -4,7 +4,7 @@
from common.permissions import AdminUserRequiredMixin
from users.models import User, UserGroup
from assets.models import Asset, SystemUser, Node
from assets.serializers import AssetGrantedSerializer, NodeGrantedSerializer, NodeSerializer
from assets.serializers import AssetGrantedSerializer, NodeSerializer
......@@ -13,11 +13,11 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='assetpermission',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='nodepermission',
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'),
),
]
......@@ -83,6 +83,35 @@ class AssetPermissionNodeSerializer(serializers.ModelSerializer):
return obj.parent_key
class NodeGrantedSerializer(serializers.ModelSerializer):
"""
授权资产组
"""
assets_granted = AssetGrantedSerializer(many=True, read_only=True)
assets_amount = serializers.SerializerMethodField()
parent = serializers.SerializerMethodField()
name = serializers.SerializerMethodField()
class Meta:
model = Node
fields = [
'id', 'key', 'name', 'value', 'parent',
'assets_granted', 'assets_amount', 'org_id',
]
@staticmethod
def get_assets_amount(obj):
return len(obj.assets_granted)
@staticmethod
def get_name(obj):
return obj.name
@staticmethod
def get_parent(obj):
return obj.parent.id
class GrantedNodeSerializer(serializers.ModelSerializer):
class Meta:
model = Node
......
......@@ -113,6 +113,7 @@ $(document).ready(function () {
e.preventDefault();
e.stopPropagation();
$("#asset_list_modal").modal();
initSelectedAssets2Table();
}
})
})
......
......@@ -78,58 +78,24 @@ var zTree, table, show = 0;
function onSelected(event, treeNode) {
setCookie('node_selected', treeNode.id);
var url = table.ajax.url();
if (treeNode.is_node) {
if (treeNode.meta.type === 'node') {
url = setUrlParam(url, 'asset', "");
url = setUrlParam(url, 'node', treeNode.node_id)
url = setUrlParam(url, 'node', treeNode.meta.node.id)
} else {
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);
table.ajax.url(url);
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) {
return treeNode.is_node
if (treeNode) {
return treeNode.meta.type === 'node'
}
return true
}
function makeLabel(data) {
......@@ -235,9 +201,8 @@ function initTree() {
},
async: {
enable: true,
url: "{% url 'api-assets:node-children-2' %}?assets=1&all=",
autoParam:["node_id=id", "name=n", "level=lv"],
dataFilter: filter,
url: "{% url 'api-assets:node-children-tree' %}?assets=1",
autoParam:["id=key", "name=n", "level=lv"],
type: 'get'
},
callback: {
......@@ -245,25 +210,7 @@ function initTree() {
beforeAsync: beforeAsync
}
};
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);
});
zTree = $.fn.zTree.init($("#assetTree"), setting);
}
function toggle() {
......@@ -299,10 +246,10 @@ $(document).ready(function(){
var _nodes = [];
var _assets = [];
$.each(nodes, function (id, node) {
if (node.is_node) {
_nodes.push(node.node_id)
if (node.meta.type === 'node') {
_nodes.push(node.meta.node.id)
} else {
_assets.push(node.node_id)
_assets.push(node.meta.asset.id)
}
});
url += "?assets=" + _assets.join(",") + "&nodes=" + _nodes.join(",");
......
......@@ -812,3 +812,32 @@ function initPopover($container, $progress, $idPassword, $el, password_check_rul
$idPassword.pwstrength(options);
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');
}
});
}
}
......@@ -94,44 +94,15 @@ class SessionReplayViewSet(viewsets.ViewSet):
serializer_class = serializers.ReplaySerializer
permission_classes = (IsOrgAdminOrAppUser,)
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):
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)
if serializer.is_valid():
file = serializer.validated_data['file']
name, err = self.save_to_storage(file)
name, err = session.save_to_storage(file)
if not name:
msg = "Failed save replay `{}`: {}".format(session_id, err)
logger.error(msg)
......@@ -145,7 +116,7 @@ class SessionReplayViewSet(viewsets.ViewSet):
def retrieve(self, request, *args, **kwargs):
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 = {
'type': 'guacamole' if self.session.protocol == 'rdp' else 'json',
......@@ -153,9 +124,9 @@ class SessionReplayViewSet(viewsets.ViewSet):
}
# 新版本和老版本的文件后缀不同
session_path = self.get_session_path() # 存在外部存储上的路径
local_path = self.get_local_path()
local_path_v1 = self.get_local_path(version=1)
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):
......
......@@ -13,11 +13,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='command',
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(
model_name='session',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
]
......@@ -10,14 +10,4 @@ class Migration(migrations.Migration):
]
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'),
),
]
from __future__ import unicode_literals
import os
import uuid
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.conf import settings
from django.core.files.storage import default_storage
from users.models import User
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_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:
db_table = "terminal_session"
ordering = ["-date_start"]
......
......@@ -4,15 +4,20 @@
import datetime
from celery import shared_task
from celery.utils.log import get_task_logger
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, \
after_app_shutdown_clean
from .models import Status, Session
from .models import Status, Session, Command
CACHE_REFRESH_INTERVAL = 10
RUNNING = False
logger = get_task_logger(__name__)
@shared_task
......@@ -34,3 +39,28 @@ def clean_orphan_session():
if not session.terminal or not session.terminal.is_active:
session.is_finished = True
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()
# -*- coding: utf-8 -*-
#
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
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():
return set(list(Session.objects.values_list('user', flat=True)))
return User.objects.values_list('username', flat=True)
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():
......
......@@ -6,7 +6,7 @@ from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination
from ..serializers import UserGroupSerializer, \
UserGroupUpdateMemeberSerializer
UserGroupUpdateMemberSerializer
from ..models import UserGroup
from common.permissions import IsOrgAdmin
from common.mixins import IDInFilterMixin
......@@ -26,5 +26,5 @@ class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet):
class UserGroupUpdateUserApi(generics.RetrieveUpdateAPIView):
queryset = UserGroup.objects.all()
serializer_class = UserGroupUpdateMemeberSerializer
serializer_class = UserGroupUpdateMemberSerializer
permission_classes = (IsOrgAdmin,)
......@@ -46,6 +46,9 @@ class UserViewSet(IDInFilterMixin, BulkModelViewSet):
self.permission_classes = (IsOrgAdminOrAppUser,)
return super().get_permissions()
def allow_bulk_destroy(self, qs, filtered):
return qs.count() == filtered.count()
class UserChangePasswordApi(generics.RetrieveUpdateAPIView):
permission_classes = (IsOrgAdmin,)
......
......@@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='usergroup',
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'),
),
]
......@@ -142,6 +142,18 @@ class User(AbstractUser):
return True
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
def is_expired(self):
if self.date_expired and self.date_expired < timezone.now():
......
......@@ -13,31 +13,18 @@ signer = get_signer()
class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
groups_display = serializers.SerializerMethodField()
groups = serializers.PrimaryKeyRelatedField(
many=True, queryset=UserGroup.objects.all(), required=False
)
class Meta:
model = User
list_serializer_class = BulkListSerializer
exclude = [
'first_name', 'last_name', 'password', '_private_key',
'_public_key', '_otp_secret_key', 'user_permissions'
fields = [
'id', 'name', 'username', 'email', 'groups', 'groups_display',
'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):
......@@ -74,7 +61,7 @@ class UserGroupSerializer(BulkSerializerMixin, serializers.ModelSerializer):
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())
class Meta:
......
......@@ -25,7 +25,7 @@
<th class="text-center">{% trans 'Role' %}</th>
<th class="text-center">{% trans 'User group' %}</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>
</tr>
</thead>
......@@ -66,11 +66,14 @@ function initTable() {
var innerHtml = cellData.length > 20 ? cellData.substring(0, 20) + '...': cellData;
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
}},
{targets: 6, createdCell: function (td, cellData) {
if (!cellData) {
$(td).html('<i class="fa fa-times text-danger"></i>')
} else {
{targets: 6, createdCell: function (td, cellData, rowData) {
if (cellData) {
$(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) {
......@@ -91,9 +94,9 @@ function initTable() {
ajax_url: '{% url "api-users:user-list" %}',
columns: [
{data: "id"}, {data: "name" }, {data: "username" },
{data: "get_role_display", orderable: false},
{data: "role_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: "id", orderable: false}
],
......@@ -246,6 +249,13 @@ $(document).ready(function(){
var uid = $this.data('uid');
var the_url = '{% url "api-users:user-detail" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", uid);
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>
{% endblock %}
......
......@@ -292,7 +292,8 @@ def check_otp_code(otp_secret_key, otp_code):
if not otp_secret_key or not otp_code:
return False
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():
......
......@@ -100,6 +100,9 @@ class Config:
}
AUTH_LDAP_START_TLS = False
#
# OTP_VALID_WINDOW = 0
def __init__(self):
pass
......@@ -200,6 +203,10 @@ class DockerConfig(Config):
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
config = DockerConfig()
......@@ -90,6 +90,9 @@ class Config:
# AUTH_OPENID_CLIENT_ID = 'client-id'
# AUTH_OPENID_CLIENT_SECRET = 'client-secret'
#
# OTP_VALID_WINDOW = 0
def __init__(self):
pass
......
......@@ -26,8 +26,8 @@ LOG_DIR = os.path.join(BASE_DIR, 'logs')
TMP_DIR = os.path.join(BASE_DIR, 'tmp')
HTTP_HOST = CONFIG.HTTP_BIND_HOST or '127.0.0.1'
HTTP_PORT = CONFIG.HTTP_LISTEN_PORT or 8080
DEBUG = CONFIG.DEBUG
LOG_LEVEL = CONFIG.LOG_LEVEL
DEBUG = CONFIG.DEBUG or False
LOG_LEVEL = CONFIG.LOG_LEVEL or 'INFO'
START_TIMEOUT = 40
WORKERS = 4
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment