mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-09-19 10:26:27 +00:00
Command (#2134)
* [Update] 任务区分org * [Update] 修改翻译 * [Update] 使用id而不是hostname * [Update] 执行命令 * [Update] 修改一些东西 * [Update] 暂存 * [Update] 用户执行命令 * [Update] 添加资产授权模块-tree * [Update] 暂时这样 * [Update] 批量命令执行 * [Update] 修改表结构 * [Update] 更新翻译 * [Update] 删除cloud模块无效中文翻译
This commit is contained in:
@@ -1,18 +1,16 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
|
||||
import sys
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.plugins.callback import CallbackBase
|
||||
from ansible.plugins.callback.default import CallbackModule
|
||||
|
||||
from .display import TeeObj
|
||||
from ansible.plugins.callback.minimal import CallbackModule as CMDCallBackModule
|
||||
|
||||
|
||||
class AdHocResultCallback(CallbackModule):
|
||||
"""
|
||||
Task result Callback
|
||||
"""
|
||||
def __init__(self, display=None, options=None, file_obj=None):
|
||||
class CallbackMixin:
|
||||
def __init__(self, display=None):
|
||||
# result_raw example: {
|
||||
# "ok": {"hostname": {"task_name": {},...},..},
|
||||
# "failed": {"hostname": {"task_name": {}..}, ..},
|
||||
@@ -20,63 +18,129 @@ class AdHocResultCallback(CallbackModule):
|
||||
# "skipped": {"hostname": {"task_name": {}, ..}, ..},
|
||||
# }
|
||||
# results_summary example: {
|
||||
# "contacted": {"hostname",...},
|
||||
# "contacted": {"hostname": {"task_name": {}}, "hostname": {}},
|
||||
# "dark": {"hostname": {"task_name": {}, "task_name": {}},...,},
|
||||
# "success": True
|
||||
# }
|
||||
self.results_raw = dict(ok={}, failed={}, unreachable={}, skipped={})
|
||||
self.results_summary = dict(contacted=[], dark={})
|
||||
self.results_raw = dict(
|
||||
ok=defaultdict(dict),
|
||||
failed=defaultdict(dict),
|
||||
unreachable=defaultdict(dict),
|
||||
skippe=defaultdict(dict),
|
||||
)
|
||||
self.results_summary = dict(
|
||||
contacted=defaultdict(dict),
|
||||
dark=defaultdict(dict),
|
||||
success=True
|
||||
)
|
||||
self.results = {
|
||||
'raw': self.results_raw,
|
||||
'summary': self.results_summary,
|
||||
}
|
||||
super().__init__()
|
||||
if file_obj is not None:
|
||||
sys.stdout = TeeObj(file_obj)
|
||||
if display:
|
||||
self._display = display
|
||||
self._display.columns = 79
|
||||
|
||||
def gather_result(self, t, res):
|
||||
self._clean_results(res._result, res._task.action)
|
||||
host = res._host.get_name()
|
||||
task_name = res.task_name
|
||||
task_result = res._result
|
||||
def display(self, msg):
|
||||
self._display.display(msg)
|
||||
|
||||
if self.results_raw[t].get(host):
|
||||
self.results_raw[t][host][task_name] = task_result
|
||||
else:
|
||||
self.results_raw[t][host] = {task_name: task_result}
|
||||
def gather_result(self, t, result):
|
||||
self._clean_results(result._result, result._task.action)
|
||||
host = result._host.get_name()
|
||||
task_name = result.task_name
|
||||
task_result = result._result
|
||||
|
||||
self.results_raw[t][host][task_name] = task_result
|
||||
self.clean_result(t, host, task_name, task_result)
|
||||
|
||||
|
||||
class AdHocResultCallback(CallbackMixin, CallbackModule, CMDCallBackModule):
|
||||
"""
|
||||
Task result Callback
|
||||
"""
|
||||
def clean_result(self, t, host, task_name, task_result):
|
||||
contacted = self.results_summary["contacted"]
|
||||
dark = self.results_summary["dark"]
|
||||
if t in ("ok", "skipped") and host not in dark:
|
||||
if host not in contacted:
|
||||
contacted.append(host)
|
||||
else:
|
||||
if dark.get(host):
|
||||
dark[host][task_name] = task_result.values
|
||||
|
||||
if task_result.get('rc') is not None:
|
||||
cmd = task_result.get('cmd')
|
||||
if isinstance(cmd, list):
|
||||
cmd = " ".join(cmd)
|
||||
else:
|
||||
dark[host] = {task_name: task_result}
|
||||
if host in contacted:
|
||||
contacted.remove(host)
|
||||
cmd = str(cmd)
|
||||
detail = {
|
||||
'cmd': cmd,
|
||||
'stderr': task_result.get('stderr'),
|
||||
'stdout': task_result.get('stdout'),
|
||||
'rc': task_result.get('rc'),
|
||||
'delta': task_result.get('delta'),
|
||||
'msg': task_result.get('msg', '')
|
||||
}
|
||||
else:
|
||||
detail = {
|
||||
"changed": task_result.get('changed', False),
|
||||
"msg": task_result.get('msg', '')
|
||||
}
|
||||
|
||||
if t in ("ok", "skipped"):
|
||||
contacted[host][task_name] = detail
|
||||
else:
|
||||
dark[host][task_name] = detail
|
||||
|
||||
def v2_runner_on_failed(self, result, ignore_errors=False):
|
||||
self.results_summary['success'] = False
|
||||
self.gather_result("failed", result)
|
||||
super().v2_runner_on_failed(result, ignore_errors=ignore_errors)
|
||||
|
||||
if result._task.action in C.MODULE_NO_JSON:
|
||||
CMDCallBackModule.v2_runner_on_failed(self,
|
||||
result, ignore_errors=ignore_errors
|
||||
)
|
||||
else:
|
||||
super().v2_runner_on_failed(
|
||||
result, ignore_errors=ignore_errors
|
||||
)
|
||||
|
||||
def v2_runner_on_ok(self, result):
|
||||
self.gather_result("ok", result)
|
||||
super().v2_runner_on_ok(result)
|
||||
if result._task.action in C.MODULE_NO_JSON:
|
||||
CMDCallBackModule.v2_runner_on_ok(self, result)
|
||||
else:
|
||||
super().v2_runner_on_ok(result)
|
||||
|
||||
def v2_runner_on_skipped(self, result):
|
||||
self.gather_result("skipped", result)
|
||||
super().v2_runner_on_skipped(result)
|
||||
|
||||
def v2_runner_on_unreachable(self, result):
|
||||
self.results_summary['success'] = False
|
||||
self.gather_result("unreachable", result)
|
||||
super().v2_runner_on_unreachable(result)
|
||||
|
||||
def on_playbook_start(self, name):
|
||||
date_start = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
self.display(
|
||||
"{} Start task: {}\r\n".format(date_start, name)
|
||||
)
|
||||
|
||||
def on_playbook_end(self, name):
|
||||
date_finished = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
self.display(
|
||||
"{} Task finish\r\n".format(date_finished)
|
||||
)
|
||||
|
||||
def display_skipped_hosts(self):
|
||||
pass
|
||||
|
||||
def display_ok_hosts(self):
|
||||
pass
|
||||
|
||||
|
||||
class CommandResultCallback(AdHocResultCallback):
|
||||
"""
|
||||
Command result callback
|
||||
"""
|
||||
def __init__(self, display=None):
|
||||
def __init__(self, display=None, **kwargs):
|
||||
# results_command: {
|
||||
# "cmd": "",
|
||||
# "stderr": "",
|
||||
|
@@ -17,7 +17,7 @@ from common.utils import get_logger
|
||||
from .exceptions import AnsibleError
|
||||
|
||||
|
||||
__all__ = ["AdHocRunner", "PlayBookRunner"]
|
||||
__all__ = ["AdHocRunner", "PlayBookRunner", "CommandRunner"]
|
||||
C.HOST_KEY_CHECKING = False
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -45,7 +45,7 @@ def get_default_options():
|
||||
listtasks=False,
|
||||
listhosts=False,
|
||||
syntax=False,
|
||||
timeout=60,
|
||||
timeout=30,
|
||||
connection='ssh',
|
||||
module_path='',
|
||||
forks=10,
|
||||
@@ -145,7 +145,7 @@ class AdHocRunner:
|
||||
)
|
||||
|
||||
def get_result_callback(self, file_obj=None):
|
||||
return self.__class__.results_callback_class(file_obj=file_obj)
|
||||
return self.__class__.results_callback_class()
|
||||
|
||||
@staticmethod
|
||||
def check_module_args(module_name, module_args=''):
|
||||
@@ -177,17 +177,16 @@ class AdHocRunner:
|
||||
options = self.__class__.default_options
|
||||
return options
|
||||
|
||||
def run(self, tasks, pattern, play_name='Ansible Ad-hoc', gather_facts='no', file_obj=None):
|
||||
def run(self, tasks, pattern, play_name='Ansible Ad-hoc', gather_facts='no'):
|
||||
"""
|
||||
:param tasks: [{'action': {'module': 'shell', 'args': 'ls'}, ...}, ]
|
||||
:param pattern: all, *, or others
|
||||
:param play_name: The play name
|
||||
:param gather_facts:
|
||||
:param file_obj: logging to file_obj
|
||||
:return:
|
||||
"""
|
||||
self.check_pattern(pattern)
|
||||
self.results_callback = self.get_result_callback(file_obj)
|
||||
self.results_callback = self.get_result_callback()
|
||||
cleaned_tasks = self.clean_tasks(tasks)
|
||||
|
||||
play_source = dict(
|
||||
@@ -211,10 +210,6 @@ class AdHocRunner:
|
||||
stdout_callback=self.results_callback,
|
||||
passwords=self.options.passwords,
|
||||
)
|
||||
print("Get matched hosts: {}".format(
|
||||
self.inventory.get_matched_hosts(pattern)
|
||||
))
|
||||
|
||||
try:
|
||||
tqm.run(play)
|
||||
return self.results_callback
|
||||
@@ -229,16 +224,14 @@ class CommandRunner(AdHocRunner):
|
||||
results_callback_class = CommandResultCallback
|
||||
modules_choices = ('shell', 'raw', 'command', 'script')
|
||||
|
||||
def execute(self, cmd, pattern, module=None):
|
||||
def execute(self, cmd, pattern, module='shell'):
|
||||
if module and module not in self.modules_choices:
|
||||
raise AnsibleError("Module should in {}".format(self.modules_choices))
|
||||
else:
|
||||
module = "shell"
|
||||
|
||||
tasks = [
|
||||
{"action": {"module": module, "args": cmd}}
|
||||
]
|
||||
hosts = self.inventory.get_hosts(pattern=pattern)
|
||||
name = "Run command {} on {}".format(cmd, ", ".join([host.name for host in hosts]))
|
||||
name = "Run command {} on {}'s hosts".format(cmd, len(hosts))
|
||||
return self.run(tasks, pattern, play_name=name)
|
||||
|
||||
|
5
apps/ops/api/__init__.py
Normal file
5
apps/ops/api/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .adhoc import *
|
||||
from .celery import *
|
||||
from .command import *
|
@@ -1,26 +1,32 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
import uuid
|
||||
import os
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import viewsets, generics
|
||||
from rest_framework.views import Response
|
||||
|
||||
from common.permissions import IsOrgAdmin
|
||||
from .models import Task, AdHoc, AdHocRunHistory, CeleryTask
|
||||
from .serializers import TaskSerializer, AdHocSerializer, \
|
||||
from orgs.utils import current_org
|
||||
from ..models import Task, AdHoc, AdHocRunHistory
|
||||
from ..serializers import TaskSerializer, AdHocSerializer, \
|
||||
AdHocRunHistorySerializer
|
||||
from .tasks import run_ansible_task
|
||||
from ..tasks import run_ansible_task
|
||||
|
||||
__all__ = [
|
||||
'TaskViewSet', 'TaskRun', 'AdHocViewSet', 'AdHocRunHistoryViewSet'
|
||||
]
|
||||
|
||||
|
||||
class TaskViewSet(viewsets.ModelViewSet):
|
||||
queryset = Task.objects.all()
|
||||
serializer_class = TaskSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
# label = None
|
||||
# help_text = ''
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
if current_org:
|
||||
queryset = queryset.filter(created_by=current_org.id)
|
||||
return queryset
|
||||
|
||||
|
||||
class TaskRun(generics.RetrieveAPIView):
|
||||
@@ -47,7 +53,7 @@ class AdHocViewSet(viewsets.ModelViewSet):
|
||||
return self.queryset
|
||||
|
||||
|
||||
class AdHocRunHistorySet(viewsets.ModelViewSet):
|
||||
class AdHocRunHistoryViewSet(viewsets.ModelViewSet):
|
||||
queryset = AdHocRunHistory.objects.all()
|
||||
serializer_class = AdHocRunHistorySerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
@@ -66,28 +72,6 @@ class AdHocRunHistorySet(viewsets.ModelViewSet):
|
||||
return self.queryset
|
||||
|
||||
|
||||
class CeleryTaskLogApi(generics.RetrieveAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
buff_size = 1024 * 10
|
||||
end = False
|
||||
queryset = CeleryTask.objects.all()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
mark = request.query_params.get("mark") or str(uuid.uuid4())
|
||||
task = self.get_object()
|
||||
log_path = task.full_log_path
|
||||
|
||||
if not log_path or not os.path.isfile(log_path):
|
||||
return Response({"data": _("Waiting ...")}, status=203)
|
||||
|
||||
with open(log_path, 'r') as f:
|
||||
offset = cache.get(mark, 0)
|
||||
f.seek(offset)
|
||||
data = f.read(self.buff_size).replace('\n', '\r\n')
|
||||
mark = str(uuid.uuid4())
|
||||
cache.set(mark, f.tell(), 5)
|
||||
|
||||
if data == '' and task.is_finished():
|
||||
self.end = True
|
||||
return Response({"data": data, 'end': self.end, 'mark': mark})
|
||||
|
53
apps/ops/api/celery.py
Normal file
53
apps/ops/api/celery.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import uuid
|
||||
import os
|
||||
|
||||
from celery.result import AsyncResult
|
||||
from django.core.cache import cache
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import generics
|
||||
from rest_framework.views import Response
|
||||
|
||||
from common.permissions import IsOrgAdmin, IsValidUser
|
||||
from ..models import CeleryTask
|
||||
from ..serializers import CeleryResultSerializer
|
||||
|
||||
|
||||
__all__ = ['CeleryTaskLogApi', 'CeleryResultApi']
|
||||
|
||||
|
||||
class CeleryTaskLogApi(generics.RetrieveAPIView):
|
||||
permission_classes = (IsValidUser,)
|
||||
buff_size = 1024 * 10
|
||||
end = False
|
||||
queryset = CeleryTask.objects.all()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
mark = request.query_params.get("mark") or str(uuid.uuid4())
|
||||
task = self.get_object()
|
||||
log_path = task.full_log_path
|
||||
|
||||
if not log_path or not os.path.isfile(log_path):
|
||||
return Response({"data": _("Waiting ...")}, status=203)
|
||||
|
||||
with open(log_path, 'r') as f:
|
||||
offset = cache.get(mark, 0)
|
||||
f.seek(offset)
|
||||
data = f.read(self.buff_size).replace('\n', '\r\n')
|
||||
mark = str(uuid.uuid4())
|
||||
cache.set(mark, f.tell(), 5)
|
||||
|
||||
if data == '' and task.is_finished():
|
||||
self.end = True
|
||||
return Response({"data": data, 'end': self.end, 'mark': mark})
|
||||
|
||||
|
||||
class CeleryResultApi(generics.RetrieveAPIView):
|
||||
permission_classes = (IsValidUser,)
|
||||
serializer_class = CeleryResultSerializer
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
return AsyncResult(pk)
|
||||
|
27
apps/ops/api/command.py
Normal file
27
apps/ops/api/command.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import viewsets
|
||||
|
||||
from common.permissions import IsValidUser
|
||||
from ..models import CommandExecution
|
||||
from ..serializers import CommandExecutionSerializer
|
||||
from ..tasks import run_command_execution
|
||||
|
||||
|
||||
class CommandExecutionViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = CommandExecutionSerializer
|
||||
permission_classes = (IsValidUser,)
|
||||
task = None
|
||||
|
||||
def get_queryset(self):
|
||||
return CommandExecution.objects.filter(
|
||||
user_id=str(self.request.user.id)
|
||||
)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
instance = serializer.save()
|
||||
instance.user = self.request.user
|
||||
instance.save()
|
||||
run_command_execution.apply_async(
|
||||
args=(instance.id,), task_id=str(instance.id)
|
||||
)
|
@@ -27,7 +27,6 @@ def on_app_ready(sender=None, headers=None, body=None, **kwargs):
|
||||
if cache.get("CELERY_APP_READY", 0) == 1:
|
||||
return
|
||||
cache.set("CELERY_APP_READY", 1, 10)
|
||||
logger.debug("App ready signal recv")
|
||||
tasks = get_after_app_ready_tasks()
|
||||
logger.debug("Start need start task: [{}]".format(
|
||||
", ".join(tasks))
|
||||
|
17
apps/ops/forms.py
Normal file
17
apps/ops/forms.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django import forms
|
||||
|
||||
from assets.models import SystemUser
|
||||
from .models import CommandExecution
|
||||
|
||||
|
||||
class CommandExecutionForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = CommandExecution
|
||||
fields = ['run_as', 'command']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
run_as_field = self.fields.get('run_as')
|
||||
run_as_field.queryset = SystemUser.objects.all()
|
@@ -2,7 +2,7 @@
|
||||
#
|
||||
|
||||
from .ansible.inventory import BaseInventory
|
||||
from assets.utils import get_assets_by_fullname_list, get_system_user_by_name
|
||||
from assets.utils import get_assets_by_id_list, get_system_user_by_id
|
||||
|
||||
__all__ = [
|
||||
'JMSInventory'
|
||||
@@ -14,19 +14,18 @@ class JMSInventory(BaseInventory):
|
||||
JMS Inventory is the manager with jumpserver assets, so you can
|
||||
write you own manager, construct you inventory
|
||||
"""
|
||||
def __init__(self, hostname_list, run_as_admin=False, run_as=None, become_info=None):
|
||||
def __init__(self, assets, run_as_admin=False, run_as=None, become_info=None):
|
||||
"""
|
||||
:param hostname_list: ["test1", ]
|
||||
:param host_id_list: ["test1", ]
|
||||
:param run_as_admin: True 是否使用管理用户去执行, 每台服务器的管理用户可能不同
|
||||
:param run_as: 是否统一使用某个系统用户去执行
|
||||
:param become_info: 是否become成某个用户去执行
|
||||
"""
|
||||
self.hostname_list = hostname_list
|
||||
self.assets = assets
|
||||
self.using_admin = run_as_admin
|
||||
self.run_as = run_as
|
||||
self.become_info = become_info
|
||||
|
||||
assets = self.get_jms_assets()
|
||||
host_list = []
|
||||
|
||||
for asset in assets:
|
||||
@@ -43,14 +42,10 @@ class JMSInventory(BaseInventory):
|
||||
host.update(become_info)
|
||||
super().__init__(host_list=host_list)
|
||||
|
||||
def get_jms_assets(self):
|
||||
assets = get_assets_by_fullname_list(self.hostname_list)
|
||||
return assets
|
||||
|
||||
def convert_to_ansible(self, asset, run_as_admin=False):
|
||||
info = {
|
||||
'id': asset.id,
|
||||
'hostname': asset.fullname,
|
||||
'hostname': asset.hostname,
|
||||
'ip': asset.ip,
|
||||
'port': asset.port,
|
||||
'vars': dict(),
|
||||
@@ -75,7 +70,7 @@ class JMSInventory(BaseInventory):
|
||||
return info
|
||||
|
||||
def get_run_user_info(self):
|
||||
system_user = get_system_user_by_name(self.run_as)
|
||||
system_user = self.run_as
|
||||
if not system_user:
|
||||
return {}
|
||||
else:
|
||||
|
56
apps/ops/migrations/0003_auto_20181207_1744.py
Normal file
56
apps/ops/migrations/0003_auto_20181207_1744.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# Generated by Django 2.1.4 on 2018-12-07 09:44
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0023_auto_20181016_1650'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('ops', '0002_celerytask'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CommandExecution',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('command', models.TextField(verbose_name='Command')),
|
||||
('_result', models.TextField(blank=True, null=True, verbose_name='Result')),
|
||||
('is_finished', models.BooleanField(default=False)),
|
||||
('date_created', models.DateTimeField(auto_now_add=True)),
|
||||
('date_start', models.DateTimeField(null=True)),
|
||||
('date_finished', models.DateTimeField(null=True)),
|
||||
('hosts', models.ManyToManyField(to='assets.Asset')),
|
||||
('run_as', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.SystemUser')),
|
||||
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='adhoc',
|
||||
name='run_as',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='adhoc',
|
||||
name='hosts',
|
||||
field=models.ManyToManyField(to='assets.Asset', verbose_name='Host'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='created_by',
|
||||
field=models.CharField(blank=True, default='', max_length=128),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='name',
|
||||
field=models.CharField(max_length=128, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='task',
|
||||
unique_together={('name', 'created_by')},
|
||||
),
|
||||
]
|
20
apps/ops/migrations/0004_adhoc_run_as.py
Normal file
20
apps/ops/migrations/0004_adhoc_run_as.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 2.1.4 on 2018-12-07 09:44
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0023_auto_20181016_1650'),
|
||||
('ops', '0003_auto_20181207_1744'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='adhoc',
|
||||
name='run_as',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.SystemUser'),
|
||||
),
|
||||
]
|
@@ -2,4 +2,5 @@
|
||||
#
|
||||
|
||||
from .adhoc import *
|
||||
from .celery import *
|
||||
from .celery import *
|
||||
from .command import *
|
||||
|
@@ -34,16 +34,17 @@ class Task(models.Model):
|
||||
One task can have some versions of adhoc, run a task only run the latest version adhoc
|
||||
"""
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
interval = models.IntegerField(verbose_name=_("Interval"), null=True, blank=True, help_text=_("Units: seconds"))
|
||||
crontab = models.CharField(verbose_name=_("Crontab"), null=True, blank=True, max_length=128, help_text=_("5 * * * *"))
|
||||
is_periodic = models.BooleanField(default=False)
|
||||
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"))
|
||||
created_by = models.CharField(max_length=128, blank=True, null=True, default='')
|
||||
created_by = models.CharField(max_length=128, blank=True, default='')
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
__latest_adhoc = None
|
||||
_ignore_auto_created_by = True
|
||||
|
||||
@property
|
||||
def short_id(self):
|
||||
@@ -94,7 +95,7 @@ class Task(models.Model):
|
||||
update_fields=None):
|
||||
from ..tasks import run_ansible_task
|
||||
super().save(
|
||||
force_insert=force_insert, force_update=force_update,
|
||||
force_insert=force_insert, force_update=force_update,
|
||||
using=using, update_fields=update_fields,
|
||||
)
|
||||
|
||||
@@ -108,7 +109,7 @@ class Task(models.Model):
|
||||
crontab = self.crontab
|
||||
|
||||
tasks = {
|
||||
self.name: {
|
||||
self.__str__(): {
|
||||
"task": run_ansible_task.name,
|
||||
"interval": interval,
|
||||
"crontab": crontab,
|
||||
@@ -119,11 +120,11 @@ class Task(models.Model):
|
||||
}
|
||||
create_or_update_celery_periodic_tasks(tasks)
|
||||
else:
|
||||
disable_celery_periodic_task(self.name)
|
||||
disable_celery_periodic_task(self.__str__())
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
super().delete(using=using, keep_parents=keep_parents)
|
||||
delete_celery_periodic_task(self.name)
|
||||
delete_celery_periodic_task(self.__str__())
|
||||
|
||||
@property
|
||||
def schedule(self):
|
||||
@@ -133,10 +134,11 @@ class Task(models.Model):
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
return self.name + '@' + str(self.created_by)
|
||||
|
||||
class Meta:
|
||||
db_table = 'ops_task'
|
||||
unique_together = ('name', 'created_by')
|
||||
get_latest_by = 'date_created'
|
||||
|
||||
|
||||
@@ -157,8 +159,9 @@ class AdHoc(models.Model):
|
||||
pattern = models.CharField(max_length=64, default='{}', verbose_name=_('Pattern'))
|
||||
_options = models.CharField(max_length=1024, default='', verbose_name=_('Options'))
|
||||
_hosts = models.TextField(blank=True, verbose_name=_('Hosts')) # ['hostname1', 'hostname2']
|
||||
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=128, default='', verbose_name=_("Run as"))
|
||||
run_as = models.ForeignKey('assets.SystemUser', null=True, on_delete=models.CASCADE)
|
||||
_become = models.CharField(max_length=1024, default='', verbose_name=_("Become"))
|
||||
created_by = models.CharField(max_length=64, default='', null=True, verbose_name=_('Create by'))
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
@@ -174,14 +177,6 @@ class AdHoc(models.Model):
|
||||
else:
|
||||
raise SyntaxError('Tasks should be a list: {}'.format(item))
|
||||
|
||||
@property
|
||||
def hosts(self):
|
||||
return json.loads(self._hosts)
|
||||
|
||||
@hosts.setter
|
||||
def hosts(self, item):
|
||||
self._hosts = json.dumps(item)
|
||||
|
||||
@property
|
||||
def inventory(self):
|
||||
if self.become:
|
||||
@@ -194,7 +189,7 @@ class AdHoc(models.Model):
|
||||
become_info = None
|
||||
|
||||
inventory = JMSInventory(
|
||||
self.hosts, run_as_admin=self.run_as_admin,
|
||||
self.hosts.all(), run_as_admin=self.run_as_admin,
|
||||
run_as=self.run_as, become_info=become_info
|
||||
)
|
||||
return inventory
|
||||
@@ -242,14 +237,13 @@ class AdHoc(models.Model):
|
||||
history.timedelta = time.time() - time_start
|
||||
history.save()
|
||||
|
||||
def _run_only(self, file_obj=None):
|
||||
def _run_only(self):
|
||||
runner = AdHocRunner(self.inventory, options=self.options)
|
||||
try:
|
||||
result = runner.run(
|
||||
self.tasks,
|
||||
self.pattern,
|
||||
self.task.name,
|
||||
file_obj=file_obj,
|
||||
)
|
||||
return result.results_raw, result.results_summary
|
||||
except AnsibleError as e:
|
||||
|
71
apps/ops/models/command.py
Normal file
71
apps/ops/models/command.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import uuid
|
||||
import json
|
||||
|
||||
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 ..ansible.runner import CommandRunner
|
||||
from ..inventory import JMSInventory
|
||||
|
||||
|
||||
class CommandExecution(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
hosts = models.ManyToManyField('assets.Asset')
|
||||
run_as = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE)
|
||||
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)
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
date_start = models.DateTimeField(null=True)
|
||||
date_finished = models.DateTimeField(null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.command[:10]
|
||||
|
||||
@property
|
||||
def inventory(self):
|
||||
return JMSInventory(self.hosts.all(), run_as=self.run_as)
|
||||
|
||||
@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 run(self):
|
||||
print('-'*10 + ' ' + ugettext('Task start') + ' ' + '-'*10)
|
||||
self.date_start = timezone.now()
|
||||
ok, msg = self.run_as.is_command_can_run(self.command)
|
||||
if ok:
|
||||
runner = CommandRunner(self.inventory)
|
||||
try:
|
||||
result = runner.execute(self.command, 'all')
|
||||
self.result = result.results_command
|
||||
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')
|
||||
self.result = {"error": msg}
|
||||
self.is_finished = True
|
||||
self.date_finished = timezone.now()
|
||||
self.save()
|
||||
print('-'*10 + ' ' + ugettext('Task end') + ' ' + '-'*10)
|
||||
return self.result
|
@@ -1,8 +1,19 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
from __future__ import unicode_literals
|
||||
from rest_framework import serializers
|
||||
from django.shortcuts import reverse
|
||||
|
||||
from .models import Task, AdHoc, AdHocRunHistory
|
||||
from .models import Task, AdHoc, AdHocRunHistory, CommandExecution
|
||||
|
||||
|
||||
class CeleryResultSerializer(serializers.Serializer):
|
||||
id = serializers.UUIDField()
|
||||
result = serializers.JSONField()
|
||||
state = serializers.CharField(max_length=16)
|
||||
|
||||
|
||||
class CeleryTaskSerializer(serializers.Serializer):
|
||||
pass
|
||||
|
||||
|
||||
class TaskSerializer(serializers.ModelSerializer):
|
||||
@@ -51,3 +62,23 @@ class AdHocRunHistorySerializer(serializers.ModelSerializer):
|
||||
fields = super().get_field_names(declared_fields, info)
|
||||
fields.extend(['summary', 'short_id'])
|
||||
return fields
|
||||
|
||||
|
||||
class CommandExecutionSerializer(serializers.ModelSerializer):
|
||||
result = serializers.JSONField(read_only=True)
|
||||
log_url = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = CommandExecution
|
||||
fields = [
|
||||
'id', 'hosts', 'run_as', 'command', 'result', 'log_url',
|
||||
'is_finished', 'date_created', 'date_finished'
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'result', 'is_finished', 'log_url', 'date_created',
|
||||
'date_finished'
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_log_url(obj):
|
||||
return reverse('api-ops:celery-task-log', kwargs={'pk': obj.id})
|
||||
|
@@ -2,7 +2,7 @@
|
||||
from celery import shared_task, subtask
|
||||
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from .models import Task
|
||||
from .models import Task, CommandExecution
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
@@ -28,6 +28,12 @@ def run_ansible_task(tid, callback=None, **kwargs):
|
||||
logger.error("No task found")
|
||||
|
||||
|
||||
@shared_task
|
||||
def run_command_execution(cid, **kwargs):
|
||||
execution = get_object_or_none(CommandExecution, id=cid)
|
||||
return execution.run()
|
||||
|
||||
|
||||
@shared_task
|
||||
def hello(name, callback=None):
|
||||
print("Hello {}".format(name))
|
||||
|
@@ -27,6 +27,7 @@
|
||||
var end = false;
|
||||
var error = false;
|
||||
var interval = 200;
|
||||
var success = true;
|
||||
|
||||
function calWinSize() {
|
||||
var t = $('#marker');
|
||||
@@ -34,20 +35,19 @@
|
||||
{#colWidth = 1.00 * t.width() / 6;#}
|
||||
}
|
||||
function resize() {
|
||||
{#console.log(rowHeight, window.innerHeight);#}
|
||||
{#console.log(colWidth, window.innerWidth);#}
|
||||
var rows = Math.floor(window.innerHeight / rowHeight) - 1;
|
||||
var cols = Math.floor(window.innerWidth / colWidth) - 2;
|
||||
console.log(rows, cols);
|
||||
term.resize(cols, rows);
|
||||
}
|
||||
function requestAndWrite() {
|
||||
if (!end) {
|
||||
if (!end && success) {
|
||||
success = false;
|
||||
$.ajax({
|
||||
url: url + '?mark=' + mark,
|
||||
method: "GET",
|
||||
contentType: "application/json; charset=utf-8"
|
||||
}).done(function(data, textStatue, jqXHR) {
|
||||
success = true;
|
||||
if (jqXHR.status === 203) {
|
||||
error = true;
|
||||
term.write('.');
|
||||
@@ -64,7 +64,14 @@
|
||||
}
|
||||
}
|
||||
$(document).ready(function () {
|
||||
term = new Terminal();
|
||||
term = new Terminal({
|
||||
cursorBlink: false,
|
||||
screenKeys: false,
|
||||
fontFamily: '"Monaco", "Consolas", "monospace"',
|
||||
fontSize: 12,
|
||||
rightClickSelectsWord: true,
|
||||
disableStdin: true
|
||||
});
|
||||
term.open(document.getElementById('term'));
|
||||
term.resize(80, 24);
|
||||
resize();
|
||||
|
254
apps/ops/templates/ops/command_execution_create.html
Normal file
254
apps/ops/templates/ops/command_execution_create.html
Normal file
@@ -0,0 +1,254 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block custom_head_css_js %}
|
||||
<link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">
|
||||
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.exhide.min.js' %}"></script>
|
||||
<script src="{% static 'js/jquery.form.min.js' %}"></script>
|
||||
<script src="{% static 'js/plugins/xterm/xterm.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'js/plugins/xterm/xterm.css' %}" />
|
||||
<script src="{% static 'js/plugins/xterm/addons/fit/fit.js' %}"></script>
|
||||
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
|
||||
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
|
||||
<style type="text/css">
|
||||
.xterm .xterm-screen canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
.select2-container .select2-selection--single {
|
||||
height: 34px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content">
|
||||
<div class="row">
|
||||
<div class="col-lg-3" id="split-left" style="padding-left: 3px">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-content mailbox-content" style="padding-top: 0;padding-left: 1px">
|
||||
<div class="file-manager ">
|
||||
<div id="assetTree" class="ztree"></div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-9 animated fadeInRight" id="split-right">
|
||||
<div class="tree-toggle">
|
||||
<div class="btn btn-sm btn-primary tree-toggle-btn" onclick="toggle()">
|
||||
<i class="fa fa-angle-left fa-x" id="toggle-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mail-box-header" style="padding-top: 5px;">
|
||||
<form enctype="multipart/form-data" method="post" class="form-horizontal" action="">
|
||||
<div class="form-group">
|
||||
<div id="term" style="height: 100%;width: 100%"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-2">
|
||||
<select class="select2 form-control" id="system-users-select">
|
||||
{% for s in system_users %}
|
||||
{% if s.protocol == 'ssh' and s.login_mode == 'auto' %}
|
||||
<option value="{{ s.id }}">{{ s }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-10">
|
||||
<div class="input-group" style="height: 100%">
|
||||
<input type="text" id="command-text" class="form-control">
|
||||
<span class="input-group-btn">
|
||||
<button type="button" class="btn btn-primary btn-execute">{% trans 'Go' %}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
var zTree, show = 0;
|
||||
var systemUserId = null;
|
||||
|
||||
function initTree() {
|
||||
var setting = {
|
||||
check: {
|
||||
enable: true
|
||||
},
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
showLine: true
|
||||
},
|
||||
data: {
|
||||
simpleData: {
|
||||
enable: true
|
||||
}
|
||||
},
|
||||
edit: {
|
||||
enable: true,
|
||||
showRemoveBtn: false,
|
||||
showRenameBtn: false,
|
||||
drag: {
|
||||
isCopy: true,
|
||||
isMove: true
|
||||
}
|
||||
},
|
||||
callback: {
|
||||
onCheck: onCheck
|
||||
}
|
||||
};
|
||||
var url = "{% url 'api-perms:my-nodes-assets-as-tree' %}";
|
||||
if (systemUserId) {
|
||||
url += '?system_user=' + systemUserId
|
||||
}
|
||||
|
||||
$.get(url, function(data, status){
|
||||
$.fn.zTree.init($("#assetTree"), setting, data);
|
||||
zTree = $.fn.zTree.getZTreeObj("assetTree");
|
||||
});
|
||||
}
|
||||
|
||||
function getSelectedAssetsNode() {
|
||||
var nodes = zTree.getCheckedNodes(true);
|
||||
var assetsNodeId = [];
|
||||
var assetsNode = [];
|
||||
nodes.forEach(function (node) {
|
||||
if (node.meta.type === 'asset' && !node.isHidden && node.meta.asset.protocol === 'ssh') {
|
||||
if (assetsNodeId.indexOf(node.id) === -1) {
|
||||
assetsNodeId.push(node.id);
|
||||
assetsNode.push(node)
|
||||
}
|
||||
}
|
||||
});
|
||||
return assetsNode;
|
||||
}
|
||||
|
||||
function onCheck(e, treeId, treeNode) {
|
||||
var nodes = getSelectedAssetsNode();
|
||||
var nodes_names = nodes.map(function (node) {
|
||||
return node.name;
|
||||
});
|
||||
var message = "已选择资产: ";
|
||||
message += nodes_names.join(", ");
|
||||
message += "\r\n";
|
||||
message += "总共: " + nodes_names.length + "个\r\n";
|
||||
term.clear();
|
||||
term.write(message)
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (show === 0) {
|
||||
$("#split-left").hide(500, function () {
|
||||
$("#split-right").attr("class", "col-lg-12");
|
||||
$("#toggle-icon").attr("class", "fa fa-angle-right fa-x");
|
||||
show = 1;
|
||||
});
|
||||
} else {
|
||||
$("#split-right").attr("class", "col-lg-9");
|
||||
$("#toggle-icon").attr("class", "fa fa-angle-left fa-x");
|
||||
$("#split-left").show(500);
|
||||
show = 0;
|
||||
}
|
||||
}
|
||||
|
||||
var term = null;
|
||||
|
||||
function initResultTerminal() {
|
||||
term = new Terminal({
|
||||
cursorBlink: false,
|
||||
screenKeys: false,
|
||||
fontFamily: '"Monaco", "Consolas", "monospace"',
|
||||
fontSize: 12,
|
||||
rightClickSelectsWord: true,
|
||||
disableStdin: true,
|
||||
theme: {
|
||||
background: '#1f1b1b'
|
||||
}
|
||||
});
|
||||
term.open(document.getElementById('term'));
|
||||
term.write("选择左侧资产, 选择运行的系统用户,批量执行命令\r\n")
|
||||
}
|
||||
|
||||
$(document).ready(function(){
|
||||
systemUserId = $('#system-users-select').val();
|
||||
$(".select2").select2().on('select2:select', function(evt) {
|
||||
var data = evt.params.data;
|
||||
systemUserId = data.id;
|
||||
initTree();
|
||||
});
|
||||
initTree();
|
||||
initResultTerminal();
|
||||
}).on('click', '.btn-execute', function () {
|
||||
if (!term) {
|
||||
initResultTerminal()
|
||||
}
|
||||
var url = '{% url "api-ops:command-execution-list" %}';
|
||||
var run_as = systemUserId;
|
||||
var command = $("#command-text").val();
|
||||
var hosts = getSelectedAssetsNode().map(function (node) {
|
||||
return node.id;
|
||||
});
|
||||
var data = {
|
||||
hosts: hosts,
|
||||
run_as: run_as,
|
||||
command: command
|
||||
};
|
||||
var mark = '';
|
||||
var log_url = null;
|
||||
var end = false;
|
||||
var error = false;
|
||||
var int = null;
|
||||
var interval = 200;
|
||||
|
||||
function writeExecutionOutput() {
|
||||
if (!end) {
|
||||
$.ajax({
|
||||
url: log_url + '?mark=' + mark,
|
||||
method: "GET",
|
||||
contentType: "application/json; charset=utf-8"
|
||||
}).done(function(data, textStatue, jqXHR) {
|
||||
if (jqXHR.status === 203) {
|
||||
error = true;
|
||||
term.write('.');
|
||||
interval = 500;
|
||||
}
|
||||
if (jqXHR.status === 200){
|
||||
term.write(data.data);
|
||||
mark = data.mark;
|
||||
if (data.end){
|
||||
end = true;
|
||||
window.clearInterval(int)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
APIUpdateAttr({
|
||||
url: url,
|
||||
body: JSON.stringify(data),
|
||||
method: 'POST',
|
||||
flash_message: false,
|
||||
success: function (resp) {
|
||||
term.write("{% trans 'Pending' %}" + "...\r\n");
|
||||
log_url = resp.log_url;
|
||||
int = setInterval(function () {
|
||||
writeExecutionOutput()
|
||||
}, interval);
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
95
apps/ops/templates/ops/command_execution_list.html
Normal file
95
apps/ops/templates/ops/command_execution_list.html
Normal file
@@ -0,0 +1,95 @@
|
||||
{% extends '_base_list.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load common_tags %}
|
||||
|
||||
{% block content_left_head %}
|
||||
{% endblock %}
|
||||
|
||||
{% block table_search %}
|
||||
<form id="search_form" method="get" action="" class="pull-right form-inline">
|
||||
<div class="form-group" id="date">
|
||||
<div class="input-daterange input-group" id="datepicker">
|
||||
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
|
||||
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" value="{{ date_from|date:'Y-m-d' }}">
|
||||
<span class="input-group-addon">to</span>
|
||||
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_to" value="{{ date_to|date:'Y-m-d' }}">
|
||||
</div>
|
||||
</div>
|
||||
{% if user_list %}
|
||||
<div class="input-group">
|
||||
<select class="select2 form-control" name="user">
|
||||
<option value="">{% trans 'User' %}</option>
|
||||
{% for u in user_list %}
|
||||
<option value="{{ u.id }}" {% if u.id == user_id %} selected {% endif %}>{{ u }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control input-sm" name="keyword" placeholder="{% trans 'Search' %}" value="{{ keyword }}">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<div class="input-group-btn">
|
||||
<button id='search_btn' type="submit" class="btn btn-sm btn-primary">
|
||||
{% trans "Search" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block table_head %}
|
||||
<th class="text-center"></th>
|
||||
<th class="text-center">{% trans 'Hosts' %}</th>
|
||||
<th class="text-center">{% trans 'User' %}</th>
|
||||
<th class="text-center">{% trans 'Command' %}</th>
|
||||
<th class="text-center">{% trans 'Output' %}</th>
|
||||
<th class="text-center">{% trans 'Finished' %}</th>
|
||||
<th class="text-center">{% trans 'Success' %}</th>
|
||||
<th class="text-center">{% trans 'Date start' %}</th>
|
||||
<th class="text-center">{% trans 'Date finished' %}</th>
|
||||
{% endblock %}
|
||||
|
||||
{% block table_body %}
|
||||
{% for object in object_list %}
|
||||
<tr class="gradeX">
|
||||
<td class="text-center"><input type="checkbox" class="cbx-term"></td>
|
||||
<td class="text-center">{{ object.hosts.count }}</td>
|
||||
<td class="text-center">{{ object.user }}</td>
|
||||
<td class="text-center">{{ object.command| truncatechars:16 }}</td>
|
||||
<td class="text-center"><a href="{% url "ops:celery-task-log" pk=object.id %}" target="_blank">查看</a></td>
|
||||
<td class="text-center">{{ object.is_finished | state_show | safe }}</td>
|
||||
<td class="text-center">{{ object.is_success | state_show | safe }}</td>
|
||||
<td class="text-center">{{ object.date_start }}</td>
|
||||
<td class="text-center">{{ object.date_finished }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('table').DataTable({
|
||||
"searching": false,
|
||||
"paging": false,
|
||||
"bInfo" : false,
|
||||
"order": []
|
||||
});
|
||||
$('.select2').select2({
|
||||
dropdownAutoWidth : true,
|
||||
width: 'auto'
|
||||
});
|
||||
$('#date .input-daterange').datepicker({
|
||||
format: "yyyy-mm-dd",
|
||||
todayBtn: "linked",
|
||||
keyboardNavigation: false,
|
||||
forceParse: false,
|
||||
calendarWeeks: true,
|
||||
autoclose: true
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@@ -4,10 +4,11 @@
|
||||
|
||||
{% block custom_head_css_js %}
|
||||
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
|
||||
<link href="{% static "css/plugins/sweetalert/sweetalert.css" %}" rel="stylesheet">
|
||||
<link href="{% static 'css/plugins/sweetalert/sweetalert.css' %}" rel="stylesheet">
|
||||
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
|
||||
<script src="{% static "js/plugins/sweetalert/sweetalert.min.js" %}"></script>
|
||||
<script src="{% static 'js/plugins/sweetalert/sweetalert.min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
@@ -25,7 +26,7 @@
|
||||
<a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="text-center celery-task-log" onclick="window.open('{% url 'ops:celery-task-log' pk=object.latest_history.pk %}','', 'width=800,height=600,left=400,top=400')"><i class="fa fa-laptop"></i> {% trans 'Last run output' %} </a>
|
||||
<a class="text-center celery-task-log" onclick="window.open("{% url 'ops:celery-task-log' pk=object.latest_history.pk %}",'', 'width=800,height=600,left=400,top=400')"><i class="fa fa-laptop"></i> {% trans 'Last run output' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@@ -11,11 +11,12 @@ app_name = "ops"
|
||||
router = DefaultRouter()
|
||||
router.register(r'tasks', api.TaskViewSet, 'task')
|
||||
router.register(r'adhoc', api.AdHocViewSet, 'adhoc')
|
||||
router.register(r'history', api.AdHocRunHistorySet, 'history')
|
||||
router.register(r'command-executions', api.CommandExecutionViewSet, 'command-execution')
|
||||
|
||||
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'),
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
|
@@ -18,4 +18,7 @@ urlpatterns = [
|
||||
path('adhoc/<uuid:pk>/history/', views.AdHocHistoryView.as_view(), name='adhoc-history'),
|
||||
path('adhoc/history/<uuid:pk>/', views.AdHocHistoryDetailView.as_view(), name='adhoc-history-detail'),
|
||||
path('celery/task/<uuid:pk>/log/', views.CeleryTaskLogView.as_view(), name='celery-task-log'),
|
||||
|
||||
path('command-execution/', views.CommandExecutionListView.as_view(), name='command-execution-list'),
|
||||
path('command-execution/start/', views.CommandExecutionStartView.as_view(), name='command-execution-start'),
|
||||
]
|
||||
|
@@ -1,5 +1,7 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from orgs.utils import set_to_root_org
|
||||
from .models import Task, AdHoc
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@@ -10,15 +12,14 @@ def get_task_by_id(task_id):
|
||||
|
||||
|
||||
def update_or_create_ansible_task(
|
||||
task_name, hosts, tasks,
|
||||
task_name, hosts, tasks, created_by,
|
||||
interval=None, crontab=None, is_periodic=False,
|
||||
callback=None, pattern='all', options=None,
|
||||
run_as_admin=False, run_as="", become_info=None,
|
||||
created_by=None,
|
||||
run_as_admin=False, run_as=None, become_info=None,
|
||||
):
|
||||
if not hosts or not tasks or not task_name:
|
||||
return
|
||||
|
||||
set_to_root_org()
|
||||
defaults = {
|
||||
'name': task_name,
|
||||
'interval': interval,
|
||||
@@ -29,22 +30,27 @@ def update_or_create_ansible_task(
|
||||
}
|
||||
|
||||
created = False
|
||||
task, _ = Task.objects.update_or_create(
|
||||
defaults=defaults, name=task_name,
|
||||
task, ok = Task.objects.update_or_create(
|
||||
defaults=defaults, name=task_name, created_by=created_by
|
||||
)
|
||||
|
||||
adhoc = task.latest_adhoc
|
||||
adhoc = task.get_latest_adhoc()
|
||||
new_adhoc = AdHoc(task=task, pattern=pattern,
|
||||
run_as_admin=run_as_admin,
|
||||
run_as=run_as)
|
||||
new_adhoc.hosts = hosts
|
||||
new_adhoc.tasks = tasks
|
||||
new_adhoc.options = options
|
||||
new_adhoc.become = become_info
|
||||
|
||||
if not adhoc or adhoc != new_adhoc:
|
||||
print("Task create new adhoc: {}".format(task_name))
|
||||
hosts_same = True
|
||||
if adhoc:
|
||||
old_hosts = set([str(asset.id) for asset in adhoc.hosts.all()])
|
||||
new_hosts = set([str(asset.id) for asset in hosts])
|
||||
hosts_same = old_hosts == new_hosts
|
||||
|
||||
if not adhoc or adhoc != new_adhoc or not hosts_same:
|
||||
logger.info(_("Update task content: {}").format(task_name))
|
||||
new_adhoc.save()
|
||||
new_adhoc.hosts.set(hosts)
|
||||
task.latest_adhoc = new_adhoc
|
||||
created = True
|
||||
return task, created
|
||||
|
3
apps/ops/views/__init__.py
Normal file
3
apps/ops/views/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .adhoc import *
|
||||
from .celery import *
|
||||
from .command import *
|
@@ -2,14 +2,22 @@
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
from django.views.generic import ListView, DetailView, TemplateView
|
||||
from django.views.generic import ListView, DetailView
|
||||
|
||||
from common.mixins import DatetimeSearchMixin
|
||||
from .models import Task, AdHoc, AdHocRunHistory, CeleryTask
|
||||
from common.permissions import SuperUserRequiredMixin, AdminUserRequiredMixin
|
||||
from common.permissions import AdminUserRequiredMixin
|
||||
from orgs.utils import current_org
|
||||
from ..models import Task, AdHoc, AdHocRunHistory
|
||||
|
||||
|
||||
class TaskListView(SuperUserRequiredMixin, DatetimeSearchMixin, ListView):
|
||||
__all__ = [
|
||||
'TaskListView', 'TaskDetailView', 'TaskHistoryView',
|
||||
'TaskAdhocView', 'AdHocDetailView', 'AdHocHistoryDetailView',
|
||||
'AdHocHistoryView'
|
||||
]
|
||||
|
||||
|
||||
class TaskListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
|
||||
paginate_by = settings.DISPLAY_PER_PAGE
|
||||
model = Task
|
||||
ordering = ('-date_created',)
|
||||
@@ -18,18 +26,23 @@ class TaskListView(SuperUserRequiredMixin, DatetimeSearchMixin, ListView):
|
||||
keyword = ''
|
||||
|
||||
def get_queryset(self):
|
||||
self.queryset = super().get_queryset()
|
||||
queryset = super().get_queryset()
|
||||
if current_org.is_real():
|
||||
queryset = queryset.filter(created_by=current_org.id)
|
||||
else:
|
||||
queryset = queryset.filter(created_by='')
|
||||
|
||||
self.keyword = self.request.GET.get('keyword', '')
|
||||
self.queryset = self.queryset.filter(
|
||||
queryset = queryset.filter(
|
||||
date_created__gt=self.date_from,
|
||||
date_created__lt=self.date_to
|
||||
)
|
||||
|
||||
if self.keyword:
|
||||
self.queryset = self.queryset.filter(
|
||||
queryset = queryset.filter(
|
||||
name__icontains=self.keyword,
|
||||
)
|
||||
return self.queryset
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
@@ -43,10 +56,16 @@ class TaskListView(SuperUserRequiredMixin, DatetimeSearchMixin, ListView):
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class TaskDetailView(SuperUserRequiredMixin, DetailView):
|
||||
class TaskDetailView(AdminUserRequiredMixin, DetailView):
|
||||
model = Task
|
||||
template_name = 'ops/task_detail.html'
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
if current_org:
|
||||
queryset = queryset.filter(created_by=current_org.id)
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Ops'),
|
||||
@@ -56,7 +75,7 @@ class TaskDetailView(SuperUserRequiredMixin, DetailView):
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class TaskAdhocView(SuperUserRequiredMixin, DetailView):
|
||||
class TaskAdhocView(AdminUserRequiredMixin, DetailView):
|
||||
model = Task
|
||||
template_name = 'ops/task_adhoc.html'
|
||||
|
||||
@@ -69,7 +88,7 @@ class TaskAdhocView(SuperUserRequiredMixin, DetailView):
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class TaskHistoryView(SuperUserRequiredMixin, DetailView):
|
||||
class TaskHistoryView(AdminUserRequiredMixin, DetailView):
|
||||
model = Task
|
||||
template_name = 'ops/task_history.html'
|
||||
|
||||
@@ -82,7 +101,7 @@ class TaskHistoryView(SuperUserRequiredMixin, DetailView):
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AdHocDetailView(SuperUserRequiredMixin, DetailView):
|
||||
class AdHocDetailView(AdminUserRequiredMixin, DetailView):
|
||||
model = AdHoc
|
||||
template_name = 'ops/adhoc_detail.html'
|
||||
|
||||
@@ -95,7 +114,7 @@ class AdHocDetailView(SuperUserRequiredMixin, DetailView):
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AdHocHistoryView(SuperUserRequiredMixin, DetailView):
|
||||
class AdHocHistoryView(AdminUserRequiredMixin, DetailView):
|
||||
model = AdHoc
|
||||
template_name = 'ops/adhoc_history.html'
|
||||
|
||||
@@ -108,7 +127,7 @@ class AdHocHistoryView(SuperUserRequiredMixin, DetailView):
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AdHocHistoryDetailView(SuperUserRequiredMixin, DetailView):
|
||||
class AdHocHistoryDetailView(AdminUserRequiredMixin, DetailView):
|
||||
model = AdHocRunHistory
|
||||
template_name = 'ops/adhoc_history_detail.html'
|
||||
|
||||
@@ -121,6 +140,5 @@ class AdHocHistoryDetailView(SuperUserRequiredMixin, DetailView):
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class CeleryTaskLogView(AdminUserRequiredMixin, DetailView):
|
||||
template_name = 'ops/celery_task_log.html'
|
||||
model = CeleryTask
|
||||
|
||||
|
14
apps/ops/views/celery.py
Normal file
14
apps/ops/views/celery.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.views.generic import DetailView
|
||||
|
||||
from common.permissions import AdminUserRequiredMixin
|
||||
from ..models import CeleryTask
|
||||
|
||||
|
||||
__all__ = ['CeleryTaskLogView']
|
||||
|
||||
|
||||
class CeleryTaskLogView(AdminUserRequiredMixin, DetailView):
|
||||
template_name = 'ops/celery_task_log.html'
|
||||
model = CeleryTask
|
76
apps/ops/views/command.py
Normal file
76
apps/ops/views/command.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
from django.views.generic import ListView, TemplateView
|
||||
|
||||
from common.mixins import DatetimeSearchMixin
|
||||
from ..models import CommandExecution
|
||||
from ..forms import CommandExecutionForm
|
||||
|
||||
|
||||
__all__ = [
|
||||
'CommandExecutionListView', 'CommandExecutionStartView'
|
||||
]
|
||||
|
||||
|
||||
class CommandExecutionListView(DatetimeSearchMixin, ListView):
|
||||
template_name = 'ops/command_execution_list.html'
|
||||
model = CommandExecution
|
||||
paginate_by = settings.DISPLAY_PER_PAGE
|
||||
ordering = ('-date_created',)
|
||||
context_object_name = 'task_list'
|
||||
keyword = ''
|
||||
|
||||
def _get_queryset(self):
|
||||
self.keyword = self.request.GET.get('keyword', '')
|
||||
queryset = super().get_queryset()
|
||||
if self.date_from:
|
||||
queryset = queryset.filter(date_start__gte=self.date_from)
|
||||
if self.date_to:
|
||||
queryset = queryset.filter(date_start__lte=self.date_to)
|
||||
if self.keyword:
|
||||
queryset = queryset.filter(command__icontains=self.keyword)
|
||||
return queryset
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self._get_queryset().filter(user=self.request.user)
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Ops'),
|
||||
'action': _('Command execution list'),
|
||||
'date_from': self.date_from,
|
||||
'date_to': self.date_to,
|
||||
'keyword': self.keyword,
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class CommandExecutionStartView(TemplateView):
|
||||
template_name = 'ops/command_execution_create.html'
|
||||
form_class = CommandExecutionForm
|
||||
|
||||
def get_user_system_users(self):
|
||||
from perms.utils import AssetPermissionUtil
|
||||
user = self.request.user
|
||||
util = AssetPermissionUtil(user)
|
||||
system_users = [s for s in util.get_system_users() if s.protocol == 'ssh']
|
||||
return system_users
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
system_users = self.get_user_system_users()
|
||||
context = {
|
||||
'app': _('Ops'),
|
||||
'action': _('Command execution'),
|
||||
'form': self.get_form(),
|
||||
'system_users': system_users
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_form(self):
|
||||
return self.form_class()
|
Reference in New Issue
Block a user