mirror of https://github.com/jumpserver/jumpserver
Merge branch 'ansible_api' into ops_dev
# Conflicts: # apps/jumpserver/urls.py # apps/locale/zh/LC_MESSAGES/django.po # apps/templates/_nav.html # requirements.txt # run_server.pypull/530/head
commit
c289d6a4c5
|
@ -180,6 +180,12 @@ LOGGING = {
|
|||
'formatter': 'main',
|
||||
'filename': os.path.join(PROJECT_DIR, 'logs', 'jumpserver.log')
|
||||
},
|
||||
'ansible_logs': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.FileHandler',
|
||||
'formatter': 'main',
|
||||
'filename': os.path.join(PROJECT_DIR, 'logs', 'ansible.log')
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'django': {
|
||||
|
@ -208,6 +214,10 @@ LOGGING = {
|
|||
'jumpserver.users.view': {
|
||||
'handlers': ['console', 'file'],
|
||||
'level': LOG_LEVEL,
|
||||
},
|
||||
'ops.ansible_api': {
|
||||
'handlers': ['console', 'ansible_logs'],
|
||||
'level': LOG_LEVEL,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ from django.views.generic.base import TemplateView
|
|||
urlpatterns = [
|
||||
url(r'^captcha/', include('captcha.urls')),
|
||||
url(r'^$', TemplateView.as_view(template_name='base.html'), name='index'),
|
||||
|
||||
url(r'^users/', include('users.urls.views_urls', namespace='users')),
|
||||
url(r'^assets/', include('assets.urls.views_urls', namespace='assets')),
|
||||
url(r'^perms/', include('perms.urls.views_urls', namespace='perms')),
|
||||
|
@ -32,6 +33,10 @@ urlpatterns = [
|
|||
url(r'^api/perms/', include('perms.urls.api_urls', namespace='api-perms')),
|
||||
url(r'^api/audits/', include('audits.urls.api_urls', namespace='api-audits')),
|
||||
url(r'^api/terminal/', include('terminal.urls.api_urls', namespace='api-terminal')),
|
||||
url(r'^(api/)?users/', include('users.urls')),
|
||||
url(r'^assets/', include('assets.urls')),
|
||||
url(r'^perms/', include('perms.urls')),
|
||||
url(r'^(api/)?ops/', include('ops.urls')),
|
||||
]
|
||||
|
||||
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -1,310 +0,0 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import json
|
||||
from ansible.executor.task_queue_manager import TaskQueueManager
|
||||
from ansible.inventory import Inventory, Host, Group
|
||||
from ansible.vars import VariableManager
|
||||
from ansible.parsing.dataloader import DataLoader
|
||||
from ansible.executor import playbook_executor
|
||||
from ansible.utils.display import Display
|
||||
from ansible.playbook.play import Play
|
||||
import ansible.constants as default_config
|
||||
from ansible.plugins.callback import CallbackBase
|
||||
|
||||
|
||||
class AnsibleError(StandardError):
|
||||
pass
|
||||
|
||||
|
||||
class Config(object):
|
||||
"""Ansible运行时配置类, 用于初始化Ansible.
|
||||
"""
|
||||
def __init__(self, verbosity=None, inventory=None, listhosts=None, subset=None, module_paths=None, extra_vars=None,
|
||||
forks=None, ask_vault_pass=None, vault_password_files=None, new_vault_password_file=None,
|
||||
output_file=None, tags=None, skip_tags=None, one_line=None, tree=None, ask_sudo_pass=None, ask_su_pass=None,
|
||||
sudo=None, sudo_user=None, become=None, become_method=None, become_user=None, become_ask_pass=None,
|
||||
ask_pass=None, private_key_file=None, remote_user=None, connection=None, timeout=None, ssh_common_args=None,
|
||||
sftp_extra_args=None, scp_extra_args=None, ssh_extra_args=None, poll_interval=None, seconds=None, check=None,
|
||||
syntax=None, diff=None, force_handlers=None, flush_cache=None, listtasks=None, listtags=None, module_path=None):
|
||||
self.verbosity = verbosity
|
||||
self.inventory = inventory
|
||||
self.listhosts = listhosts
|
||||
self.subset = subset
|
||||
self.module_paths = module_paths
|
||||
self.extra_vars = extra_vars
|
||||
self.forks = forks
|
||||
self.ask_vault_pass = ask_vault_pass
|
||||
self.vault_password_files = vault_password_files
|
||||
self.new_vault_password_file = new_vault_password_file
|
||||
self.output_file = output_file
|
||||
self.tags = tags
|
||||
self.skip_tags = skip_tags
|
||||
self.one_line = one_line
|
||||
self.tree = tree
|
||||
self.ask_sudo_pass = ask_sudo_pass
|
||||
self.ask_su_pass = ask_su_pass
|
||||
self.sudo = sudo
|
||||
self.sudo_user = sudo_user
|
||||
self.become = become
|
||||
self.become_method = become_method
|
||||
self.become_user = become_user
|
||||
self.become_ask_pass = become_ask_pass
|
||||
self.ask_pass = ask_pass
|
||||
self.private_key_file = private_key_file
|
||||
self.remote_user = remote_user
|
||||
self.connection = connection
|
||||
self.timeout = timeout
|
||||
self.ssh_common_args = ssh_common_args
|
||||
self.sftp_extra_args = sftp_extra_args
|
||||
self.scp_extra_args = scp_extra_args
|
||||
self.ssh_extra_args = ssh_extra_args
|
||||
self.poll_interval = poll_interval
|
||||
self.seconds = seconds
|
||||
self.check = check
|
||||
self.syntax = syntax
|
||||
self.diff = diff
|
||||
self.force_handlers = force_handlers
|
||||
self.flush_cache = flush_cache
|
||||
self.listtasks = listtasks
|
||||
self.listtags = listtags
|
||||
self.module_path = module_path
|
||||
self.__overwrite_default()
|
||||
|
||||
def __overwrite_default(self):
|
||||
"""上面并不能包含Ansible所有的配置, 如果有其他的配置,
|
||||
可以通过替换default_config模块里面的变量进行重载,
|
||||
比如 default_config.DEFAULT_ASK_PASS = False.
|
||||
"""
|
||||
default_config.HOST_KEY_CHECKING = False
|
||||
|
||||
|
||||
class MyInventory(object):
|
||||
"""Ansible Inventory对象的封装, Inventory是Ansbile中的核心概念(资产清单),
|
||||
这个概念和CMDB很像,都是对资产的抽象. 为了简化Inventory的使用, 通过传入资产列表即可初始化Inventory.
|
||||
"""
|
||||
|
||||
def __init__(self, *assets, **group):
|
||||
"""初始化Inventory对象, args为一个资产列表, kwargs是资产组变量列表, 比如
|
||||
args:
|
||||
[{
|
||||
"name": "asset_name",
|
||||
"ip": "asset_ip",
|
||||
"port": "asset_port",
|
||||
"username": "asset_user",
|
||||
"password": "asset_pass",
|
||||
"key": "asset_private_key",
|
||||
"group": "asset_group_name",
|
||||
...
|
||||
}]
|
||||
kwargs:
|
||||
"groupName1": {"group_variable1": "value1",...}
|
||||
"groupName2": {"group_variable1": "value1",...}
|
||||
"""
|
||||
self.assets = assets
|
||||
self.assets_group = group
|
||||
self.loader = DataLoader()
|
||||
self.variable_manager = VariableManager()
|
||||
self.groups = []
|
||||
self.inventory = self.gen_inventory()
|
||||
|
||||
def __gen_group(self):
|
||||
"""初始化Ansible Group, 将资产添加到Inventory里面
|
||||
:return: None
|
||||
"""
|
||||
# 创建Ansible Group.
|
||||
for asset in self.assets:
|
||||
g_name = asset.get('group', 'default')
|
||||
if g_name not in [g.name for g in self.groups]:
|
||||
group = Group(name=asset.get('group', 'default'))
|
||||
|
||||
self.groups.append(group)
|
||||
|
||||
# 初始化组变量
|
||||
for group_name, variables in self.assets_group.iteritems():
|
||||
for g in self.groups:
|
||||
if g.name == group_name:
|
||||
for v_name, v_value in variables:
|
||||
g.set_variable(v_name, v_value)
|
||||
|
||||
# 往组里面添加Host
|
||||
for asset in self.assets:
|
||||
host = Host(name=asset['name'], port=asset['port'])
|
||||
host.set_variable('ansible_ssh_host', asset['ip'])
|
||||
host.set_variable('ansible_ssh_port', asset['port'])
|
||||
host.set_variable('ansible_ssh_user', asset['username'])
|
||||
|
||||
if asset.get('password'):
|
||||
host.set_variable('ansible_ssh_pass', asset['password'])
|
||||
if asset.get('key'):
|
||||
host.set_variable('ansible_ssh_private_key_file', asset['key'])
|
||||
|
||||
for key, value in asset.iteritems():
|
||||
if key not in ["name", "port", "ip", "username", "password", "key"]:
|
||||
host.set_variable(key, value)
|
||||
for g in self.groups:
|
||||
if g.name == asset.get('group', 'default'):
|
||||
g.add_host(host)
|
||||
|
||||
def validate(self):
|
||||
pass
|
||||
|
||||
def gen_inventory(self):
|
||||
self.validate()
|
||||
i = Inventory(loader=self.loader, variable_manager=self.variable_manager, host_list=[])
|
||||
self.__gen_group()
|
||||
for g in self.groups:
|
||||
i.add_group(g)
|
||||
self.variable_manager.set_inventory(i)
|
||||
return i
|
||||
|
||||
|
||||
class PlayBookRunner(object):
|
||||
"""用于执行AnsiblePlaybook的接口.简化Playbook对象的使用
|
||||
"""
|
||||
|
||||
def __init__(self, inventory, config, palybook_path, playbook_var, become_pass, verbosity=0):
|
||||
"""
|
||||
:param inventory: myinventory实例
|
||||
:param config: Config实例
|
||||
:param palybook_path: playbook的路径
|
||||
:param playbook_var: 执行Playbook时的变量
|
||||
:param become_pass: sudo passsword
|
||||
:param verbosity: --verbosity
|
||||
"""
|
||||
|
||||
self.options = config
|
||||
self.options.verbosity = verbosity
|
||||
self.options.connection = 'smart'
|
||||
|
||||
# 设置verbosity级别, 及命令行的--verbose选项
|
||||
self.display = Display()
|
||||
self.display.verbosity = self.options.verbosity
|
||||
playbook_executor.verbosity = self.options.verbosity
|
||||
|
||||
# sudo成其他用户的配置
|
||||
self.options.become = True
|
||||
self.options.become_method = 'sudo'
|
||||
self.options.become_user = 'root'
|
||||
passwords = {'become_pass': become_pass}
|
||||
|
||||
# 传入playbook的路径,以及执行需要的变量
|
||||
inventory.variable_manager.extra_vars = playbook_var
|
||||
pb_dir = os.path.dirname(__file__)
|
||||
playbook = "%s/%s" % (pb_dir, palybook_path)
|
||||
|
||||
# 初始化playbook的executor
|
||||
self.pbex = playbook_executor.PlaybookExecutor(
|
||||
playbooks=[playbook],
|
||||
inventory=inventory,
|
||||
variable_manager=inventory.variable_manager,
|
||||
loader=inventory.loader,
|
||||
options=self.options,
|
||||
passwords=passwords)
|
||||
|
||||
def run(self):
|
||||
"""执行Playbook, 记录执行日志, 处理执行结果.
|
||||
:return: <AnsibleResult>对象
|
||||
"""
|
||||
self.pbex.run()
|
||||
stats = self.pbex._tqm._stats
|
||||
|
||||
# 测试执行是否成功
|
||||
run_success = True
|
||||
hosts = sorted(stats.processed.keys())
|
||||
for h in hosts:
|
||||
t = stats.summarize(h)
|
||||
if t['unreachable'] > 0 or t['failures'] > 0:
|
||||
run_success = False
|
||||
|
||||
# TODO: 记录执行日志, 处理执行结果.
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
class ADHocRunner(object):
|
||||
"""ADHoc接口
|
||||
"""
|
||||
def __init__(self, inventory, config, become_pass=None, verbosity=0):
|
||||
"""
|
||||
:param inventory: myinventory实例
|
||||
:param config: Config实例
|
||||
:param play_data:
|
||||
play_data = dict(
|
||||
name="Ansible Ad-Hoc",
|
||||
hosts=pattern,
|
||||
gather_facts=True,
|
||||
tasks=[dict(action=dict(module='service', args={'name': 'vsftpd', 'state': 'restarted'}), async=async, poll=poll)]
|
||||
)
|
||||
"""
|
||||
|
||||
self.options = config
|
||||
self.options.verbosity = verbosity
|
||||
self.options.connection = 'smart'
|
||||
|
||||
# 设置verbosity级别, 及命令行的--verbose选项
|
||||
self.display = Display()
|
||||
self.display.verbosity = self.options.verbosity
|
||||
playbook_executor.verbosity = self.options.verbosity
|
||||
|
||||
# sudo成其他用户的配置
|
||||
self.options.become = True
|
||||
self.options.become_method = 'sudo'
|
||||
self.options.become_user = 'root'
|
||||
self.passwords = {'become_pass': become_pass}
|
||||
|
||||
# 初始化callback插件
|
||||
# self.results_callback = ResultCallback()
|
||||
|
||||
# 初始化Play
|
||||
play_source = {
|
||||
"name": "Ansible Play",
|
||||
"hosts": "*",
|
||||
"gather_facts": "no",
|
||||
"tasks": [
|
||||
dict(action=dict(module='shell', args='id'), register='shell_out'),
|
||||
dict(action=dict(module='debug', args=dict(msg='{{shell_out.stdout}}')))
|
||||
]
|
||||
}
|
||||
|
||||
self.play = Play().load(play_source, variable_manager=inventory.variable_manager, loader=inventory.loader)
|
||||
self.inventory = inventory
|
||||
|
||||
def run(self):
|
||||
"""执行ADHoc 记录日志, 处理结果
|
||||
"""
|
||||
tqm = None
|
||||
# TODO:日志和结果分析
|
||||
try:
|
||||
tqm = TaskQueueManager(
|
||||
inventory=self.inventory.inventory,
|
||||
variable_manager=self.inventory.variable_manager,
|
||||
loader=self.inventory.loader,
|
||||
stdout_callback=default_config.DEFAULT_STDOUT_CALLBACK,
|
||||
options=self.options,
|
||||
passwords=self.passwords
|
||||
)
|
||||
|
||||
result = tqm.run(self.play)
|
||||
return result
|
||||
finally:
|
||||
if tqm:
|
||||
tqm.cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
conf = Config()
|
||||
assets = [{
|
||||
"name": "localhost",
|
||||
"ip": "localhost",
|
||||
"port": "22",
|
||||
"username": "yumaojun",
|
||||
"password": "xxx",
|
||||
"key": "asset_private_key",
|
||||
}]
|
||||
inv = MyInventory(*assets)
|
||||
print inv.inventory.get_group('default').get_hosts()
|
||||
hoc = ADHocRunner(inv, conf, 'xxx')
|
||||
hoc.run()
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
#
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
from __future__ import unicode_literals, print_function
|
||||
|
||||
from rest_framework.exceptions import APIException
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
|
||||
class ServiceUnavailable(APIException):
|
||||
status_code = default_code = 503
|
||||
default_detail = _('Service temporarily unavailable, try again later.')
|
||||
|
||||
|
||||
class ServiceNotImplemented(APIException):
|
||||
status_code = default_code = 501
|
||||
default_detail = _('This service maybe implemented in the future, but now not implemented!')
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import permissions
|
||||
|
||||
|
||||
class AdminUserRequired(permissions.BasePermission):
|
||||
"""
|
||||
Custom permission to only allow admin user to access the resource.
|
||||
"""
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
# Read permissions are allowed to any request,
|
||||
# so we'll always allow GET, HEAD or OPTIONS requests.
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return True
|
||||
|
||||
# Write permissions are only allowed to the admin role.
|
||||
return request.user.is_staff
|
|
@ -0,0 +1,53 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from ..models import HostAlia, UserAlia, CmdAlia, RunasAlia, Extra_conf, Privilege, Sudo, CronTable
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class HostAliaSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = HostAlia
|
||||
|
||||
|
||||
class CmdAliaSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CmdAlia
|
||||
|
||||
|
||||
class UserAliaSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = UserAlia
|
||||
|
||||
|
||||
class RunasAliaSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RunasAlia
|
||||
|
||||
|
||||
class ExtraconfSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Extra_conf
|
||||
|
||||
|
||||
class PrivilegeSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Privilege
|
||||
|
||||
|
||||
class SudoSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Sudo
|
||||
|
||||
|
||||
class CronTableSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CronTable
|
|
@ -0,0 +1,97 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
from __future__ import unicode_literals
|
||||
from rest_framework import viewsets, mixins
|
||||
|
||||
from serializers import *
|
||||
from permissions import *
|
||||
|
||||
|
||||
class HostAliaViewSet(mixins.CreateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
queryset = HostAlia.objects.all()
|
||||
serializer_class = HostAliaSerializer
|
||||
permission_classes = (AdminUserRequired,)
|
||||
|
||||
|
||||
class CmdAliaViewSet(mixins.CreateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
queryset = CmdAlia.objects.all()
|
||||
serializer_class = CmdAliaSerializer
|
||||
permission_classes = (AdminUserRequired,)
|
||||
|
||||
|
||||
class UserAliaViewSet(mixins.CreateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
queryset = UserAlia.objects.all()
|
||||
serializer_class = UserAliaSerializer
|
||||
permission_classes = (AdminUserRequired,)
|
||||
|
||||
|
||||
class RunasAliaViewSet(mixins.CreateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
queryset = RunasAlia.objects.all()
|
||||
serializer_class = RunasAliaSerializer
|
||||
permission_classes = (AdminUserRequired,)
|
||||
|
||||
|
||||
class ExtraconfViewSet(mixins.CreateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
queryset = Extra_conf.objects.all()
|
||||
serializer_class = ExtraconfSerializer
|
||||
permission_classes = (AdminUserRequired,)
|
||||
|
||||
|
||||
class PrivilegeViewSet(mixins.CreateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
queryset = Privilege.objects.all()
|
||||
serializer_class = PrivilegeSerializer
|
||||
permission_classes = (AdminUserRequired,)
|
||||
|
||||
|
||||
class SudoViewSet(mixins.CreateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
queryset = Sudo.objects.all()
|
||||
serializer_class = SudoSerializer
|
||||
permission_classes = (AdminUserRequired,)
|
||||
|
||||
|
||||
class CronTableViewSet(mixins.CreateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
queryset = CronTable.objects.all()
|
||||
serializer_class = CronTableSerializer
|
||||
permission_classes = (AdminUserRequired,)
|
||||
|
||||
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
|
@ -0,0 +1,3 @@
|
|||
from ansible import Tasker, AnsiblePlay, AnsibleTask, AnsibleHostResult
|
||||
from sudo import HostAlia, UserAlia, CmdAlia, RunasAlia, Privilege, Extra_conf, Sudo
|
||||
from cron import CronTable
|
|
@ -0,0 +1,221 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
from __future__ import unicode_literals, absolute_import
|
||||
|
||||
import logging
|
||||
import json
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Tasker(models.Model):
|
||||
uuid = models.CharField(max_length=128, verbose_name=_('UUID'), primary_key=True)
|
||||
name = models.CharField(max_length=128, blank=True, verbose_name=_('Name'))
|
||||
start = models.DateTimeField(auto_now_add=True, verbose_name=_('Start Time'))
|
||||
end = models.DateTimeField(blank=True, null=True, verbose_name=_('End Time'))
|
||||
exit_code = models.IntegerField(default=0, verbose_name=_('Exit Code'))
|
||||
completed = models.BooleanField(default=False, verbose_name=_('Is Completed'))
|
||||
hosts = models.TextField(blank=True, null=True, verbose_name=_('Hosts'))
|
||||
|
||||
def __unicode__(self):
|
||||
return "%s" % self.uuid
|
||||
|
||||
@property
|
||||
def total_hosts(self):
|
||||
return self.hosts.split(',')
|
||||
|
||||
|
||||
class AnsiblePlay(models.Model):
|
||||
tasker = models.ForeignKey(Tasker, related_name='plays', blank=True, null=True)
|
||||
uuid = models.CharField(max_length=128, verbose_name=_('UUID'), primary_key=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
|
||||
def __unicode__(self):
|
||||
return "%s<%s>" % (self.name, self.uuid)
|
||||
|
||||
def to_dict(self):
|
||||
return {"uuid": self.uuid, "name": self.name}
|
||||
|
||||
|
||||
class AnsibleTask(models.Model):
|
||||
play = models.ForeignKey(AnsiblePlay, related_name='tasks', blank=True, null=True)
|
||||
uuid = models.CharField(max_length=128, verbose_name=_('UUID'), primary_key=True)
|
||||
name = models.CharField(max_length=128, blank=True, verbose_name=_('Name'))
|
||||
|
||||
def __unicode__(self):
|
||||
return "%s<%s>" % (self.name, self.uuid)
|
||||
|
||||
def to_dict(self):
|
||||
return {"uuid": self.uuid, "name": self.name}
|
||||
|
||||
def failed(self):
|
||||
pass
|
||||
|
||||
def success(self):
|
||||
pass
|
||||
|
||||
|
||||
class AnsibleHostResult(models.Model):
|
||||
task = models.ForeignKey(AnsibleTask, related_name='host_results', blank=True, null=True)
|
||||
name = models.CharField(max_length=128, blank=True, verbose_name=_('Name'))
|
||||
success = models.TextField(blank=True, verbose_name=_('Success'))
|
||||
skipped = models.TextField(blank=True, verbose_name=_('Skipped'))
|
||||
failed = models.TextField(blank=True, verbose_name=_('Failed'))
|
||||
unreachable = models.TextField(blank=True, verbose_name=_('Unreachable'))
|
||||
no_host = models.TextField(blank=True, verbose_name=_('NoHost'))
|
||||
|
||||
def __unicode__(self):
|
||||
return "%s %s<%s>" % (self.name, str(self.is_success), self.task.uuid)
|
||||
|
||||
@property
|
||||
def is_failed(self):
|
||||
if self.failed or self.unreachable or self.no_host:
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_success(self):
|
||||
return not self.is_failed
|
||||
|
||||
@property
|
||||
def _success_data(self):
|
||||
if self.success:
|
||||
return json.loads(self.success)
|
||||
elif self.skipped:
|
||||
return json.loads(self.skipped)
|
||||
|
||||
@property
|
||||
def _failed_data(self):
|
||||
if self.failed:
|
||||
return json.loads(self.failed)
|
||||
elif self.unreachable:
|
||||
return json.loads(self.unreachable)
|
||||
elif self.no_host:
|
||||
return {"msg": self.no_host}
|
||||
|
||||
@property
|
||||
def failed_msg(self):
|
||||
return self._failed_data.get("msg")
|
||||
|
||||
@staticmethod
|
||||
def __filter_disk(ansible_devices, exclude_devices):
|
||||
"""
|
||||
过滤磁盘设备,丢弃掉不需要的设备
|
||||
|
||||
:param ansible_devices: 对应的facts字段
|
||||
:param exclude_devices: <list> 一个需要被丢弃的设备,匹配规则是startwith, 比如需要丢弃sr0子类的 ['sr']
|
||||
:return: <dict> 过滤获取的结果
|
||||
"""
|
||||
for start_str in exclude_devices:
|
||||
for key in ansible_devices.keys():
|
||||
if key.startswith(start_str):
|
||||
ansible_devices.pop(key)
|
||||
return ansible_devices
|
||||
|
||||
@staticmethod
|
||||
def __filter_interface(ansible_interfaces, exclude_interface):
|
||||
"""
|
||||
过滤网卡设备,丢弃掉不需要的网卡, 比如lo
|
||||
|
||||
:param ansible_interface: 对应的facts字段
|
||||
:param exclude_interface: <list> 一个需要被丢弃的设备,匹配规则是startwith, 比如需要丢弃lo子类的 ['lo']
|
||||
:return: <dict> 过滤获取的结果
|
||||
"""
|
||||
for interface in ansible_interfaces:
|
||||
for start_str in exclude_interface:
|
||||
if interface.startswith(start_str):
|
||||
i = ansible_interfaces.index(interface)
|
||||
ansible_interfaces.pop(i)
|
||||
return ansible_interfaces
|
||||
|
||||
@staticmethod
|
||||
def __gather_interface(facts, interfaces):
|
||||
"""
|
||||
收集所有interface的具体信息
|
||||
|
||||
:param facts: ansible faces
|
||||
:param interfaces: 需要收集的intreface列表
|
||||
:return: <dict> interface的详情
|
||||
"""
|
||||
result = {}
|
||||
for key in interfaces:
|
||||
gather_key = "ansible_" + key
|
||||
if gather_key in facts.keys():
|
||||
result[key] = facts.get(gather_key)
|
||||
return result
|
||||
|
||||
def __deal_setup(self):
|
||||
"""
|
||||
处理ansible setup模块收集到的数据,提取资产需要的部分
|
||||
|
||||
:return: <dict> {"msg": <str>, "data": <dict>}, 注意msg是异常信息, 有msg时 data为None
|
||||
"""
|
||||
result = self._success_data
|
||||
module_name = result['invocation'].get('module_name') if result.get('invocation') else None
|
||||
if module_name is not None:
|
||||
if module_name != "setup":
|
||||
return {"msg": "the property only for ansible setup module result!, can't support other module", "data":None}
|
||||
else:
|
||||
data = {}
|
||||
facts =result.get('ansible_facts')
|
||||
interfaces = self.__filter_interface(facts.get('ansible_interfaces'), ['lo'])
|
||||
|
||||
cpu_describe = "%s %s" % (facts.get('ansible_processor')[0], facts.get('ansible_processor')[1]) if len(facts.get('ansible_processor')) >= 2 else ""
|
||||
|
||||
data['sn'] = facts.get('ansible_product_serial')
|
||||
data['env'] = facts.get('ansible_env')
|
||||
data['os'] = "%s %s(%s)" % (facts.get('ansible_distribution'),
|
||||
facts.get('ansible_distribution_version'),
|
||||
facts.get('ansible_distribution_release'))
|
||||
data['mem'] = facts.get('ansible_memtotal_mb')
|
||||
data['cpu'] = "%s %d核" % (cpu_describe, facts.get('ansible_processor_count'))
|
||||
data['disk'] = self.__filter_disk(facts.get('ansible_devices'), ['sr'])
|
||||
data['interface'] = self.__gather_interface(facts, interfaces)
|
||||
return {"msg": None, "data": data}
|
||||
else:
|
||||
return {"msg": "there result isn't ansible setup module result! can't process this data format", "data": None}
|
||||
|
||||
@property
|
||||
def deal_setup(self):
|
||||
try:
|
||||
return self.__deal_setup()
|
||||
except Exception as e:
|
||||
return {"msg": "deal with setup data failed, %s" % e.message, "data": None}
|
||||
|
||||
def __deal_ping(self):
|
||||
"""
|
||||
处理ansible ping模块收集到的数据
|
||||
|
||||
:return: <dict> {"msg": <str>, "data": {"success": <bool>}}, 注意msg是异常信息, 有msg时 data为None
|
||||
"""
|
||||
result = self._success_data
|
||||
module_name = result['invocation'].get('module_name') if result.get('invocation') else None
|
||||
if module_name is not None:
|
||||
if module_name != "ping":
|
||||
return {"msg": "the property only for ansible setup module result!, can't support other module", "data":None}
|
||||
else:
|
||||
ping = True if result.get('ping') == "pong" else False
|
||||
|
||||
return {"msg": None, "data": {"success": ping}}
|
||||
else:
|
||||
return {"msg": "there isn't module_name field! can't process this data format", "data": None}
|
||||
|
||||
@property
|
||||
def deal_ping(self):
|
||||
try:
|
||||
return self.__deal_ping()
|
||||
except Exception as e:
|
||||
return {"msg": "deal with ping data failed, %s" % e.message, "data": None}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
from __future__ import unicode_literals, absolute_import
|
||||
|
||||
from django.db import models
|
||||
from assets.models import Asset
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class CronTable(models.Model):
|
||||
name = models.CharField(max_length=128, blank=True, null=True, unique=True, verbose_name=_('Name'),
|
||||
help_text=_("Description of a crontab entry"))
|
||||
month = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Month'),
|
||||
help_text=_("Month of the year the job should run ( 1-12, *, */2, etc )"))
|
||||
weekday = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('WeekDay'),
|
||||
help_text=_("Day of the week that the job should run"
|
||||
" ( 0-6 for Sunday-Saturday, *, etc )"))
|
||||
day = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Day'),
|
||||
help_text=_("Day of the month the job should run ( 1-31, *, */2, etc )"))
|
||||
hour = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Hour'),
|
||||
help_text=_("Hour when the job should run ( 0-23, *, */2, etc )"))
|
||||
minute = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Minute'),
|
||||
help_text=_("Minute when the job should run ( 0-59, *, */2, etc )"))
|
||||
job = models.CharField(max_length=4096, blank=True, null=True, verbose_name=_('Job'),
|
||||
help_text=_("The command to execute or, if env is set, the value of "
|
||||
"environment variable. Required if state=present."))
|
||||
user = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('User'),
|
||||
help_text=_("The specific user whose crontab should be modified."))
|
||||
asset = models.ForeignKey(Asset, null=True, blank=True, related_name='crontables')
|
||||
|
||||
@property
|
||||
def describe(self):
|
||||
return "http://docs.ansible.com/ansible/cron_module.html"
|
|
@ -0,0 +1,187 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
from __future__ import unicode_literals, absolute_import
|
||||
|
||||
from jinja2 import Template
|
||||
from django.db import models
|
||||
from assets.models import Asset
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class HostAlia(models.Model):
|
||||
name = models.CharField(max_length=128, blank=True, null=True, unique=True, verbose_name=_('Host_Alias'))
|
||||
host_items = models.TextField(blank=True, null=True, verbose_name=_('Host_Items'))
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class UserAlia(models.Model):
|
||||
name = models.CharField(max_length=128, blank=True, null=True, unique=True, verbose_name=_('User_Alias'))
|
||||
user_items = models.TextField(blank=True, null=True, verbose_name=_('Host_Items'))
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class CmdAlia(models.Model):
|
||||
name = models.CharField(max_length=128, blank=True, null=True, unique=True, verbose_name=_('Command_Alias'))
|
||||
cmd_items = models.TextField(blank=True, null=True, verbose_name=_('Host_Items'))
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class RunasAlia(models.Model):
|
||||
name = models.CharField(max_length=128, blank=True, null=True, unique=True, verbose_name=_('Runas_Alias'))
|
||||
runas_items = models.TextField(blank=True, null=True, verbose_name=_('Host_Items'))
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Privilege(models.Model):
|
||||
user = models.ForeignKey(UserAlia, blank=True, null=True, related_name='privileges')
|
||||
host = models.ForeignKey(HostAlia, blank=True, null=True, related_name='privileges')
|
||||
runas = models.ForeignKey(RunasAlia, blank=True, null=True, related_name='privileges')
|
||||
command = models.ForeignKey(CmdAlia, blank=True, null=True, related_name='privileges')
|
||||
nopassword = models.BooleanField(default=True, verbose_name=_('Is_NoPassword'))
|
||||
|
||||
def __unicode__(self):
|
||||
return "[%s %s %s %s %s]" % (self.user.name,
|
||||
self.host.name,
|
||||
self.runas.name,
|
||||
self.command.name,
|
||||
self.nopassword)
|
||||
|
||||
def to_tuple(self):
|
||||
return self.user.name, self.host.name, self.runas.name, self.command.name, self.nopassword
|
||||
|
||||
|
||||
class Extra_conf(models.Model):
|
||||
line = models.TextField(blank=True, null=True, verbose_name=_('Extra_Item'))
|
||||
|
||||
def __unicode__(self):
|
||||
return self.line
|
||||
|
||||
|
||||
class Sudo(models.Model):
|
||||
"""
|
||||
Sudo配置文件对象, 用于配置sudo的配置文件
|
||||
|
||||
:param extra_lines: <list> [<line1>, <line2>,...]
|
||||
:param privileges: <list> [(user, host, runas, command, nopassword),]
|
||||
"""
|
||||
|
||||
asset = models.ForeignKey(Asset, null=True, blank=True, related_name='sudos')
|
||||
extra_lines = models.ManyToManyField(Extra_conf, related_name='sudos', blank=True)
|
||||
privilege_items = models.ManyToManyField(Privilege, related_name='sudos', blank=True)
|
||||
|
||||
@property
|
||||
def users(self):
|
||||
return {privilege.user.name: privilege.user.user_items.split(',') for privilege in self.privilege_items.all()}
|
||||
|
||||
@property
|
||||
def commands(self):
|
||||
return {privilege.command.name: privilege.command.cmd_items.split(',') for privilege in self.privilege_items.all()}
|
||||
|
||||
@property
|
||||
def hosts(self):
|
||||
return {privilege.host.name: privilege.host.host_items.split(',') for privilege in self.privilege_items.all()}
|
||||
|
||||
@property
|
||||
def runas(self):
|
||||
return {privilege.runas.name: privilege.runas.runas_items.split(',') for privilege in self.privilege_items.all()}
|
||||
|
||||
@property
|
||||
def extras(self):
|
||||
return [extra.line for extra in self.extra_lines.all()]
|
||||
|
||||
@property
|
||||
def privileges(self):
|
||||
return [privilege.to_tuple() for privilege in self.privilege_items.all()]
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
template = Template(self.__sudoers_jinja2_tmp__)
|
||||
context = {"User_Alias": self.users,
|
||||
"Cmnd_Alias": self.commands,
|
||||
"Host_Alias": self.hosts,
|
||||
"Runas_Alias": self.runas,
|
||||
"Extra_Lines": self.extras,
|
||||
"Privileges": self.privileges}
|
||||
return template.render(context)
|
||||
|
||||
@property
|
||||
def __sudoers_jinja2_tmp__(self):
|
||||
return """# management by JumpServer
|
||||
# This file MUST be edited with the 'visudo' command as root.
|
||||
#
|
||||
# Please consider adding local content in /etc/sudoers.d/ instead of
|
||||
# directly modifying this file.
|
||||
#
|
||||
# See the man page for details on how to write a sudoers file.
|
||||
#
|
||||
Defaults env_reset
|
||||
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
# JumpServer Generate Other Configure is here
|
||||
{% if Extra_Lines -%}
|
||||
{% for line in Extra_Lines -%}
|
||||
{{ line }}
|
||||
{% endfor %}
|
||||
{%- endif %}
|
||||
|
||||
# Host alias specification
|
||||
{% if Host_Alias -%}
|
||||
{% for flag, items in Host_Alias.iteritems() -%}
|
||||
Host_Alias {{ flag }} = {{ items|join(', ') }}
|
||||
{% endfor %}
|
||||
{%- endif %}
|
||||
|
||||
# User alias specification
|
||||
{% if User_Alias -%}
|
||||
{% for flag, items in User_Alias.iteritems() -%}
|
||||
User_Alias {{ flag }} = {{ items|join(', ') }}
|
||||
{% endfor %}
|
||||
{%- endif %}
|
||||
|
||||
|
||||
# Cmnd alias specification
|
||||
{% if Cmnd_Alias -%}
|
||||
{% for flag, items in Cmnd_Alias.iteritems() -%}
|
||||
Cmnd_Alias {{ flag }} = {{ items|join(', ') }}
|
||||
{% endfor %}
|
||||
{%- endif %}
|
||||
|
||||
# Run as alias specification
|
||||
{% if Runas_Alias -%}
|
||||
{% for flag, items in Runas_Alias.iteritems() -%}
|
||||
Runas_Alias {{ flag }} = {{ items|join(', ') }}
|
||||
{% endfor %}
|
||||
{%- endif %}
|
||||
|
||||
# User privilege specification
|
||||
root ALL=(ALL:ALL) ALL
|
||||
|
||||
# JumpServer Generate User privilege is here.
|
||||
# Note privileges is a tuple list like [(user, host, runas, command, nopassword),]
|
||||
{% if Privileges -%}
|
||||
{% for User_Flag, Host_Flag, Runas_Flag, Command_Flag, NopassWord in Privileges -%}
|
||||
{% if NopassWord -%}
|
||||
{{ User_Flag }} {{ Host_Flag }}=({{ Runas_Flag }}) NOPASSWD: {{ Command_Flag }}
|
||||
{%- else -%}
|
||||
{{ User_Flag }} {{ Host_Flag }}=({{ Runas_Flag }}) {{ Command_Flag }}
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
{%- endif %}
|
||||
|
||||
# Members of the admin group may gain root privileges
|
||||
%admin ALL=(ALL) ALL
|
||||
|
||||
# Allow members of group sudo to execute any command
|
||||
%sudo ALL=(ALL:ALL) ALL
|
||||
|
||||
# See sudoers(5) for more information on "#include" directives:
|
||||
|
||||
#includedir /etc/sudoers.d
|
||||
"""
|
|
@ -1,9 +0,0 @@
|
|||
from .tasks import longtime_add
|
||||
import time
|
||||
|
||||
result = longtime_add.delay(1,2)
|
||||
print 'Task finished? ', result.ready()
|
||||
print 'Task result: ', result.result
|
||||
time.sleep(10)
|
||||
print 'Task finished? ', result.ready()
|
||||
print 'Task result: ', result.result
|
|
@ -1,19 +0,0 @@
|
|||
from __future__ import absolute_import
|
||||
import time
|
||||
|
||||
from celery import shared_task
|
||||
from common import celery_app
|
||||
|
||||
|
||||
@shared_task
|
||||
def longtime_add(x, y):
|
||||
print 'long time task begins'
|
||||
# sleep 5 seconds
|
||||
time.sleep(5)
|
||||
print 'long time task finished'
|
||||
return x + y
|
||||
|
||||
|
||||
@celery_app.task(name='hello-world')
|
||||
def hello():
|
||||
print('hello world!')
|
|
@ -0,0 +1,48 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from celery import shared_task
|
||||
|
||||
from common import celery_app
|
||||
from ops.utils.ansible_api import Config, ADHocRunner
|
||||
|
||||
|
||||
@shared_task(name="get_asset_hardware_info")
|
||||
def get_asset_hardware_info(task_name, task_uuid, *assets):
|
||||
conf = Config()
|
||||
play_source = {
|
||||
"name": "Get host hardware information",
|
||||
"hosts": "default",
|
||||
"gather_facts": "no",
|
||||
"tasks": [
|
||||
dict(action=dict(module='setup'))
|
||||
]
|
||||
}
|
||||
hoc = ADHocRunner(conf, play_source, *assets)
|
||||
ext_code, result = hoc.run(task_name, task_uuid)
|
||||
return ext_code, result
|
||||
|
||||
|
||||
@shared_task(name="asset_test_ping_check")
|
||||
def asset_test_ping_check(task_name, task_uuid, *assets):
|
||||
conf = Config()
|
||||
play_source = {
|
||||
"name": "Test host connection use ping",
|
||||
"hosts": "default",
|
||||
"gather_facts": "no",
|
||||
"tasks": [
|
||||
dict(action=dict(module='ping'))
|
||||
]
|
||||
}
|
||||
hoc = ADHocRunner(conf, play_source, *assets)
|
||||
ext_code, result = hoc.run(task_name, task_uuid)
|
||||
return ext_code, result
|
||||
|
||||
|
||||
@shared_task(name="add_user_to_assert")
|
||||
def add_user_to_asset():
|
||||
pass
|
||||
|
||||
|
||||
@celery_app.task(name='hello-world')
|
||||
def hello():
|
||||
print('hello world!')
|
|
@ -0,0 +1,120 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from ops.tasks import _celery_tasks
|
||||
|
||||
from ops.models import Tasker
|
||||
from uuid import uuid1
|
||||
from celery.result import AsyncResult
|
||||
|
||||
|
||||
def get_result(task_id):
|
||||
result = AsyncResult(task_id)
|
||||
if result.ready():
|
||||
return {"Completed": True, "data": result.get()}
|
||||
else:
|
||||
return {"Completed": False, "data": None}
|
||||
|
||||
|
||||
def __get_result_by_tasker_id(tasker_uuid, deal_method):
|
||||
tasker = Tasker.objects.get(uuid=tasker_uuid)
|
||||
total = tasker.total_hosts
|
||||
total_len = len(total)
|
||||
host_results = []
|
||||
|
||||
# 存储数据
|
||||
for play in tasker.plays.all():
|
||||
for t in play.tasks.all():
|
||||
task = {'name': t.name, 'uuid': t.uuid, 'percentage': 0, 'completed': {'success': {}, 'failed': {}}}
|
||||
completed = []
|
||||
count = 0
|
||||
for h in t.host_results.all():
|
||||
completed.append(h.name)
|
||||
count += 1
|
||||
if h.is_success:
|
||||
result = getattr(h, deal_method)
|
||||
if result.get('msg') is None:
|
||||
task['completed']['success'][h.name] = result.get('data')
|
||||
else:
|
||||
task['completed']['failed'][h.name] = result.get('msg')
|
||||
else:
|
||||
task['completed']['failed'][h.name] = h.failed_msg
|
||||
|
||||
# 计算进度
|
||||
task['percentage'] = float(count * 100 / total_len)
|
||||
task['waited'] = list(set(total) - set(completed))
|
||||
|
||||
host_results.append(task)
|
||||
|
||||
return host_results
|
||||
|
||||
|
||||
def start_get_hardware_info(*assets):
|
||||
name = "Get host hardware information"
|
||||
uuid = "tasker-" + uuid1().hex
|
||||
_celery_tasks.get_asset_hardware_info.delay(name, uuid, *assets)
|
||||
return uuid
|
||||
|
||||
|
||||
def __get_hardware_info(tasker_uuid):
|
||||
return __get_result_by_tasker_id(tasker_uuid, 'deal_setup')
|
||||
|
||||
|
||||
def get_hardware_info(tasker_uuid):
|
||||
"""
|
||||
|
||||
:param assets: 资产列表
|
||||
:return: 返回数据结构样列
|
||||
{u'data': [{u'completed': {
|
||||
u'failed': {u'192.168.232.135': u'Authentication failure.'},
|
||||
u'success': {u'192.168.1.119': {u'cpu': u'GenuineIntel Intel Xeon E312xx (Sandy Bridge) 6\u6838',
|
||||
u'disk': {<device_name>: <device_detail_dict>},
|
||||
u'env': {<env_name>: <env_value>},
|
||||
u'interface': {<interface_name>: <interface_detail_dict>},
|
||||
u'mem': 3951,
|
||||
u'os': u'Ubuntu 16.04(xenial)',
|
||||
u'sn': u'NA'}}},
|
||||
u'name': u'',
|
||||
u'percentage': 100.0,
|
||||
u'uuid': u'87cfedfe-ba55-44ff-bc43-e7e73b869ca1',
|
||||
u'waited': []}
|
||||
],
|
||||
u'msg': None}
|
||||
"""
|
||||
try:
|
||||
return {"msg": None, "data": __get_hardware_info(tasker_uuid)}
|
||||
except Exception as e:
|
||||
return {"msg": "query data failed!, %s" % e.message, "data": None}
|
||||
|
||||
|
||||
def start_ping_test(*assets):
|
||||
name = "Test host connection"
|
||||
uuid = "tasker-" + uuid1().hex
|
||||
_celery_tasks.asset_test_ping_check.delay(name, uuid, *assets)
|
||||
return uuid
|
||||
|
||||
|
||||
def __get_ping_test(tasker_uuid):
|
||||
return __get_result_by_tasker_id(tasker_uuid, 'deal_ping')
|
||||
|
||||
|
||||
def get_ping_test(tasker_uuid):
|
||||
"""
|
||||
|
||||
:param assets: 资产列表
|
||||
:return: 返回数据结构样列
|
||||
{u'data': [{u'completed': {
|
||||
u'failed': {u'192.168.232.135': u'Authentication failure.'},
|
||||
u'success': {u'192.168.1.119': {u'success': True}}},
|
||||
u'name': u'',
|
||||
u'percentage': 100.0,
|
||||
u'uuid': u'3e6e0d3b-bee0-4383-b19e-bec6ba55d346',
|
||||
u'waited': []}
|
||||
],
|
||||
u'msg': None}
|
||||
"""
|
||||
try:
|
||||
return {"msg": None, "data": __get_ping_test(tasker_uuid)}
|
||||
except Exception as e:
|
||||
return {"msg": "query data failed!, %s" % e.message, "data": None}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap %}
|
||||
{% block custom_head_css_js %}
|
||||
<link href="{% static "css/plugins/select2/select2.min.css" %}" rel="stylesheet">
|
||||
<script src="{% static "js/plugins/select2/select2.full.min.js" %}"></script>
|
||||
<link href="{% static "css/plugins/datepicker/datepicker3.css" %}" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<h5>{% block user_template_title %}{% trans 'Create user' %}{% endblock %}</h5>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<form method="post" class="form-horizontal" action="" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<h3>{% trans 'Account' %}</h3>
|
||||
{% block username %} {% endblock %}
|
||||
{{ form.email|bootstrap_horizontal }}
|
||||
{{ form.name|bootstrap_horizontal }}
|
||||
{{ form.groups|bootstrap_horizontal }}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
{% block password %} {% endblock %}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
<h3>{% trans 'Security and Role' %}</h3>
|
||||
{{ form.role|bootstrap_horizontal }}
|
||||
<div class="form-group {% if form.date_expired.errors %} has-error {% endif %}" id="date_5">
|
||||
<label for="{{ form.date_expired.id_for_label }}" class="col-sm-2 control-label">{{ form.date_expired.label }}</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group date">
|
||||
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
|
||||
<input id="{{ form.date_expired.id_for_label }}" name="{{ form.date_expired.html_name }}" type="text" class="form-control" value="{{ form.date_expired.value|date:'Y-m-d' }}">
|
||||
</div>
|
||||
<span class="help-block ">{{ form.date_expired.errors }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{# {{ form.date_expired|bootstrap_horizontal }}#}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.enable_otp.id_for_label }}" class="col-sm-2 control-label">{% trans 'Enable OTP' %}</label>
|
||||
<div class="col-sm-8">
|
||||
{{ form.enable_otp }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hr-line-dashed"></div>
|
||||
<h3>{% trans 'Profile' %}</h3>
|
||||
{{ form.phone|bootstrap_horizontal }}
|
||||
{{ form.wechat|bootstrap_horizontal }}
|
||||
{{ form.comment|bootstrap_horizontal }}
|
||||
<div class="hr-line-dashed"></div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-4 col-sm-offset-2">
|
||||
<button class="btn btn-white" type="reset">{% trans 'Reset' %}</button>
|
||||
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script src="{% static 'js/plugins/datapicker/bootstrap-datepicker.js' %}"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2();
|
||||
|
||||
$('.input-group.date').datepicker({
|
||||
format: "yyyy-mm-dd",
|
||||
todayBtn: "linked",
|
||||
keyboardNavigation: false,
|
||||
forceParse: false,
|
||||
calendarWeeks: true,
|
||||
autoclose: true
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,16 @@
|
|||
{% extends 'cron/_cron.html' %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap %}
|
||||
{% block user_template_title %}{% trans "Create user" %}{% endblock %}
|
||||
{% block username %}
|
||||
{{ form.username|bootstrap_horizontal }}
|
||||
{% endblock %}
|
||||
{% block password %}
|
||||
<h3>{% trans 'Password' %}</h3>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">{% trans 'Password' %}</label>
|
||||
<div class="col-sm-8 controls" >
|
||||
{% trans 'Reset link will be generated and sent to the user. ' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,393 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load common_tags %}
|
||||
{% load users_tags %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% 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">
|
||||
<script src="{% static "js/plugins/select2/select2.full.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">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="panel-options">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active">
|
||||
<a href="{% url 'users:user-detail' pk=user_object.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'User detail' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'users:user-asset-permission' pk=user_object.id %}" class="text-center"><i class="fa fa-bar-chart-o"></i> {% trans 'Asset permission' %}</a>
|
||||
</li>
|
||||
<li><a href="{% url 'users:user-granted-asset' pk=user_object.id %}" class="text-center"><i class="fa fa-cubes"></i> {% trans 'Asset granted' %}</a></li>
|
||||
<li><a href="{% url 'users:user-login-history' pk=user_object.id %}" class="text-center"><i class="fa fa-calculator-o"></i> {% trans 'Login history' %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-7" style="padding-left: 0">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span class="label"><b>{{ user_object.name }}</b></span>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-user">
|
||||
</ul>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<td colspan="2">
|
||||
<img src="{{ user_object | user_avatar_url }}" class="img-circle" width="64" height="64">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="20%">{% trans 'Name' %}:</td>
|
||||
<td><b>{{ user_object.name }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Username' %}:</td>
|
||||
<td><b>{{ user_object.username }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Email' %}:</td>
|
||||
<td><b>{{ user_object.email }}</b></td>
|
||||
</tr>
|
||||
{% if user_object.phone %}
|
||||
<tr>
|
||||
<td>{% trans 'Phone' %}:</td>
|
||||
<td><b>{{ user_object.phone }}</b></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if user_object.wechat %}
|
||||
<tr>
|
||||
<td>{% trans 'Wechat' %}:</td>
|
||||
<td><b>{{ user_object.wechat }}</b></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>{% trans 'Role' %}:</td>
|
||||
<td><b>{{ user_object.get_role_display }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Date expired' %}:</td>
|
||||
<td><b>{{ user_object.date_expired|date:"Y-m-j H:i:s" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Created by' %}:</td>
|
||||
<td><b>{{ user_object.created_by }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Date joined' %}:</td>
|
||||
<td><b>{{ user_object.date_joined|date:"Y-m-j H:i:s" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Last login' %}:</td>
|
||||
<td><b>{{ user_object.last_login|date:"Y-m-j H:i:s" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Comment' %}:</td>
|
||||
<td><b>{{ user_object.comment }}</b></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-5" style="padding-left: 0;padding-right: 0">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Quick modify' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<td width="50%">{% trans 'Active' %}:</td>
|
||||
<td><span class="pull-right">
|
||||
<div class="switch">
|
||||
<div class="onoffswitch">
|
||||
<input type="checkbox" {% if user_object.is_active %} checked {% endif %} class="onoffswitch-checkbox" id="is_active">
|
||||
<label class="onoffswitch-label" for="is_active">
|
||||
<span class="onoffswitch-inner"></span>
|
||||
<span class="onoffswitch-switch"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Enable OTP' %}:</td>
|
||||
<td><span class="pull-right">
|
||||
<div class="switch">
|
||||
<div class="onoffswitch">
|
||||
<input type="checkbox" class="onoffswitch-checkbox" {% if user_object.enable_otp %} checked {% endif %}
|
||||
id="enable_otp">
|
||||
<label class="onoffswitch-label" for="enable_otp">
|
||||
<span class="onoffswitch-inner"></span>
|
||||
<span class="onoffswitch-switch"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Reset password' %}:</td>
|
||||
<td>
|
||||
<span class="pull-right">
|
||||
<button type="button" class="btn btn-primary btn-xs" id="btn_reset_password" style="width: 54px">{% trans 'Reset' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Reset ssh key' %}:</td>
|
||||
<td>
|
||||
<span class="pull-right">
|
||||
<button type="button" class="btn btn-primary btn-xs" id="btn_reset_pk" style="width: 54px;">{% trans 'Reset' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Update ssh key' %}:</td>
|
||||
<td>
|
||||
<span class="pull-right">
|
||||
<button type="button" class="btn btn-primary btn-xs" id="btn_update_pk" style="width: 54px;" data-toggle="modal" data-target="#user_update_pk_modal">{% trans 'Update' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'User group' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table group_edit">
|
||||
<tbody>
|
||||
<form>
|
||||
<tr>
|
||||
<td colspan="2" class="no-borders">
|
||||
<select data-placeholder="{% trans 'Join user groups' %}" id="slct_groups" class="select2" style="width: 100%" multiple="" tabindex="4">
|
||||
{% for group in groups %}
|
||||
<option value="{{ group.id }}" id="opt_{{ group.id }}">{{ group.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" class="no-borders">
|
||||
<button type="button" class="btn btn-info btn-small" id="btn_add_user_group">{% trans 'Join' %}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</form>
|
||||
|
||||
{% for group in user_object.groups.all %}
|
||||
<tr>
|
||||
<td ><b class="bdg_user_group" data-gid={{ group.id }}>{{ group.name }}</b></td>
|
||||
<td>
|
||||
<button class="btn btn-danger pull-right btn-xs btn_delete_user_group" type="button"><i class="fa fa-minus"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'users/_user_update_pk_modal.html' %}
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
jumpserver.selected_groups = {};
|
||||
|
||||
function updateUserGroups(user_groups) {
|
||||
var the_url = "{% url 'users:group-user-edit-api' pk=user_object.id %}";
|
||||
var body = {
|
||||
id: {{ user_object.id }},
|
||||
groups: Object.assign([], user_groups)
|
||||
};
|
||||
var success = function(data) {
|
||||
// remove all the selected groups from select > option and rendered ul element;
|
||||
$('.select2-selection__rendered').empty();
|
||||
$('#slct_groups').val('');
|
||||
$.map(jumpserver.selected_groups, function(group_name, index) {
|
||||
$('#opt_' + index).remove();
|
||||
// change tr html of user groups.
|
||||
$('.group_edit tbody').append(
|
||||
'<tr>' +
|
||||
'<td><b class="bdg_user_group" data-gid="' + index + '">' + group_name + '</b></td>' +
|
||||
'<td><button class="btn btn-danger btn-xs pull-right btn_delete_user_group" type="button"><i class="fa fa-minus"></i></button></td>' +
|
||||
'</tr>'
|
||||
)
|
||||
});
|
||||
// clear jumpserver.selected_groups
|
||||
jumpserver.selected_groups = {};
|
||||
toastr.success('{% trans "UserGroup Update Success!" %}')
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(body),
|
||||
success: success,
|
||||
method: 'PUT'
|
||||
});
|
||||
}
|
||||
$(document).ready(function() {
|
||||
$('.select2').select2()
|
||||
.on('select2:select', function(evt) {
|
||||
var data = evt.params.data;
|
||||
jumpserver.selected_groups[data.id] = data.text;
|
||||
}).on('select2:unselect', function(evt) {
|
||||
var data = evt.params.data;
|
||||
delete jumpserver.selected_groups[data.id]
|
||||
})
|
||||
}).on('click', '#is_active', function() {
|
||||
var the_url = "{% url 'users:user-patch-api' pk=user_object.id %}";
|
||||
var checked = !$(this).prop('checked');
|
||||
var body = {
|
||||
'is_active': checked
|
||||
};
|
||||
var success = '{% trans "Update Successfully!" %}';
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(body),
|
||||
success_message: success
|
||||
});
|
||||
}).on('click', '#enable_otp', function() {
|
||||
var the_url = "{% url 'users:user-patch-api' pk=user_object.id %}";
|
||||
var checked = !$(this).prop('checked');
|
||||
var body = {
|
||||
'enable_otp': checked
|
||||
};
|
||||
var success = '{% trans "Update Successfully!" %}';
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(body),
|
||||
success_message: success
|
||||
});
|
||||
}).on('click', '#btn_add_user_group', function() {
|
||||
if (Object.keys(jumpserver.selected_groups).length === 0) {
|
||||
return false;
|
||||
}
|
||||
var user_groups = $('.bdg_user_group').map(function() {
|
||||
return $(this).data('gid');
|
||||
}).get();
|
||||
$.map(jumpserver.selected_groups, function(value, index) {
|
||||
user_groups.push(parseInt(index));
|
||||
$('#opt_' + index).remove();
|
||||
});
|
||||
updateUserGroups(user_groups)
|
||||
}).on('click', '.btn_delete_user_group', function() {
|
||||
var $this = $(this);
|
||||
var $tr = $this.closest('tr');
|
||||
var $badge = $tr.find('.bdg_user_group');
|
||||
var gid = $badge.data('gid');
|
||||
var group_name = $badge.html() || $badge.text();
|
||||
$('#slct_groups').append(
|
||||
'<option value="' + gid + '" id="opt_' + gid + '">' + group_name + '</option>'
|
||||
);
|
||||
$tr.remove();
|
||||
var user_groups = $('.bdg_user_group').map(function() {
|
||||
return $(this).data('gid');
|
||||
}).get();
|
||||
updateUserGroups(user_groups)
|
||||
}).on('click', '#btn_reset_password', function() {
|
||||
function doReset() {
|
||||
var the_url = '{% url "users:user-reset-password-api" pk=user_object.id %}';
|
||||
var body = {};
|
||||
var success = function() {
|
||||
var msg = "{% trans 'E-mail sent successfully. An e-mail has been sent to the user\'s mailbox.' %}";
|
||||
swal("{% trans 'Password-Reset' %}", msg, "success");
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(body),
|
||||
success: success
|
||||
});
|
||||
}
|
||||
swal({
|
||||
title: "{% trans 'Are you sure?' %}",
|
||||
text: "{% trans 'This will reset the user\'s password.' %}",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: false
|
||||
}, function() {
|
||||
doReset();
|
||||
});
|
||||
}).on('click', '#btn_reset_pk', function() {
|
||||
function doReset() {
|
||||
var the_url = '{% url "users:user-reset-pk-api" pk=user_object.id %}';
|
||||
var body = {};
|
||||
var success = function() {
|
||||
var msg = "{% trans 'The reset-ssh-public-key E-mail has been sent successfully. Please inform the user to update his new ssh public key.' %}";
|
||||
swal("{% trans 'SSH-Public-Key Reset' %}", msg, "success");
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(body),
|
||||
success: success
|
||||
});
|
||||
}
|
||||
swal({
|
||||
title: "{% trans 'Are you sure?' %}",
|
||||
text: "{% trans 'This will reset the user\'s public key.' %}",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: false
|
||||
}, function() {
|
||||
doReset();
|
||||
});
|
||||
}).on('click', '#btn_user_update_pk', function(){
|
||||
var $this = $(this);
|
||||
var pk = $('#txt_pk').val();
|
||||
var the_url = '{% url "users:user-update-pk-api" pk=user_object.id %}';
|
||||
var body = {'_public_key': pk};
|
||||
var success = function() {
|
||||
$('#txt_pk').val('');
|
||||
$this.closest('.modal').modal('hide');
|
||||
var msg = "{% trans 'Successfully updated the SSH public key.' %}";
|
||||
swal("{% trans 'User SSH Public Key Update' %}", msg, "success");
|
||||
};
|
||||
var fail = function() {
|
||||
var msg = "{% trans 'Failed to update the user\'s SSH public key.' %}";
|
||||
swal({
|
||||
title: "{% trans 'User SSH Public Key Update' %}",
|
||||
text: msg,
|
||||
type: "error",
|
||||
showCancelButton: false,
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: true
|
||||
}, function () {
|
||||
$('#txt_pk').focus();
|
||||
}
|
||||
);
|
||||
}
|
||||
APIUpdateAttr({ url: the_url, body: JSON.stringify(body), success: success, error: fail});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,242 @@
|
|||
{% extends '_base_list.html' %}
|
||||
{% load i18n static %}
|
||||
{% block custom_head_css_js %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
div.dataTables_wrapper div.dataTables_filter,
|
||||
.dataTables_length {
|
||||
float: right !important;
|
||||
}
|
||||
|
||||
div.dataTables_wrapper div.dataTables_filter {
|
||||
margin-left: 15px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block table_search %}{% endblock %}
|
||||
{% block table_container %}
|
||||
<div class="uc pull-left"><a href="javascript:void(0);" class="btn btn-sm btn-primary" data-toggle="modal" data-target="#user_import_modal"> {% trans "Import user" %} </a></div>
|
||||
<div class="uc pull-left m-l-5 m-r-5"><a href="{% url "users:user-create" %}" class="btn btn-sm btn-primary"> {% trans "Create user" %} </a></div>
|
||||
<table class="table table-striped table-bordered table-hover " id="user_list_table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
<div class="checkbox checkbox-default"><input id="" type="checkbox" class="ipt_check_all"><label></label></div>
|
||||
</th>
|
||||
<th class="text-center">{% trans 'Name' %}</a></th>
|
||||
<th class="text-center">{% trans 'Username' %}</a></th>
|
||||
<th class="text-center">{% trans 'Role' %}</th>
|
||||
<th class="text-center">{% trans 'User group' %}</th>
|
||||
<th class="text-center">{% trans 'Asset num' %}</th>
|
||||
<th class="text-center">{% trans 'Active' %}</a></th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="actions" class="hide">
|
||||
<div class="input-group">
|
||||
<select class="form-control m-b" style="width: auto" id="slct_bulk_update">
|
||||
<option value="delete">{% trans 'Delete selected' %}</option>
|
||||
<option value="update">{% trans 'Update selected' %}</option>
|
||||
<option value="deactive">{% trans 'Deactive selected' %}</option>
|
||||
</select>
|
||||
<div class="input-group-btn pull-left" style="padding-left: 5px;">
|
||||
<button id='btn_bulk_update' style="height: 32px;" class="btn btn-sm btn-primary">
|
||||
{% trans 'Submit' %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "users/_user_bulk_update_modal.html" %}
|
||||
{% include "users/_user_import_modal.html" %}
|
||||
{% endblock %}
|
||||
{% block content_bottom_left %}{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script src="{% static 'js/jquery.form.min.js' %}"></script>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
var options = {
|
||||
ele: $('#user_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
var detail_btn = '<a href="{% url "users:user-detail" pk=99991937 %}">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('99991937', rowData.id));
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
var innerHtml = cellData.length > 8 ? cellData.substring(0, 8) + '...': cellData;
|
||||
$(td).html('<a href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</a>');
|
||||
}},
|
||||
{targets: 6, createdCell: function (td, cellData) {
|
||||
if (!cellData) {
|
||||
$(td).html('<i class="fa fa-times text-danger"></i>')
|
||||
} else {
|
||||
$(td).html('<i class="fa fa-check text-navy"></i>')
|
||||
}
|
||||
}},
|
||||
{targets: 7, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "users:user-update" pk=99991937 %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace('99991937', cellData);
|
||||
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_user_delete" data-uid="99991937">{% trans "Delete" %}</a>'.replace('99991937', cellData);
|
||||
if (rowData.id === 1) {
|
||||
$(td).html(update_btn)
|
||||
} else {
|
||||
$(td).html(update_btn + del_btn)
|
||||
}
|
||||
}}],
|
||||
ajax_url: '{% url "users:user-bulk-update-api" %}',
|
||||
columns: [{data: function(){return ""}}, {data: "username" }, {data: "name" }, {data: "get_role_display" }, {data: "group_display" },
|
||||
{data: function(){return 999}}, {data: "active_display" }, {data: "id" }],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
jumpserver.initDataTable(options);
|
||||
}).on('click', '#btn_bulk_update', function(){
|
||||
var action = $('#slct_bulk_update').val();
|
||||
var $data_table = $('#user_list_table').DataTable()
|
||||
var id_list = [];
|
||||
var plain_id_list = [];
|
||||
$data_table.rows({selected: true}).every(function(){
|
||||
id_list.push({id: this.data().id});
|
||||
plain_id_list.push(this.data().id);
|
||||
});
|
||||
if (id_list === []) {
|
||||
return false;
|
||||
};
|
||||
var the_url = "{% url 'users:user-bulk-update-api' %}";
|
||||
function doDeactive() {
|
||||
var body = $.each(id_list, function(index, user_object) {
|
||||
user_object['is_active'] = false;
|
||||
});
|
||||
APIUpdateAttr({url: the_url, method: 'PATCH', body: JSON.stringify(body)});
|
||||
$data_table.ajax.reload();
|
||||
jumpserver.checked = false;
|
||||
}
|
||||
function doDelete() {
|
||||
swal({
|
||||
title: "{% trans 'Are you sure?' %}",
|
||||
text: "{% trans 'This will delete the selected users !!!' %}",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: false
|
||||
}, function() {
|
||||
var success = function() {
|
||||
var msg = "{% trans 'User Deleted.' %}";
|
||||
swal("{% trans 'User Delete' %}", msg, "success");
|
||||
$('#user_list_table').DataTable().ajax.reload();
|
||||
};
|
||||
var fail = function() {
|
||||
var msg = "{% trans 'User Deleting failed.' %}";
|
||||
swal("{% trans 'User Delete' %}", msg, "error");
|
||||
};
|
||||
var url_delete = the_url + '?id__in=' + JSON.stringify(plain_id_list);
|
||||
APIUpdateAttr({url: url_delete, method: 'DELETE', success: success, error: fail});
|
||||
jumpserver.checked = false;
|
||||
});
|
||||
}
|
||||
function doUpdate() {
|
||||
$('#user_bulk_update_modal').modal('show');
|
||||
}
|
||||
switch(action) {
|
||||
case 'deactive':
|
||||
doDeactive();
|
||||
break;
|
||||
case 'delete':
|
||||
doDelete();
|
||||
break;
|
||||
case 'update':
|
||||
doUpdate();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}).on('click', '.btn_user_delete', function(){
|
||||
var $this = $(this);
|
||||
function doDelete() {
|
||||
var uid = $this.data('uid');
|
||||
var the_url = '{% url "users:user-patch-api" pk=99991937 %}'.replace('99991937', uid);
|
||||
var body = {};
|
||||
var success = function() {
|
||||
var msg = "{% trans 'User Deleted.' %}";
|
||||
swal("{% trans 'User Delete' %}", msg, "success");
|
||||
$('#user_list_table').DataTable().ajax.reload();
|
||||
};
|
||||
var fail = function() {
|
||||
var msg = "{% trans 'User Deleting failed.' %}";
|
||||
swal("{% trans 'User Delete' %}", msg, "error");
|
||||
}
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(body),
|
||||
method: 'DELETE',
|
||||
success: success,
|
||||
error: fail
|
||||
});
|
||||
}
|
||||
swal({
|
||||
title: "{% trans 'Are you sure?' %}",
|
||||
text: "{% trans 'This will delete the selected user.' %}",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: false
|
||||
}, function() {
|
||||
doDelete();
|
||||
});
|
||||
}).on('click', '#btn_user_bulk_update', function(){
|
||||
var json_data = $('#fm_user_bulk_update').serializeObject();
|
||||
var body = {};
|
||||
body.enable_otp = (json_data.enable_otp === 'on')? true: false;
|
||||
if (json_data.role != '') {
|
||||
body.role = json_data.role;
|
||||
}
|
||||
if (json_data.groups != undefined) {
|
||||
body.groups = json_data.groups;
|
||||
}
|
||||
if (typeof body.groups === 'string') {
|
||||
body.groups = [parseInt(body.groups)]
|
||||
} else if(typeof body.groups === 'array') {
|
||||
new_groups = body.groups.map(Number);
|
||||
body.groups = new_groups;
|
||||
}
|
||||
var $data_table = $('#user_list_table').DataTable()
|
||||
var post_list = [];
|
||||
$data_table.rows({selected: true}).every(function(){
|
||||
var content = Object.assign({id: this.data().id}, body);
|
||||
post_list.push(content);
|
||||
});
|
||||
if (post_list === []) {
|
||||
return false;
|
||||
};
|
||||
var the_url = "{% url 'users:user-bulk-update-api' %}";
|
||||
var success = function() {
|
||||
var msg = "{% trans 'The selected users has been updated successfully.' %}";
|
||||
swal("{% trans 'User Updated' %}", msg, "success");
|
||||
$('#user_list_table').DataTable().ajax.reload();
|
||||
jumpserver.checked = false;
|
||||
}
|
||||
APIUpdateAttr({url: the_url, method: 'PATCH', body: JSON.stringify(post_list), success: success});
|
||||
$('#user_bulk_update_modal').modal('hide');
|
||||
}).on('click', '#btn_user_import', function() {
|
||||
var $form = $('#fm_user_import');
|
||||
$form.find('.help-block').remove();
|
||||
function success (data) {
|
||||
if (data.success === false) {
|
||||
var $help = $form.find('.help-block');
|
||||
$('<span />', {class: 'help-block text-danger'}).html(data.msg).insertAfter($('#id_excel'));
|
||||
} else {
|
||||
$('#user_import_modal').modal('hide');
|
||||
var $data_table = $('#user_list_table').DataTable();
|
||||
toastr.success("{% trans 'Import User Success.' %}")
|
||||
$data_table.ajax.reload();
|
||||
}
|
||||
}
|
||||
$form.ajaxSubmit({success: success});
|
||||
}).on('change', '#id_excel', function() {
|
||||
$(this).siblings('.help-block').remove();
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{% extends 'cron/_cron.html' %}
|
||||
{% load i18n %}
|
||||
{% block user_template_title %}{% trans "Update user" %}{% endblock %}
|
||||
{% block username %}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.username.id_for_label }}" class="col-sm-2 control-label">{% trans 'Username' %}</label>
|
||||
<div class="col-sm-9 controls" >
|
||||
<input id="{{ form.username.id_for_label }}" name="{{ form.username.html_name }}" type="text" value="{{ user_object.username }}" readonly class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block password %}
|
||||
<h3>{% trans 'Password' %}</h3>
|
||||
<div class="form-group">
|
||||
<label for="password" class="col-sm-2 control-label">{% trans 'Password' %}</label>
|
||||
<div class="col-sm-9 controls" >
|
||||
<input id="password" name="password" type="password" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,97 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap %}
|
||||
{% block custom_head_css_js %}
|
||||
<link href="{% static "css/plugins/select2/select2.min.css" %}" rel="stylesheet">
|
||||
<script src="{% static "js/plugins/select2/select2.full.min.js" %}"></script>
|
||||
<link href="{% static "css/plugins/datepicker/datepicker3.css" %}" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<h5>{% block user_template_title %}{% trans 'Create user' %}{% endblock %}</h5>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<form method="post" class="form-horizontal" action="" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<h3>{% trans 'Account' %}</h3>
|
||||
{% block username %} {% endblock %}
|
||||
{{ form.email|bootstrap_horizontal }}
|
||||
{{ form.name|bootstrap_horizontal }}
|
||||
{{ form.groups|bootstrap_horizontal }}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
{% block password %} {% endblock %}
|
||||
|
||||
<div class="hr-line-dashed"></div>
|
||||
<h3>{% trans 'Security and Role' %}</h3>
|
||||
{{ form.role|bootstrap_horizontal }}
|
||||
<div class="form-group {% if form.date_expired.errors %} has-error {% endif %}" id="date_5">
|
||||
<label for="{{ form.date_expired.id_for_label }}" class="col-sm-2 control-label">{{ form.date_expired.label }}</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group date">
|
||||
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
|
||||
<input id="{{ form.date_expired.id_for_label }}" name="{{ form.date_expired.html_name }}" type="text" class="form-control" value="{{ form.date_expired.value|date:'Y-m-d' }}">
|
||||
</div>
|
||||
<span class="help-block ">{{ form.date_expired.errors }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{# {{ form.date_expired|bootstrap_horizontal }}#}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.enable_otp.id_for_label }}" class="col-sm-2 control-label">{% trans 'Enable OTP' %}</label>
|
||||
<div class="col-sm-8">
|
||||
{{ form.enable_otp }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hr-line-dashed"></div>
|
||||
<h3>{% trans 'Profile' %}</h3>
|
||||
{{ form.phone|bootstrap_horizontal }}
|
||||
{{ form.wechat|bootstrap_horizontal }}
|
||||
{{ form.comment|bootstrap_horizontal }}
|
||||
<div class="hr-line-dashed"></div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-4 col-sm-offset-2">
|
||||
<button class="btn btn-white" type="reset">{% trans 'Reset' %}</button>
|
||||
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script src="{% static 'js/plugins/datapicker/bootstrap-datepicker.js' %}"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2();
|
||||
|
||||
$('.input-group.date').datepicker({
|
||||
format: "yyyy-mm-dd",
|
||||
todayBtn: "linked",
|
||||
keyboardNavigation: false,
|
||||
forceParse: false,
|
||||
calendarWeeks: true,
|
||||
autoclose: true
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,16 @@
|
|||
{% extends 'sudo/_sudo.html' %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap %}
|
||||
{% block user_template_title %}{% trans "Create user" %}{% endblock %}
|
||||
{% block username %}
|
||||
{{ form.username|bootstrap_horizontal }}
|
||||
{% endblock %}
|
||||
{% block password %}
|
||||
<h3>{% trans 'Password' %}</h3>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">{% trans 'Password' %}</label>
|
||||
<div class="col-sm-8 controls" >
|
||||
{% trans 'Reset link will be generated and sent to the user. ' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,393 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load common_tags %}
|
||||
{% load users_tags %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% 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">
|
||||
<script src="{% static "js/plugins/select2/select2.full.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">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="panel-options">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active">
|
||||
<a href="{% url 'users:user-detail' pk=user_object.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'User detail' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'users:user-asset-permission' pk=user_object.id %}" class="text-center"><i class="fa fa-bar-chart-o"></i> {% trans 'Asset permission' %}</a>
|
||||
</li>
|
||||
<li><a href="{% url 'users:user-granted-asset' pk=user_object.id %}" class="text-center"><i class="fa fa-cubes"></i> {% trans 'Asset granted' %}</a></li>
|
||||
<li><a href="{% url 'users:user-login-history' pk=user_object.id %}" class="text-center"><i class="fa fa-calculator-o"></i> {% trans 'Login history' %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-7" style="padding-left: 0">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span class="label"><b>{{ user_object.name }}</b></span>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-user">
|
||||
</ul>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<td colspan="2">
|
||||
<img src="{{ user_object | user_avatar_url }}" class="img-circle" width="64" height="64">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="20%">{% trans 'Name' %}:</td>
|
||||
<td><b>{{ user_object.name }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Username' %}:</td>
|
||||
<td><b>{{ user_object.username }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Email' %}:</td>
|
||||
<td><b>{{ user_object.email }}</b></td>
|
||||
</tr>
|
||||
{% if user_object.phone %}
|
||||
<tr>
|
||||
<td>{% trans 'Phone' %}:</td>
|
||||
<td><b>{{ user_object.phone }}</b></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if user_object.wechat %}
|
||||
<tr>
|
||||
<td>{% trans 'Wechat' %}:</td>
|
||||
<td><b>{{ user_object.wechat }}</b></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>{% trans 'Role' %}:</td>
|
||||
<td><b>{{ user_object.get_role_display }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Date expired' %}:</td>
|
||||
<td><b>{{ user_object.date_expired|date:"Y-m-j H:i:s" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Created by' %}:</td>
|
||||
<td><b>{{ user_object.created_by }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Date joined' %}:</td>
|
||||
<td><b>{{ user_object.date_joined|date:"Y-m-j H:i:s" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Last login' %}:</td>
|
||||
<td><b>{{ user_object.last_login|date:"Y-m-j H:i:s" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Comment' %}:</td>
|
||||
<td><b>{{ user_object.comment }}</b></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-5" style="padding-left: 0;padding-right: 0">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Quick modify' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<td width="50%">{% trans 'Active' %}:</td>
|
||||
<td><span class="pull-right">
|
||||
<div class="switch">
|
||||
<div class="onoffswitch">
|
||||
<input type="checkbox" {% if user_object.is_active %} checked {% endif %} class="onoffswitch-checkbox" id="is_active">
|
||||
<label class="onoffswitch-label" for="is_active">
|
||||
<span class="onoffswitch-inner"></span>
|
||||
<span class="onoffswitch-switch"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Enable OTP' %}:</td>
|
||||
<td><span class="pull-right">
|
||||
<div class="switch">
|
||||
<div class="onoffswitch">
|
||||
<input type="checkbox" class="onoffswitch-checkbox" {% if user_object.enable_otp %} checked {% endif %}
|
||||
id="enable_otp">
|
||||
<label class="onoffswitch-label" for="enable_otp">
|
||||
<span class="onoffswitch-inner"></span>
|
||||
<span class="onoffswitch-switch"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Reset password' %}:</td>
|
||||
<td>
|
||||
<span class="pull-right">
|
||||
<button type="button" class="btn btn-primary btn-xs" id="btn_reset_password" style="width: 54px">{% trans 'Reset' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Reset ssh key' %}:</td>
|
||||
<td>
|
||||
<span class="pull-right">
|
||||
<button type="button" class="btn btn-primary btn-xs" id="btn_reset_pk" style="width: 54px;">{% trans 'Reset' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Update ssh key' %}:</td>
|
||||
<td>
|
||||
<span class="pull-right">
|
||||
<button type="button" class="btn btn-primary btn-xs" id="btn_update_pk" style="width: 54px;" data-toggle="modal" data-target="#user_update_pk_modal">{% trans 'Update' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'User group' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table group_edit">
|
||||
<tbody>
|
||||
<form>
|
||||
<tr>
|
||||
<td colspan="2" class="no-borders">
|
||||
<select data-placeholder="{% trans 'Join user groups' %}" id="slct_groups" class="select2" style="width: 100%" multiple="" tabindex="4">
|
||||
{% for group in groups %}
|
||||
<option value="{{ group.id }}" id="opt_{{ group.id }}">{{ group.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" class="no-borders">
|
||||
<button type="button" class="btn btn-info btn-small" id="btn_add_user_group">{% trans 'Join' %}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</form>
|
||||
|
||||
{% for group in user_object.groups.all %}
|
||||
<tr>
|
||||
<td ><b class="bdg_user_group" data-gid={{ group.id }}>{{ group.name }}</b></td>
|
||||
<td>
|
||||
<button class="btn btn-danger pull-right btn-xs btn_delete_user_group" type="button"><i class="fa fa-minus"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'users/_user_update_pk_modal.html' %}
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
jumpserver.selected_groups = {};
|
||||
|
||||
function updateUserGroups(user_groups) {
|
||||
var the_url = "{% url 'users:group-user-edit-api' pk=user_object.id %}";
|
||||
var body = {
|
||||
id: {{ user_object.id }},
|
||||
groups: Object.assign([], user_groups)
|
||||
};
|
||||
var success = function(data) {
|
||||
// remove all the selected groups from select > option and rendered ul element;
|
||||
$('.select2-selection__rendered').empty();
|
||||
$('#slct_groups').val('');
|
||||
$.map(jumpserver.selected_groups, function(group_name, index) {
|
||||
$('#opt_' + index).remove();
|
||||
// change tr html of user groups.
|
||||
$('.group_edit tbody').append(
|
||||
'<tr>' +
|
||||
'<td><b class="bdg_user_group" data-gid="' + index + '">' + group_name + '</b></td>' +
|
||||
'<td><button class="btn btn-danger btn-xs pull-right btn_delete_user_group" type="button"><i class="fa fa-minus"></i></button></td>' +
|
||||
'</tr>'
|
||||
)
|
||||
});
|
||||
// clear jumpserver.selected_groups
|
||||
jumpserver.selected_groups = {};
|
||||
toastr.success('{% trans "UserGroup Update Success!" %}')
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(body),
|
||||
success: success,
|
||||
method: 'PUT'
|
||||
});
|
||||
}
|
||||
$(document).ready(function() {
|
||||
$('.select2').select2()
|
||||
.on('select2:select', function(evt) {
|
||||
var data = evt.params.data;
|
||||
jumpserver.selected_groups[data.id] = data.text;
|
||||
}).on('select2:unselect', function(evt) {
|
||||
var data = evt.params.data;
|
||||
delete jumpserver.selected_groups[data.id]
|
||||
})
|
||||
}).on('click', '#is_active', function() {
|
||||
var the_url = "{% url 'users:user-patch-api' pk=user_object.id %}";
|
||||
var checked = !$(this).prop('checked');
|
||||
var body = {
|
||||
'is_active': checked
|
||||
};
|
||||
var success = '{% trans "Update Successfully!" %}';
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(body),
|
||||
success_message: success
|
||||
});
|
||||
}).on('click', '#enable_otp', function() {
|
||||
var the_url = "{% url 'users:user-patch-api' pk=user_object.id %}";
|
||||
var checked = !$(this).prop('checked');
|
||||
var body = {
|
||||
'enable_otp': checked
|
||||
};
|
||||
var success = '{% trans "Update Successfully!" %}';
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(body),
|
||||
success_message: success
|
||||
});
|
||||
}).on('click', '#btn_add_user_group', function() {
|
||||
if (Object.keys(jumpserver.selected_groups).length === 0) {
|
||||
return false;
|
||||
}
|
||||
var user_groups = $('.bdg_user_group').map(function() {
|
||||
return $(this).data('gid');
|
||||
}).get();
|
||||
$.map(jumpserver.selected_groups, function(value, index) {
|
||||
user_groups.push(parseInt(index));
|
||||
$('#opt_' + index).remove();
|
||||
});
|
||||
updateUserGroups(user_groups)
|
||||
}).on('click', '.btn_delete_user_group', function() {
|
||||
var $this = $(this);
|
||||
var $tr = $this.closest('tr');
|
||||
var $badge = $tr.find('.bdg_user_group');
|
||||
var gid = $badge.data('gid');
|
||||
var group_name = $badge.html() || $badge.text();
|
||||
$('#slct_groups').append(
|
||||
'<option value="' + gid + '" id="opt_' + gid + '">' + group_name + '</option>'
|
||||
);
|
||||
$tr.remove();
|
||||
var user_groups = $('.bdg_user_group').map(function() {
|
||||
return $(this).data('gid');
|
||||
}).get();
|
||||
updateUserGroups(user_groups)
|
||||
}).on('click', '#btn_reset_password', function() {
|
||||
function doReset() {
|
||||
var the_url = '{% url "users:user-reset-password-api" pk=user_object.id %}';
|
||||
var body = {};
|
||||
var success = function() {
|
||||
var msg = "{% trans 'E-mail sent successfully. An e-mail has been sent to the user\'s mailbox.' %}";
|
||||
swal("{% trans 'Password-Reset' %}", msg, "success");
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(body),
|
||||
success: success
|
||||
});
|
||||
}
|
||||
swal({
|
||||
title: "{% trans 'Are you sure?' %}",
|
||||
text: "{% trans 'This will reset the user\'s password.' %}",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: false
|
||||
}, function() {
|
||||
doReset();
|
||||
});
|
||||
}).on('click', '#btn_reset_pk', function() {
|
||||
function doReset() {
|
||||
var the_url = '{% url "users:user-reset-pk-api" pk=user_object.id %}';
|
||||
var body = {};
|
||||
var success = function() {
|
||||
var msg = "{% trans 'The reset-ssh-public-key E-mail has been sent successfully. Please inform the user to update his new ssh public key.' %}";
|
||||
swal("{% trans 'SSH-Public-Key Reset' %}", msg, "success");
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(body),
|
||||
success: success
|
||||
});
|
||||
}
|
||||
swal({
|
||||
title: "{% trans 'Are you sure?' %}",
|
||||
text: "{% trans 'This will reset the user\'s public key.' %}",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: false
|
||||
}, function() {
|
||||
doReset();
|
||||
});
|
||||
}).on('click', '#btn_user_update_pk', function(){
|
||||
var $this = $(this);
|
||||
var pk = $('#txt_pk').val();
|
||||
var the_url = '{% url "users:user-update-pk-api" pk=user_object.id %}';
|
||||
var body = {'_public_key': pk};
|
||||
var success = function() {
|
||||
$('#txt_pk').val('');
|
||||
$this.closest('.modal').modal('hide');
|
||||
var msg = "{% trans 'Successfully updated the SSH public key.' %}";
|
||||
swal("{% trans 'User SSH Public Key Update' %}", msg, "success");
|
||||
};
|
||||
var fail = function() {
|
||||
var msg = "{% trans 'Failed to update the user\'s SSH public key.' %}";
|
||||
swal({
|
||||
title: "{% trans 'User SSH Public Key Update' %}",
|
||||
text: msg,
|
||||
type: "error",
|
||||
showCancelButton: false,
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: true
|
||||
}, function () {
|
||||
$('#txt_pk').focus();
|
||||
}
|
||||
);
|
||||
}
|
||||
APIUpdateAttr({ url: the_url, body: JSON.stringify(body), success: success, error: fail});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,242 @@
|
|||
{% extends '_base_list.html' %}
|
||||
{% load i18n static %}
|
||||
{% block custom_head_css_js %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
div.dataTables_wrapper div.dataTables_filter,
|
||||
.dataTables_length {
|
||||
float: right !important;
|
||||
}
|
||||
|
||||
div.dataTables_wrapper div.dataTables_filter {
|
||||
margin-left: 15px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block table_search %}{% endblock %}
|
||||
{% block table_container %}
|
||||
<div class="uc pull-left"><a href="javascript:void(0);" class="btn btn-sm btn-primary" data-toggle="modal" data-target="#user_import_modal"> {% trans "Import user" %} </a></div>
|
||||
<div class="uc pull-left m-l-5 m-r-5"><a href="{% url "users:user-create" %}" class="btn btn-sm btn-primary"> {% trans "Create user" %} </a></div>
|
||||
<table class="table table-striped table-bordered table-hover " id="user_list_table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
<div class="checkbox checkbox-default"><input id="" type="checkbox" class="ipt_check_all"><label></label></div>
|
||||
</th>
|
||||
<th class="text-center">{% trans 'Name' %}</a></th>
|
||||
<th class="text-center">{% trans 'Username' %}</a></th>
|
||||
<th class="text-center">{% trans 'Role' %}</th>
|
||||
<th class="text-center">{% trans 'User group' %}</th>
|
||||
<th class="text-center">{% trans 'Asset num' %}</th>
|
||||
<th class="text-center">{% trans 'Active' %}</a></th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="actions" class="hide">
|
||||
<div class="input-group">
|
||||
<select class="form-control m-b" style="width: auto" id="slct_bulk_update">
|
||||
<option value="delete">{% trans 'Delete selected' %}</option>
|
||||
<option value="update">{% trans 'Update selected' %}</option>
|
||||
<option value="deactive">{% trans 'Deactive selected' %}</option>
|
||||
</select>
|
||||
<div class="input-group-btn pull-left" style="padding-left: 5px;">
|
||||
<button id='btn_bulk_update' style="height: 32px;" class="btn btn-sm btn-primary">
|
||||
{% trans 'Submit' %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "users/_user_bulk_update_modal.html" %}
|
||||
{% include "users/_user_import_modal.html" %}
|
||||
{% endblock %}
|
||||
{% block content_bottom_left %}{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script src="{% static 'js/jquery.form.min.js' %}"></script>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
var options = {
|
||||
ele: $('#user_list_table'),
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
var detail_btn = '<a href="{% url "users:user-detail" pk=99991937 %}">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('99991937', rowData.id));
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
var innerHtml = cellData.length > 8 ? cellData.substring(0, 8) + '...': cellData;
|
||||
$(td).html('<a href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</a>');
|
||||
}},
|
||||
{targets: 6, createdCell: function (td, cellData) {
|
||||
if (!cellData) {
|
||||
$(td).html('<i class="fa fa-times text-danger"></i>')
|
||||
} else {
|
||||
$(td).html('<i class="fa fa-check text-navy"></i>')
|
||||
}
|
||||
}},
|
||||
{targets: 7, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "users:user-update" pk=99991937 %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace('99991937', cellData);
|
||||
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_user_delete" data-uid="99991937">{% trans "Delete" %}</a>'.replace('99991937', cellData);
|
||||
if (rowData.id === 1) {
|
||||
$(td).html(update_btn)
|
||||
} else {
|
||||
$(td).html(update_btn + del_btn)
|
||||
}
|
||||
}}],
|
||||
ajax_url: '{% url "users:user-bulk-update-api" %}',
|
||||
columns: [{data: function(){return ""}}, {data: "username" }, {data: "name" }, {data: "get_role_display" }, {data: "group_display" },
|
||||
{data: function(){return 999}}, {data: "active_display" }, {data: "id" }],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
jumpserver.initDataTable(options);
|
||||
}).on('click', '#btn_bulk_update', function(){
|
||||
var action = $('#slct_bulk_update').val();
|
||||
var $data_table = $('#user_list_table').DataTable()
|
||||
var id_list = [];
|
||||
var plain_id_list = [];
|
||||
$data_table.rows({selected: true}).every(function(){
|
||||
id_list.push({id: this.data().id});
|
||||
plain_id_list.push(this.data().id);
|
||||
});
|
||||
if (id_list === []) {
|
||||
return false;
|
||||
};
|
||||
var the_url = "{% url 'users:user-bulk-update-api' %}";
|
||||
function doDeactive() {
|
||||
var body = $.each(id_list, function(index, user_object) {
|
||||
user_object['is_active'] = false;
|
||||
});
|
||||
APIUpdateAttr({url: the_url, method: 'PATCH', body: JSON.stringify(body)});
|
||||
$data_table.ajax.reload();
|
||||
jumpserver.checked = false;
|
||||
}
|
||||
function doDelete() {
|
||||
swal({
|
||||
title: "{% trans 'Are you sure?' %}",
|
||||
text: "{% trans 'This will delete the selected users !!!' %}",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: false
|
||||
}, function() {
|
||||
var success = function() {
|
||||
var msg = "{% trans 'User Deleted.' %}";
|
||||
swal("{% trans 'User Delete' %}", msg, "success");
|
||||
$('#user_list_table').DataTable().ajax.reload();
|
||||
};
|
||||
var fail = function() {
|
||||
var msg = "{% trans 'User Deleting failed.' %}";
|
||||
swal("{% trans 'User Delete' %}", msg, "error");
|
||||
};
|
||||
var url_delete = the_url + '?id__in=' + JSON.stringify(plain_id_list);
|
||||
APIUpdateAttr({url: url_delete, method: 'DELETE', success: success, error: fail});
|
||||
jumpserver.checked = false;
|
||||
});
|
||||
}
|
||||
function doUpdate() {
|
||||
$('#user_bulk_update_modal').modal('show');
|
||||
}
|
||||
switch(action) {
|
||||
case 'deactive':
|
||||
doDeactive();
|
||||
break;
|
||||
case 'delete':
|
||||
doDelete();
|
||||
break;
|
||||
case 'update':
|
||||
doUpdate();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}).on('click', '.btn_user_delete', function(){
|
||||
var $this = $(this);
|
||||
function doDelete() {
|
||||
var uid = $this.data('uid');
|
||||
var the_url = '{% url "users:user-patch-api" pk=99991937 %}'.replace('99991937', uid);
|
||||
var body = {};
|
||||
var success = function() {
|
||||
var msg = "{% trans 'User Deleted.' %}";
|
||||
swal("{% trans 'User Delete' %}", msg, "success");
|
||||
$('#user_list_table').DataTable().ajax.reload();
|
||||
};
|
||||
var fail = function() {
|
||||
var msg = "{% trans 'User Deleting failed.' %}";
|
||||
swal("{% trans 'User Delete' %}", msg, "error");
|
||||
}
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(body),
|
||||
method: 'DELETE',
|
||||
success: success,
|
||||
error: fail
|
||||
});
|
||||
}
|
||||
swal({
|
||||
title: "{% trans 'Are you sure?' %}",
|
||||
text: "{% trans 'This will delete the selected user.' %}",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: "{% trans 'Confirm' %}",
|
||||
closeOnConfirm: false
|
||||
}, function() {
|
||||
doDelete();
|
||||
});
|
||||
}).on('click', '#btn_user_bulk_update', function(){
|
||||
var json_data = $('#fm_user_bulk_update').serializeObject();
|
||||
var body = {};
|
||||
body.enable_otp = (json_data.enable_otp === 'on')? true: false;
|
||||
if (json_data.role != '') {
|
||||
body.role = json_data.role;
|
||||
}
|
||||
if (json_data.groups != undefined) {
|
||||
body.groups = json_data.groups;
|
||||
}
|
||||
if (typeof body.groups === 'string') {
|
||||
body.groups = [parseInt(body.groups)]
|
||||
} else if(typeof body.groups === 'array') {
|
||||
new_groups = body.groups.map(Number);
|
||||
body.groups = new_groups;
|
||||
}
|
||||
var $data_table = $('#user_list_table').DataTable()
|
||||
var post_list = [];
|
||||
$data_table.rows({selected: true}).every(function(){
|
||||
var content = Object.assign({id: this.data().id}, body);
|
||||
post_list.push(content);
|
||||
});
|
||||
if (post_list === []) {
|
||||
return false;
|
||||
};
|
||||
var the_url = "{% url 'users:user-bulk-update-api' %}";
|
||||
var success = function() {
|
||||
var msg = "{% trans 'The selected users has been updated successfully.' %}";
|
||||
swal("{% trans 'User Updated' %}", msg, "success");
|
||||
$('#user_list_table').DataTable().ajax.reload();
|
||||
jumpserver.checked = false;
|
||||
}
|
||||
APIUpdateAttr({url: the_url, method: 'PATCH', body: JSON.stringify(post_list), success: success});
|
||||
$('#user_bulk_update_modal').modal('hide');
|
||||
}).on('click', '#btn_user_import', function() {
|
||||
var $form = $('#fm_user_import');
|
||||
$form.find('.help-block').remove();
|
||||
function success (data) {
|
||||
if (data.success === false) {
|
||||
var $help = $form.find('.help-block');
|
||||
$('<span />', {class: 'help-block text-danger'}).html(data.msg).insertAfter($('#id_excel'));
|
||||
} else {
|
||||
$('#user_import_modal').modal('hide');
|
||||
var $data_table = $('#user_list_table').DataTable();
|
||||
toastr.success("{% trans 'Import User Success.' %}")
|
||||
$data_table.ajax.reload();
|
||||
}
|
||||
}
|
||||
$form.ajaxSubmit({success: success});
|
||||
}).on('change', '#id_excel', function() {
|
||||
$(this).siblings('.help-block').remove();
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{% extends 'sudo/_sudo.html' %}
|
||||
{% load i18n %}
|
||||
{% block user_template_title %}{% trans "Update user" %}{% endblock %}
|
||||
{% block username %}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.username.id_for_label }}" class="col-sm-2 control-label">{% trans 'Username' %}</label>
|
||||
<div class="col-sm-9 controls" >
|
||||
<input id="{{ form.username.id_for_label }}" name="{{ form.username.html_name }}" type="text" value="{{ user_object.username }}" readonly class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block password %}
|
||||
<h3>{% trans 'Password' %}</h3>
|
||||
<div class="form-group">
|
||||
<label for="password" class="col-sm-2 control-label">{% trans 'Password' %}</label>
|
||||
<div class="col-sm-9 controls" >
|
||||
<input id="password" name="password" type="password" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,42 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from django.conf.urls import url, include
|
||||
|
||||
|
||||
from api import views as api_view
|
||||
import views as page_view
|
||||
|
||||
|
||||
app_name = 'ops'
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'host_alia', api_view.HostAliaViewSet)
|
||||
router.register(r'user_alia', api_view.UserAliaViewSet)
|
||||
router.register(r'cmd_alia', api_view.CmdAliaViewSet)
|
||||
router.register(r'runas_alia', api_view.RunasAliaViewSet)
|
||||
router.register(r'extra_conf', api_view.ExtraconfViewSet)
|
||||
router.register(r'privilege', api_view.PrivilegeViewSet)
|
||||
router.register(r'sudo', api_view.SudoViewSet)
|
||||
router.register(r'cron', api_view.CronTableViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
# Resource Sudo url
|
||||
url(r'^sudo/list$', page_view.SudoListView.as_view(), name='page-sudo-list'),
|
||||
url(r'^sudo/create$', page_view.SudoCreateView.as_view(), name='page-sudo-create'),
|
||||
url(r'^sudo/detail$', page_view.SudoDetailView.as_view(), name='page-sudo-detail'),
|
||||
url(r'^sudo/update$', page_view.SudoUpdateView.as_view(), name='page-sudo-update'),
|
||||
|
||||
# Resource Cron url
|
||||
url(r'^cron/list$', page_view.CronListView.as_view(), name='page-cron-list'),
|
||||
url(r'^cron/create$', page_view.CronCreateView.as_view(), name='page-cron-create'),
|
||||
url(r'^cron/detail$', page_view.CronDetailView.as_view(), name='page-cron-detail'),
|
||||
url(r'^cron/update$', page_view.CronUpdateView.as_view(), name='page-cron-update'),
|
||||
]
|
||||
|
||||
urlpatterns += [
|
||||
url(r'^v1/sudo', include(router.urls)),
|
||||
]
|
||||
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
#
|
|
@ -0,0 +1,521 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
from __future__ import unicode_literals, print_function
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import traceback
|
||||
import ansible.constants as default_config
|
||||
|
||||
from uuid import uuid4
|
||||
from django.utils import timezone
|
||||
from ansible.executor.task_queue_manager import TaskQueueManager
|
||||
from ansible.inventory import Inventory, Host, Group
|
||||
from ansible.vars import VariableManager
|
||||
from ansible.parsing.dataloader import DataLoader
|
||||
from ansible.executor import playbook_executor
|
||||
from ansible.utils.display import Display
|
||||
from ansible.playbook.play import Play
|
||||
from ansible.plugins.callback import CallbackBase
|
||||
|
||||
from ops.models import Tasker, AnsiblePlay, AnsibleTask, AnsibleHostResult
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AnsibleError(StandardError):
|
||||
pass
|
||||
|
||||
|
||||
class Config(object):
|
||||
"""Ansible运行时配置类, 用于初始化Ansible的一些默认配置.
|
||||
"""
|
||||
def __init__(self, verbosity=None, inventory=None, listhosts=None, subset=None, module_paths=None, extra_vars=None,
|
||||
forks=10, ask_vault_pass=False, vault_password_files=None, new_vault_password_file=None,
|
||||
output_file=None, tags=None, skip_tags=None, one_line=None, tree=None, ask_sudo_pass=False, ask_su_pass=False,
|
||||
sudo=None, sudo_user=None, become=None, become_method=None, become_user=None, become_ask_pass=False,
|
||||
ask_pass=False, private_key_file=None, remote_user=None, connection="smart", timeout=10, ssh_common_args=None,
|
||||
sftp_extra_args=None, scp_extra_args=None, ssh_extra_args=None, poll_interval=None, seconds=None, check=False,
|
||||
syntax=None, diff=None, force_handlers=None, flush_cache=None, listtasks=None, listtags=None, module_path=None):
|
||||
self.verbosity = verbosity
|
||||
self.inventory = inventory
|
||||
self.listhosts = listhosts
|
||||
self.subset = subset
|
||||
self.module_paths = module_paths
|
||||
self.extra_vars = extra_vars
|
||||
self.forks = forks
|
||||
self.ask_vault_pass = ask_vault_pass
|
||||
self.vault_password_files = vault_password_files
|
||||
self.new_vault_password_file = new_vault_password_file
|
||||
self.output_file = output_file
|
||||
self.tags = tags
|
||||
self.skip_tags = skip_tags
|
||||
self.one_line = one_line
|
||||
self.tree = tree
|
||||
self.ask_sudo_pass = ask_sudo_pass
|
||||
self.ask_su_pass = ask_su_pass
|
||||
self.sudo = sudo
|
||||
self.sudo_user = sudo_user
|
||||
self.become = become
|
||||
self.become_method = become_method
|
||||
self.become_user = become_user
|
||||
self.become_ask_pass = become_ask_pass
|
||||
self.ask_pass = ask_pass
|
||||
self.private_key_file = private_key_file
|
||||
self.remote_user = remote_user
|
||||
self.connection = connection
|
||||
self.timeout = timeout
|
||||
self.ssh_common_args = ssh_common_args
|
||||
self.sftp_extra_args = sftp_extra_args
|
||||
self.scp_extra_args = scp_extra_args
|
||||
self.ssh_extra_args = ssh_extra_args
|
||||
self.poll_interval = poll_interval
|
||||
self.seconds = seconds
|
||||
self.check = check
|
||||
self.syntax = syntax
|
||||
self.diff = diff
|
||||
self.force_handlers = force_handlers
|
||||
self.flush_cache = flush_cache
|
||||
self.listtasks = listtasks
|
||||
self.listtags = listtags
|
||||
self.module_path = module_path
|
||||
self.__overwrite_default()
|
||||
|
||||
def __overwrite_default(self):
|
||||
"""上面并不能包含Ansible所有的配置, 如果有其他的配置,
|
||||
可以通过替换default_config模块里面的变量进行重载,
|
||||
比如 default_config.DEFAULT_ASK_PASS = False.
|
||||
"""
|
||||
default_config.HOST_KEY_CHECKING = False
|
||||
|
||||
|
||||
class InventoryMixin(object):
|
||||
"""提供生成Ansible inventory对象的方法
|
||||
"""
|
||||
|
||||
def gen_inventory(self):
|
||||
"""用于生成动态构建Ansible Inventory.
|
||||
self.hosts: [
|
||||
{"host": <ip>,
|
||||
"port": <port>,
|
||||
"user": <user>,
|
||||
"pass": <pass>,
|
||||
"key": <sshKey>,
|
||||
"group": <default>
|
||||
"other_host_var": <other>},
|
||||
{...},
|
||||
]
|
||||
self.group_vars: {
|
||||
"groupName1": {"var1": <value>, "var2": <value>, ...},
|
||||
"groupName2": {"var1": <value>, "var2": <value>, ...},
|
||||
}
|
||||
|
||||
:return: 返回一个Ansible的inventory对象
|
||||
"""
|
||||
|
||||
# TODO: 验证输入
|
||||
|
||||
# 创建Ansible Group,如果没有则创建default组
|
||||
for asset in self.hosts:
|
||||
g_name = asset.get('group', 'default')
|
||||
if g_name not in [g.name for g in self.groups]:
|
||||
group = Group(name=g_name)
|
||||
self.groups.append(group)
|
||||
|
||||
# 添加组变量到相应的组上
|
||||
for group_name, variables in self.group_vars.iteritems():
|
||||
for g in self.groups:
|
||||
if g.name == group_name:
|
||||
for v_name, v_value in variables.iteritems():
|
||||
g.set_variable(v_name, v_value)
|
||||
|
||||
# 往组里面添加Host
|
||||
for asset in self.hosts:
|
||||
# 添加Host链接的常用变量(host,port,user,pass,key)
|
||||
host = Host(name=asset['name'], port=asset['port'])
|
||||
host.set_variable('ansible_host', asset['ip'])
|
||||
host.set_variable('ansible_port', asset['port'])
|
||||
host.set_variable('ansible_user', asset['username'])
|
||||
|
||||
# 添加密码和秘钥
|
||||
if asset.get('password'):
|
||||
host.set_variable('ansible_ssh_pass', asset['password'])
|
||||
if asset.get('key'):
|
||||
host.set_variable('ansible_ssh_private_key_file', asset['key'])
|
||||
|
||||
# 添加become支持
|
||||
become = asset.get("become", None)
|
||||
if become is not None:
|
||||
host.set_variable("ansible_become", True)
|
||||
host.set_variable("ansible_become_method", become.get('method'))
|
||||
host.set_variable("ansible_become_user", become.get('user'))
|
||||
host.set_variable("ansible_become_pass", become.get('pass'))
|
||||
else:
|
||||
host.set_variable("ansible_become", False)
|
||||
|
||||
# 添加其他Host的额外变量
|
||||
for key, value in asset.iteritems():
|
||||
if key not in ["name", "port", "ip", "username", "password", "key"]:
|
||||
host.set_variable(key, value)
|
||||
|
||||
# 将host添加到组里面
|
||||
for g in self.groups:
|
||||
if g.name == asset.get('group', 'default'):
|
||||
g.add_host(host)
|
||||
|
||||
# 将组添加到Inventory里面,生成真正的inventory对象
|
||||
inventory = Inventory(loader=self.loader, variable_manager=self.variable_manager, host_list=[])
|
||||
for g in self.groups:
|
||||
inventory.add_group(g)
|
||||
self.variable_manager.set_inventory(inventory)
|
||||
return inventory
|
||||
|
||||
|
||||
class CallbackModule(CallbackBase):
|
||||
"""处理和分析Ansible运行结果,并保存数据.
|
||||
"""
|
||||
CALLBACK_VERSION = 2.0
|
||||
CALLBACK_TYPE = 'stdout'
|
||||
CALLBACK_NAME = 'json'
|
||||
|
||||
def __init__(self, tasker_id, display=None):
|
||||
super(CallbackModule, self).__init__(display)
|
||||
self.results = []
|
||||
self.output = {}
|
||||
self.tasker_id = tasker_id
|
||||
|
||||
def _new_play(self, play):
|
||||
"""将Play保持到数据里面
|
||||
"""
|
||||
ret = {
|
||||
'tasker': self.tasker_id,
|
||||
'name': play.name,
|
||||
'uuid': str(play._uuid),
|
||||
'tasks': []
|
||||
}
|
||||
|
||||
try:
|
||||
tasker = Tasker.objects.get(uuid=self.tasker_id)
|
||||
play = AnsiblePlay(tasker, name=ret['name'], uuid=ret['uuid'])
|
||||
play.save()
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
logger.error("Save ansible play uuid to database error!, %s" % e.message)
|
||||
|
||||
return ret
|
||||
|
||||
def _new_task(self, task):
|
||||
"""将Task保持到数据库里,需要和Play进行关联
|
||||
"""
|
||||
ret = {
|
||||
'name': task.name,
|
||||
'uuid': str(task._uuid),
|
||||
'failed': {},
|
||||
'unreachable': {},
|
||||
'skipped': {},
|
||||
'no_hosts': {},
|
||||
'success': {}
|
||||
}
|
||||
|
||||
try:
|
||||
play = AnsiblePlay.objects.get(uuid=self.__play_uuid)
|
||||
task = AnsibleTask(play=play, uuid=ret['uuid'], name=ret['name'])
|
||||
task.save()
|
||||
except Exception as e:
|
||||
logger.error("Save ansible task uuid to database error!, %s" % e.message)
|
||||
|
||||
return ret
|
||||
|
||||
@property
|
||||
def __task_uuid(self):
|
||||
return self.results[-1]['tasks'][-1]['uuid']
|
||||
|
||||
@property
|
||||
def __play_uuid(self):
|
||||
return self.results[-1]['uuid']
|
||||
|
||||
def save_task_result(self, result, status):
|
||||
try:
|
||||
task = AnsibleTask.objects.get(uuid=self.__task_uuid)
|
||||
host_result = AnsibleHostResult(task=task, name=result._host)
|
||||
if status == "failed":
|
||||
host_result.failed = json.dumps(result._result)
|
||||
elif status == "unreachable":
|
||||
host_result.unreachable = json.dumps(result._result)
|
||||
elif status == "skipped":
|
||||
host_result.skipped = json.dumps(result._result)
|
||||
elif status == "success":
|
||||
host_result.success = json.dumps(result._result)
|
||||
else:
|
||||
logger.error("No such status(failed|unreachable|skipped|success), please check!")
|
||||
host_result.save()
|
||||
except Exception as e:
|
||||
logger.error("Save Ansible host result to database error!, %s" % e.message)
|
||||
|
||||
@staticmethod
|
||||
def save_no_host_result(task):
|
||||
try:
|
||||
task = AnsibleTask.objects.get(uuid=task._uuid)
|
||||
host_result = AnsibleHostResult(task=task, no_host="no host to run this task")
|
||||
host_result.save()
|
||||
except Exception as e:
|
||||
logger.error("Save Ansible host result to database error!, %s" % e.message)
|
||||
|
||||
def v2_runner_on_failed(self, result, ignore_errors=False):
|
||||
self.save_task_result(result, "failed")
|
||||
host = result._host
|
||||
self.results[-1]['tasks'][-1]['failed'][host.name] = result._result
|
||||
|
||||
def v2_runner_on_unreachable(self, result):
|
||||
self.save_task_result(result, "unreachable")
|
||||
host = result._host
|
||||
self.results[-1]['tasks'][-1]['unreachable'][host.name] = result._result
|
||||
|
||||
def v2_runner_on_skipped(self, result):
|
||||
self.save_task_result(result, "skipped")
|
||||
host = result._host
|
||||
self.results[-1]['tasks'][-1]['skipped'][host.name] = result._result
|
||||
|
||||
def v2_runner_on_no_hosts(self, task):
|
||||
self.save_no_host_result(task)
|
||||
self.results[-1]['tasks'][-1]['no_hosts']['msg'] = "no host to run this task"
|
||||
|
||||
def v2_runner_on_ok(self, result):
|
||||
self.save_task_result(result, "success")
|
||||
host = result._host
|
||||
self.results[-1]['tasks'][-1]['success'][host.name] = result._result
|
||||
|
||||
def v2_playbook_on_play_start(self, play):
|
||||
self.results.append(self._new_play(play))
|
||||
|
||||
def v2_playbook_on_task_start(self, task, is_conditional):
|
||||
self.results[-1]['tasks'].append(self._new_task(task))
|
||||
|
||||
def v2_playbook_on_stats(self, stats):
|
||||
"""AdHoc模式下这个钩子不会执行
|
||||
"""
|
||||
hosts = sorted(stats.processed.keys())
|
||||
|
||||
summary = {}
|
||||
for h in hosts:
|
||||
s = stats.summarize(h)
|
||||
summary[h] = s
|
||||
|
||||
self.output['plays'] = self.results
|
||||
self.output['stats'] = summary
|
||||
print("summary: %s", summary)
|
||||
|
||||
|
||||
class PlayBookRunner(InventoryMixin):
|
||||
"""用于执行AnsiblePlaybook的接口.简化Playbook对象的使用.
|
||||
"""
|
||||
|
||||
def __init__(self, config, palybook_path, playbook_var, become_pass, *hosts, **group_vars):
|
||||
"""
|
||||
|
||||
:param config: Config实例
|
||||
:param palybook_path: playbook的路径
|
||||
:param playbook_var: 执行Playbook时的变量
|
||||
:param become_pass: sudo passsword
|
||||
:param hosts: 可变位置参数, 为一个资产列表, 每一个资产用dict表示, 以下是这个dict必须包含的key
|
||||
[{
|
||||
"name": "asset_name",
|
||||
"ip": "asset_ip",
|
||||
"port": "asset_port",
|
||||
"username": "asset_user",
|
||||
"password": "asset_pass",
|
||||
"key": "asset_private_key",
|
||||
"group": "asset_group_name",
|
||||
...
|
||||
}]
|
||||
:param group_vars: 可变关键字参数, 是资产组变量, 记录对应的资产组变量
|
||||
"groupName1": {"group_variable1": "value1",...}
|
||||
"groupName2": {"group_variable1": "value1",...}
|
||||
"""
|
||||
|
||||
self.options = config
|
||||
|
||||
# 设置verbosity级别, 及命令行的--verbose选项
|
||||
self.display = Display()
|
||||
self.display.verbosity = self.options.verbosity
|
||||
playbook_executor.verbosity = self.options.verbosity
|
||||
|
||||
# sudo成其他用户的配置
|
||||
self.options.become = True
|
||||
self.options.become_method = 'sudo'
|
||||
self.options.become_user = 'root'
|
||||
passwords = {'become_pass': become_pass}
|
||||
|
||||
# 传入playbook的路径,以及执行需要的变量
|
||||
pb_dir = os.path.dirname(__file__)
|
||||
playbook = "%s/%s" % (pb_dir, palybook_path)
|
||||
|
||||
# 生成Ansible inventory, 这些变量Mixin都会用到
|
||||
self.hosts = hosts
|
||||
self.group_vars = group_vars
|
||||
self.loader = DataLoader()
|
||||
self.variable_manager = VariableManager()
|
||||
self.groups = []
|
||||
self.variable_manager.extra_vars = playbook_var
|
||||
self.inventory = self.gen_inventory()
|
||||
|
||||
# 初始化playbook的executor
|
||||
self.pbex = playbook_executor.PlaybookExecutor(
|
||||
playbooks=[playbook],
|
||||
inventory=self.inventory,
|
||||
variable_manager=self.variable_manager,
|
||||
loader=self.loader,
|
||||
options=self.options,
|
||||
passwords=passwords)
|
||||
|
||||
def run(self):
|
||||
"""执行Playbook, 记录执行日志, 处理执行结果.
|
||||
:return: <AnsibleResult>对象
|
||||
"""
|
||||
self.pbex.run()
|
||||
stats = self.pbex._tqm._stats
|
||||
|
||||
# 测试执行是否成功
|
||||
run_success = True
|
||||
hosts = sorted(stats.processed.keys())
|
||||
for h in hosts:
|
||||
t = stats.summarize(h)
|
||||
if t['unreachable'] > 0 or t['failures'] > 0:
|
||||
run_success = False
|
||||
|
||||
# TODO: 记录执行日志, 处理执行结果.
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
class ADHocRunner(InventoryMixin):
|
||||
"""ADHoc接口
|
||||
"""
|
||||
def __init__(self, config, play_data, *hosts, **group_vars):
|
||||
"""
|
||||
:param hosts: 见PlaybookRunner参数
|
||||
:param group_vars: 见PlaybookRunner参数
|
||||
:param config: Config实例
|
||||
|
||||
:param play_data:
|
||||
play_data = dict(
|
||||
name="Ansible Ad-Hoc",
|
||||
hosts=pattern,
|
||||
gather_facts=True,
|
||||
tasks=[dict(action=dict(module='service', args={'name': 'vsftpd', 'state': 'restarted'}), async=async, poll=poll)]
|
||||
)
|
||||
"""
|
||||
|
||||
self.options = config
|
||||
|
||||
# 设置verbosity级别, 及命令行的--verbose选项
|
||||
self.display = Display()
|
||||
self.display.verbosity = self.options.verbosity
|
||||
|
||||
# sudo的配置移到了Host级别去了,因此这里不再需要处理
|
||||
self.passwords = None
|
||||
|
||||
# 生成Ansible inventory, 这些变量Mixin都会用到
|
||||
self.hosts = hosts
|
||||
self.group_vars = group_vars
|
||||
self.loader = DataLoader()
|
||||
self.variable_manager = VariableManager()
|
||||
self.groups = []
|
||||
self.inventory = self.gen_inventory()
|
||||
|
||||
self.play = Play().load(play_data, variable_manager=self.variable_manager, loader=self.loader)
|
||||
|
||||
@staticmethod
|
||||
def update_db_tasker(tasker_id, ext_code):
|
||||
try:
|
||||
tasker = Tasker.objects.get(uuid=tasker_id)
|
||||
tasker.end = timezone.now()
|
||||
tasker.completed = True
|
||||
tasker.exit_code = ext_code
|
||||
tasker.save()
|
||||
except Exception as e:
|
||||
logger.error("Update Tasker Status into database error!, %s" % e.message)
|
||||
|
||||
def create_db_tasker(self, name, uuid):
|
||||
try:
|
||||
hosts = [host.get('name') for host in self.hosts]
|
||||
tasker = Tasker(name=name, uuid=uuid, hosts=','.join(hosts), start=timezone.now())
|
||||
tasker.save()
|
||||
except Exception as e:
|
||||
logger.error("Save Tasker to database error!, %s" % e.message)
|
||||
|
||||
def run(self, tasker_name, tasker_uuid):
|
||||
"""执行ADHoc, 执行完后, 修改AnsiblePlay的状态为完成状态.
|
||||
|
||||
:param tasker_uuid <str> 用于标示此次task
|
||||
"""
|
||||
# 初始化callback插件,以及Tasker
|
||||
|
||||
self.create_db_tasker(tasker_name, tasker_uuid)
|
||||
self.results_callback = CallbackModule(tasker_uuid)
|
||||
|
||||
tqm = None
|
||||
# TODO:日志和结果分析
|
||||
try:
|
||||
tqm = TaskQueueManager(
|
||||
inventory=self.inventory,
|
||||
variable_manager=self.variable_manager,
|
||||
loader=self.loader,
|
||||
stdout_callback=self.results_callback,
|
||||
options=self.options,
|
||||
passwords=self.passwords
|
||||
)
|
||||
ext_code = tqm.run(self.play)
|
||||
result = self.results_callback.results
|
||||
|
||||
# 任务运行结束, 标示任务完成
|
||||
self.update_db_tasker(tasker_uuid, ext_code)
|
||||
|
||||
ret = json.dumps(result)
|
||||
return ext_code, ret
|
||||
|
||||
finally:
|
||||
if tqm:
|
||||
tqm.cleanup()
|
||||
|
||||
|
||||
def test_run():
|
||||
conf = Config()
|
||||
assets = [
|
||||
{
|
||||
"name": "192.168.1.119",
|
||||
"ip": "192.168.1.119",
|
||||
"port": "22",
|
||||
"username": "root",
|
||||
"password": "tongfang_test",
|
||||
"key": "asset_private_key",
|
||||
},
|
||||
{
|
||||
"name": "192.168.232.135",
|
||||
"ip": "192.168.232.135",
|
||||
"port": "22",
|
||||
"username": "yumaojun",
|
||||
"password": "xxx",
|
||||
"key": "asset_private_key",
|
||||
"become": {"method": "sudo", "user": "root", "pass": "xxx"}
|
||||
},
|
||||
]
|
||||
# 初始化Play
|
||||
play_source = {
|
||||
"name": "Ansible Play",
|
||||
"hosts": "default",
|
||||
"gather_facts": "no",
|
||||
"tasks": [
|
||||
dict(action=dict(module='ping')),
|
||||
]
|
||||
}
|
||||
hoc = ADHocRunner(conf, play_source, *assets)
|
||||
uuid = "tasker-" + uuid4().hex
|
||||
ext_code, result = hoc.run("test_task", uuid)
|
||||
print(ext_code)
|
||||
print(result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_run()
|
|
@ -0,0 +1,13 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
|
||||
|
||||
class CreateSudoPrivilegesMixin(object):
|
||||
|
||||
def create_privilege(self):
|
||||
pass
|
||||
|
||||
|
||||
class ListSudoPrivilegesMixin(object):
|
||||
|
||||
def get_all_privilege(self):
|
||||
pass
|
|
@ -1,3 +1,58 @@
|
|||
from django.shortcuts import render
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.views.generic.list import ListView, MultipleObjectMixin
|
||||
from django.views.generic.edit import CreateView, DeleteView, UpdateView
|
||||
from django.views.generic.detail import DetailView, SingleObjectMixin
|
||||
|
||||
from users.utils import AdminUserRequiredMixin
|
||||
from .utils.mixins import CreateSudoPrivilegesMixin, ListSudoPrivilegesMixin
|
||||
from models import *
|
||||
|
||||
|
||||
class SudoListView(AdminUserRequiredMixin, ListSudoPrivilegesMixin, ListView):
|
||||
paginate_by = settings.CONFIG.DISPLAY_PER_PAGE
|
||||
model = Sudo
|
||||
context_object_name = 'sudos'
|
||||
template_name = 'sudo/list.html'
|
||||
|
||||
|
||||
class SudoCreateView(AdminUserRequiredMixin, CreateSudoPrivilegesMixin, CreateView):
|
||||
model = Sudo
|
||||
template_name = 'sudo/create.html'
|
||||
|
||||
|
||||
class SudoUpdateView(AdminUserRequiredMixin, UpdateView):
|
||||
model = Sudo
|
||||
template_name = 'sudo/update.html'
|
||||
|
||||
|
||||
class SudoDetailView(DetailView):
|
||||
model = Sudo
|
||||
context_object_name = 'sudo'
|
||||
template_name = 'sudo/detail.html'
|
||||
|
||||
|
||||
class CronListView(AdminUserRequiredMixin, ListSudoPrivilegesMixin, ListView):
|
||||
paginate_by = settings.CONFIG.DISPLAY_PER_PAGE
|
||||
model = CronTable
|
||||
context_object_name = 'crons'
|
||||
template_name = 'sudo/list.html'
|
||||
|
||||
|
||||
class CronCreateView(AdminUserRequiredMixin, CreateSudoPrivilegesMixin, CreateView):
|
||||
model = CronTable
|
||||
template_name = 'cron/create.html'
|
||||
|
||||
|
||||
class CronUpdateView(AdminUserRequiredMixin, UpdateView):
|
||||
model = CronTable
|
||||
template_name = 'cron/update.html'
|
||||
|
||||
|
||||
class CronDetailView(DetailView):
|
||||
model = CronTable
|
||||
context_object_name = 'sudo'
|
||||
template_name = 'cron/detail.html'
|
||||
|
||||
# Create your views here.
|
||||
|
|
|
@ -34,9 +34,25 @@
|
|||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li id="terminal">
|
||||
<a href="{% url 'terminal:terminal-list' %}">
|
||||
<i class="fa fa-desktop"></i><span class="nav-label">{% trans 'Terminal' %}</span><span class="label label-info pull-right"></span>
|
||||
|
||||
|
||||
<li id="ops">
|
||||
<a>
|
||||
<i class="fa fa-inbox"></i> <span class="nav-label">{% trans 'Job Center' %}</span><span class="fa arrow"></span>
|
||||
</a>
|
||||
<ul class="nav nav-second-level">
|
||||
<li id="sudo"><a href="{% url 'ops:page-sudo-list' %}">{% trans 'Sudo' %}</a></li>
|
||||
<li id="cron"><a href="{% url 'ops:page-cron-list' %}">{% trans 'Cron' %}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li id="">
|
||||
<a href="">
|
||||
<i class="fa fa-files-o"></i><span class="nav-label">{% trans 'Audits' %}</span><span class="label label-info pull-right"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li id="audits">
|
||||
|
|
|
@ -6,7 +6,7 @@ djangorestframework==3.4.5
|
|||
ForgeryPy==0.1
|
||||
openpyxl==2.4.0
|
||||
celery==3.1.23
|
||||
ansible==2.1.1.0
|
||||
ansible==2.1.2.0
|
||||
django-simple-captcha==0.5.2
|
||||
django-formtools==1.0
|
||||
sshpubkeys==2.2.0
|
||||
|
@ -14,4 +14,7 @@ djangorestframework-bulk==0.2.1
|
|||
paramiko==2.0.2
|
||||
django-redis-cache==1.7.1
|
||||
requests==2.11.1
|
||||
itsdangerous==0.24
|
||||
itsdangerous==0.24
|
||||
python-gssapi==0.6.4
|
||||
tornado==4.4.2
|
||||
eventlet==0.19.0
|
||||
|
|
|
@ -30,7 +30,7 @@ def start_celery():
|
|||
os.chdir(apps_dir)
|
||||
os.environ.setdefault('C_FORCE_ROOT', '1')
|
||||
print('start celery')
|
||||
subprocess.call('celery -A common worker -B -s /tmp/celerybeat-schedule -l info ', shell=True)
|
||||
subprocess.call('celery -A common worker -P eventlet -B -s /tmp/celerybeat-schedule -l info ', shell=True)
|
||||
|
||||
|
||||
def main():
|
||||
|
|
Loading…
Reference in New Issue