perf: 修改 ansible 表结构

pull/8970/head
ibuler 2022-10-08 16:55:14 +08:00 committed by 老广
parent df5e63b3be
commit 0fb4b52232
25 changed files with 438 additions and 983 deletions

View File

@ -4,15 +4,14 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.const.choices import Trigger from common.const.choices import Trigger
from common.mixins.models import CommonModelMixin
from common.db.fields import EncryptJsonDictTextField 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.mixin import PeriodTaskModelMixin
from ops.tasks import execute_automation_strategy from ops.tasks import execute_automation_strategy
from ops.task_handlers import ExecutionManager from ops.task_handlers import ExecutionManager
class BaseAutomation(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin): class BaseAutomation(JMSOrgBaseModel, PeriodTaskModelMixin):
accounts = models.JSONField(default=list, verbose_name=_("Accounts")) accounts = models.JSONField(default=list, verbose_name=_("Accounts"))
nodes = models.ManyToManyField( nodes = models.ManyToManyField(
'assets.Node', related_name='automation_strategy', blank=True, verbose_name=_("Nodes") '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') default=dict, blank=True, null=True, verbose_name=_('Automation snapshot')
) )
strategy = models.ForeignKey( 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') verbose_name=_('Automation strategy')
) )
trigger = models.CharField( trigger = models.CharField(

View File

@ -14,16 +14,12 @@ class Label(OrgModelMixin):
("S", _("System")), ("S", _("System")),
("U", _("User")) ("U", _("User"))
) )
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_("Name")) name = models.CharField(max_length=128, verbose_name=_("Name"))
value = models.CharField(max_length=128, verbose_name=_("Value")) value = models.CharField(max_length=128, verbose_name=_("Value"))
category = models.CharField(max_length=128, choices=CATEGORY_CHOICES, category = models.CharField(max_length=128, choices=CATEGORY_CHOICES,
default=USER_CATEGORY, verbose_name=_("Category")) default=USER_CATEGORY, verbose_name=_("Category"))
is_active = models.BooleanField(default=True, verbose_name=_("Is active")) is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
comment = models.TextField(blank=True, null=True, verbose_name=_("Comment")) 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 @classmethod
def get_queryset_group_by_name(cls): def get_queryset_group_by_name(cls):

View File

@ -11,16 +11,14 @@ from common.drf.filters import DatetimeRangeFilter
from common.api import CommonGenericViewSet from common.api import CommonGenericViewSet
from orgs.mixins.api import OrgGenericViewSet, OrgBulkModelViewSet, OrgRelationMixin from orgs.mixins.api import OrgGenericViewSet, OrgBulkModelViewSet, OrgRelationMixin
from orgs.utils import current_org from orgs.utils import current_org
from ops.models import CommandExecution # from ops.models import CommandExecution
from . import filters from . import filters
from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog
from .serializers import FTPLogSerializer, UserLoginLogSerializer, CommandExecutionSerializer from .serializers import FTPLogSerializer, UserLoginLogSerializer
from .serializers import OperateLogSerializer, PasswordChangeLogSerializer, CommandExecutionHostsRelationSerializer from .serializers import OperateLogSerializer, PasswordChangeLogSerializer
class FTPLogViewSet(CreateModelMixin, class FTPLogViewSet(CreateModelMixin, ListModelMixin, OrgGenericViewSet):
ListModelMixin,
OrgGenericViewSet):
model = FTPLog model = FTPLog
serializer_class = FTPLogSerializer serializer_class = FTPLogSerializer
extra_filter_backends = [DatetimeRangeFilter] extra_filter_backends = [DatetimeRangeFilter]
@ -98,53 +96,53 @@ class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet):
) )
return queryset return queryset
# Todo: 看看怎么搞
class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet): # class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
model = CommandExecution # model = CommandExecution
serializer_class = CommandExecutionSerializer # serializer_class = CommandExecutionSerializer
extra_filter_backends = [DatetimeRangeFilter] # extra_filter_backends = [DatetimeRangeFilter]
date_range_filter_fields = [ # date_range_filter_fields = [
('date_start', ('date_from', 'date_to')) # ('date_start', ('date_from', 'date_to'))
] # ]
filterset_fields = [ # filterset_fields = [
'user__name', 'user__username', 'command', # 'user__name', 'user__username', 'command',
'account', 'is_finished' # 'account', 'is_finished'
] # ]
search_fields = [ # search_fields = [
'command', 'user__name', 'user__username', # 'command', 'user__name', 'user__username',
'account__username', # 'account__username',
] # ]
ordering = ['-date_created'] # ordering = ['-date_created']
#
def get_queryset(self): # def get_queryset(self):
queryset = super().get_queryset() # queryset = super().get_queryset()
if getattr(self, 'swagger_fake_view', False): # if getattr(self, 'swagger_fake_view', False):
return queryset.model.objects.none() # return queryset.model.objects.none()
if current_org.is_root(): # if current_org.is_root():
return queryset # return queryset
# queryset = queryset.filter(run_as__org_id=current_org.org_id()) # # queryset = queryset.filter(run_as__org_id=current_org.org_id())
return queryset # return queryset
#
#
class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet): # class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet):
serializer_class = CommandExecutionHostsRelationSerializer # serializer_class = CommandExecutionHostsRelationSerializer
m2m_field = CommandExecution.hosts.field # m2m_field = CommandExecution.hosts.field
filterset_fields = [ # filterset_fields = [
'id', 'asset', 'commandexecution' # 'id', 'asset', 'commandexecution'
] # ]
search_fields = ('asset__name', ) # search_fields = ('asset__name', )
http_method_names = ['options', 'get'] # http_method_names = ['options', 'get']
rbac_perms = { # rbac_perms = {
'GET': 'ops.view_commandexecution', # 'GET': 'ops.view_commandexecution',
'list': 'ops.view_commandexecution', # 'list': 'ops.view_commandexecution',
} # }
#
def get_queryset(self): # def get_queryset(self):
queryset = super().get_queryset() # queryset = super().get_queryset()
queryset = queryset.annotate( # queryset = queryset.annotate(
asset_display=Concat( # asset_display=Concat(
F('asset__name'), Value('('), # F('asset__name'), Value('('),
F('asset__address'), Value(')') # F('asset__address'), Value(')')
) # )
) # )
return queryset # return queryset

View File

@ -5,10 +5,9 @@ from rest_framework import filters
from rest_framework.compat import coreapi, coreschema from rest_framework.compat import coreapi, coreschema
from orgs.utils import current_org from orgs.utils import current_org
from ops.models import CommandExecution
from common.drf.filters import BaseFilterSet from common.drf.filters import BaseFilterSet
__all__ = ['CurrentOrgMembersFilter', 'CommandExecutionFilter'] __all__ = ['CurrentOrgMembersFilter']
class CurrentOrgMembersFilter(filters.BaseFilterBackend): class CurrentOrgMembersFilter(filters.BaseFilterBackend):
@ -35,21 +34,21 @@ class CurrentOrgMembersFilter(filters.BaseFilterBackend):
queryset = queryset.filter(user__in=self._get_user_list()) queryset = queryset.filter(user__in=self._get_user_list())
return queryset return queryset
#
class CommandExecutionFilter(BaseFilterSet): # class CommandExecutionFilter(BaseFilterSet):
hostname_ip = CharFilter(method='filter_hostname_ip') # hostname_ip = CharFilter(method='filter_hostname_ip')
#
class Meta: # class Meta:
model = CommandExecution.hosts.through # model = CommandExecution.hosts.through
fields = ( # fields = (
'id', 'asset', 'commandexecution', 'hostname_ip' # 'id', 'asset', 'commandexecution', 'hostname_ip'
) # )
#
def filter_hostname_ip(self, queryset, name, value): # def filter_hostname_ip(self, queryset, name, value):
queryset = queryset.annotate( # queryset = queryset.annotate(
hostname_ip=Concat( # hostname_ip=Concat(
F('asset__hostname'), Value('('), # F('asset__hostname'), Value('('),
F('asset__address'), Value(')') # F('asset__address'), Value(')')
) # )
).filter(hostname_ip__icontains=value) # ).filter(hostname_ip__icontains=value)
return queryset # return queryset

View File

@ -5,7 +5,6 @@ from rest_framework import serializers
from common.drf.serializers import BulkSerializerMixin from common.drf.serializers import BulkSerializerMixin
from terminal.models import Session from terminal.models import Session
from ops.models import CommandExecution
from . import models from . import models
@ -76,42 +75,42 @@ class SessionAuditSerializer(serializers.ModelSerializer):
model = Session model = Session
fields = '__all__' fields = '__all__'
#
class CommandExecutionSerializer(serializers.ModelSerializer): # class CommandExecutionSerializer(serializers.ModelSerializer):
is_success = serializers.BooleanField(read_only=True, label=_('Is success')) # is_success = serializers.BooleanField(read_only=True, label=_('Is success'))
hosts_display = serializers.ListSerializer( # hosts_display = serializers.ListSerializer(
child=serializers.CharField(), source='hosts', read_only=True, label=_('Hosts display') # child=serializers.CharField(), source='hosts', read_only=True, label=_('Hosts display')
) # )
#
class Meta: # class Meta:
model = CommandExecution # model = CommandExecution
fields_mini = ['id'] # fields_mini = ['id']
fields_small = fields_mini + [ # fields_small = fields_mini + [
'command', 'is_finished', 'user', # 'command', 'is_finished', 'user',
'date_start', 'result', 'is_success', 'org_id' # 'date_start', 'result', 'is_success', 'org_id'
] # ]
fields = fields_small + ['hosts', 'hosts_display', 'user_display'] # fields = fields_small + ['hosts', 'hosts_display', 'user_display']
extra_kwargs = { # extra_kwargs = {
'result': {'label': _('Result')}, # model 上的方法,只能在这修改 # 'result': {'label': _('Result')}, # model 上的方法,只能在这修改
'is_success': {'label': _('Is success')}, # 'is_success': {'label': _('Is success')},
'hosts': {'label': _('Hosts')}, # 外键,会生成 sql。不在 model 上修改 # 'hosts': {'label': _('Hosts')}, # 外键,会生成 sql。不在 model 上修改
'user': {'label': _('User')}, # 'user': {'label': _('User')},
'user_display': {'label': _('User display')}, # 'user_display': {'label': _('User display')},
} # }
#
@classmethod # @classmethod
def setup_eager_loading(cls, queryset): # def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """ # """ Perform necessary eager loading of data. """
queryset = queryset.prefetch_related('user', 'hosts') # queryset = queryset.prefetch_related('user', 'hosts')
return queryset # return queryset
#
#
class CommandExecutionHostsRelationSerializer(BulkSerializerMixin, serializers.ModelSerializer): # class CommandExecutionHostsRelationSerializer(BulkSerializerMixin, serializers.ModelSerializer):
asset_display = serializers.ReadOnlyField() # asset_display = serializers.ReadOnlyField()
commandexecution_display = serializers.ReadOnlyField() # commandexecution_display = serializers.ReadOnlyField()
#
class Meta: # class Meta:
model = CommandExecution.hosts.through # model = CommandExecution.hosts.through
fields = [ # fields = [
'id', 'asset', 'asset_display', 'commandexecution', 'commandexecution_display' # 'id', 'asset', 'asset_display', 'commandexecution', 'commandexecution_display'
] # ]

View File

@ -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'login-logs', api.UserLoginLogViewSet, 'login-log')
router.register(r'operate-logs', api.OperateLogViewSet, 'operate-log') router.register(r'operate-logs', api.OperateLogViewSet, 'operate-log')
router.register(r'password-change-logs', api.PasswordChangeLogViewSet, 'password-change-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-execution-logs', api.CommandExecutionViewSet, 'command-execution-log')
router.register(r'command-executions-hosts-relations', api.CommandExecutionHostRelationViewSet, 'command-executions-hosts-relation') # router.register(r'command-executions-hosts-relations', api.CommandExecutionHostRelationViewSet, 'command-executions-hosts-relation')
urlpatterns = [ urlpatterns = [

View File

@ -16,6 +16,7 @@ VERSION = const.VERSION
BASE_DIR = const.BASE_DIR BASE_DIR = const.BASE_DIR
PROJECT_DIR = const.PROJECT_DIR PROJECT_DIR = const.PROJECT_DIR
DATA_DIR = os.path.join(PROJECT_DIR, 'data') DATA_DIR = os.path.join(PROJECT_DIR, 'data')
ANSIBLE_DIR = os.path.join(DATA_DIR, 'ansible')
CERTS_DIR = os.path.join(DATA_DIR, 'certs') CERTS_DIR = os.path.join(DATA_DIR, 'certs')
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production

View File

@ -15,7 +15,7 @@ class DefaultCallback:
dark={}, dark={},
skipped=[], skipped=[],
) )
self.status = 'starting' self.status = 'running'
self.finished = False self.finished = False
def is_success(self): def is_success(self):

View File

@ -3,21 +3,19 @@ from collections import defaultdict
import json import json
__all__ = [ __all__ = ['JMSInventory']
'JMSInventory',
]
class 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 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 account_policy:
:param host_var_callback: :param host_var_callback:
""" """
self.assets = self.clean_assets(assets) self.assets = self.clean_assets(assets)
self.account_username = account_username self.account_username = account
self.account_policy = account_policy self.account_policy = account_policy
self.host_var_callback = host_var_callback self.host_var_callback = host_var_callback

View File

@ -68,3 +68,11 @@ class PlaybookRunner:
**kwargs **kwargs
) )
return self.cb 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)

View File

@ -2,4 +2,3 @@
# #
from .adhoc import * from .adhoc import *
from .celery import * from .celery import *
from .command import *

View File

@ -6,52 +6,18 @@ from rest_framework import viewsets, generics
from rest_framework.views import Response from rest_framework.views import Response
from common.drf.serializers import CeleryTaskSerializer from common.drf.serializers import CeleryTaskSerializer
from ..models import Task, AdHoc, AdHocExecution from ..models import AdHoc, AdHocExecution
from ..serializers import ( from ..serializers import (
TaskSerializer,
AdHocSerializer, AdHocSerializer,
AdHocExecutionSerializer, AdHocExecutionSerializer,
TaskDetailSerializer,
AdHocDetailSerializer, AdHocDetailSerializer,
) )
from ..tasks import run_ansible_task
from orgs.mixins.api import OrgBulkModelViewSet
__all__ = [ __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): class AdHocViewSet(viewsets.ModelViewSet):
queryset = AdHoc.objects.all() queryset = AdHoc.objects.all()
serializer_class = AdHocSerializer serializer_class = AdHocSerializer
@ -61,23 +27,17 @@ class AdHocViewSet(viewsets.ModelViewSet):
return AdHocDetailSerializer return AdHocDetailSerializer
return super().get_serializer_class() 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 AdHocExecutionViewSet(viewsets.ModelViewSet):
class AdHocRunHistoryViewSet(viewsets.ModelViewSet):
queryset = AdHocExecution.objects.all() queryset = AdHocExecution.objects.all()
serializer_class = AdHocExecutionSerializer serializer_class = AdHocExecutionSerializer
def get_queryset(self): def get_queryset(self):
task_id = self.request.query_params.get('task') task_id = self.request.query_params.get('task')
adhoc_id = self.request.query_params.get('adhoc') adhoc_id = self.request.query_params.get('adhoc')
if task_id: 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() adhocs = task.adhoc.all()
self.queryset = self.queryset.filter(adhoc__in=adhocs) self.queryset = self.queryset.filter(adhoc__in=adhocs)

View File

@ -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)
))

View File

@ -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
}

View File

@ -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',
),
]

View File

@ -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'),
),
]

View File

@ -14,12 +14,10 @@ from .celery.utils import (
__all__ = [ __all__ = [
'PeriodTaskModelMixin', 'PeriodTaskSerializerMixin', 'PeriodTaskModelMixin', 'PeriodTaskSerializerMixin',
'PeriodTaskFormMixin',
] ]
class PeriodTaskModelMixin(models.Model): class PeriodTaskModelMixin(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField( name = models.CharField(
max_length=128, unique=False, verbose_name=_("Name") max_length=128, unique=False, verbose_name=_("Name")
) )
@ -140,42 +138,3 @@ class PeriodTaskSerializerMixin(serializers.Serializer):
msg = _("Require periodic or regularly perform setting") msg = _("Require periodic or regularly perform setting")
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
return ok 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

View File

@ -3,4 +3,3 @@
from .adhoc import * from .adhoc import *
from .celery import * from .celery import *
from .command import *

View File

@ -1,336 +1,40 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
import uuid
import os
import time
import datetime
from celery import current_task
from django.db import models from django.db import models
from django.conf import settings
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.utils import get_logger, lazyproperty from common.utils import get_logger
from common.utils.translate import translate_value from .base import BaseAnsibleTask, BaseAnsibleExecution
from common.db.fields import ( from ..ansible import AdHocRunner
JsonListTextField, JsonDictCharField, EncryptJsonDictCharField,
JsonDictTextField,
)
from orgs.mixins.models import OrgModelMixin
from ..ansible import AdHocRunner, AnsibleError
from ..inventory import JMSInventory
from ..mixin import PeriodTaskModelMixin
__all__ = ["Task", "AdHoc", "AdHocExecution"] __all__ = ["AdHoc", "AdHocExecution"]
logger = get_logger(__file__) logger = get_logger(__file__)
class Task(PeriodTaskModelMixin, OrgModelMixin): class AdHoc(BaseAnsibleTask):
""" pattern = models.CharField(max_length=1024, verbose_name=_("Pattern"), default='all')
This task is different ansible task, Task like 'push system user', 'get asset info' .. module = models.CharField(max_length=128, default='shell', verbose_name=_('Module'))
One task can have some versions of adhoc, run a task only run the latest version adhoc 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)
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
def __str__(self): def __str__(self):
return self.name + '@' + str(self.org_id) return "{}: {}".format(self.module, self.args)
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'))
]
class AdHoc(OrgModelMixin): class AdHocExecution(BaseAnsibleExecution):
"""
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):
""" """
AdHoc running history. AdHoc running history.
""" """
id = models.UUIDField(default=uuid.uuid4, primary_key=True) task = models.ForeignKey('AdHoc', verbose_name=_("Adhoc"), related_name='executions', on_delete=models.CASCADE)
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'))
@property def get_runner(self):
def short_id(self): return AdHocRunner(
return str(self.id).split('-')[-1] self.task.inventory, self.task.module, self.task.args,
pattern=self.task.pattern, project_dir=self.private_dir
@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
)
@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: class Meta:
db_table = "ops_adhoc_execution" db_table = "ops_adhoc_execution"

106
apps/ops/models/base.py Normal file
View File

@ -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

View File

@ -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")

View File

@ -2,15 +2,34 @@ from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from orgs.mixins.models import JMSOrgBaseModel from orgs.mixins.models import JMSOrgBaseModel
from ..mixin import PeriodTaskModelMixin from .base import BaseAnsibleExecution, BaseAnsibleTask
class PlaybookTask(PeriodTaskModelMixin, JMSOrgBaseModel): class PlaybookTemplate(JMSOrgBaseModel):
assets = models.ManyToManyField('assets.Asset', verbose_name=_("Assets")) name = models.CharField(max_length=128, verbose_name=_("Name"))
account = models.CharField(max_length=128, default='root', verbose_name=_('Account')) path = models.FilePathField(verbose_name=_("Path"))
playbook = models.FilePathField(max_length=1024, verbose_name=_("Playbook")) comment = models.TextField(verbose_name=_("Comment"), blank=True)
owner = models.CharField(max_length=1024, verbose_name=_("Owner"))
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")) 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): def get_register_task(self):
pass 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"))

View File

@ -3,8 +3,7 @@ from __future__ import unicode_literals
from rest_framework import serializers from rest_framework import serializers
from django.shortcuts import reverse from django.shortcuts import reverse
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import AdHoc, AdHocExecution
from ..models import Task, AdHoc, AdHocExecution, CommandExecution
class AdHocExecutionSerializer(serializers.ModelSerializer): 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): class AdHocSerializer(serializers.ModelSerializer):
become_display = serializers.ReadOnlyField() become_display = serializers.ReadOnlyField()
tasks = serializers.ListField() tasks = serializers.ListField()
@ -127,26 +96,26 @@ class AdHocDetailSerializer(AdHocSerializer):
] ]
class CommandExecutionSerializer(serializers.ModelSerializer): # class CommandExecutionSerializer(serializers.ModelSerializer):
result = serializers.JSONField(read_only=True) # result = serializers.JSONField(read_only=True)
log_url = serializers.SerializerMethodField() # log_url = serializers.SerializerMethodField()
#
class Meta: # class Meta:
model = CommandExecution # model = CommandExecution
fields_mini = ['id'] # fields_mini = ['id']
fields_small = fields_mini + [ # fields_small = fields_mini + [
'command', 'result', 'log_url', # 'command', 'result', 'log_url',
'is_finished', 'date_created', 'date_finished' # 'is_finished', 'date_created', 'date_finished'
] # ]
fields_m2m = ['hosts'] # fields_m2m = ['hosts']
fields = fields_small + fields_m2m # fields = fields_small + fields_m2m
read_only_fields = [ # read_only_fields = [
'result', 'is_finished', 'log_url', 'date_created', # 'result', 'is_finished', 'log_url', 'date_created',
'date_finished' # 'date_finished'
] # ]
ref_name = 'OpsCommandExecution' # ref_name = 'OpsCommandExecution'
#
@staticmethod # @staticmethod
def get_log_url(obj): # def get_log_url(obj):
return reverse('api-ops:celery-task-log', kwargs={'pk': obj.id}) # return reverse('api-ops:celery-task-log', kwargs={'pk': obj.id})

View File

@ -20,7 +20,7 @@ from .celery.utils import (
create_or_update_celery_periodic_tasks, get_celery_periodic_task, create_or_update_celery_periodic_tasks, get_celery_periodic_task,
disable_celery_periodic_task, delete_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 from .notifications import ServerPerformanceCheckUtil
logger = get_logger(__file__) logger = get_logger(__file__)

View File

@ -12,14 +12,11 @@ app_name = "ops"
router = DefaultRouter() router = DefaultRouter()
bulk_router = BulkRouter() bulk_router = BulkRouter()
bulk_router.register(r'tasks', api.TaskViewSet, 'task')
router.register(r'adhoc', api.AdHocViewSet, 'adhoc') router.register(r'adhoc', api.AdHocViewSet, 'adhoc')
router.register(r'adhoc-executions', api.AdHocRunHistoryViewSet, 'execution') router.register(r'adhoc-executions', api.AdHocExecutionViewSet, 'execution')
router.register(r'command-executions', api.CommandExecutionViewSet, 'command-execution')
router.register(r'celery/period-tasks', api.CeleryPeriodTaskViewSet, 'celery-period-task') router.register(r'celery/period-tasks', api.CeleryPeriodTaskViewSet, 'celery-period-task')
urlpatterns = [ 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>/log/', api.CeleryTaskLogApi.as_view(), name='celery-task-log'),
path('celery/task/<uuid:pk>/result/', api.CeleryResultApi.as_view(), name='celery-result'), path('celery/task/<uuid:pk>/result/', api.CeleryResultApi.as_view(), name='celery-result'),