mirror of https://github.com/jumpserver/jumpserver
perf: asset activity
parent
a3b0439be5
commit
9631dec210
|
@ -90,10 +90,10 @@ class ChangeSecretDashboardApi(APIView):
|
||||||
|
|
||||||
def get_change_secret_asset_queryset(self):
|
def get_change_secret_asset_queryset(self):
|
||||||
qs = self.change_secrets_queryset
|
qs = self.change_secrets_queryset
|
||||||
node_ids = qs.filter(nodes__isnull=False).values_list('nodes', flat=True).distinct()
|
node_ids = qs.values_list('nodes', flat=True).distinct()
|
||||||
nodes = Node.objects.filter(id__in=node_ids)
|
nodes = Node.objects.filter(id__in=node_ids).only('id', 'key')
|
||||||
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
|
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
|
||||||
direct_asset_ids = qs.filter(assets__isnull=False).values_list('assets', flat=True).distinct()
|
direct_asset_ids = qs.values_list('assets', flat=True).distinct()
|
||||||
asset_ids = set(list(direct_asset_ids) + list(node_asset_ids))
|
asset_ids = set(list(direct_asset_ids) + list(node_asset_ids))
|
||||||
return Asset.objects.filter(id__in=asset_ids)
|
return Asset.objects.filter(id__in=asset_ids)
|
||||||
|
|
||||||
|
|
|
@ -20,3 +20,7 @@ class Category(ChoicesMixin, models.TextChoices):
|
||||||
_category = getattr(cls, category.upper(), None)
|
_category = getattr(cls, category.upper(), None)
|
||||||
choices = [(_category.value, _category.label)] if _category else cls.choices
|
choices = [(_category.value, _category.label)] if _category else cls.choices
|
||||||
return choices
|
return choices
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def as_dict(cls):
|
||||||
|
return {choice.value: choice.label for choice in cls}
|
||||||
|
|
|
@ -53,7 +53,7 @@ class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
||||||
return name
|
return name
|
||||||
|
|
||||||
def get_all_assets(self):
|
def get_all_assets(self):
|
||||||
nodes = self.nodes.all()
|
nodes = self.nodes.only("id", "key")
|
||||||
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list("id", flat=True)
|
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list("id", flat=True)
|
||||||
direct_asset_ids = self.assets.all().values_list("id", flat=True)
|
direct_asset_ids = self.assets.all().values_list("id", flat=True)
|
||||||
asset_ids = set(list(direct_asset_ids) + list(node_asset_ids))
|
asset_ids = set(list(direct_asset_ids) + list(node_asset_ids))
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
|
from .assets import *
|
||||||
from .report import *
|
from .report import *
|
||||||
from .users import *
|
from .users import *
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
|
from .activity import *
|
||||||
from .asset import *
|
from .asset import *
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from django.db.models import Count, Q
|
||||||
|
from django.http.response import JsonResponse
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from assets.const import AllTypes
|
||||||
|
from assets.models import Asset
|
||||||
|
from common.permissions import IsValidLicense
|
||||||
|
from common.utils import lazyproperty
|
||||||
|
from rbac.permissions import RBACPermission
|
||||||
|
from reports.api.assets.base import group_stats
|
||||||
|
from reports.mixins import DateRangeMixin
|
||||||
|
from terminal.const import LoginFrom
|
||||||
|
from terminal.models import Session
|
||||||
|
|
||||||
|
__all__ = ['AssetActivityApi']
|
||||||
|
|
||||||
|
|
||||||
|
class AssetActivityApi(DateRangeMixin, APIView):
|
||||||
|
http_method_names = ['get']
|
||||||
|
# TODO: Define the required RBAC permissions for this API
|
||||||
|
rbac_perms = {
|
||||||
|
'GET': 'terminal.view_session',
|
||||||
|
}
|
||||||
|
permission_classes = [RBACPermission, IsValidLicense]
|
||||||
|
|
||||||
|
def get_asset_login_metrics(self, queryset):
|
||||||
|
data = defaultdict(set)
|
||||||
|
for t, asset in queryset.values_list('date_start', 'asset'):
|
||||||
|
date_str = str(t.date())
|
||||||
|
data[date_str].add(asset)
|
||||||
|
|
||||||
|
metrics = [len(data.get(str(d), set())) for d in self.date_range_list]
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def session_qs(self):
|
||||||
|
return Session.objects.all()
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
qs = self.session_qs
|
||||||
|
qs = self.filter_by_date_range(qs, 'date_start')
|
||||||
|
all_type_dict = dict(AllTypes.choices())
|
||||||
|
|
||||||
|
stats = qs.aggregate(
|
||||||
|
total=Count(1),
|
||||||
|
asset_online=Count(1, filter=Q(is_finished=False)),
|
||||||
|
asset_count=Count('asset_id', distinct=True),
|
||||||
|
user_count=Count('user_id', distinct=True),
|
||||||
|
is_success_count=Count(1, filter=Q(is_success=True)),
|
||||||
|
)
|
||||||
|
|
||||||
|
asset_ids = {str(_id) for _id in qs.values_list('asset_id', flat=True).distinct()}
|
||||||
|
assets = Asset.objects.filter(id__in=asset_ids)
|
||||||
|
|
||||||
|
asset_login_by_protocol = group_stats(
|
||||||
|
qs, 'protocol_label', 'protocol'
|
||||||
|
)
|
||||||
|
|
||||||
|
asset_login_by_from = group_stats(
|
||||||
|
qs, 'login_from_label', 'login_from', LoginFrom.as_dict()
|
||||||
|
)
|
||||||
|
|
||||||
|
asset_by_type = group_stats(
|
||||||
|
assets, 'type', 'platform__type', all_type_dict,
|
||||||
|
)
|
||||||
|
dates_metrics_date = [date.strftime('%m-%d') for date in self.date_range_list] or ['0']
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
**stats,
|
||||||
|
'asset_login_by_type': asset_by_type,
|
||||||
|
'asset_login_by_from': asset_login_by_from,
|
||||||
|
'asset_login_by_protocol': asset_login_by_protocol,
|
||||||
|
'asset_login_log_metrics': {
|
||||||
|
'dates_metrics_date': dates_metrics_date,
|
||||||
|
'dates_metrics_total': self.get_asset_login_metrics(qs),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return JsonResponse(payload, status=200)
|
|
@ -1,16 +1,17 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
|
||||||
from django.db.models import Count, Q, F
|
from django.db.models import Count, Q
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from assets.const import AllTypes, Connectivity, Category
|
from assets.const import AllTypes, Connectivity, Category
|
||||||
from assets.models import Asset
|
from assets.models import Asset, Platform
|
||||||
from common.permissions import IsValidLicense
|
from common.permissions import IsValidLicense
|
||||||
from common.utils import lazyproperty
|
from common.utils import lazyproperty
|
||||||
from rbac.permissions import RBACPermission
|
from rbac.permissions import RBACPermission
|
||||||
|
from reports.api.assets.base import group_stats
|
||||||
|
|
||||||
__all__ = ['AssetStatisticApi']
|
__all__ = ['AssetStatisticApi']
|
||||||
|
|
||||||
|
@ -26,56 +27,48 @@ class AssetStatisticApi(APIView):
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def base_qs(self):
|
def base_qs(self):
|
||||||
return Asset.objects.only(
|
return Asset.objects.only(
|
||||||
'id', 'platform', 'zone', 'connectivity', 'created_time'
|
'id', 'platform', 'zone', 'connectivity', 'is_active'
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _group_stats(queryset, alias, key, label_map=None):
|
|
||||||
grouped = (
|
|
||||||
queryset
|
|
||||||
.values(**{alias: F(key)})
|
|
||||||
.annotate(total=Count('id'))
|
|
||||||
)
|
|
||||||
|
|
||||||
data = [
|
|
||||||
{
|
|
||||||
alias: val,
|
|
||||||
'total': cnt,
|
|
||||||
**({'label': label_map.get(val, val)} if label_map else {})
|
|
||||||
}
|
|
||||||
for val, cnt in grouped.values_list(alias, 'total')
|
|
||||||
]
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
qs = self.base_qs
|
qs = self.base_qs
|
||||||
|
all_type_dict = dict(AllTypes.choices())
|
||||||
|
|
||||||
|
platform_by_type = group_stats(
|
||||||
|
Platform.objects.all(), 'type_label', 'type', all_type_dict,
|
||||||
|
)
|
||||||
|
|
||||||
stats = qs.aggregate(
|
stats = qs.aggregate(
|
||||||
total=Count('id'),
|
total=Count(1),
|
||||||
active=Count('id', filter=Q(is_active=True)),
|
active=Count(1, filter=Q(is_active=True)),
|
||||||
connected=Count('id', filter=Q(connectivity=Connectivity.OK)),
|
connected=Count(1, filter=Q(connectivity=Connectivity.OK)),
|
||||||
)
|
)
|
||||||
|
|
||||||
by_type = self._group_stats(
|
by_type = group_stats(
|
||||||
qs, 'type', 'platform__type', dict(AllTypes.choices()),
|
qs, 'type', 'platform__type', all_type_dict,
|
||||||
)
|
)
|
||||||
|
|
||||||
by_category = self._group_stats(
|
by_category = group_stats(
|
||||||
qs, 'category', 'platform__category', dict(Category.choices())
|
qs, 'category', 'platform__category', Category.as_dict()
|
||||||
)
|
)
|
||||||
|
|
||||||
by_zone = self._group_stats(
|
by_zone = group_stats(
|
||||||
qs, 'zone', 'zone__name'
|
qs, 'zone_label', 'zone__name'
|
||||||
)
|
)
|
||||||
|
|
||||||
week_start = timezone.now() + timezone.timedelta(days=7)
|
week_start = timezone.now() + timezone.timedelta(days=7)
|
||||||
assets_added_this_week = qs.filter(date_created__gte=week_start).count()
|
assets_added_this_week_qs = qs.filter(date_created__gte=week_start)
|
||||||
|
assets_added_this_week_by_type = group_stats(
|
||||||
|
assets_added_this_week_qs, 'type', 'platform__type', all_type_dict,
|
||||||
|
)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
**stats,
|
**stats,
|
||||||
'assets_by_platform_type': by_type,
|
'platform_by_type': platform_by_type,
|
||||||
|
'assets_by_type': by_type,
|
||||||
'assets_by_category': by_category,
|
'assets_by_category': by_category,
|
||||||
'assets_by_zone': by_zone,
|
'assets_by_zone': by_zone,
|
||||||
'assets_added_this_week': assets_added_this_week,
|
'assets_added_this_week_count': assets_added_this_week_qs.count(),
|
||||||
|
'assets_added_this_week_by_type': assets_added_this_week_by_type,
|
||||||
}
|
}
|
||||||
return JsonResponse(payload, status=200)
|
return JsonResponse(payload, status=200)
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
from django.db.models import Count, F
|
||||||
|
|
||||||
|
|
||||||
|
def group_stats(queryset, alias, key, label_map=None):
|
||||||
|
grouped = (
|
||||||
|
queryset
|
||||||
|
.exclude(**{f'{key}__isnull': True})
|
||||||
|
.values(**{alias: F(key)})
|
||||||
|
.annotate(total=Count('id'))
|
||||||
|
)
|
||||||
|
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
alias: val,
|
||||||
|
'total': cnt,
|
||||||
|
**({'label': label_map.get(val, val)} if label_map else {})
|
||||||
|
}
|
||||||
|
for val, cnt in grouped.values_list(alias, 'total')
|
||||||
|
]
|
||||||
|
|
||||||
|
return data
|
|
@ -7,5 +7,7 @@ app_name = 'reports'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('reports/', api.ReportViewSet.as_view(), name='report-list'),
|
path('reports/', api.ReportViewSet.as_view(), name='report-list'),
|
||||||
path('reports/users/', api.UserReportApi.as_view(), name='user-list'),
|
path('reports/users/', api.UserReportApi.as_view(), name='user-list'),
|
||||||
path('reports/user-change-password/', api.UserChangeSecretApi.as_view(), name='user-change-password')
|
path('reports/user-change-password/', api.UserChangeSecretApi.as_view(), name='user-change-password'),
|
||||||
|
path('reports/asset-statistic/', api.AssetStatisticApi.as_view(), name='asset-statistic'),
|
||||||
|
path('reports/asset-activity/', api.AssetActivityApi.as_view(), name='asset-activity')
|
||||||
]
|
]
|
||||||
|
|
|
@ -106,3 +106,14 @@ class SessionErrorReason(TextChoices):
|
||||||
replay_upload_failed = 'replay_upload_failed', _('Replay upload failed')
|
replay_upload_failed = 'replay_upload_failed', _('Replay upload failed')
|
||||||
replay_convert_failed = 'replay_convert_failed', _('Replay convert failed')
|
replay_convert_failed = 'replay_convert_failed', _('Replay convert failed')
|
||||||
replay_unsupported = 'replay_unsupported', _('Replay unsupported')
|
replay_unsupported = 'replay_unsupported', _('Replay unsupported')
|
||||||
|
|
||||||
|
|
||||||
|
class LoginFrom(TextChoices):
|
||||||
|
ST = 'ST', 'SSH Terminal'
|
||||||
|
RT = 'RT', 'RDP Terminal'
|
||||||
|
WT = 'WT', 'Web Terminal'
|
||||||
|
DT = 'DT', 'DB Terminal'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def as_dict(cls):
|
||||||
|
return {choice.value: choice.label for choice in cls}
|
||||||
|
|
|
@ -17,17 +17,11 @@ from common.const.signals import OP_LOG_SKIP_SIGNAL
|
||||||
from common.utils import get_object_or_none, lazyproperty
|
from common.utils import get_object_or_none, lazyproperty
|
||||||
from orgs.mixins.models import OrgModelMixin
|
from orgs.mixins.models import OrgModelMixin
|
||||||
from terminal.backends import get_multi_command_storage
|
from terminal.backends import get_multi_command_storage
|
||||||
from terminal.const import SessionType, TerminalType
|
from terminal.const import SessionType, TerminalType, LoginFrom
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
|
||||||
|
|
||||||
class Session(OrgModelMixin):
|
class Session(OrgModelMixin):
|
||||||
class LOGIN_FROM(models.TextChoices):
|
|
||||||
ST = 'ST', 'SSH Terminal'
|
|
||||||
RT = 'RT', 'RDP Terminal'
|
|
||||||
WT = 'WT', 'Web Terminal'
|
|
||||||
DT = 'DT', 'DB Terminal'
|
|
||||||
|
|
||||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
user = models.CharField(max_length=128, verbose_name=_("User"), db_index=True)
|
user = models.CharField(max_length=128, verbose_name=_("User"), db_index=True)
|
||||||
user_id = models.CharField(blank=True, default='', max_length=36, db_index=True)
|
user_id = models.CharField(blank=True, default='', max_length=36, db_index=True)
|
||||||
|
@ -36,7 +30,7 @@ class Session(OrgModelMixin):
|
||||||
account = models.CharField(max_length=128, verbose_name=_("Account"), db_index=True)
|
account = models.CharField(max_length=128, verbose_name=_("Account"), db_index=True)
|
||||||
account_id = models.CharField(max_length=128, verbose_name=_("Account ID"), db_index=True)
|
account_id = models.CharField(max_length=128, verbose_name=_("Account ID"), db_index=True)
|
||||||
protocol = models.CharField(default='ssh', max_length=16, db_index=True)
|
protocol = models.CharField(default='ssh', max_length=16, db_index=True)
|
||||||
login_from = models.CharField(max_length=2, choices=LOGIN_FROM.choices, default="ST", verbose_name=_("Login from"))
|
login_from = models.CharField(max_length=2, choices=LoginFrom.choices, default="ST", verbose_name=_("Login from"))
|
||||||
type = models.CharField(max_length=16, default='normal', db_index=True)
|
type = models.CharField(max_length=16, default='normal', db_index=True)
|
||||||
remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True)
|
remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True)
|
||||||
is_success = models.BooleanField(default=True, db_index=True)
|
is_success = models.BooleanField(default=True, db_index=True)
|
||||||
|
|
|
@ -2,15 +2,15 @@ import datetime
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from common.db.models import JMSBaseModel
|
from common.db.models import JMSBaseModel
|
||||||
from common.utils import is_uuid
|
from common.utils import is_uuid
|
||||||
from orgs.mixins.models import OrgModelMixin
|
from orgs.mixins.models import OrgModelMixin
|
||||||
from orgs.utils import tmp_to_root_org
|
from orgs.utils import tmp_to_root_org
|
||||||
|
from terminal.const import LoginFrom
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from .session import Session
|
|
||||||
|
|
||||||
__all__ = ['SessionSharing', 'SessionJoinRecord']
|
__all__ = ['SessionSharing', 'SessionJoinRecord']
|
||||||
|
|
||||||
|
@ -89,8 +89,6 @@ class SessionSharing(JMSBaseModel, OrgModelMixin):
|
||||||
|
|
||||||
|
|
||||||
class SessionJoinRecord(JMSBaseModel, OrgModelMixin):
|
class SessionJoinRecord(JMSBaseModel, OrgModelMixin):
|
||||||
LOGIN_FROM = Session.LOGIN_FROM
|
|
||||||
|
|
||||||
session = models.ForeignKey(
|
session = models.ForeignKey(
|
||||||
'terminal.Session', on_delete=models.CASCADE, verbose_name=_('Session')
|
'terminal.Session', on_delete=models.CASCADE, verbose_name=_('Session')
|
||||||
)
|
)
|
||||||
|
@ -114,7 +112,7 @@ class SessionJoinRecord(JMSBaseModel, OrgModelMixin):
|
||||||
db_index=True
|
db_index=True
|
||||||
)
|
)
|
||||||
login_from = models.CharField(
|
login_from = models.CharField(
|
||||||
max_length=2, choices=LOGIN_FROM.choices, default="WT",
|
max_length=2, choices=LoginFrom.choices, default="WT",
|
||||||
verbose_name=_("Login from")
|
verbose_name=_("Login from")
|
||||||
)
|
)
|
||||||
is_success = models.BooleanField(
|
is_success = models.BooleanField(
|
||||||
|
|
Loading…
Reference in New Issue