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):
|
||||
qs = self.change_secrets_queryset
|
||||
node_ids = qs.filter(nodes__isnull=False).values_list('nodes', flat=True).distinct()
|
||||
nodes = Node.objects.filter(id__in=node_ids)
|
||||
node_ids = qs.values_list('nodes', flat=True).distinct()
|
||||
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)
|
||||
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))
|
||||
return Asset.objects.filter(id__in=asset_ids)
|
||||
|
||||
|
|
|
@ -20,3 +20,7 @@ class Category(ChoicesMixin, models.TextChoices):
|
|||
_category = getattr(cls, category.upper(), None)
|
||||
choices = [(_category.value, _category.label)] if _category else cls.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
|
||||
|
||||
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)
|
||||
direct_asset_ids = self.assets.all().values_list("id", flat=True)
|
||||
asset_ids = set(list(direct_asset_ids) + list(node_asset_ids))
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
from .assets import *
|
||||
from .report import *
|
||||
from .users import *
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from .activity 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 -*-
|
||||
#
|
||||
|
||||
from django.db.models import Count, Q, F
|
||||
from django.db.models import Count, Q
|
||||
from django.http import JsonResponse
|
||||
from django.utils import timezone
|
||||
from rest_framework.views import APIView
|
||||
|
||||
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.utils import lazyproperty
|
||||
from rbac.permissions import RBACPermission
|
||||
from reports.api.assets.base import group_stats
|
||||
|
||||
__all__ = ['AssetStatisticApi']
|
||||
|
||||
|
@ -26,56 +27,48 @@ class AssetStatisticApi(APIView):
|
|||
@lazyproperty
|
||||
def base_qs(self):
|
||||
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):
|
||||
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(
|
||||
total=Count('id'),
|
||||
active=Count('id', filter=Q(is_active=True)),
|
||||
connected=Count('id', filter=Q(connectivity=Connectivity.OK)),
|
||||
total=Count(1),
|
||||
active=Count(1, filter=Q(is_active=True)),
|
||||
connected=Count(1, filter=Q(connectivity=Connectivity.OK)),
|
||||
)
|
||||
|
||||
by_type = self._group_stats(
|
||||
qs, 'type', 'platform__type', dict(AllTypes.choices()),
|
||||
by_type = group_stats(
|
||||
qs, 'type', 'platform__type', all_type_dict,
|
||||
)
|
||||
|
||||
by_category = self._group_stats(
|
||||
qs, 'category', 'platform__category', dict(Category.choices())
|
||||
by_category = group_stats(
|
||||
qs, 'category', 'platform__category', Category.as_dict()
|
||||
)
|
||||
|
||||
by_zone = self._group_stats(
|
||||
qs, 'zone', 'zone__name'
|
||||
by_zone = group_stats(
|
||||
qs, 'zone_label', 'zone__name'
|
||||
)
|
||||
|
||||
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 = {
|
||||
**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_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)
|
||||
|
|
|
@ -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 = [
|
||||
path('reports/', api.ReportViewSet.as_view(), name='report-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_convert_failed = 'replay_convert_failed', _('Replay convert failed')
|
||||
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 orgs.mixins.models import OrgModelMixin
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
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)
|
||||
|
@ -36,7 +30,7 @@ class Session(OrgModelMixin):
|
|||
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)
|
||||
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)
|
||||
remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True)
|
||||
is_success = models.BooleanField(default=True, db_index=True)
|
||||
|
|
|
@ -2,15 +2,15 @@ import datetime
|
|||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.db.models import JMSBaseModel
|
||||
from common.utils import is_uuid
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from terminal.const import LoginFrom
|
||||
from users.models import User
|
||||
from .session import Session
|
||||
|
||||
__all__ = ['SessionSharing', 'SessionJoinRecord']
|
||||
|
||||
|
@ -89,8 +89,6 @@ class SessionSharing(JMSBaseModel, OrgModelMixin):
|
|||
|
||||
|
||||
class SessionJoinRecord(JMSBaseModel, OrgModelMixin):
|
||||
LOGIN_FROM = Session.LOGIN_FROM
|
||||
|
||||
session = models.ForeignKey(
|
||||
'terminal.Session', on_delete=models.CASCADE, verbose_name=_('Session')
|
||||
)
|
||||
|
@ -114,7 +112,7 @@ class SessionJoinRecord(JMSBaseModel, OrgModelMixin):
|
|||
db_index=True
|
||||
)
|
||||
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")
|
||||
)
|
||||
is_success = models.BooleanField(
|
||||
|
|
Loading…
Reference in New Issue