mirror of https://github.com/jumpserver/jumpserver
perf: 修改 ansible 表结构
parent
df5e63b3be
commit
0fb4b52232
|
@ -4,15 +4,14 @@ from django.db import models
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.const.choices import Trigger
|
||||
from common.mixins.models import CommonModelMixin
|
||||
from common.db.fields import EncryptJsonDictTextField
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel
|
||||
from ops.mixin import PeriodTaskModelMixin
|
||||
from ops.tasks import execute_automation_strategy
|
||||
from ops.task_handlers import ExecutionManager
|
||||
|
||||
|
||||
class BaseAutomation(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin):
|
||||
class BaseAutomation(JMSOrgBaseModel, PeriodTaskModelMixin):
|
||||
accounts = models.JSONField(default=list, verbose_name=_("Accounts"))
|
||||
nodes = models.ManyToManyField(
|
||||
'assets.Node', related_name='automation_strategy', blank=True, verbose_name=_("Nodes")
|
||||
|
@ -67,7 +66,7 @@ class AutomationStrategyExecution(OrgModelMixin):
|
|||
default=dict, blank=True, null=True, verbose_name=_('Automation snapshot')
|
||||
)
|
||||
strategy = models.ForeignKey(
|
||||
'assets.models.automation.base.BaseAutomation', related_name='execution', on_delete=models.CASCADE,
|
||||
'BaseAutomation', related_name='execution', on_delete=models.CASCADE,
|
||||
verbose_name=_('Automation strategy')
|
||||
)
|
||||
trigger = models.CharField(
|
||||
|
|
|
@ -14,16 +14,12 @@ class Label(OrgModelMixin):
|
|||
("S", _("System")),
|
||||
("U", _("User"))
|
||||
)
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_("Name"))
|
||||
value = models.CharField(max_length=128, verbose_name=_("Value"))
|
||||
category = models.CharField(max_length=128, choices=CATEGORY_CHOICES,
|
||||
default=USER_CATEGORY, verbose_name=_("Category"))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
||||
comment = models.TextField(blank=True, null=True, verbose_name=_("Comment"))
|
||||
date_created = models.DateTimeField(
|
||||
auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_queryset_group_by_name(cls):
|
||||
|
|
|
@ -11,16 +11,14 @@ from common.drf.filters import DatetimeRangeFilter
|
|||
from common.api import CommonGenericViewSet
|
||||
from orgs.mixins.api import OrgGenericViewSet, OrgBulkModelViewSet, OrgRelationMixin
|
||||
from orgs.utils import current_org
|
||||
from ops.models import CommandExecution
|
||||
# from ops.models import CommandExecution
|
||||
from . import filters
|
||||
from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog
|
||||
from .serializers import FTPLogSerializer, UserLoginLogSerializer, CommandExecutionSerializer
|
||||
from .serializers import OperateLogSerializer, PasswordChangeLogSerializer, CommandExecutionHostsRelationSerializer
|
||||
from .serializers import FTPLogSerializer, UserLoginLogSerializer
|
||||
from .serializers import OperateLogSerializer, PasswordChangeLogSerializer
|
||||
|
||||
|
||||
class FTPLogViewSet(CreateModelMixin,
|
||||
ListModelMixin,
|
||||
OrgGenericViewSet):
|
||||
class FTPLogViewSet(CreateModelMixin, ListModelMixin, OrgGenericViewSet):
|
||||
model = FTPLog
|
||||
serializer_class = FTPLogSerializer
|
||||
extra_filter_backends = [DatetimeRangeFilter]
|
||||
|
@ -98,53 +96,53 @@ class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet):
|
|||
)
|
||||
return queryset
|
||||
|
||||
|
||||
class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
|
||||
model = CommandExecution
|
||||
serializer_class = CommandExecutionSerializer
|
||||
extra_filter_backends = [DatetimeRangeFilter]
|
||||
date_range_filter_fields = [
|
||||
('date_start', ('date_from', 'date_to'))
|
||||
]
|
||||
filterset_fields = [
|
||||
'user__name', 'user__username', 'command',
|
||||
'account', 'is_finished'
|
||||
]
|
||||
search_fields = [
|
||||
'command', 'user__name', 'user__username',
|
||||
'account__username',
|
||||
]
|
||||
ordering = ['-date_created']
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
if getattr(self, 'swagger_fake_view', False):
|
||||
return queryset.model.objects.none()
|
||||
if current_org.is_root():
|
||||
return queryset
|
||||
# queryset = queryset.filter(run_as__org_id=current_org.org_id())
|
||||
return queryset
|
||||
|
||||
|
||||
class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet):
|
||||
serializer_class = CommandExecutionHostsRelationSerializer
|
||||
m2m_field = CommandExecution.hosts.field
|
||||
filterset_fields = [
|
||||
'id', 'asset', 'commandexecution'
|
||||
]
|
||||
search_fields = ('asset__name', )
|
||||
http_method_names = ['options', 'get']
|
||||
rbac_perms = {
|
||||
'GET': 'ops.view_commandexecution',
|
||||
'list': 'ops.view_commandexecution',
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.annotate(
|
||||
asset_display=Concat(
|
||||
F('asset__name'), Value('('),
|
||||
F('asset__address'), Value(')')
|
||||
)
|
||||
)
|
||||
return queryset
|
||||
# Todo: 看看怎么搞
|
||||
# class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
|
||||
# model = CommandExecution
|
||||
# serializer_class = CommandExecutionSerializer
|
||||
# extra_filter_backends = [DatetimeRangeFilter]
|
||||
# date_range_filter_fields = [
|
||||
# ('date_start', ('date_from', 'date_to'))
|
||||
# ]
|
||||
# filterset_fields = [
|
||||
# 'user__name', 'user__username', 'command',
|
||||
# 'account', 'is_finished'
|
||||
# ]
|
||||
# search_fields = [
|
||||
# 'command', 'user__name', 'user__username',
|
||||
# 'account__username',
|
||||
# ]
|
||||
# ordering = ['-date_created']
|
||||
#
|
||||
# def get_queryset(self):
|
||||
# queryset = super().get_queryset()
|
||||
# if getattr(self, 'swagger_fake_view', False):
|
||||
# return queryset.model.objects.none()
|
||||
# if current_org.is_root():
|
||||
# return queryset
|
||||
# # queryset = queryset.filter(run_as__org_id=current_org.org_id())
|
||||
# return queryset
|
||||
#
|
||||
#
|
||||
# class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet):
|
||||
# serializer_class = CommandExecutionHostsRelationSerializer
|
||||
# m2m_field = CommandExecution.hosts.field
|
||||
# filterset_fields = [
|
||||
# 'id', 'asset', 'commandexecution'
|
||||
# ]
|
||||
# search_fields = ('asset__name', )
|
||||
# http_method_names = ['options', 'get']
|
||||
# rbac_perms = {
|
||||
# 'GET': 'ops.view_commandexecution',
|
||||
# 'list': 'ops.view_commandexecution',
|
||||
# }
|
||||
#
|
||||
# def get_queryset(self):
|
||||
# queryset = super().get_queryset()
|
||||
# queryset = queryset.annotate(
|
||||
# asset_display=Concat(
|
||||
# F('asset__name'), Value('('),
|
||||
# F('asset__address'), Value(')')
|
||||
# )
|
||||
# )
|
||||
# return queryset
|
||||
|
|
|
@ -5,10 +5,9 @@ from rest_framework import filters
|
|||
from rest_framework.compat import coreapi, coreschema
|
||||
|
||||
from orgs.utils import current_org
|
||||
from ops.models import CommandExecution
|
||||
from common.drf.filters import BaseFilterSet
|
||||
|
||||
__all__ = ['CurrentOrgMembersFilter', 'CommandExecutionFilter']
|
||||
__all__ = ['CurrentOrgMembersFilter']
|
||||
|
||||
|
||||
class CurrentOrgMembersFilter(filters.BaseFilterBackend):
|
||||
|
@ -35,21 +34,21 @@ class CurrentOrgMembersFilter(filters.BaseFilterBackend):
|
|||
queryset = queryset.filter(user__in=self._get_user_list())
|
||||
return queryset
|
||||
|
||||
|
||||
class CommandExecutionFilter(BaseFilterSet):
|
||||
hostname_ip = CharFilter(method='filter_hostname_ip')
|
||||
|
||||
class Meta:
|
||||
model = CommandExecution.hosts.through
|
||||
fields = (
|
||||
'id', 'asset', 'commandexecution', 'hostname_ip'
|
||||
)
|
||||
|
||||
def filter_hostname_ip(self, queryset, name, value):
|
||||
queryset = queryset.annotate(
|
||||
hostname_ip=Concat(
|
||||
F('asset__hostname'), Value('('),
|
||||
F('asset__address'), Value(')')
|
||||
)
|
||||
).filter(hostname_ip__icontains=value)
|
||||
return queryset
|
||||
#
|
||||
# class CommandExecutionFilter(BaseFilterSet):
|
||||
# hostname_ip = CharFilter(method='filter_hostname_ip')
|
||||
#
|
||||
# class Meta:
|
||||
# model = CommandExecution.hosts.through
|
||||
# fields = (
|
||||
# 'id', 'asset', 'commandexecution', 'hostname_ip'
|
||||
# )
|
||||
#
|
||||
# def filter_hostname_ip(self, queryset, name, value):
|
||||
# queryset = queryset.annotate(
|
||||
# hostname_ip=Concat(
|
||||
# F('asset__hostname'), Value('('),
|
||||
# F('asset__address'), Value(')')
|
||||
# )
|
||||
# ).filter(hostname_ip__icontains=value)
|
||||
# return queryset
|
||||
|
|
|
@ -5,7 +5,6 @@ from rest_framework import serializers
|
|||
|
||||
from common.drf.serializers import BulkSerializerMixin
|
||||
from terminal.models import Session
|
||||
from ops.models import CommandExecution
|
||||
from . import models
|
||||
|
||||
|
||||
|
@ -76,42 +75,42 @@ class SessionAuditSerializer(serializers.ModelSerializer):
|
|||
model = Session
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class CommandExecutionSerializer(serializers.ModelSerializer):
|
||||
is_success = serializers.BooleanField(read_only=True, label=_('Is success'))
|
||||
hosts_display = serializers.ListSerializer(
|
||||
child=serializers.CharField(), source='hosts', read_only=True, label=_('Hosts display')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CommandExecution
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'command', 'is_finished', 'user',
|
||||
'date_start', 'result', 'is_success', 'org_id'
|
||||
]
|
||||
fields = fields_small + ['hosts', 'hosts_display', 'user_display']
|
||||
extra_kwargs = {
|
||||
'result': {'label': _('Result')}, # model 上的方法,只能在这修改
|
||||
'is_success': {'label': _('Is success')},
|
||||
'hosts': {'label': _('Hosts')}, # 外键,会生成 sql。不在 model 上修改
|
||||
'user': {'label': _('User')},
|
||||
'user_display': {'label': _('User display')},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset.prefetch_related('user', 'hosts')
|
||||
return queryset
|
||||
|
||||
|
||||
class CommandExecutionHostsRelationSerializer(BulkSerializerMixin, serializers.ModelSerializer):
|
||||
asset_display = serializers.ReadOnlyField()
|
||||
commandexecution_display = serializers.ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
model = CommandExecution.hosts.through
|
||||
fields = [
|
||||
'id', 'asset', 'asset_display', 'commandexecution', 'commandexecution_display'
|
||||
]
|
||||
#
|
||||
# class CommandExecutionSerializer(serializers.ModelSerializer):
|
||||
# is_success = serializers.BooleanField(read_only=True, label=_('Is success'))
|
||||
# hosts_display = serializers.ListSerializer(
|
||||
# child=serializers.CharField(), source='hosts', read_only=True, label=_('Hosts display')
|
||||
# )
|
||||
#
|
||||
# class Meta:
|
||||
# model = CommandExecution
|
||||
# fields_mini = ['id']
|
||||
# fields_small = fields_mini + [
|
||||
# 'command', 'is_finished', 'user',
|
||||
# 'date_start', 'result', 'is_success', 'org_id'
|
||||
# ]
|
||||
# fields = fields_small + ['hosts', 'hosts_display', 'user_display']
|
||||
# extra_kwargs = {
|
||||
# 'result': {'label': _('Result')}, # model 上的方法,只能在这修改
|
||||
# 'is_success': {'label': _('Is success')},
|
||||
# 'hosts': {'label': _('Hosts')}, # 外键,会生成 sql。不在 model 上修改
|
||||
# 'user': {'label': _('User')},
|
||||
# 'user_display': {'label': _('User display')},
|
||||
# }
|
||||
#
|
||||
# @classmethod
|
||||
# def setup_eager_loading(cls, queryset):
|
||||
# """ Perform necessary eager loading of data. """
|
||||
# queryset = queryset.prefetch_related('user', 'hosts')
|
||||
# return queryset
|
||||
#
|
||||
#
|
||||
# class CommandExecutionHostsRelationSerializer(BulkSerializerMixin, serializers.ModelSerializer):
|
||||
# asset_display = serializers.ReadOnlyField()
|
||||
# commandexecution_display = serializers.ReadOnlyField()
|
||||
#
|
||||
# class Meta:
|
||||
# model = CommandExecution.hosts.through
|
||||
# fields = [
|
||||
# 'id', 'asset', 'asset_display', 'commandexecution', 'commandexecution_display'
|
||||
# ]
|
||||
|
|
|
@ -15,8 +15,8 @@ router.register(r'ftp-logs', api.FTPLogViewSet, 'ftp-log')
|
|||
router.register(r'login-logs', api.UserLoginLogViewSet, 'login-log')
|
||||
router.register(r'operate-logs', api.OperateLogViewSet, 'operate-log')
|
||||
router.register(r'password-change-logs', api.PasswordChangeLogViewSet, 'password-change-log')
|
||||
router.register(r'command-execution-logs', api.CommandExecutionViewSet, 'command-execution-log')
|
||||
router.register(r'command-executions-hosts-relations', api.CommandExecutionHostRelationViewSet, 'command-executions-hosts-relation')
|
||||
# router.register(r'command-execution-logs', api.CommandExecutionViewSet, 'command-execution-log')
|
||||
# router.register(r'command-executions-hosts-relations', api.CommandExecutionHostRelationViewSet, 'command-executions-hosts-relation')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
|
|
@ -16,6 +16,7 @@ VERSION = const.VERSION
|
|||
BASE_DIR = const.BASE_DIR
|
||||
PROJECT_DIR = const.PROJECT_DIR
|
||||
DATA_DIR = os.path.join(PROJECT_DIR, 'data')
|
||||
ANSIBLE_DIR = os.path.join(DATA_DIR, 'ansible')
|
||||
CERTS_DIR = os.path.join(DATA_DIR, 'certs')
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
|
|
|
@ -15,7 +15,7 @@ class DefaultCallback:
|
|||
dark={},
|
||||
skipped=[],
|
||||
)
|
||||
self.status = 'starting'
|
||||
self.status = 'running'
|
||||
self.finished = False
|
||||
|
||||
def is_success(self):
|
||||
|
|
|
@ -3,21 +3,19 @@ from collections import defaultdict
|
|||
import json
|
||||
|
||||
|
||||
__all__ = [
|
||||
'JMSInventory',
|
||||
]
|
||||
__all__ = ['JMSInventory']
|
||||
|
||||
|
||||
class JMSInventory:
|
||||
def __init__(self, assets, account_username=None, account_policy='smart', host_var_callback=None):
|
||||
def __init__(self, assets, account='', account_policy='smart', host_var_callback=None):
|
||||
"""
|
||||
:param assets:
|
||||
:param account_username: account username name if not set use account_policy
|
||||
:param account: account username name if not set use account_policy
|
||||
:param account_policy:
|
||||
:param host_var_callback:
|
||||
"""
|
||||
self.assets = self.clean_assets(assets)
|
||||
self.account_username = account_username
|
||||
self.account_username = account
|
||||
self.account_policy = account_policy
|
||||
self.host_var_callback = host_var_callback
|
||||
|
||||
|
|
|
@ -68,3 +68,11 @@ class PlaybookRunner:
|
|||
**kwargs
|
||||
)
|
||||
return self.cb
|
||||
|
||||
|
||||
class CommandRunner(AdHocRunner):
|
||||
def __init__(self, inventory, command, pattern='*', project_dir='/tmp/'):
|
||||
super().__init__(inventory, 'shell', command, pattern, project_dir)
|
||||
|
||||
def run(self, verbosity=0, **kwargs):
|
||||
return super().run(verbosity, **kwargs)
|
||||
|
|
|
@ -2,4 +2,3 @@
|
|||
#
|
||||
from .adhoc import *
|
||||
from .celery import *
|
||||
from .command import *
|
||||
|
|
|
@ -6,52 +6,18 @@ from rest_framework import viewsets, generics
|
|||
from rest_framework.views import Response
|
||||
|
||||
from common.drf.serializers import CeleryTaskSerializer
|
||||
from ..models import Task, AdHoc, AdHocExecution
|
||||
from ..models import AdHoc, AdHocExecution
|
||||
from ..serializers import (
|
||||
TaskSerializer,
|
||||
AdHocSerializer,
|
||||
AdHocExecutionSerializer,
|
||||
TaskDetailSerializer,
|
||||
AdHocDetailSerializer,
|
||||
)
|
||||
from ..tasks import run_ansible_task
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
|
||||
__all__ = [
|
||||
'TaskViewSet', 'TaskRun', 'AdHocViewSet', 'AdHocRunHistoryViewSet'
|
||||
'AdHocViewSet', 'AdHocExecutionViewSet'
|
||||
]
|
||||
|
||||
|
||||
class TaskViewSet(OrgBulkModelViewSet):
|
||||
model = Task
|
||||
filterset_fields = ("name",)
|
||||
search_fields = filterset_fields
|
||||
serializer_class = TaskSerializer
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'retrieve':
|
||||
return TaskDetailSerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.select_related('latest_execution')
|
||||
return queryset
|
||||
|
||||
|
||||
class TaskRun(generics.RetrieveAPIView):
|
||||
queryset = Task.objects.all()
|
||||
serializer_class = CeleryTaskSerializer
|
||||
rbac_perms = {
|
||||
'retrieve': 'ops.add_adhoc'
|
||||
}
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
task = self.get_object()
|
||||
t = run_ansible_task.delay(str(task.id))
|
||||
return Response({"task": t.id})
|
||||
|
||||
|
||||
class AdHocViewSet(viewsets.ModelViewSet):
|
||||
queryset = AdHoc.objects.all()
|
||||
serializer_class = AdHocSerializer
|
||||
|
@ -61,23 +27,17 @@ class AdHocViewSet(viewsets.ModelViewSet):
|
|||
return AdHocDetailSerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
def get_queryset(self):
|
||||
task_id = self.request.query_params.get('task')
|
||||
if task_id:
|
||||
task = get_object_or_404(Task, id=task_id)
|
||||
self.queryset = self.queryset.filter(task=task)
|
||||
return self.queryset
|
||||
|
||||
|
||||
class AdHocRunHistoryViewSet(viewsets.ModelViewSet):
|
||||
class AdHocExecutionViewSet(viewsets.ModelViewSet):
|
||||
queryset = AdHocExecution.objects.all()
|
||||
serializer_class = AdHocExecutionSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
task_id = self.request.query_params.get('task')
|
||||
adhoc_id = self.request.query_params.get('adhoc')
|
||||
|
||||
if task_id:
|
||||
task = get_object_or_404(Task, id=task_id)
|
||||
task = get_object_or_404(AdHoc, id=task_id)
|
||||
adhocs = task.adhoc.all()
|
||||
self.queryset = self.queryset.filter(adhoc__in=adhocs)
|
||||
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
|
||||
from assets.models import Asset, Node
|
||||
from orgs.mixins.api import RootOrgViewMixin
|
||||
from rbac.permissions import RBACPermission
|
||||
from ..models import CommandExecution
|
||||
from ..serializers import CommandExecutionSerializer
|
||||
from ..tasks import run_command_execution
|
||||
|
||||
|
||||
class CommandExecutionViewSet(RootOrgViewMixin, viewsets.ModelViewSet):
|
||||
serializer_class = CommandExecutionSerializer
|
||||
permission_classes = (RBACPermission,)
|
||||
|
||||
def get_queryset(self):
|
||||
return CommandExecution.objects.filter(user_id=str(self.request.user.id))
|
||||
|
||||
def check_hosts(self, serializer):
|
||||
data = serializer.validated_data
|
||||
assets = data["hosts"]
|
||||
user = self.request.user
|
||||
|
||||
# TOdo:
|
||||
# Q(granted_by_permissions__system_users__id=system_user.id) &
|
||||
q = (
|
||||
Q(granted_by_permissions__users=user) |
|
||||
Q(granted_by_permissions__user_groups__users=user)
|
||||
)
|
||||
|
||||
permed_assets = set()
|
||||
permed_assets.update(Asset.objects.filter(id__in=[a.id for a in assets]).filter(q).distinct())
|
||||
node_keys = Node.objects.filter(q).distinct().values_list('key', flat=True)
|
||||
|
||||
nodes_assets_q = Q()
|
||||
for _key in node_keys:
|
||||
nodes_assets_q |= Q(nodes__key__startswith=f'{_key}:')
|
||||
nodes_assets_q |= Q(nodes__key=_key)
|
||||
|
||||
permed_assets.update(
|
||||
Asset.objects.filter(
|
||||
id__in=[a.id for a in assets]
|
||||
).filter(
|
||||
nodes_assets_q
|
||||
).distinct()
|
||||
)
|
||||
|
||||
invalid_assets = set(assets) - set(permed_assets)
|
||||
if invalid_assets:
|
||||
msg = _("Not has host {} permission").format(
|
||||
[str(a.id) for a in invalid_assets]
|
||||
)
|
||||
raise ValidationError({"hosts": msg})
|
||||
|
||||
def check_permissions(self, request):
|
||||
if not settings.SECURITY_COMMAND_EXECUTION:
|
||||
return self.permission_denied(request, "Command execution disabled")
|
||||
return super().check_permissions(request)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
self.check_hosts(serializer)
|
||||
instance = serializer.save()
|
||||
instance.user = self.request.user
|
||||
instance.save()
|
||||
cols = self.request.query_params.get("cols", '80')
|
||||
rows = self.request.query_params.get("rows", '24')
|
||||
transaction.on_commit(lambda: run_command_execution.apply_async(
|
||||
args=(instance.id,), kwargs={"cols": cols, "rows": rows},
|
||||
task_id=str(instance.id)
|
||||
))
|
|
@ -1,149 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.conf import settings
|
||||
from .ansible.inventory import BaseInventory
|
||||
|
||||
from common.utils import get_logger
|
||||
|
||||
__all__ = [
|
||||
'JMSInventory', 'JMSCustomInventory',
|
||||
]
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class JMSBaseInventory(BaseInventory):
|
||||
def convert_to_ansible(self, asset, run_as_admin=False):
|
||||
info = {
|
||||
'id': asset.id,
|
||||
'name': asset.name,
|
||||
'ip': asset.address,
|
||||
'port': asset.ssh_port,
|
||||
'vars': dict(),
|
||||
'groups': [],
|
||||
}
|
||||
if asset.domain and asset.domain.has_gateway():
|
||||
info["vars"].update(self.make_proxy_command(asset))
|
||||
if run_as_admin:
|
||||
info.update(asset.get_auth_info(with_become=True))
|
||||
if asset.is_windows():
|
||||
info["vars"].update({
|
||||
"ansible_connection": "ssh",
|
||||
"ansible_shell_type": settings.WINDOWS_SSH_DEFAULT_SHELL,
|
||||
})
|
||||
for label in asset.labels.all():
|
||||
info["vars"].update({
|
||||
label.name: label.value
|
||||
})
|
||||
if asset.domain:
|
||||
info["vars"].update({
|
||||
"domain": asset.domain.name,
|
||||
})
|
||||
return info
|
||||
|
||||
@staticmethod
|
||||
def make_proxy_command(asset):
|
||||
gateway = asset.domain.random_gateway()
|
||||
proxy_command_list = [
|
||||
"ssh", "-o", "Port={}".format(gateway.port),
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"{}@{}".format(gateway.username, gateway.address),
|
||||
"-W", "%h:%p", "-q",
|
||||
]
|
||||
|
||||
if gateway.password:
|
||||
proxy_command_list.insert(
|
||||
0, "sshpass -p '{}'".format(gateway.password)
|
||||
)
|
||||
if gateway.private_key:
|
||||
proxy_command_list.append("-i {}".format(gateway.private_key_file))
|
||||
|
||||
proxy_command = "'-o ProxyCommand={}'".format(
|
||||
" ".join(proxy_command_list)
|
||||
)
|
||||
return {"ansible_ssh_common_args": proxy_command}
|
||||
|
||||
|
||||
class JMSInventory(JMSBaseInventory):
|
||||
"""
|
||||
JMS Inventory is the inventory with jumpserver assets, so you can
|
||||
write you own inventory, construct you inventory,
|
||||
user_info is obtained from admin_user or asset_user
|
||||
"""
|
||||
def __init__(self, assets, run_as_admin=False, run_as=None, become_info=None, system_user=None):
|
||||
"""
|
||||
:param assets: assets
|
||||
:param run_as_admin: True 是否使用管理用户去执行, 每台服务器的管理用户可能不同
|
||||
:param run_as: 用户名(添加了统一的资产用户管理器之后AssetUserManager加上之后修改为username)
|
||||
:param become_info: 是否become成某个用户去执行
|
||||
"""
|
||||
self.assets = assets
|
||||
self.using_admin = run_as_admin
|
||||
self.run_as = run_as
|
||||
self.system_user = system_user
|
||||
self.become_info = become_info
|
||||
|
||||
host_list = []
|
||||
|
||||
for asset in assets:
|
||||
host = self.convert_to_ansible(asset, run_as_admin=run_as_admin)
|
||||
if run_as is not None:
|
||||
run_user_info = self.get_run_user_info(host)
|
||||
host.update(run_user_info)
|
||||
if become_info and asset.is_unixlike():
|
||||
host.update(become_info)
|
||||
host_list.append(host)
|
||||
|
||||
super().__init__(host_list=host_list)
|
||||
|
||||
def get_run_user_info(self, host):
|
||||
if not self.run_as and not self.system_user:
|
||||
return {}
|
||||
|
||||
asset_id = host.get('id', '')
|
||||
asset = self.assets.filter(id=asset_id).first()
|
||||
if not asset:
|
||||
logger.error('Host not found: ', asset_id)
|
||||
return {}
|
||||
|
||||
if self.system_user:
|
||||
self.system_user.load_asset_special_auth(asset=asset, username=self.run_as)
|
||||
return self.system_user._to_secret_json()
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
class JMSCustomInventory(JMSBaseInventory):
|
||||
"""
|
||||
JMS Custom Inventory is the inventory with jumpserver assets,
|
||||
user_info is obtained from custom parameter
|
||||
"""
|
||||
|
||||
def __init__(self, assets, username, password=None, public_key=None, private_key=None):
|
||||
"""
|
||||
"""
|
||||
self.assets = assets
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.public_key = public_key
|
||||
self.private_key = private_key
|
||||
|
||||
host_list = []
|
||||
|
||||
for asset in assets:
|
||||
host = self.convert_to_ansible(asset)
|
||||
run_user_info = self.get_run_user_info()
|
||||
host.update(run_user_info)
|
||||
host_list.append(host)
|
||||
|
||||
super().__init__(host_list=host_list)
|
||||
|
||||
def get_run_user_info(self):
|
||||
return {
|
||||
'username': self.username,
|
||||
'password': self.password,
|
||||
'public_key': self.public_key,
|
||||
'private_key': self.private_key
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
# Generated by Django 3.2.14 on 2022-10-08 07:19
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0106_auto_20220916_1556'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('ops', '0023_auto_20220929_2025'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='adhocexecution',
|
||||
name='adhoc',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='adhocexecution',
|
||||
name='task',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='commandexecution',
|
||||
name='hosts',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='commandexecution',
|
||||
name='user',
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='task',
|
||||
unique_together=None,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='task',
|
||||
name='latest_adhoc',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='task',
|
||||
name='latest_execution',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='AdHoc',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='AdHocExecution',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='CommandExecution',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Task',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,72 @@
|
|||
# Generated by Django 3.2.14 on 2022-10-08 08:31
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0106_auto_20220916_1556'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('ops', '0024_auto_20221008_1514'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AdHoc',
|
||||
fields=[
|
||||
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
|
||||
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('is_periodic', models.BooleanField(default=False)),
|
||||
('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')),
|
||||
('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')),
|
||||
('account', models.CharField(default='root', max_length=128, verbose_name='Account')),
|
||||
('account_policy', models.CharField(default='root', max_length=128, verbose_name='Account policy')),
|
||||
('date_last_run', models.DateTimeField(null=True, verbose_name='Date last run')),
|
||||
('pattern', models.CharField(default='all', max_length=1024, verbose_name='Pattern')),
|
||||
('module', models.CharField(default='shell', max_length=128, verbose_name='Module')),
|
||||
('args', models.CharField(default='', max_length=1024, verbose_name='Args')),
|
||||
('assets', models.ManyToManyField(to='assets.Asset', verbose_name='Assets')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AdHocExecution',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('status', models.CharField(default='running', max_length=16, verbose_name='Status')),
|
||||
('result', models.JSONField(blank=True, null=True, verbose_name='Result')),
|
||||
('summary', models.JSONField(default=dict, verbose_name='Summary')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
|
||||
('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')),
|
||||
('date_finished', models.DateTimeField(null=True)),
|
||||
('creator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Creator')),
|
||||
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='executions', to='ops.adhoc', verbose_name='Adhoc')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'AdHoc execution',
|
||||
'db_table': 'ops_adhoc_execution',
|
||||
'get_latest_by': 'date_start',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='adhoc',
|
||||
name='last_execution',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ops.adhocexecution', verbose_name='Last execution'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='adhoc',
|
||||
name='owner',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Creator'),
|
||||
),
|
||||
]
|
|
@ -14,12 +14,10 @@ from .celery.utils import (
|
|||
|
||||
__all__ = [
|
||||
'PeriodTaskModelMixin', 'PeriodTaskSerializerMixin',
|
||||
'PeriodTaskFormMixin',
|
||||
]
|
||||
|
||||
|
||||
class PeriodTaskModelMixin(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(
|
||||
max_length=128, unique=False, verbose_name=_("Name")
|
||||
)
|
||||
|
@ -140,42 +138,3 @@ class PeriodTaskSerializerMixin(serializers.Serializer):
|
|||
msg = _("Require periodic or regularly perform setting")
|
||||
raise serializers.ValidationError(msg)
|
||||
return ok
|
||||
|
||||
|
||||
class PeriodTaskFormMixin(forms.Form):
|
||||
is_periodic = forms.BooleanField(
|
||||
initial=True, required=False, label=_('Periodic perform')
|
||||
)
|
||||
crontab = forms.CharField(
|
||||
max_length=128, required=False, label=_('Regularly perform'),
|
||||
help_text=_("eg: Every Sunday 03:05 run <5 3 * * 0> <br> "
|
||||
"Tips: "
|
||||
"Using 5 digits linux crontab expressions "
|
||||
"<min hour day month week> "
|
||||
"(<a href='https://tool.lu/crontab/' target='_blank'>Online tools</a>) <br>"
|
||||
"Note: "
|
||||
"If both Regularly perform and Cycle perform are set, "
|
||||
"give priority to Regularly perform"),
|
||||
)
|
||||
interval = forms.IntegerField(
|
||||
required=False, initial=24,
|
||||
help_text=_('Unit: hour'), label=_("Cycle perform"),
|
||||
)
|
||||
|
||||
def get_initial_for_field(self, field, field_name):
|
||||
"""
|
||||
Return initial data for field on form. Use initial data from the form
|
||||
or the field, in that order. Evaluate callable values.
|
||||
"""
|
||||
if field_name not in ['is_periodic', 'crontab', 'interval']:
|
||||
return super().get_initial_for_field(field, field_name)
|
||||
instance = getattr(self, 'instance', None)
|
||||
if instance is None:
|
||||
return super().get_initial_for_field(field, field_name)
|
||||
init_attr_name = field_name + '_initial'
|
||||
value = getattr(self, init_attr_name, None)
|
||||
if value is None:
|
||||
return super().get_initial_for_field(field, field_name)
|
||||
return value
|
||||
|
||||
|
||||
|
|
|
@ -3,4 +3,3 @@
|
|||
|
||||
from .adhoc import *
|
||||
from .celery import *
|
||||
from .command import *
|
||||
|
|
|
@ -1,337 +1,41 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
|
||||
import uuid
|
||||
import os
|
||||
import time
|
||||
import datetime
|
||||
|
||||
from celery import current_task
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import get_logger, lazyproperty
|
||||
from common.utils.translate import translate_value
|
||||
from common.db.fields import (
|
||||
JsonListTextField, JsonDictCharField, EncryptJsonDictCharField,
|
||||
JsonDictTextField,
|
||||
)
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from ..ansible import AdHocRunner, AnsibleError
|
||||
from ..inventory import JMSInventory
|
||||
from ..mixin import PeriodTaskModelMixin
|
||||
from common.utils import get_logger
|
||||
from .base import BaseAnsibleTask, BaseAnsibleExecution
|
||||
from ..ansible import AdHocRunner
|
||||
|
||||
__all__ = ["Task", "AdHoc", "AdHocExecution"]
|
||||
__all__ = ["AdHoc", "AdHocExecution"]
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class Task(PeriodTaskModelMixin, OrgModelMixin):
|
||||
"""
|
||||
This task is different ansible task, Task like 'push system user', 'get asset info' ..
|
||||
One task can have some versions of adhoc, run a task only run the latest version adhoc
|
||||
"""
|
||||
callback = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Callback")) # Callback must be a registered celery task
|
||||
is_deleted = models.BooleanField(default=False)
|
||||
comment = models.TextField(blank=True, verbose_name=_("Comment"))
|
||||
date_created = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name=_("Date created"))
|
||||
date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated"))
|
||||
latest_adhoc = models.ForeignKey('ops.AdHoc', on_delete=models.SET_NULL,
|
||||
null=True, related_name='task_latest')
|
||||
latest_execution = models.ForeignKey('ops.AdHocExecution', on_delete=models.SET_NULL, null=True, related_name='task_latest')
|
||||
total_run_amount = models.IntegerField(default=0)
|
||||
success_run_amount = models.IntegerField(default=0)
|
||||
_ignore_auto_created_by = True
|
||||
|
||||
@property
|
||||
def short_id(self):
|
||||
return str(self.id).split('-')[-1]
|
||||
|
||||
@lazyproperty
|
||||
def versions(self):
|
||||
return self.adhoc.all().count()
|
||||
|
||||
@property
|
||||
def is_success(self):
|
||||
if self.latest_execution:
|
||||
return self.latest_execution.is_success
|
||||
else:
|
||||
return False
|
||||
|
||||
@lazyproperty
|
||||
def display_name(self):
|
||||
value = translate_value(self.name)
|
||||
return value
|
||||
|
||||
@property
|
||||
def timedelta(self):
|
||||
if self.latest_execution:
|
||||
return self.latest_execution.timedelta
|
||||
else:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def date_start(self):
|
||||
if self.latest_execution:
|
||||
return self.latest_execution.date_start
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def assets_amount(self):
|
||||
if self.latest_execution:
|
||||
return self.latest_execution.hosts_amount
|
||||
return 0
|
||||
|
||||
def get_latest_adhoc(self):
|
||||
if self.latest_adhoc:
|
||||
return self.latest_adhoc
|
||||
try:
|
||||
adhoc = self.adhoc.all().latest()
|
||||
self.latest_adhoc = adhoc
|
||||
self.save()
|
||||
return adhoc
|
||||
except AdHoc.DoesNotExist:
|
||||
return None
|
||||
|
||||
@property
|
||||
def history_summary(self):
|
||||
total = self.total_run_amount
|
||||
success = self.success_run_amount
|
||||
failed = total - success
|
||||
return {'total': total, 'success': success, 'failed': failed}
|
||||
|
||||
def get_run_execution(self):
|
||||
return self.execution.all()
|
||||
|
||||
def run(self):
|
||||
latest_adhoc = self.get_latest_adhoc()
|
||||
if latest_adhoc:
|
||||
return latest_adhoc.run()
|
||||
else:
|
||||
return {'error': 'No adhoc'}
|
||||
|
||||
@property
|
||||
def period_key(self):
|
||||
return self.__str__()
|
||||
|
||||
def get_register_task(self):
|
||||
from ..tasks import run_ansible_task
|
||||
name = self.__str__()
|
||||
task = run_ansible_task.name
|
||||
args = (str(self.id),)
|
||||
kwargs = {"callback": self.callback}
|
||||
return name, task, args, kwargs
|
||||
class AdHoc(BaseAnsibleTask):
|
||||
pattern = models.CharField(max_length=1024, verbose_name=_("Pattern"), default='all')
|
||||
module = models.CharField(max_length=128, default='shell', verbose_name=_('Module'))
|
||||
args = models.CharField(max_length=1024, default='', verbose_name=_('Args'))
|
||||
last_execution = models.ForeignKey('AdHocExecution', verbose_name=_("Last execution"), on_delete=models.SET_NULL, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name + '@' + str(self.org_id)
|
||||
|
||||
class Meta:
|
||||
db_table = 'ops_task'
|
||||
unique_together = ('name', 'org_id')
|
||||
ordering = ('-date_updated',)
|
||||
verbose_name = _("Task")
|
||||
get_latest_by = 'date_created'
|
||||
permissions = [
|
||||
('view_taskmonitor', _('Can view task monitor'))
|
||||
]
|
||||
return "{}: {}".format(self.module, self.args)
|
||||
|
||||
|
||||
class AdHoc(OrgModelMixin):
|
||||
"""
|
||||
task: A task reference
|
||||
_tasks: [{'name': 'task_name', 'action': {'module': '', 'args': ''}, 'other..': ''}, ]
|
||||
_options: ansible options, more see ops.ansible.runner.Options
|
||||
run_as_admin: if true, then need get every host admin user run it, because every host may be have different admin user, so we choise host level
|
||||
run_as: username(Add the uniform AssetUserManager <AssetUserManager> and change it to username)
|
||||
_become: May be using become [sudo, su] options. {method: "sudo", user: "user", pass: "pass"]
|
||||
pattern: Even if we set _hosts, We only use that to make inventory, We also can set `patter` to run task on match hosts
|
||||
"""
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
task = models.ForeignKey(Task, related_name='adhoc', on_delete=models.CASCADE)
|
||||
tasks = JsonListTextField(verbose_name=_('Tasks'))
|
||||
pattern = models.CharField(max_length=64, default='{}', verbose_name=_('Pattern'))
|
||||
options = JsonDictCharField(max_length=1024, default='', verbose_name=_('Options'))
|
||||
hosts = models.ManyToManyField('assets.Asset', verbose_name=_("Host"))
|
||||
run_as_admin = models.BooleanField(default=False, verbose_name=_('Run as admin'))
|
||||
run_as = models.CharField(max_length=64, default='', blank=True, null=True, verbose_name=_('Username'))
|
||||
become = EncryptJsonDictCharField(max_length=1024, default='', blank=True, null=True, verbose_name=_("Become"))
|
||||
created_by = models.CharField(max_length=64, default='', blank=True, null=True, verbose_name=_('Create by'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
|
||||
@lazyproperty
|
||||
def run_times(self):
|
||||
return self.execution.count()
|
||||
|
||||
@property
|
||||
def inventory(self):
|
||||
if self.become:
|
||||
become_info = {
|
||||
'become': {
|
||||
self.become
|
||||
}
|
||||
}
|
||||
else:
|
||||
become_info = None
|
||||
|
||||
inventory = JMSInventory(
|
||||
self.hosts.all(), run_as_admin=self.run_as_admin,
|
||||
run_as=self.run_as, become_info=become_info, system_user=self.run_system_user
|
||||
)
|
||||
return inventory
|
||||
|
||||
@property
|
||||
def become_display(self):
|
||||
if self.become:
|
||||
return self.become.get("user", "")
|
||||
return ""
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
celery_task_id = current_task.request.id
|
||||
except AttributeError:
|
||||
celery_task_id = None
|
||||
|
||||
execution = AdHocExecution(
|
||||
celery_task_id=celery_task_id,
|
||||
adhoc=self, task=self.task,
|
||||
task_display=str(self.task)[:128],
|
||||
date_start=timezone.now(),
|
||||
hosts_amount=self.hosts.count(),
|
||||
)
|
||||
execution.save()
|
||||
return execution.start()
|
||||
|
||||
@property
|
||||
def short_id(self):
|
||||
return str(self.id).split('-')[-1]
|
||||
|
||||
@property
|
||||
def latest_execution(self):
|
||||
try:
|
||||
return self.execution.all().latest()
|
||||
except AdHocExecution.DoesNotExist:
|
||||
return None
|
||||
|
||||
def save(self, **kwargs):
|
||||
instance = super().save(**kwargs)
|
||||
self.task.latest_adhoc = instance
|
||||
self.task.save()
|
||||
return instance
|
||||
|
||||
def __str__(self):
|
||||
return "{} of {}".format(self.task.name, self.short_id)
|
||||
|
||||
def same_with(self, other):
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
fields_check = []
|
||||
for field in self.__class__._meta.fields:
|
||||
if field.name not in ['id', 'date_created']:
|
||||
fields_check.append(field)
|
||||
for field in fields_check:
|
||||
if getattr(self, field.name) != getattr(other, field.name):
|
||||
return False
|
||||
return True
|
||||
|
||||
class Meta:
|
||||
db_table = "ops_adhoc"
|
||||
get_latest_by = 'date_created'
|
||||
verbose_name = _('AdHoc')
|
||||
|
||||
|
||||
class AdHocExecution(OrgModelMixin):
|
||||
class AdHocExecution(BaseAnsibleExecution):
|
||||
"""
|
||||
AdHoc running history.
|
||||
"""
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
task = models.ForeignKey(Task, related_name='execution', on_delete=models.SET_NULL, null=True)
|
||||
task_display = models.CharField(max_length=128, blank=True, default='', verbose_name=_("Task display"))
|
||||
celery_task_id = models.UUIDField(default=None, null=True)
|
||||
hosts_amount = models.IntegerField(default=0, verbose_name=_("Host amount"))
|
||||
adhoc = models.ForeignKey(AdHoc, related_name='execution', on_delete=models.SET_NULL, null=True)
|
||||
date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Start time'))
|
||||
date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('End time'))
|
||||
timedelta = models.FloatField(default=0.0, verbose_name=_('Time'), null=True)
|
||||
is_finished = models.BooleanField(default=False, verbose_name=_('Is finished'))
|
||||
is_success = models.BooleanField(default=False, verbose_name=_('Is success'))
|
||||
result = JsonDictTextField(blank=True, null=True, verbose_name=_('Adhoc raw result'))
|
||||
summary = JsonDictTextField(blank=True, null=True, verbose_name=_('Adhoc result summary'))
|
||||
task = models.ForeignKey('AdHoc', verbose_name=_("Adhoc"), related_name='executions', on_delete=models.CASCADE)
|
||||
|
||||
@property
|
||||
def short_id(self):
|
||||
return str(self.id).split('-')[-1]
|
||||
|
||||
@property
|
||||
def adhoc_short_id(self):
|
||||
return str(self.adhoc_id).split('-')[-1]
|
||||
|
||||
@property
|
||||
def log_path(self):
|
||||
dt = datetime.datetime.now().strftime('%Y-%m-%d')
|
||||
log_dir = os.path.join(settings.PROJECT_DIR, 'data', 'ansible', dt)
|
||||
if not os.path.exists(log_dir):
|
||||
os.makedirs(log_dir)
|
||||
return os.path.join(log_dir, str(self.id) + '.log')
|
||||
|
||||
def start_runner(self):
|
||||
runner = AdHocRunner(self.adhoc.inventory, options=self.adhoc.options)
|
||||
try:
|
||||
result = runner.run(
|
||||
self.adhoc.tasks,
|
||||
self.adhoc.pattern,
|
||||
self.task.name,
|
||||
execution_id=self.id
|
||||
)
|
||||
return result.results_raw, result.results_summary
|
||||
except AnsibleError as e:
|
||||
logger.warn("Failed run adhoc {}, {}".format(self.task.name, e))
|
||||
return {}, {}
|
||||
|
||||
def start(self):
|
||||
self.task.latest_execution = self
|
||||
self.task.save()
|
||||
time_start = time.time()
|
||||
summary = {}
|
||||
raw = ''
|
||||
|
||||
try:
|
||||
raw, summary = self.start_runner()
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
raw = {"dark": {"all": str(e)}, "contacted": []}
|
||||
finally:
|
||||
self.clean_up(summary, time_start)
|
||||
return raw, summary
|
||||
|
||||
def clean_up(self, summary, time_start):
|
||||
is_success = summary.get('success', False)
|
||||
task = Task.objects.get(id=self.task_id)
|
||||
task.total_run_amount = models.F('total_run_amount') + 1
|
||||
if is_success:
|
||||
task.success_run_amount = models.F('success_run_amount') + 1
|
||||
task.save()
|
||||
AdHocExecution.objects.filter(id=self.id).update(
|
||||
is_finished=True,
|
||||
is_success=is_success,
|
||||
date_finished=timezone.now(),
|
||||
timedelta=time.time() - time_start,
|
||||
summary=summary
|
||||
def get_runner(self):
|
||||
return AdHocRunner(
|
||||
self.task.inventory, self.task.module, self.task.args,
|
||||
pattern=self.task.pattern, project_dir=self.private_dir
|
||||
)
|
||||
|
||||
@property
|
||||
def success_hosts(self):
|
||||
return self.summary.get('contacted', [])
|
||||
|
||||
@property
|
||||
def failed_hosts(self):
|
||||
return self.summary.get('dark', {})
|
||||
|
||||
def __str__(self):
|
||||
return self.short_id
|
||||
|
||||
class Meta:
|
||||
db_table = "ops_adhoc_execution"
|
||||
get_latest_by = 'date_start'
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
import os.path
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
|
||||
from orgs.mixins.models import JMSOrgBaseModel
|
||||
from ..ansible.inventory import JMSInventory
|
||||
from ..mixin import PeriodTaskModelMixin
|
||||
|
||||
|
||||
class BaseAnsibleTask(PeriodTaskModelMixin, JMSOrgBaseModel):
|
||||
owner = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True)
|
||||
assets = models.ManyToManyField('assets.Asset', verbose_name=_("Assets"))
|
||||
account = models.CharField(max_length=128, default='root', verbose_name=_('Account'))
|
||||
account_policy = models.CharField(max_length=128, default='root', verbose_name=_('Account policy'))
|
||||
last_execution = models.ForeignKey('BaseAnsibleExecution', verbose_name=_("Last execution"), on_delete=models.SET_NULL, null=True)
|
||||
date_last_run = models.DateTimeField(null=True, verbose_name=_('Date last run'))
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@property
|
||||
def inventory(self):
|
||||
inv = JMSInventory(self.assets.all(), self.account, self.account_policy)
|
||||
return inv.generate()
|
||||
|
||||
def get_register_task(self):
|
||||
raise NotImplemented
|
||||
|
||||
def to_json(self):
|
||||
raise NotImplemented
|
||||
|
||||
|
||||
class BaseAnsibleExecution(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
|
||||
status = models.CharField(max_length=16, verbose_name=_('Status'), default='running')
|
||||
task = models.ForeignKey(BaseAnsibleTask, on_delete=models.CASCADE, null=True)
|
||||
result = models.JSONField(blank=True, null=True, verbose_name=_('Result'))
|
||||
summary = models.JSONField(default=dict, verbose_name=_('Summary'))
|
||||
creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True)
|
||||
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created'))
|
||||
date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True)
|
||||
date_finished = models.DateTimeField(null=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
ordering = ["-date_start"]
|
||||
|
||||
def __str__(self):
|
||||
return str(self.id)
|
||||
|
||||
def private_dir(self):
|
||||
uniq = self.date_created.strftime('%Y%m%d_%H%M%S') + '_' + self.short_id
|
||||
return os.path.join(settings.ANSIBLE_DIR, self.task.name, uniq)
|
||||
|
||||
def get_runner(self):
|
||||
raise NotImplemented
|
||||
|
||||
def update_task(self):
|
||||
self.task.last_execution = self
|
||||
self.task.date_last_run = timezone.now()
|
||||
self.task.save(update_fields=['last_execution', 'date_last_run'])
|
||||
|
||||
def start(self, **kwargs):
|
||||
runner = self.get_runner()
|
||||
try:
|
||||
cb = runner.run(**kwargs)
|
||||
self.status = cb.status
|
||||
self.summary = cb.summary
|
||||
self.result = cb.result
|
||||
self.date_finished = timezone.now()
|
||||
except Exception as e:
|
||||
self.status = 'failed'
|
||||
self.summary = {'error': str(e)}
|
||||
finally:
|
||||
self.save()
|
||||
self.update_task()
|
||||
|
||||
@property
|
||||
def is_finished(self):
|
||||
return self.status in ['succeeded', 'failed']
|
||||
|
||||
@property
|
||||
def is_success(self):
|
||||
return self.status == 'succeeded'
|
||||
|
||||
@property
|
||||
def time_cost(self):
|
||||
if self.date_finished and self.date_start:
|
||||
return (self.date_finished - self.date_start).total_seconds()
|
||||
return None
|
||||
|
||||
@property
|
||||
def short_id(self):
|
||||
return str(self.id).split('-')[-1]
|
||||
|
||||
@property
|
||||
def timedelta(self):
|
||||
if self.date_start and self.date_finished:
|
||||
return self.date_finished - self.date_start
|
||||
return None
|
||||
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import uuid
|
||||
import json
|
||||
|
||||
from celery.exceptions import SoftTimeLimitExceeded
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ugettext
|
||||
from django.db import models
|
||||
|
||||
from terminal.notifications import CommandExecutionAlert
|
||||
from assets.models import Asset
|
||||
from common.utils import lazyproperty
|
||||
from orgs.models import Organization
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from orgs.utils import tmp_to_org
|
||||
from ..ansible.runner import CommandRunner
|
||||
from ..inventory import JMSInventory
|
||||
|
||||
|
||||
class CommandExecution(OrgModelMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
hosts = models.ManyToManyField('assets.Asset')
|
||||
account = models.CharField(max_length=128, default='', verbose_name=_('account'))
|
||||
command = models.TextField(verbose_name=_("Command"))
|
||||
_result = models.TextField(blank=True, null=True, verbose_name=_('Result'))
|
||||
user = models.ForeignKey('users.User', on_delete=models.CASCADE, null=True)
|
||||
is_finished = models.BooleanField(default=False, verbose_name=_('Is finished'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created'))
|
||||
date_start = models.DateTimeField(null=True, verbose_name=_('Date start'))
|
||||
date_finished = models.DateTimeField(null=True, verbose_name=_('Date finished'))
|
||||
|
||||
def __str__(self):
|
||||
return self.command[:10]
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
with tmp_to_org(self.run_as.org_id):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def inventory(self):
|
||||
if self.run_as.username_same_with_user:
|
||||
username = self.user.username
|
||||
else:
|
||||
username = self.run_as.username
|
||||
inv = JMSInventory(self.allow_assets, run_as=username, system_user=self.run_as)
|
||||
return inv
|
||||
|
||||
@lazyproperty
|
||||
def user_display(self):
|
||||
return str(self.user)
|
||||
|
||||
@lazyproperty
|
||||
def hosts_display(self):
|
||||
return ','.join(self.hosts.all().values_list('name', flat=True))
|
||||
|
||||
@property
|
||||
def result(self):
|
||||
if self._result:
|
||||
return json.loads(self._result)
|
||||
else:
|
||||
return {}
|
||||
|
||||
@result.setter
|
||||
def result(self, item):
|
||||
self._result = json.dumps(item)
|
||||
|
||||
@property
|
||||
def is_success(self):
|
||||
if 'error' in self.result:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_hosts_names(self):
|
||||
return ','.join(self.hosts.all().values_list('name', flat=True))
|
||||
|
||||
def cmd_filter_rules(self, asset_id=None):
|
||||
from assets.models import CommandFilterRule
|
||||
user_id = self.user.id
|
||||
system_user_id = self.run_as.id
|
||||
rules = CommandFilterRule.get_queryset(
|
||||
user_id=user_id,
|
||||
system_user_id=system_user_id,
|
||||
asset_id=asset_id,
|
||||
)
|
||||
return rules
|
||||
|
||||
def is_command_can_run(self, command, asset_id=None):
|
||||
for rule in self.cmd_filter_rules(asset_id=asset_id):
|
||||
action, matched_cmd = rule.match(command)
|
||||
if action == rule.ActionChoices.allow:
|
||||
return True, None
|
||||
elif action == rule.ActionChoices.deny:
|
||||
return False, matched_cmd
|
||||
return True, None
|
||||
|
||||
@property
|
||||
def allow_assets(self):
|
||||
allow_asset_ids = []
|
||||
for asset in self.hosts.all():
|
||||
ok, __ = self.is_command_can_run(self.command, asset_id=asset.id)
|
||||
if ok:
|
||||
allow_asset_ids.append(asset.id)
|
||||
allow_assets = Asset.objects.filter(id__in=allow_asset_ids)
|
||||
return allow_assets
|
||||
|
||||
def run(self):
|
||||
print('-' * 10 + ' ' + ugettext('Task start') + ' ' + '-' * 10)
|
||||
org = Organization.get_instance(self.run_as.org_id)
|
||||
org.change_to()
|
||||
self.date_start = timezone.now()
|
||||
ok, msg = self.is_command_can_run(self.command)
|
||||
if ok:
|
||||
allow_assets = self.allow_assets
|
||||
deny_assets = set(list(self.hosts.all())) - set(list(allow_assets))
|
||||
for asset in deny_assets:
|
||||
print(f'资产{asset}: 命令{self.command}不允许执行')
|
||||
if not allow_assets:
|
||||
self.result = {
|
||||
"error": 'There are currently no assets that can be executed'
|
||||
}
|
||||
self.save()
|
||||
return self.result
|
||||
runner = CommandRunner(self.inventory)
|
||||
try:
|
||||
host = allow_assets.first()
|
||||
if host and host.is_windows():
|
||||
shell = 'win_shell'
|
||||
elif host and host.is_unixlike():
|
||||
shell = 'shell'
|
||||
else:
|
||||
shell = 'raw'
|
||||
result = runner.execute(self.command, 'all', module=shell)
|
||||
self.result = result.results_command
|
||||
except SoftTimeLimitExceeded as e:
|
||||
print("Run timeout than 60s")
|
||||
self.result = {"error": str(e)}
|
||||
except Exception as e:
|
||||
print("Error occur: {}".format(e))
|
||||
self.result = {"error": str(e)}
|
||||
else:
|
||||
msg = _("Command `{}` is forbidden ........").format(self.command)
|
||||
print('\033[31m' + msg + '\033[0m')
|
||||
CommandExecutionAlert({
|
||||
'input': self.command,
|
||||
'assets': self.hosts.all(),
|
||||
'user': str(self.user),
|
||||
'risk_level': 5,
|
||||
}).publish_async()
|
||||
self.result = {"error": msg}
|
||||
self.org_id = self.run_as.org_id
|
||||
self.is_finished = True
|
||||
self.date_finished = timezone.now()
|
||||
self.save()
|
||||
print('-' * 10 + ' ' + ugettext('Task end') + ' ' + '-' * 10)
|
||||
return self.result
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Command execution")
|
|
@ -2,15 +2,34 @@ from django.db import models
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from orgs.mixins.models import JMSOrgBaseModel
|
||||
from ..mixin import PeriodTaskModelMixin
|
||||
from .base import BaseAnsibleExecution, BaseAnsibleTask
|
||||
|
||||
|
||||
class PlaybookTask(PeriodTaskModelMixin, JMSOrgBaseModel):
|
||||
assets = models.ManyToManyField('assets.Asset', verbose_name=_("Assets"))
|
||||
account = models.CharField(max_length=128, default='root', verbose_name=_('Account'))
|
||||
playbook = models.FilePathField(max_length=1024, verbose_name=_("Playbook"))
|
||||
owner = models.CharField(max_length=1024, verbose_name=_("Owner"))
|
||||
class PlaybookTemplate(JMSOrgBaseModel):
|
||||
name = models.CharField(max_length=128, verbose_name=_("Name"))
|
||||
path = models.FilePathField(verbose_name=_("Path"))
|
||||
comment = models.TextField(verbose_name=_("Comment"), blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
verbose_name = _("Playbook template")
|
||||
unique_together = [('org_id', 'name')]
|
||||
|
||||
|
||||
class Playbook(BaseAnsibleTask):
|
||||
path = models.FilePathField(max_length=1024, verbose_name=_("Playbook"))
|
||||
owner = models.ForeignKey('users.User', verbose_name=_("Owner"), on_delete=models.SET_NULL, null=True)
|
||||
comment = models.TextField(blank=True, verbose_name=_("Comment"))
|
||||
template = models.ForeignKey('PlaybookTemplate', verbose_name=_("Template"), on_delete=models.SET_NULL, null=True)
|
||||
last_execution = models.ForeignKey('PlaybookExecution', verbose_name=_("Last execution"), on_delete=models.SET_NULL, null=True, blank=True)
|
||||
|
||||
def get_register_task(self):
|
||||
pass
|
||||
|
||||
|
||||
class PlaybookExecution(BaseAnsibleExecution):
|
||||
task = models.ForeignKey('Playbook', verbose_name=_("Task"), on_delete=models.CASCADE)
|
||||
path = models.FilePathField(max_length=1024, verbose_name=_("Run dir"))
|
||||
|
|
|
@ -3,8 +3,7 @@ from __future__ import unicode_literals
|
|||
from rest_framework import serializers
|
||||
from django.shortcuts import reverse
|
||||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from ..models import Task, AdHoc, AdHocExecution, CommandExecution
|
||||
from ..models import AdHoc, AdHocExecution
|
||||
|
||||
|
||||
class AdHocExecutionSerializer(serializers.ModelSerializer):
|
||||
|
@ -50,36 +49,6 @@ class AdHocExecutionExcludeResultSerializer(AdHocExecutionSerializer):
|
|||
]
|
||||
|
||||
|
||||
class TaskSerializer(BulkOrgResourceModelSerializer):
|
||||
summary = serializers.ReadOnlyField(source='history_summary')
|
||||
latest_execution = AdHocExecutionExcludeResultSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Task
|
||||
fields_mini = ['id', 'name', 'display_name']
|
||||
fields_small = fields_mini + [
|
||||
'interval', 'crontab',
|
||||
'is_periodic', 'is_deleted',
|
||||
'date_created', 'date_updated',
|
||||
'comment',
|
||||
]
|
||||
fields_fk = ['latest_execution']
|
||||
fields_custom = ['summary']
|
||||
fields = fields_small + fields_fk + fields_custom
|
||||
read_only_fields = [
|
||||
'is_deleted', 'date_created', 'date_updated',
|
||||
'latest_adhoc', 'latest_execution', 'total_run_amount',
|
||||
'success_run_amount', 'summary',
|
||||
]
|
||||
|
||||
|
||||
class TaskDetailSerializer(TaskSerializer):
|
||||
contents = serializers.ListField(source='latest_adhoc.tasks')
|
||||
|
||||
class Meta(TaskSerializer.Meta):
|
||||
fields = TaskSerializer.Meta.fields + ['contents']
|
||||
|
||||
|
||||
class AdHocSerializer(serializers.ModelSerializer):
|
||||
become_display = serializers.ReadOnlyField()
|
||||
tasks = serializers.ListField()
|
||||
|
@ -127,26 +96,26 @@ class AdHocDetailSerializer(AdHocSerializer):
|
|||
]
|
||||
|
||||
|
||||
class CommandExecutionSerializer(serializers.ModelSerializer):
|
||||
result = serializers.JSONField(read_only=True)
|
||||
log_url = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = CommandExecution
|
||||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'command', 'result', 'log_url',
|
||||
'is_finished', 'date_created', 'date_finished'
|
||||
]
|
||||
fields_m2m = ['hosts']
|
||||
fields = fields_small + fields_m2m
|
||||
read_only_fields = [
|
||||
'result', 'is_finished', 'log_url', 'date_created',
|
||||
'date_finished'
|
||||
]
|
||||
ref_name = 'OpsCommandExecution'
|
||||
|
||||
@staticmethod
|
||||
def get_log_url(obj):
|
||||
return reverse('api-ops:celery-task-log', kwargs={'pk': obj.id})
|
||||
# class CommandExecutionSerializer(serializers.ModelSerializer):
|
||||
# result = serializers.JSONField(read_only=True)
|
||||
# log_url = serializers.SerializerMethodField()
|
||||
#
|
||||
# class Meta:
|
||||
# model = CommandExecution
|
||||
# fields_mini = ['id']
|
||||
# fields_small = fields_mini + [
|
||||
# 'command', 'result', 'log_url',
|
||||
# 'is_finished', 'date_created', 'date_finished'
|
||||
# ]
|
||||
# fields_m2m = ['hosts']
|
||||
# fields = fields_small + fields_m2m
|
||||
# read_only_fields = [
|
||||
# 'result', 'is_finished', 'log_url', 'date_created',
|
||||
# 'date_finished'
|
||||
# ]
|
||||
# ref_name = 'OpsCommandExecution'
|
||||
#
|
||||
# @staticmethod
|
||||
# def get_log_url(obj):
|
||||
# return reverse('api-ops:celery-task-log', kwargs={'pk': obj.id})
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ from .celery.utils import (
|
|||
create_or_update_celery_periodic_tasks, get_celery_periodic_task,
|
||||
disable_celery_periodic_task, delete_celery_periodic_task
|
||||
)
|
||||
from .models import Task, CommandExecution, CeleryTask
|
||||
from .models import CommandExecution, CeleryTask
|
||||
from .notifications import ServerPerformanceCheckUtil
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
|
|
@ -12,14 +12,11 @@ app_name = "ops"
|
|||
router = DefaultRouter()
|
||||
bulk_router = BulkRouter()
|
||||
|
||||
bulk_router.register(r'tasks', api.TaskViewSet, 'task')
|
||||
router.register(r'adhoc', api.AdHocViewSet, 'adhoc')
|
||||
router.register(r'adhoc-executions', api.AdHocRunHistoryViewSet, 'execution')
|
||||
router.register(r'command-executions', api.CommandExecutionViewSet, 'command-execution')
|
||||
router.register(r'adhoc-executions', api.AdHocExecutionViewSet, 'execution')
|
||||
router.register(r'celery/period-tasks', api.CeleryPeriodTaskViewSet, 'celery-period-task')
|
||||
|
||||
urlpatterns = [
|
||||
path('tasks/<uuid:pk>/run/', api.TaskRun.as_view(), name='task-run'),
|
||||
path('celery/task/<uuid:pk>/log/', api.CeleryTaskLogApi.as_view(), name='celery-task-log'),
|
||||
path('celery/task/<uuid:pk>/result/', api.CeleryResultApi.as_view(), name='celery-result'),
|
||||
|
||||
|
|
Loading…
Reference in New Issue