* [Update] 任务区分org

* [Update] 修改翻译

* [Update] 使用id而不是hostname

* [Update] 执行命令

* [Update] 修改一些东西

* [Update] 暂存

* [Update] 用户执行命令

* [Update] 添加资产授权模块-tree

* [Update] 暂时这样

* [Update] 批量命令执行

* [Update] 修改表结构

* [Update] 更新翻译

* [Update] 删除cloud模块无效中文翻译
This commit is contained in:
老广
2018-12-10 10:11:54 +08:00
committed by GitHub
parent d91599ffab
commit 3d13f3a17d
81 changed files with 18809 additions and 7278 deletions

View File

@@ -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": "",

View File

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

@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
#
from .adhoc import *
from .celery import *
from .command import *

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View 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 %}

View 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 %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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