mirror of https://github.com/jumpserver/jumpserver
[Feture] 添加ops 页面
parent
18fd04d63c
commit
0c9e24dc59
|
@ -26,12 +26,13 @@ from .hands import IsSuperUser, IsAppUser, IsValidUser, \
|
|||
get_user_granted_assets, push_users
|
||||
from .models import AssetGroup, Asset, Cluster, SystemUser, AdminUser
|
||||
from . import serializers
|
||||
from .tasks import update_assets_hardware_info
|
||||
from .utils import test_admin_user_connective_manual
|
||||
from .tasks import update_assets_hardware_info, test_admin_user_connectability_manual
|
||||
|
||||
|
||||
class AssetViewSet(IDInFilterMixin, BulkModelViewSet):
|
||||
"""API endpoint that allows Asset to be viewed or edited."""
|
||||
"""
|
||||
API endpoint that allows Asset to be viewed or edited.
|
||||
"""
|
||||
queryset = Asset.objects.all()
|
||||
serializer_class = serializers.AssetSerializer
|
||||
permission_classes = (IsValidUser,)
|
||||
|
@ -195,7 +196,7 @@ class AssetAdminUserTestView(AssetRefreshHardwareView):
|
|||
def retrieve(self, request, *args, **kwargs):
|
||||
asset_id = kwargs.get('pk')
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
result = test_admin_user_connective_manual([asset])
|
||||
result = test_admin_user_connectability_manual(asset)
|
||||
if result:
|
||||
return Response('1')
|
||||
else:
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
|
@ -1,8 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
print("Import assets model")
|
||||
|
||||
from .user import AdminUser, SystemUser
|
||||
from .cluster import *
|
||||
from .group import *
|
||||
|
|
|
@ -7,6 +7,7 @@ import uuid
|
|||
from django.db import models
|
||||
import logging
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.cache import cache
|
||||
|
||||
from .cluster import Cluster
|
||||
from .group import AssetGroup
|
||||
|
@ -21,6 +22,7 @@ def get_default_cluster():
|
|||
|
||||
|
||||
class Asset(models.Model):
|
||||
# Todo: Move them to settings
|
||||
STATUS_CHOICES = (
|
||||
('In use', _('In use')),
|
||||
('Out of use', _('Out of use')),
|
||||
|
@ -103,6 +105,9 @@ class Asset(models.Model):
|
|||
'groups': [group.name for group in self.groups.all()],
|
||||
}
|
||||
|
||||
def is_connective(self):
|
||||
return cache.get(self.hostname)
|
||||
|
||||
def _to_secret_json(self):
|
||||
"""
|
||||
Ansible use it create inventory
|
||||
|
|
|
@ -19,7 +19,6 @@ logger = logging.getLogger(__name__)
|
|||
class AssetGroup(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=64, unique=True, verbose_name=_('Name'))
|
||||
system_users = models.ManyToManyField(SystemUser, related_name='asset_groups', blank=True)
|
||||
created_by = models.CharField(max_length=32, blank=True, verbose_name=_('Created by'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, null=True, verbose_name=_('Date created'))
|
||||
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
||||
|
|
|
@ -201,21 +201,6 @@ class SystemUser(models.Model):
|
|||
def public_key(self, public_key_raw):
|
||||
self._public_key = signer.sign(public_key_raw)
|
||||
|
||||
def get_assets_inherit_from_asset_groups(self):
|
||||
assets = set()
|
||||
asset_groups = self.asset_groups.all()
|
||||
for asset_group in asset_groups:
|
||||
for asset in asset_group.assets.all():
|
||||
setattr(asset, 'is_inherit_from_asset_groups', True)
|
||||
setattr(asset, 'inherit_from_asset_groups',
|
||||
getattr(asset, 'inherit_from_asset_groups', set()).add(asset_group))
|
||||
assets.add(asset)
|
||||
return assets
|
||||
|
||||
def get_assets(self):
|
||||
assets = set(self.assets.all()) | self.get_assets_inherit_from_asset_groups()
|
||||
return list(assets)
|
||||
|
||||
def _to_secret_json(self):
|
||||
"""Push system user use it"""
|
||||
return {
|
||||
|
@ -232,10 +217,6 @@ class SystemUser(models.Model):
|
|||
def assets_amount(self):
|
||||
return self.assets.count()
|
||||
|
||||
@property
|
||||
def asset_group_amount(self):
|
||||
return self.asset_groups.count()
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.cache import cache
|
||||
from rest_framework import viewsets, serializers, generics
|
||||
from .models import AssetGroup, Asset, Cluster, AdminUser, SystemUser
|
||||
from common.mixins import IDInFilterMixin
|
||||
from rest_framework_bulk import BulkListSerializer, BulkSerializerMixin
|
||||
|
||||
from .models import AssetGroup, Asset, Cluster, AdminUser, SystemUser
|
||||
from .tasks import SYSTEM_USER_CONN_CACHE_KEY_PREFIX, ADMIN_USER_CONN_CACHE_KEY_PREFIX
|
||||
|
||||
|
||||
class AssetGroupSerializer(BulkSerializerMixin, serializers.ModelSerializer):
|
||||
assets_amount = serializers.SerializerMethodField()
|
||||
|
@ -64,11 +64,20 @@ class ClusterUpdateAssetsSerializer(serializers.ModelSerializer):
|
|||
|
||||
class AdminUserSerializer(serializers.ModelSerializer):
|
||||
assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all())
|
||||
unreachable_amount = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = AdminUser
|
||||
fields = '__all__'
|
||||
|
||||
@staticmethod
|
||||
def get_unreachable_amount(obj):
|
||||
data = cache.get(ADMIN_USER_CONN_CACHE_KEY_PREFIX + obj.name)
|
||||
if data:
|
||||
return len(data.get('dark'))
|
||||
else:
|
||||
return 'Unknown'
|
||||
|
||||
def get_field_names(self, declared_fields, info):
|
||||
fields = super(AdminUserSerializer, self).get_field_names(declared_fields, info)
|
||||
fields.append('assets_amount')
|
||||
|
@ -76,10 +85,20 @@ class AdminUserSerializer(serializers.ModelSerializer):
|
|||
|
||||
|
||||
class SystemUserSerializer(serializers.ModelSerializer):
|
||||
unreachable_amount = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
exclude = ('_password', '_private_key', '_public_key')
|
||||
|
||||
@staticmethod
|
||||
def get_unreachable_amount(obj):
|
||||
data = cache.get(SYSTEM_USER_CONN_CACHE_KEY_PREFIX + obj.name)
|
||||
if data:
|
||||
return len(data.get('dark'))
|
||||
else:
|
||||
return "Unknown"
|
||||
|
||||
def get_field_names(self, declared_fields, info):
|
||||
fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info)
|
||||
fields.extend(['assets_amount'])
|
||||
|
@ -167,8 +186,7 @@ class AssetGrantedSerializer(serializers.ModelSerializer):
|
|||
|
||||
@staticmethod
|
||||
def get_system_users_join(obj):
|
||||
return ', '.join([system_user.username
|
||||
for system_user in obj.system_users_granted])
|
||||
return ', '.join([system_user.username for system_user in obj.system_users_granted])
|
||||
|
||||
|
||||
class MyAssetGrantedSerializer(AssetGrantedSerializer):
|
||||
|
|
|
@ -1,23 +1,45 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
from celery import shared_task
|
||||
import json
|
||||
|
||||
from celery import shared_task
|
||||
from django.core.cache import cache
|
||||
|
||||
from ops.tasks import run_AdHoc
|
||||
from common.utils import get_object_or_none, capacity_convert, sum_capacity
|
||||
from assets.models import SystemUser, AdminUser
|
||||
from common.utils import get_object_or_none, capacity_convert, sum_capacity, encrypt_password, get_logger
|
||||
from .models import Asset
|
||||
|
||||
|
||||
FORKS = 10
|
||||
TIMEOUT = 60
|
||||
logger = get_logger(__file__)
|
||||
ADMIN_USER_CONN_CACHE_KEY_PREFIX = "ADMIN_USER_CONN_"
|
||||
SYSTEM_USER_CONN_CACHE_KEY_PREFIX = 'SYSTEM_USER_CONN_'
|
||||
|
||||
|
||||
@shared_task
|
||||
def update_assets_hardware_info(assets):
|
||||
task_tuple = (
|
||||
('setup', ''),
|
||||
)
|
||||
summary, result = run_AdHoc(task_tuple, assets, record=False)
|
||||
for hostname, info in result['contacted'].items():
|
||||
"""
|
||||
Using ansible api to update asset hardware info
|
||||
:param assets: asset seq
|
||||
:return: result summary ['contacted': {}, 'dark': {}]
|
||||
"""
|
||||
from ops.utils import run_adhoc
|
||||
name = "GET_ASSETS_HARDWARE_INFO"
|
||||
tasks = [
|
||||
{
|
||||
'name': name,
|
||||
'action': {
|
||||
'module': 'setup'
|
||||
}
|
||||
}
|
||||
]
|
||||
hostname_list = [asset.hostname for asset in assets]
|
||||
result = run_adhoc(hostname_list, pattern='all', tasks=tasks,
|
||||
name=name, run_as_admin=True)
|
||||
summary, result_raw = result.results_summary, result.results_raw
|
||||
for hostname, info in result_raw['ok'].items():
|
||||
if info:
|
||||
info = info[0]['ansible_facts']
|
||||
info = info[name]['ansible_facts']
|
||||
else:
|
||||
continue
|
||||
asset = get_object_or_none(Asset, hostname=hostname)
|
||||
|
@ -58,23 +80,193 @@ def update_assets_hardware_info(assets):
|
|||
|
||||
@shared_task
|
||||
def update_assets_hardware_period():
|
||||
"""
|
||||
Update asset hardware period task
|
||||
:return:
|
||||
"""
|
||||
assets = Asset.objects.filter(type__in=['Server', 'VM'])
|
||||
update_assets_hardware_info(assets)
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_admin_user_connective_period():
|
||||
assets = Asset.objects.filter(type__in=['Server', 'VM'])
|
||||
task_tuple = (
|
||||
('ping', ''),
|
||||
)
|
||||
summary, _ = run_AdHoc(task_tuple, assets, record=False)
|
||||
for i in summary['success']:
|
||||
cache.set(i, '1', 2*60*60*60)
|
||||
|
||||
for i, msg in summary['failed']:
|
||||
cache.set(i, '0', 60*60*60)
|
||||
return summary
|
||||
def test_admin_user_connectability(admin_user):
|
||||
"""
|
||||
Test asset admin user can connect or not. Using ansible api do that
|
||||
:param admin_user:
|
||||
:return:
|
||||
"""
|
||||
from ops.utils import run_adhoc
|
||||
assets = admin_user.assets.all()
|
||||
# assets = Asset.objects.filter(type__in=['Server', 'VM'])
|
||||
hosts = [asset.hostname for asset in assets]
|
||||
tasks = [
|
||||
{
|
||||
"name": "TEST_ADMIN_CONNECTIVE",
|
||||
"action": {
|
||||
"module": "ping",
|
||||
}
|
||||
}
|
||||
]
|
||||
result = run_adhoc(hosts, tasks=tasks, pattern="all", run_as_admin=True)
|
||||
return result.results_summary
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_admin_user_connectability_period():
|
||||
# assets = Asset.objects.filter(type__in=['Server', 'VM'])
|
||||
admin_users = AdminUser.objects.all()
|
||||
for admin_user in admin_users:
|
||||
summary = test_admin_user_connectability(admin_user)
|
||||
|
||||
cache.set(ADMIN_USER_CONN_CACHE_KEY_PREFIX + admin_user.name, summary, 60*60*60)
|
||||
for i in summary['contacted']:
|
||||
cache.set(ADMIN_USER_CONN_CACHE_KEY_PREFIX + i, 1, 60*60*60)
|
||||
|
||||
for i in summary['dark']:
|
||||
cache.set(ADMIN_USER_CONN_CACHE_KEY_PREFIX + i, 0, 60*60*60)
|
||||
|
||||
|
||||
def test_admin_user_connectability_manual(asset):
|
||||
from ops.utils import run_adhoc
|
||||
# assets = Asset.objects.filter(type__in=['Server', 'VM'])
|
||||
hosts = [asset.hostname]
|
||||
tasks = [
|
||||
{
|
||||
"name": "TEST_ADMIN_CONNECTIVE",
|
||||
"action": {
|
||||
"module": "ping",
|
||||
}
|
||||
}
|
||||
]
|
||||
result = run_adhoc(hosts, tasks=tasks, pattern="all", run_as_admin=True)
|
||||
if result.results_summary['dark']:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_system_user_connectability(system_user):
|
||||
"""
|
||||
Test system cant connect his assets or not.
|
||||
:param system_user:
|
||||
:return:
|
||||
"""
|
||||
from ops.utils import run_adhoc
|
||||
assets = system_user.assets.all()
|
||||
hosts = [asset.hostname for asset in assets]
|
||||
tasks = [
|
||||
{
|
||||
"name": "TEST_SYSTEM_USER_CONNECTIVE",
|
||||
"action": {
|
||||
"module": "ping",
|
||||
}
|
||||
}
|
||||
]
|
||||
result = run_adhoc(hosts, tasks=tasks, pattern="all", run_as=system_user.name)
|
||||
return result.results_summary
|
||||
|
||||
|
||||
@shared_task
|
||||
def test_system_user_connectability_period():
|
||||
for system_user in SystemUser.objects.all():
|
||||
summary = test_system_user_connectability(system_user)
|
||||
cache.set(SYSTEM_USER_CONN_CACHE_KEY_PREFIX + system_user.name , summary, 60*60*60)
|
||||
|
||||
|
||||
def get_push_system_user_tasks(system_user):
|
||||
tasks = [
|
||||
{
|
||||
'name': 'Add user',
|
||||
'action': {
|
||||
'module': 'user',
|
||||
'args': 'name={} shell={} state=present password={}'.format(
|
||||
system_user.username, system_user.shell,
|
||||
encrypt_password(system_user.password),
|
||||
),
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Set authorized key',
|
||||
'action': {
|
||||
'module': 'authorized_key',
|
||||
'args': "user={} state=present key='{}'".format(
|
||||
system_user.username, system_user.public_key
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Set sudoers',
|
||||
'action': {
|
||||
'module': 'lineinfile',
|
||||
'args': "dest=/etc/sudoers state=present regexp='^{0} ALL=' "
|
||||
"line='{0} ALL=(ALL) NOPASSWD: {1}' "
|
||||
"validate='visudo -cf %s'".format(
|
||||
system_user.username,
|
||||
system_user.sudo,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
return tasks
|
||||
|
||||
|
||||
PUSH_SYSTEM_USER_PERIOD_TASK_NAME = 'PUSH SYSTEM USER {} PERIOD...'
|
||||
PUSH_SYSTEM_USER_TASK_NAME = 'PUSH SYSTEM USER {} ASSETS'
|
||||
|
||||
|
||||
def get_push_system_user_task(system_user):
|
||||
from ops.utils import get_task_by_name
|
||||
task = get_task_by_name(PUSH_SYSTEM_USER_PERIOD_TASK_NAME.format(system_user.name))
|
||||
return task
|
||||
|
||||
|
||||
def push_system_user(system_user, assets, name):
|
||||
from ops.utils import get_task_by_name, run_adhoc_object, \
|
||||
create_task, create_adhoc
|
||||
|
||||
if system_user.auto_push and assets:
|
||||
task = get_task_by_name(name)
|
||||
if not task:
|
||||
task = create_task(name, created_by="System")
|
||||
task.save()
|
||||
tasks = get_push_system_user_tasks(system_user)
|
||||
hosts = [asset.hostname for asset in assets]
|
||||
options = {'forks': FORKS, 'timeout': TIMEOUT}
|
||||
|
||||
adhoc = task.get_latest_adhoc()
|
||||
if not adhoc or adhoc.task != tasks or adhoc.hosts != hosts:
|
||||
adhoc = create_adhoc(task=task, tasks=tasks, pattern='all',
|
||||
options=options, hosts=hosts, run_as_admin=True)
|
||||
return run_adhoc_object(adhoc)
|
||||
|
||||
|
||||
@shared_task
|
||||
def push_system_user_period():
|
||||
logger.debug("Push system user period")
|
||||
for s in SystemUser.objects.filter(auto_push=True):
|
||||
assets = s.assets.all()
|
||||
|
||||
name = PUSH_SYSTEM_USER_PERIOD_TASK_NAME.format(s.name)
|
||||
push_system_user(s, assets, name)
|
||||
|
||||
|
||||
def push_system_user_to_assets_if_need(system_user, assets=None, asset_groups=None):
|
||||
assets_to_push = []
|
||||
system_user_assets = system_user.assets.all()
|
||||
if assets:
|
||||
assets_to_push.extend(assets)
|
||||
if asset_groups:
|
||||
for group in asset_groups:
|
||||
assets_to_push.extend(group.assets.all())
|
||||
|
||||
assets_need_push = set(assets_to_push) - set(system_user_assets)
|
||||
if not assets_need_push:
|
||||
return
|
||||
logger.debug("Push system user {} to {} assets".format(
|
||||
system_user.name, ', '.join([asset.hostname for asset in assets_need_push])
|
||||
))
|
||||
result = push_system_user(system_user, assets_need_push, PUSH_SYSTEM_USER_TASK_NAME)
|
||||
system_user.assets.add(*tuple(assets_need_push))
|
||||
return result
|
||||
|
|
|
@ -0,0 +1,387 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% 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>
|
||||
{% 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>
|
||||
<a href="{% url 'assets:admin-user-detail' pk=admin_user.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
|
||||
</li>
|
||||
<li class="active">
|
||||
<a href="{% url 'assets:admin-user-assets' pk=admin_user.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Assets list' %} </a>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-default" href="{% url 'assets:admin-user-update' pk=admin_user.id %}"><i class="fa fa-edit"></i>Update</a>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-danger btn-delete-admin-user">
|
||||
<i class="fa fa-edit"></i>Delete
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-8" style="padding-left: 0;">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span style="float: left">{% trans 'Asset list of ' %} <b>{{ admin_user.name }}</b> <span class="badge"> {{ total_amount }}</span> <span class="badge badge-danger">{{ unreachable_amount }}</span></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 table-hover" id="system_user_liste">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans 'Hostname' %}</th>
|
||||
<th>{% trans 'IP' %}</th>
|
||||
<th>{% trans 'Port' %}</th>
|
||||
<th>{% trans 'Alive' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for asset in page_obj %}
|
||||
<tr>
|
||||
<td>{{ asset.hostname }}</td>
|
||||
<td>{{ asset.ip }}</td>
|
||||
<td>{{ asset.port }}</td>
|
||||
{% if asset.is_connective == '1' %}
|
||||
<td>
|
||||
<i class="fa fa-check text-navy"></i>
|
||||
</td>
|
||||
{% else %}
|
||||
<td>
|
||||
<i class="fa fa-times text-danger"></i>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="row">
|
||||
{% include '_pagination.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4" 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 update' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<td width="50%">{% trans 'Retest connectivity' %}:</td>
|
||||
<td>
|
||||
<span style="float: right">
|
||||
<button type="button" class="btn btn-primary btn-xs" style="width: 54px">{% trans 'Start' %}</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 'Replace asset admin user with this' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<form>
|
||||
<tr class="no-borders-tr">
|
||||
<td colspan="2">
|
||||
<select data-placeholder="{% trans 'Select asset' %}" class="select2" style="width: 100%" multiple="" tabindex="4">
|
||||
{% for asset in assets_remain %}
|
||||
<option value="{{ asset.id }}">{{ asset.ip }}:{{ asset.port }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="no-borders-tr">
|
||||
<td colspan="2">
|
||||
<button type="button" class="btn btn-info btn-sm btn-replace-asset-admin_user">{% trans 'Replace' %}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</form>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-warning">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Replace asset group admin user with this' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<form>
|
||||
<tr class="no-borders-tr">
|
||||
<td colspan="2">
|
||||
<select data-placeholder="{% trans 'Select asset groups' %}" class="select2" style="width: 100%" multiple="" tabindex="4">
|
||||
{% for asset_group in asset_groups %}
|
||||
<option value="{{ asset_group.id }}">{{ asset_group.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="no-borders-tr">
|
||||
<td colspan="2">
|
||||
<button type="button" class="btn btn-warning btn-sm btn-replace-asset_groups-admin_user">{% trans 'Replace' %}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</form>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
Array.prototype.remove = function(val) {
|
||||
var index = this.indexOf(val);
|
||||
if (index > -1) {
|
||||
this.splice(index, 1);
|
||||
}
|
||||
};
|
||||
Array.prototype.unique = function(){
|
||||
var res = [];
|
||||
var json = {};
|
||||
for(var i = 0; i < this.length; i++){
|
||||
if(!json[this[i]]){
|
||||
res.push(this[i]);
|
||||
json[this[i]] = 1;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
function objectRemove(obj, name, url, data) {
|
||||
function doRemove() {
|
||||
var body = data;
|
||||
var success = function() {
|
||||
swal('Remove!', "[ "+name+"]"+" has been deleted ", "success");
|
||||
$(obj).parent().parent().remove();
|
||||
};
|
||||
var fail = function() {
|
||||
swal("Failed", "Remove"+"[ "+name+" ]"+"failed", "error");
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: url,
|
||||
body: JSON.stringify(body),
|
||||
method: 'PATCH',
|
||||
success: success,
|
||||
error: fail
|
||||
});
|
||||
}
|
||||
swal({
|
||||
title: 'Are you sure remove ?',
|
||||
text: " [" + name + "] ",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonText: 'Cancel',
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: 'Confirm',
|
||||
closeOnConfirm: false
|
||||
}, function () {
|
||||
doRemove()
|
||||
});
|
||||
}
|
||||
jumpserver.assets_selected = {};
|
||||
jumpserver.asset_groups_selected = {};
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2()
|
||||
.on("select2:select", function (evt) {
|
||||
var data = evt.params.data;
|
||||
jumpserver.assets_selected[data.id] = data.text;
|
||||
jumpserver.asset_groups_selected[data.id] = data.text;
|
||||
})
|
||||
.on('select2:unselect', function(evt) {
|
||||
var data = evt.params.data;
|
||||
delete jumpserver.assets_selected[data.id];
|
||||
delete jumpserver.asset_groups_selected[data.id]
|
||||
});
|
||||
var options = {
|
||||
ele: $('#system_user_assets_table'),
|
||||
buttons: [],
|
||||
order: [],
|
||||
columnDefs: [
|
||||
{targets: 0, createdCell: function (td, cellData, rowData) {
|
||||
var detail_btn = '<a href="{% url "assets:asset-detail" pk=99991937 %}" data-aid="'+rowData.id+'">' + cellData + '</a>';
|
||||
$(td).html(detail_btn.replace('99991937', rowData.id));
|
||||
}},
|
||||
{targets: 3, 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: 4, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = '<a href="{% url "assets:asset-update" pk=99991937 %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace('99991937', rowData.id);
|
||||
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_asset_remove" data-aid="99991937">{% trans "Remove" %}</a>'.replace('99991937', rowData.id);
|
||||
$(td).html(update_btn + del_btn)
|
||||
}}
|
||||
|
||||
],
|
||||
ajax_url: '{% url "api-assets:asset-list" %}?admin_user_id={{ admin_user.id }}',
|
||||
columns: [{data: "hostname" }, {data: "ip" }, {data: "port" },
|
||||
{data: "is_active" }, {data: "id"}],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
jumpserver.initDataTable(options);
|
||||
|
||||
function adminUserDelete(name, url) {
|
||||
function doDelete() {
|
||||
var body = {};
|
||||
var success = function() {
|
||||
swal('Deleted!', "[ "+name+"]"+" has been deleted ", "success");
|
||||
window.location.href="{% url 'assets:cluster-list' %}";
|
||||
};
|
||||
var fail = function() {
|
||||
swal("Failed", "Delete"+"[ "+name+" ]"+"failed", "error");
|
||||
};
|
||||
APIUpdateAttr({
|
||||
url: url,
|
||||
body: JSON.stringify(body),
|
||||
method: 'DELETE',
|
||||
success: success,
|
||||
error: fail
|
||||
});
|
||||
}
|
||||
swal({
|
||||
title: 'Are you sure delete ?',
|
||||
text: " [" + name + "] ",
|
||||
type: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonText: 'Cancel',
|
||||
confirmButtonColor: "#DD6B55",
|
||||
confirmButtonText: 'Confirm',
|
||||
closeOnConfirm: false
|
||||
}, function () {
|
||||
doDelete()
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
.on('click', '.btn-replace-asset-admin_user', function () {
|
||||
if (Object.keys(jumpserver.assets_selected).length === 0) {
|
||||
return false;
|
||||
}
|
||||
jumpserver.asset_groups_selected = {};
|
||||
var $data_table = $("#system_user_assets_table").DataTable();
|
||||
var assets = [];
|
||||
$.map(jumpserver.assets_selected, function(value, index) {
|
||||
assets.push(parseInt(index));
|
||||
});
|
||||
assets.unique();
|
||||
var data = [];
|
||||
var admin_user_id = "{{ admin_user.id }}";
|
||||
var the_url = '{% url "api-assets:asset-list" %}';
|
||||
for (var i=0; i<assets.length; i++) {
|
||||
data.push({"id": assets[i], "admin_user": admin_user_id});
|
||||
}
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(data),
|
||||
method: 'PATCH'
|
||||
});
|
||||
$data_table.ajax.reload();
|
||||
})
|
||||
|
||||
.on('click', '.btn-replace-asset_groups-admin_user', function () {
|
||||
if (Object.keys(jumpserver.asset_groups_selected).length === 0) {
|
||||
return false;
|
||||
}
|
||||
jumpserver.assets_selected = {};
|
||||
var $data_table = $("#system_user_assets_table").DataTable();
|
||||
var asset_groups = [];
|
||||
var assets = [];
|
||||
var data = [];
|
||||
var the_url = '{% url "api-assets:asset-list" %}';
|
||||
$.map(jumpserver.asset_groups_selected, function(value, index) {
|
||||
asset_groups.push(parseInt(index));
|
||||
});
|
||||
$.ajax({
|
||||
url: '{% url "api-assets:asset-group-list" %}?id__in=['+asset_groups.join(',')+']',
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
success: function (result) {
|
||||
for (var i=0; i<result.length; i++) {
|
||||
for (var j=0; j<result[i]['assets'].length; j++) {
|
||||
assets.push(result[i]['assets'][j])
|
||||
}
|
||||
}
|
||||
for (var z=0; z<assets.length; z++) {
|
||||
data.push({"id":assets[z], "admin_user":"{{admin_user.id}}" });
|
||||
}
|
||||
APIUpdateAttr({
|
||||
url: the_url,
|
||||
body: JSON.stringify(data),
|
||||
method: 'PATCH'
|
||||
});
|
||||
$data_table.ajax.reload();
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
.on('click', '.btn_asset_remove', function () {
|
||||
var $this = $(this);
|
||||
var the_url = "{% url 'api-assets:admin-user-detail' pk=admin_user.id %}";
|
||||
var name = $(this).closest("tr").find(":nth-child(1) > a").html();
|
||||
var assets = [];
|
||||
var delete_asset_id = $(this).data('aid');
|
||||
$.ajax({
|
||||
url: the_url,
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
success: function (result) {
|
||||
for (var i=0; i<result['assets'].length; i++) {
|
||||
assets.push(result['assets'][i])
|
||||
}
|
||||
assets.remove(delete_asset_id);
|
||||
var data = {"assets": assets};
|
||||
objectRemove($this, name, the_url, data);
|
||||
}
|
||||
})
|
||||
}).on('click', '.btn-delete-admin-user', function () {
|
||||
var $this = $(this);
|
||||
var name = "{{ admin_user.name }}";
|
||||
var uid = "{{ admin_user.id }}";
|
||||
var the_url = '{% url "api-assets:admin-user-detail" pk=99991937 %}'.replace('99991937', uid);
|
||||
var redirect_url = "{% url 'assets:admin-user-list' %}";
|
||||
objectDelete($this, name, the_url, redirect_url);
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -15,7 +15,10 @@
|
|||
<div class="panel-options">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active">
|
||||
<a href="" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
|
||||
<a href="{% url 'assets:admin-user-detail' pk=admin_user.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'assets:admin-user-assets' pk=admin_user.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Assets list' %} </a>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<a class="btn btn-outline btn-default" href="{% url 'assets:admin-user-update' pk=admin_user.id %}"><i class="fa fa-edit"></i>Update</a>
|
||||
|
@ -73,51 +76,6 @@
|
|||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span style="float: left">{% trans 'Asset list of ' %} <b>{{ admin_user.name }}</b> <span class="badge"> {{ paginator.count }}</span></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 table-hover" id="system_user_assets_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans 'Hostname' %}</th>
|
||||
<th>{% trans 'IP' %}</th>
|
||||
<th>{% trans 'Port' %}</th>
|
||||
<th>{% trans 'Alive' %}</th>
|
||||
<th>{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{# {% for asset in page_obj %}#}
|
||||
{# <tr>#}
|
||||
{# <td>{{ asset.hostname }}</td>#}
|
||||
{# <td>{{ asset.ip }}</td>#}
|
||||
{# <td>{{ asset.port }}</td>#}
|
||||
{# <td>Alive</td>#}
|
||||
{# </tr>#}
|
||||
{# {% endfor %}#}
|
||||
</tbody>
|
||||
</table>
|
||||
{# <div class="row">#}
|
||||
{# {% include '_pagination.html' %}#}
|
||||
{# </div>#}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-5" style="padding-left: 0;padding-right: 0">
|
||||
<div class="panel panel-primary">
|
||||
|
@ -128,24 +86,6 @@
|
|||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<td width="50%">{% trans 'Get install script' %}:</td>
|
||||
<td>
|
||||
<span style="float: right">
|
||||
<button type="button" class="btn btn-primary btn-xs" style="width: 54px">{% trans 'Get' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="50%">{% trans 'Retest asset connectivity' %}:</td>
|
||||
<td>
|
||||
<span style="float: right">
|
||||
<button type="button" class="btn btn-primary btn-xs" style="width: 54px">{% trans 'Start' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td width="50%">{% trans 'Reset private key' %}:</td>
|
||||
<td>
|
||||
<span style="float: right">
|
||||
|
@ -153,62 +93,14 @@
|
|||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Replace asset admin user with this' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<form>
|
||||
<tr class="no-borders-tr">
|
||||
<td colspan="2">
|
||||
<select data-placeholder="{% trans 'Select asset' %}" class="select2" style="width: 100%" multiple="" tabindex="4">
|
||||
{% for asset in assets_remain %}
|
||||
<option value="{{ asset.id }}">{{ asset.ip }}:{{ asset.port }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="no-borders-tr">
|
||||
<td colspan="2">
|
||||
<button type="button" class="btn btn-info btn-sm btn-replace-asset-admin_user">{% trans 'Replace' %}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</form>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-warning">
|
||||
<div class="panel-heading">
|
||||
<i class="fa fa-info-circle"></i> {% trans 'Replace asset admin user with this admin user' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<form>
|
||||
<tr class="no-borders-tr">
|
||||
<td colspan="2">
|
||||
<select data-placeholder="{% trans 'Select asset groups' %}" class="select2" style="width: 100%" multiple="" tabindex="4">
|
||||
{% for asset_group in asset_groups %}
|
||||
<option value="{{ asset_group.id }}">{{ asset_group.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="no-borders-tr">
|
||||
<td colspan="2">
|
||||
<button type="button" class="btn btn-warning btn-sm btn-replace-asset_groups-admin_user">{% trans 'Replace' %}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</form>
|
||||
<tr>
|
||||
<td width="50%">{% trans 'Reset password' %}:</td>
|
||||
<td>
|
||||
<span style="float: right">
|
||||
<button type="button" class="btn btn-primary btn-xs" style="width: 54px">{% trans 'Reset' %}</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -361,7 +253,7 @@ $(document).ready(function () {
|
|||
});
|
||||
assets.unique();
|
||||
var data = [];
|
||||
var admin_user_id = {{ admin_user.id }};
|
||||
var admin_user_id = "{{ admin_user.id }}";
|
||||
var the_url = '{% url "api-assets:asset-list" %}';
|
||||
for (var i=0; i<assets.length; i++) {
|
||||
data.push({"id": assets[i], "admin_user": admin_user_id});
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
<th class="text-center">{% trans 'Name' %}</th>
|
||||
<th class="text-center">{% trans 'Username' %}</th>
|
||||
<th class="text-center">{% trans 'Asset num' %}</th>
|
||||
<th class="text-center">{% trans 'Unreachable' %}</th>
|
||||
<th class="text-center">{% trans 'Comment' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
</tr>
|
||||
|
@ -34,11 +35,7 @@ $(document).ready(function(){
|
|||
var detail_btn = '<a href="{% url "assets:admin-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, 24) + '...': cellData;
|
||||
$(td).html('<a href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</a>');
|
||||
}},
|
||||
{targets: 5, createdCell: function (td, cellData, rowData) {
|
||||
{targets: 6, createdCell: function (td, cellData, rowData) {
|
||||
{# var script_btn = '<a href="{% url "assets:admin-user-update" pk=99991937 %}" class="btn btn-xs btn-primary">{% trans "Script" %}</a>'.replace('99991937', cellData);#}
|
||||
var update_btn = '<a href="{% url "assets:admin-user-update" pk=99991937 %}" class="btn btn-xs m-l-xs btn-info">{% trans "Update" %}</a>'.replace('99991937', cellData);
|
||||
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_admin_user_delete" data-uid="99991937">{% trans "Delete" %}</a>'.replace('99991937', cellData);
|
||||
|
@ -47,7 +44,7 @@ $(document).ready(function(){
|
|||
}}],
|
||||
ajax_url: '{% url "api-assets:admin-user-list" %}',
|
||||
columns: [{data: function(){return ""}}, {data: "name" }, {data: "username" }, {data: "assets_amount" },
|
||||
{data: "comment" }, {data: "id" }],
|
||||
{data: "unreachable_amount"}, {data: "comment" }, {data: "id" }]
|
||||
};
|
||||
jumpserver.initDataTable(options);
|
||||
})
|
||||
|
|
|
@ -58,18 +58,16 @@
|
|||
<td>{{ asset.ip }}</td>
|
||||
<td>{{ asset.port }}</td>
|
||||
<td>
|
||||
|
||||
<i class="fa fa-check text-navy"></i>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-danger pull-right btn-xs {% if asset.is_inherit_from_asset_groups %} disabled {% endif %}" type="button"><i class="fa fa-minus"></i></button>
|
||||
<button class="btn btn-danger pull-right btn-xs" type="button"><i class="fa fa-minus"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{# <div class="row">#}
|
||||
{# {% include '_pagination.html' %}#}
|
||||
{# </div>#}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -60,7 +60,7 @@ $(document).ready(function(){
|
|||
$(td).html(update_btn + del_btn)
|
||||
}}],
|
||||
ajax_url: '{% url "api-assets:system-user-list" %}',
|
||||
columns: [{data: "id" }, {data: "name" }, {data: "username" }, {data: "assets_amount" }, {data: function () { return "3"}},
|
||||
columns: [{data: "id" }, {data: "name" }, {data: "username" }, {data: "assets_amount" }, {data: "unreachable_amount"},
|
||||
{data: "comment" }, {data: "id" }],
|
||||
op_html: $('#actions').html()
|
||||
};
|
||||
|
|
|
@ -41,6 +41,7 @@ urlpatterns = [
|
|||
url(r'^admin-user/(?P<pk>[0-9a-zA-Z\-]+)/$', views.AdminUserDetailView.as_view(), name='admin-user-detail'),
|
||||
url(r'^admin-user/(?P<pk>[0-9a-zA-Z\-]+)/update/$', views.AdminUserUpdateView.as_view(), name='admin-user-update'),
|
||||
url(r'^admin-user/(?P<pk>[0-9a-zA-Z\-]+)/delete/$', views.AdminUserDeleteView.as_view(), name='admin-user-delete'),
|
||||
url(r'^admin-user/(?P<pk>[0-9a-zA-Z\-]+)/assets/$', views.AdminUserAssetsView.as_view(), name='admin-user-assets'),
|
||||
|
||||
# Resource system user url
|
||||
url(r'^system-user/$', views.SystemUserListView.as_view(), name='system-user-list'),
|
||||
|
|
|
@ -1,20 +1,7 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
#
|
||||
from .models import Asset
|
||||
|
||||
|
||||
def test_admin_user_connective_manual(asset):
|
||||
from ops.utils import run_AdHoc
|
||||
if not isinstance(asset, list):
|
||||
asset = [asset]
|
||||
task_tuple = (
|
||||
('ping', ''),
|
||||
)
|
||||
summary, _ = run_AdHoc(task_tuple, asset, record=False)
|
||||
if len(summary['failed']) != 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
from common.utils import get_object_or_none
|
||||
from .models import Asset, SystemUser
|
||||
|
||||
|
||||
def get_assets_by_id_list(id_list):
|
||||
|
@ -25,26 +12,6 @@ def get_assets_by_hostname_list(hostname_list):
|
|||
return Asset.objects.filter(hostname__in=hostname_list)
|
||||
|
||||
|
||||
def get_asset_admin_user(user, asset):
|
||||
if user.is_superuser:
|
||||
return asset.admin_user
|
||||
else:
|
||||
msg = "{} have no permission for admin user".format(user.username)
|
||||
raise PermissionError(msg)
|
||||
|
||||
|
||||
def get_asset_system_user(user, asset, system_user_name):
|
||||
from perms.utils import get_user_granted_assets
|
||||
assets = get_user_granted_assets(user)
|
||||
system_users = {system_user.name: system_user for system_user in assets.get(asset)}
|
||||
|
||||
if system_user_name in system_users:
|
||||
return system_users[system_user_name]
|
||||
else:
|
||||
msg = "{} have no permission for {}".format(user.name, system_user_name)
|
||||
raise PermissionError(msg)
|
||||
|
||||
|
||||
def get_assets_with_admin_by_hostname_list(hostname_list):
|
||||
assets = Asset.objects.filter(hostname__in=hostname_list)
|
||||
return [(asset, asset.admin_user) for asset in assets]
|
||||
def get_system_user_by_name(name):
|
||||
system_user = get_object_or_none(SystemUser, name=name)
|
||||
return system_user
|
||||
|
|
|
@ -14,7 +14,7 @@ from ..hands import AdminUserRequiredMixin
|
|||
|
||||
__all__ = ['AdminUserCreateView', 'AdminUserDetailView',
|
||||
'AdminUserDeleteView', 'AdminUserListView',
|
||||
'AdminUserUpdateView',
|
||||
'AdminUserUpdateView', 'AdminUserAssetsView',
|
||||
]
|
||||
|
||||
|
||||
|
@ -104,6 +104,31 @@ class AdminUserDetailView(AdminUserRequiredMixin, SingleObjectMixin, ListView):
|
|||
return super(AdminUserDetailView, self).get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AdminUserAssetsView(AdminUserRequiredMixin, SingleObjectMixin, ListView):
|
||||
paginate_by = settings.CONFIG.DISPLAY_PER_PAGE
|
||||
template_name = 'assets/admin_user_assets.html'
|
||||
context_object_name = 'admin_user'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object(queryset=AdminUser.objects.all())
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
self.queryset = self.object.assets.all()
|
||||
sorted(self.queryset, key=lambda x: x.is_connective() is False)
|
||||
return self.queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': 'assets',
|
||||
'action': 'Admin user detail',
|
||||
"total_amount": len(self.queryset),
|
||||
'unreachable_amount': len([asset for asset in self.queryset if asset.is_connective() is False])
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class AdminUserDeleteView(AdminUserRequiredMixin, DeleteView):
|
||||
model = AdminUser
|
||||
template_name = 'assets/delete_confirm.html'
|
||||
|
|
|
@ -117,25 +117,18 @@ class SystemUserAssetView(AdminUserRequiredMixin, SingleObjectMixin, ListView):
|
|||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object(queryset=SystemUser.objects.all())
|
||||
return super(SystemUserAssetView, self).get(request, *args, **kwargs)
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_asset_groups(self):
|
||||
return self.object.asset_groups.all()
|
||||
|
||||
# Todo: queryset default order by connectivity, need ops support
|
||||
def get_queryset(self):
|
||||
return list(self.object.get_assets())
|
||||
return self.object.assets.all()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
asset_groups = self.get_asset_groups()
|
||||
assets = self.get_queryset()
|
||||
context = {
|
||||
'app': 'assets',
|
||||
'action': 'System user asset',
|
||||
'assets_remain': [asset for asset in Asset.objects.all() if asset not in assets],
|
||||
'asset_groups': asset_groups,
|
||||
'asset_groups_remain': [asset_group for asset_group in AssetGroup.objects.all()
|
||||
if asset_group not in asset_groups]
|
||||
'asset_groups': AssetGroup.objects.all(),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super(SystemUserAssetView, self).get_context_data(**kwargs)
|
||||
|
|
|
@ -27,8 +27,8 @@ app.conf.update(
|
|||
'schedule': 60*60*60*24,
|
||||
'args': (),
|
||||
},
|
||||
'test-admin-user-connective': {
|
||||
'task': 'assets.tasks.test_admin_user_connective_period',
|
||||
'test-admin-user-connectability_periode': {
|
||||
'task': 'assets.tasks.test_admin_user_connectability_period',
|
||||
'schedule': 60*60*60,
|
||||
'args': (),
|
||||
},
|
||||
|
|
|
@ -15,6 +15,7 @@ from email.utils import formatdate
|
|||
import calendar
|
||||
import threading
|
||||
from six import StringIO
|
||||
import uuid
|
||||
|
||||
import paramiko
|
||||
import sshpubkeys
|
||||
|
@ -378,4 +379,8 @@ def sum_capacity(cap_list):
|
|||
return capacity_convert(total, expect='auto')
|
||||
|
||||
|
||||
def get_short_uuid_str():
|
||||
return str(uuid.uuid4()).split('-')[-1]
|
||||
|
||||
|
||||
signer = Signer()
|
||||
|
|
|
@ -299,6 +299,7 @@ REST_FRAMEWORK = {
|
|||
'users.authentication.SessionAuthentication',
|
||||
),
|
||||
'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',),
|
||||
'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S',
|
||||
}
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
|
|
|
@ -5,21 +5,21 @@ from ansible.plugins.callback import CallbackBase
|
|||
|
||||
class AdHocResultCallback(CallbackBase):
|
||||
"""
|
||||
AdHoc result Callback
|
||||
Task result Callback
|
||||
"""
|
||||
def __init__(self, display=None):
|
||||
# result_raw example: {
|
||||
# "ok": {"hostname": [{"task_name": {},...],..},
|
||||
# "failed": {"hostname": ["task_name": {}..], ..},
|
||||
# "unreachable: {"hostname": ["task_name": {}, ..]},
|
||||
# "skipped": {"hostname": ["task_name": {}, ..], ..},
|
||||
# "ok": {"hostname": {"task_name": {},...},..},
|
||||
# "failed": {"hostname": {"task_name": {}..}, ..},
|
||||
# "unreachable: {"hostname": {"task_name": {}, ..}},
|
||||
# "skipped": {"hostname": {"task_name": {}, ..}, ..},
|
||||
# }
|
||||
# results_summary example: {
|
||||
# "contacted": {"hostname",...},
|
||||
# "dark": {"hostname": [{"task_name": "error"},...],},
|
||||
# "dark": {"hostname": {"task_name": {}, "task_name": {}},...,},
|
||||
# }
|
||||
self.results_raw = dict(ok={}, failed={}, unreachable={}, skipped={})
|
||||
self.results_summary = dict(contacted=set(), dark={})
|
||||
self.results_summary = dict(contacted=[], dark={})
|
||||
super().__init__(display)
|
||||
|
||||
def gather_result(self, t, res):
|
||||
|
@ -28,23 +28,24 @@ class AdHocResultCallback(CallbackBase):
|
|||
task_result = res._result
|
||||
|
||||
if self.results_raw[t].get(host):
|
||||
self.results_raw[t][host].append({task_name: task_result})
|
||||
self.results_raw[t][host][task_name] = task_result
|
||||
else:
|
||||
self.results_raw[t][host] = [{task_name: task_result}]
|
||||
self.results_raw[t][host] = {task_name: task_result}
|
||||
self.clean_result(t, host, task_name, task_result)
|
||||
|
||||
def clean_result(self, t, host, task_name, task_result):
|
||||
contacted = self.results_summary["contacted"]
|
||||
dark = self.results_summary["dark"]
|
||||
if t in ("ok", "skipped") and host not in dark:
|
||||
contacted.add(host)
|
||||
if host not in contacted:
|
||||
contacted.append(host)
|
||||
else:
|
||||
if dark.get(host):
|
||||
dark[host].append({task_name: task_result})
|
||||
dark[host][task_name] = task_result
|
||||
else:
|
||||
dark[host] = [{task_name: task_result}]
|
||||
dark[host] = {task_name: task_result}
|
||||
if host in contacted:
|
||||
contacted.remove(dark)
|
||||
contacted.remove(host)
|
||||
|
||||
def v2_runner_on_failed(self, result, ignore_errors=False):
|
||||
self.gather_result("failed", result)
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
from ansible.inventory.group import Group
|
||||
from ansible.inventory.host import Host
|
||||
from ansible.vars.manager import VariableManager
|
||||
from ansible.inventory.manager import InventoryManager
|
||||
from ansible.parsing.dataloader import DataLoader
|
||||
|
||||
|
||||
class JMSHost(Host):
|
||||
__all__ = [
|
||||
'BaseHost', 'BaseInventory'
|
||||
]
|
||||
|
||||
|
||||
class BaseHost(Host):
|
||||
def __init__(self, host_data):
|
||||
"""
|
||||
初始化
|
||||
|
@ -14,6 +18,7 @@ class JMSHost(Host):
|
|||
"hostname": "",
|
||||
"ip": "",
|
||||
"port": "",
|
||||
# behind is not must be required
|
||||
"username": "",
|
||||
"password": "",
|
||||
"private_key": "",
|
||||
|
@ -29,7 +34,7 @@ class JMSHost(Host):
|
|||
self.host_data = host_data
|
||||
hostname = host_data.get('hostname') or host_data.get('ip')
|
||||
port = host_data.get('port') or 22
|
||||
super(JMSHost, self).__init__(hostname, port)
|
||||
super().__init__(hostname, port)
|
||||
self.__set_required_variables()
|
||||
self.__set_extra_variables()
|
||||
|
||||
|
@ -37,7 +42,9 @@ class JMSHost(Host):
|
|||
host_data = self.host_data
|
||||
self.set_variable('ansible_host', host_data['ip'])
|
||||
self.set_variable('ansible_port', host_data['port'])
|
||||
self.set_variable('ansible_user', host_data['username'])
|
||||
|
||||
if host_data.get('username'):
|
||||
self.set_variable('ansible_user', host_data['username'])
|
||||
|
||||
# 添加密码和秘钥
|
||||
if host_data.get('password'):
|
||||
|
@ -63,30 +70,15 @@ class JMSHost(Host):
|
|||
return self.name
|
||||
|
||||
|
||||
class JMSInventory(InventoryManager):
|
||||
class BaseInventory(InventoryManager):
|
||||
"""
|
||||
提供生成Ansible inventory对象的方法
|
||||
"""
|
||||
loader_class = DataLoader
|
||||
variable_manager_class = VariableManager
|
||||
host_manager_class = JMSHost
|
||||
host_manager_class = BaseHost
|
||||
|
||||
def __init__(self, host_list=None):
|
||||
if host_list is None:
|
||||
host_list = []
|
||||
self.host_list = host_list
|
||||
assert isinstance(host_list, list)
|
||||
self.loader = self.loader_class()
|
||||
self.variable_manager = self.variable_manager_class()
|
||||
super().__init__(self.loader)
|
||||
|
||||
def get_groups(self):
|
||||
return self._inventory.groups
|
||||
|
||||
def get_group(self, name):
|
||||
return self._inventory.groups.get(name, None)
|
||||
|
||||
def parse_sources(self, cache=False):
|
||||
"""
|
||||
用于生成动态构建Ansible Inventory. super().__init__ 会自动调用
|
||||
host_list: [{
|
||||
|
@ -105,9 +97,23 @@ class JMSInventory(InventoryManager):
|
|||
"vars": {},
|
||||
},
|
||||
]
|
||||
|
||||
:return: None
|
||||
:param host_list:
|
||||
"""
|
||||
if host_list is None:
|
||||
host_list = []
|
||||
self.host_list = host_list
|
||||
assert isinstance(host_list, list)
|
||||
self.loader = self.loader_class()
|
||||
self.variable_manager = self.variable_manager_class()
|
||||
super().__init__(self.loader)
|
||||
|
||||
def get_groups(self):
|
||||
return self._inventory.groups
|
||||
|
||||
def get_group(self, name):
|
||||
return self._inventory.groups.get(name, None)
|
||||
|
||||
def parse_sources(self, cache=False):
|
||||
group_all = self.get_group('all')
|
||||
ungrouped = self.get_group('ungrouped')
|
||||
|
||||
|
@ -119,9 +125,14 @@ class JMSInventory(InventoryManager):
|
|||
for group_name in groups_data:
|
||||
group = self.get_group(group_name)
|
||||
if group is None:
|
||||
group = Group(group_name)
|
||||
self.add_group(group)
|
||||
self.add_group(group_name)
|
||||
group = self.get_group(group_name)
|
||||
group.add_host(host)
|
||||
else:
|
||||
ungrouped.add_host(host)
|
||||
group_all.add_host(host)
|
||||
|
||||
def get_matched_hosts(self, pattern):
|
||||
return self.get_hosts(pattern)
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
from collections import namedtuple
|
||||
|
@ -11,7 +10,6 @@ from ansible.executor.playbook_executor import PlaybookExecutor
|
|||
from ansible.playbook.play import Play
|
||||
import ansible.constants as C
|
||||
|
||||
from .inventory import JMSInventory
|
||||
from .callback import AdHocResultCallback, PlaybookResultCallBack, \
|
||||
CommandResultCallback
|
||||
from common.utils import get_logger
|
||||
|
@ -71,36 +69,19 @@ class PlayBookRunner:
|
|||
|
||||
# Default results callback
|
||||
results_callback_class = PlaybookResultCallBack
|
||||
inventory_class = JMSInventory
|
||||
loader_class = DataLoader
|
||||
variable_manager_class = VariableManager
|
||||
options = get_default_options()
|
||||
|
||||
def __init__(self, hosts=None, options=None):
|
||||
def __init__(self, inventory=None, options=None):
|
||||
"""
|
||||
:param options: Ansible options like ansible.cfg
|
||||
:param hosts: [
|
||||
{
|
||||
"hostname": "",
|
||||
"ip": "",
|
||||
"port": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"private_key": "",
|
||||
"become": {
|
||||
"method": "",
|
||||
"user": "",
|
||||
"pass": "",
|
||||
},
|
||||
"groups": [],
|
||||
"vars": {},
|
||||
},
|
||||
]
|
||||
:param inventory: Ansible inventory
|
||||
"""
|
||||
if options:
|
||||
self.options = options
|
||||
C.RETRY_FILES_ENABLED = False
|
||||
self.inventory = self.inventory_class(hosts)
|
||||
self.inventory = inventory
|
||||
self.loader = self.loader_class()
|
||||
self.results_callback = self.results_callback_class()
|
||||
self.playbook_path = options.playbook_path
|
||||
|
@ -141,20 +122,19 @@ class AdHocRunner:
|
|||
ADHoc Runner接口
|
||||
"""
|
||||
results_callback_class = AdHocResultCallback
|
||||
inventory_class = JMSInventory
|
||||
loader_class = DataLoader
|
||||
variable_manager_class = VariableManager
|
||||
options = get_default_options()
|
||||
default_options = get_default_options()
|
||||
|
||||
def __init__(self, hosts, options=None):
|
||||
def __init__(self, inventory, options=None):
|
||||
if options:
|
||||
self.options = options
|
||||
|
||||
self.pattern = ''
|
||||
self.inventory = inventory
|
||||
self.loader = DataLoader()
|
||||
self.inventory = self.inventory_class(hosts)
|
||||
self.variable_manager = VariableManager(loader=self.loader, inventory=self.inventory)
|
||||
self.variable_manager = VariableManager(
|
||||
loader=self.loader, inventory=self.inventory
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def check_module_args(module_name, module_args=''):
|
||||
|
@ -163,14 +143,22 @@ class AdHocRunner:
|
|||
raise AnsibleError(err)
|
||||
|
||||
def check_pattern(self, pattern):
|
||||
if not pattern:
|
||||
raise AnsibleError("Pattern `{}` is not valid!".format(pattern))
|
||||
if not self.inventory.list_hosts("all"):
|
||||
raise AnsibleError("Inventory is empty.")
|
||||
|
||||
if not self.inventory.list_hosts(pattern):
|
||||
raise AnsibleError(
|
||||
"pattern: %s dose not match any hosts." % pattern
|
||||
)
|
||||
|
||||
def clean_tasks(self, tasks):
|
||||
cleaned_tasks = []
|
||||
for task in tasks:
|
||||
self.check_module_args(task['action']['module'], task['action'].get('args'))
|
||||
cleaned_tasks.append(task)
|
||||
return cleaned_tasks
|
||||
|
||||
def set_option(self, k, v):
|
||||
kwargs = {k: v}
|
||||
self.options = self.options._replace(**kwargs)
|
||||
|
@ -182,17 +170,15 @@ class AdHocRunner:
|
|||
:param play_name: The play name
|
||||
:return:
|
||||
"""
|
||||
self.check_pattern(pattern)
|
||||
results_callback = self.results_callback_class()
|
||||
clean_tasks = []
|
||||
for task in tasks:
|
||||
self.check_module_args(task['action']['module'], task['action'].get('args'))
|
||||
clean_tasks.append(task)
|
||||
cleaned_tasks = self.clean_tasks(tasks)
|
||||
|
||||
play_source = dict(
|
||||
name=play_name,
|
||||
hosts=pattern,
|
||||
gather_facts=gather_facts,
|
||||
tasks=clean_tasks
|
||||
tasks=cleaned_tasks
|
||||
)
|
||||
|
||||
play = Play().load(
|
||||
|
@ -209,6 +195,9 @@ class AdHocRunner:
|
|||
stdout_callback=results_callback,
|
||||
passwords=self.options.passwords,
|
||||
)
|
||||
logger.debug("Get inventory matched hosts: {}".format(
|
||||
self.inventory.get_matched_hosts(pattern)
|
||||
))
|
||||
|
||||
try:
|
||||
tqm.run(play)
|
||||
|
|
|
@ -6,7 +6,7 @@ import unittest
|
|||
|
||||
|
||||
sys.path.insert(0, '../..')
|
||||
from ops.ansible.inventory import JMSInventory
|
||||
from ops.ansible.inventory import BaseInventory
|
||||
|
||||
|
||||
class TestJMSInventory(unittest.TestCase):
|
||||
|
@ -41,7 +41,7 @@ class TestJMSInventory(unittest.TestCase):
|
|||
"vars": {"love": "yes"},
|
||||
}]
|
||||
|
||||
self.inventory = JMSInventory(host_list=host_list)
|
||||
self.inventory = BaseInventory(host_list=host_list)
|
||||
|
||||
def test_hosts(self):
|
||||
print("#"*10 + "Hosts" + "#"*10)
|
|
@ -7,6 +7,7 @@ import sys
|
|||
sys.path.insert(0, "../..")
|
||||
|
||||
from ops.ansible.runner import AdHocRunner, CommandRunner
|
||||
from ops.ansible.inventory import BaseInventory
|
||||
|
||||
|
||||
class TestAdHocRunner(unittest.TestCase):
|
||||
|
@ -20,7 +21,8 @@ class TestAdHocRunner(unittest.TestCase):
|
|||
"password": "redhat",
|
||||
},
|
||||
]
|
||||
self.runner = AdHocRunner(hosts=host_data)
|
||||
inventory = BaseInventory(host_data)
|
||||
self.runner = AdHocRunner(inventory)
|
||||
|
||||
def test_run(self):
|
||||
tasks = [
|
||||
|
@ -43,7 +45,8 @@ class TestCommandRunner(unittest.TestCase):
|
|||
"password": "redhat",
|
||||
},
|
||||
]
|
||||
self.runner = CommandRunner(hosts=host_data)
|
||||
inventory = BaseInventory(host_data)
|
||||
self.runner = CommandRunner(inventory)
|
||||
|
||||
def test_execute(self):
|
||||
res = self.runner.execute('ls', 'all')
|
|
@ -1,15 +1,42 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import viewsets
|
||||
|
||||
from .hands import IsSuperUser
|
||||
from .models import AdHoc
|
||||
from .serializers import TaskSerializer
|
||||
from .models import Task, AdHoc, AdHocRunHistory
|
||||
from .serializers import TaskSerializer, AdHocSerializer, AdHocRunHistorySerializer
|
||||
|
||||
|
||||
class TaskViewSet(viewsets.ModelViewSet):
|
||||
queryset = AdHoc.objects.all()
|
||||
queryset = Task.objects.all()
|
||||
serializer_class = TaskSerializer
|
||||
permission_classes = (IsSuperUser,)
|
||||
|
||||
|
||||
class AdHocViewSet(viewsets.ModelViewSet):
|
||||
queryset = AdHoc.objects.all()
|
||||
serializer_class = AdHocSerializer
|
||||
permission_classes = (IsSuperUser,)
|
||||
|
||||
def get_queryset(self):
|
||||
task_id = self.request.query_params.get('task')
|
||||
if task_id:
|
||||
task = get_object_or_404(Task, id=task_id)
|
||||
self.queryset = self.queryset.filter(task=task)
|
||||
return self.queryset
|
||||
|
||||
|
||||
class AdHocRunHistorySet(viewsets.ModelViewSet):
|
||||
queryset = AdHocRunHistory.objects.all()
|
||||
serializer_class = AdHocRunHistorySerializer
|
||||
permission_classes = (IsSuperUser,)
|
||||
|
||||
def get_queryset(self):
|
||||
task_id = self.request.query_params.get('task')
|
||||
if task_id:
|
||||
task = get_object_or_404(Task, id=task_id)
|
||||
adhocs = task.adhoc.all()
|
||||
self.queryset = self.queryset.filter(adhoc__in=adhocs)
|
||||
return self.queryset
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .ansible.inventory import BaseInventory
|
||||
from assets.utils import get_assets_by_hostname_list, get_system_user_by_name
|
||||
|
||||
__all__ = [
|
||||
'JMSInventory'
|
||||
]
|
||||
|
||||
|
||||
class JMSInventory(BaseInventory):
|
||||
"""
|
||||
JMS Inventory is the manager with jumpserver assets, so you can
|
||||
write you own manager, construct you inventory
|
||||
"""
|
||||
def __init__(self, hostname_list, run_as_admin=False, run_as=None, become_info=None):
|
||||
self.hostname_list = hostname_list
|
||||
self.using_admin = run_as_admin
|
||||
self.run_as = run_as
|
||||
self.become_info = become_info
|
||||
|
||||
assets = self.get_jms_assets()
|
||||
if run_as_admin:
|
||||
host_list = [asset._to_secret_json() for asset in assets]
|
||||
else:
|
||||
host_list = [asset.to_json() for asset in assets]
|
||||
if run_as:
|
||||
run_user_info = self.get_run_user_info()
|
||||
for host in host_list:
|
||||
host.update(run_user_info)
|
||||
if become_info:
|
||||
for host in host_list:
|
||||
host.update(become_info)
|
||||
super().__init__(host_list=host_list)
|
||||
|
||||
def get_jms_assets(self):
|
||||
assets = get_assets_by_hostname_list(self.hostname_list)
|
||||
return assets
|
||||
|
||||
def get_run_user_info(self):
|
||||
system_user = get_system_user_by_name(self.run_as)
|
||||
if not system_user:
|
||||
return {}
|
||||
else:
|
||||
return system_user._to_secret_json()
|
|
@ -6,20 +6,25 @@ import uuid
|
|||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import signer
|
||||
|
||||
__all__ = ["AdHoc", "AdHocRunHistory"]
|
||||
__all__ = ["Task", "AdHoc", "AdHocRunHistory"]
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AdHoc(models.Model):
|
||||
class Task(models.Model):
|
||||
"""
|
||||
This task is different ansible task, Task like 'push system user', 'get asset info' ..
|
||||
One task can have some versions of adhoc, run a task only run the latest version adhoc
|
||||
"""
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, blank=True, verbose_name=_('Name'))
|
||||
is_deleted = models.BooleanField(default=False)
|
||||
created_by = models.CharField(max_length=128, blank=True, default='')
|
||||
date_create = models.DateTimeField(auto_now_add=True)
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@property
|
||||
def short_id(self):
|
||||
|
@ -28,24 +33,48 @@ class AdHoc(models.Model):
|
|||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_latest_adhoc(self):
|
||||
return self.adhoc.all().order_by('date_created').last()
|
||||
|
||||
class AdHocData(models.Model):
|
||||
BECOME_METHOD_CHOICES = (
|
||||
('sudo', 'sudo'),
|
||||
('su', 'su'),
|
||||
)
|
||||
version = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
subject = models.ForeignKey(AdHoc, on_delete=models.CASCADE)
|
||||
_tasks = models.TextField(verbose_name=_('Tasks')) # [{'name': 'task_name', 'action': {'module': '', 'args': ''}, 'other..': ''}, ]
|
||||
def get_latest_history(self):
|
||||
return self.get_latest_adhoc().get_latest_history()
|
||||
|
||||
def get_all_run_history(self):
|
||||
adhocs = self.adhoc.all()
|
||||
return AdHocRunHistory.objects.filter(adhoc__in=adhocs)
|
||||
|
||||
def get_all_run_times(self):
|
||||
history_all = self.get_all_run_history()
|
||||
total = len(history_all)
|
||||
success = len([history for history in history_all if history.is_success])
|
||||
failed = len([history for history in history_all if not history.is_success])
|
||||
return {'total': total, 'success': success, 'failed': failed}
|
||||
|
||||
class Meta:
|
||||
db_table = 'ops_task'
|
||||
|
||||
|
||||
class AdHoc(models.Model):
|
||||
"""
|
||||
task: A task reference
|
||||
_tasks: [{'name': 'task_name', 'action': {'module': '', 'args': ''}, 'other..': ''}, ]
|
||||
_options: ansible options, more see ops.ansible.runner.Options
|
||||
_hosts: ["hostname1", "hostname2"], hostname must be unique key of cmdb
|
||||
run_as_admin: if true, then need get every host admin user run it, because every host may be have different admin user, so we choise host level
|
||||
run_as: if not run as admin, it run it as a system/common user from cmdb
|
||||
_become: May be using become [sudo, su] options. {method: "sudo", user: "user", pass: "pass"]
|
||||
pattern: Even if we set _hosts, We only use that to make inventory, We also can set `patter` to run task on match hosts
|
||||
"""
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
task = models.ForeignKey(Task, related_name='adhoc', on_delete=models.CASCADE)
|
||||
_tasks = models.TextField(verbose_name=_('Tasks'))
|
||||
pattern = models.CharField(max_length=64, default='', verbose_name=_('Pattern'))
|
||||
_options = models.CharField(max_length=1024, default='', verbose_name=_('Options'))
|
||||
_hosts = models.TextField(blank=True, verbose_name=_('Hosts')) # ['hostname1', 'hostname2']
|
||||
run_as_admin = models.BooleanField(default=False, verbose_name=_('Run as admin'))
|
||||
run_as = models.CharField(max_length=128, verbose_name=_("Run as"))
|
||||
become = models.BooleanField(default=False, verbose_name=_("Become"))
|
||||
become_method = models.CharField(choices=BECOME_METHOD_CHOICES, default='sudo', max_length=4)
|
||||
become_user = models.CharField(default='root', max_length=64)
|
||||
_become_pass = models.CharField(default='', max_length=128)
|
||||
pattern = models.CharField(max_length=64, default='', verbose_name=_('Pattern'))
|
||||
created_by = models.CharField(max_length=64, verbose_name=_('Create by'))
|
||||
run_as = models.CharField(max_length=128, default='', verbose_name=_("Run as"))
|
||||
_become = models.CharField(max_length=1024, default='', verbose_name=_("Become"))
|
||||
created_by = models.CharField(max_length=64, default='', verbose_name=_('Create by'))
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@property
|
||||
|
@ -54,7 +83,10 @@ class AdHocData(models.Model):
|
|||
|
||||
@tasks.setter
|
||||
def tasks(self, item):
|
||||
self._tasks = json.dumps(item)
|
||||
if item and isinstance(item, list):
|
||||
self._tasks = json.dumps(item)
|
||||
else:
|
||||
raise SyntaxError('Tasks should be a list')
|
||||
|
||||
@property
|
||||
def hosts(self):
|
||||
|
@ -65,42 +97,83 @@ class AdHocData(models.Model):
|
|||
self._hosts = json.dumps(item)
|
||||
|
||||
@property
|
||||
def become_pass(self):
|
||||
return signer.unsign(self._become_pass)
|
||||
def become(self):
|
||||
if self._become:
|
||||
return json.loads(signer.unsign(self._become))
|
||||
else:
|
||||
return {}
|
||||
|
||||
@become_pass.setter
|
||||
def become_pass(self, password):
|
||||
self._become_pass = signer.sign(password)
|
||||
@become.setter
|
||||
def become(self, item):
|
||||
"""
|
||||
:param item: {
|
||||
method: "sudo",
|
||||
user: "user",
|
||||
pass: "pass",
|
||||
}
|
||||
:return:
|
||||
"""
|
||||
self._become = signer.sign(json.dumps(item))
|
||||
|
||||
@property
|
||||
def short_version(self):
|
||||
return str(self.version).split('-')[-1]
|
||||
def options(self):
|
||||
if self._options:
|
||||
return json.loads(self._options)
|
||||
else:
|
||||
return {}
|
||||
|
||||
def run(self):
|
||||
pass
|
||||
@options.setter
|
||||
def options(self, item):
|
||||
self._options = json.dumps(item)
|
||||
|
||||
@property
|
||||
def short_id(self):
|
||||
return str(self.id).split('-')[-1]
|
||||
|
||||
def get_latest_history(self):
|
||||
return self.history.all().order_by('date_start').last()
|
||||
|
||||
def __str__(self):
|
||||
return "{} of {}".format(self.subject.name, self.short_version)
|
||||
return "{} of {}".format(self.task.name, self.short_id)
|
||||
|
||||
class Meta:
|
||||
db_table = "ops_adhoc_data"
|
||||
db_table = "ops_adhoc"
|
||||
|
||||
|
||||
class AdHocRunHistory(models.Model):
|
||||
uuid = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
adhoc = models.ForeignKey(AdHocData, on_delete=models.CASCADE)
|
||||
"""
|
||||
AdHoc running history.
|
||||
"""
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
adhoc = models.ForeignKey(AdHoc, related_name='history', on_delete=models.CASCADE)
|
||||
date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Start time'))
|
||||
date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('End time'))
|
||||
timedelta = models.FloatField(default=0.0, verbose_name=_('Time'), null=True)
|
||||
is_finished = models.BooleanField(default=False, verbose_name=_('Is finished'))
|
||||
is_success = models.BooleanField(default=False, verbose_name=_('Is success'))
|
||||
result = models.TextField(blank=True, null=True, verbose_name=_('Playbook raw result'))
|
||||
summary = models.TextField(blank=True, null=True, verbose_name=_('Playbook summary'))
|
||||
_result = models.TextField(blank=True, null=True, verbose_name=_('Adhoc raw result'))
|
||||
_summary = models.TextField(blank=True, null=True, verbose_name=_('Adhoc result summary'))
|
||||
|
||||
@property
|
||||
def short_id(self):
|
||||
return str(self.id).split('-')[-1]
|
||||
|
||||
@property
|
||||
def result(self):
|
||||
return json.loads(self._result)
|
||||
|
||||
@result.setter
|
||||
def result(self, item):
|
||||
self._result = json.dumps(item)
|
||||
|
||||
@property
|
||||
def summary(self):
|
||||
return json.loads(self._summary)
|
||||
|
||||
@summary.setter
|
||||
def summary(self, item):
|
||||
self._summary = json.dumps(item)
|
||||
|
||||
def __str__(self):
|
||||
return self.short_id
|
||||
|
||||
|
|
|
@ -2,12 +2,43 @@
|
|||
from __future__ import unicode_literals
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import AdHoc
|
||||
from .models import Task, AdHoc, AdHocRunHistory
|
||||
|
||||
|
||||
class TaskSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = AdHoc
|
||||
model = Task
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class AdHocSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = AdHoc
|
||||
exclude = ('_tasks', '_options', '_hosts', '_become')
|
||||
|
||||
def get_field_names(self, declared_fields, info):
|
||||
fields = super().get_field_names(declared_fields, info)
|
||||
fields.extend(['tasks', 'options', 'hosts', 'become', 'short_id'])
|
||||
return fields
|
||||
|
||||
|
||||
class AdHocRunHistorySerializer(serializers.ModelSerializer):
|
||||
task = serializers.SerializerMethodField()
|
||||
adhoc_short_id = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = AdHocRunHistory
|
||||
exclude = ('_result', '_summary')
|
||||
|
||||
@staticmethod
|
||||
def get_adhoc_short_id(obj):
|
||||
return obj.adhoc.short_id
|
||||
|
||||
@staticmethod
|
||||
def get_task(obj):
|
||||
return obj.adhoc.task.id
|
||||
|
||||
def get_field_names(self, declared_fields, info):
|
||||
fields = super().get_field_names(declared_fields, info)
|
||||
fields.extend(['summary', 'short_id'])
|
||||
return fields
|
||||
|
|
|
@ -1,22 +1,13 @@
|
|||
# coding: utf-8
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from celery import shared_task
|
||||
|
||||
from common.utils import get_logger
|
||||
from .utils import run_AdHoc
|
||||
from .utils import run_adhoc
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
def rerun_task():
|
||||
pass
|
||||
|
||||
|
||||
@shared_task
|
||||
def rerun_task(task_id):
|
||||
from .models import Playbook
|
||||
record = Playbook.objects.get(uuid=task_id)
|
||||
assets = record.assets_json
|
||||
task_tuple = record.module_args
|
||||
pattern = record.pattern
|
||||
task_name = record.name
|
||||
return run_AdHoc(task_tuple, assets, pattern=pattern,
|
||||
task_name=task_name, task_id=task_id)
|
||||
def run_add_hoc_and_record_async(adhoc, **options):
|
||||
return run_adhoc(adhoc, **options)
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
{% extends 'base.html' %}
|
||||
{% 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>
|
||||
<a href="{% url 'ops:task-detail' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Task detail' %} </a>
|
||||
</li>
|
||||
<li class="active">
|
||||
<a href="{% url 'ops:task-adhoc' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Task versions' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-12" style="padding-left: 0">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span style="float: left">{% trans 'Versions of ' %} <b>{{ 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 table-hover " id="task-version-list-table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
<input type="checkbox" id="check_all" class="ipt_check_all" >
|
||||
</th>
|
||||
<th>{% trans 'Version' %}</th>
|
||||
<th>{% trans 'Hosts' %}</th>
|
||||
<th>{% trans 'Pattern' %}</th>
|
||||
<th>{% trans 'Run as' %}</th>
|
||||
<th>{% trans 'Become' %}</th>
|
||||
<th>{% trans 'Datetime' %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
var options = {
|
||||
ele: $('#task-version-list-table'),
|
||||
buttons: [],
|
||||
order: [],
|
||||
select: [],
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
{# var detail_btn = '<a href="' + cellData + '</a>';#}
|
||||
$(td).html(cellData);
|
||||
}},
|
||||
{targets: 2, createdCell: function (td, cellData, rowData) {
|
||||
var dataLength = cellData.length;
|
||||
$(td).html(dataLength);
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
if (!cellData) {
|
||||
$(td).html("Admin")
|
||||
} else {
|
||||
$(td).html(cellData)
|
||||
}
|
||||
}},
|
||||
{targets: 5, createdCell: function (td, cellData, rowData) {
|
||||
if (!cellData) {
|
||||
$(td).html("")
|
||||
} else {
|
||||
$(td).html(cellData.user)
|
||||
}
|
||||
|
||||
}}
|
||||
],
|
||||
ajax_url: '{% url "api-ops:adhoc-list" %}?task={{ object.pk }}',
|
||||
columns: [{data: function(){return ""}}, {data: "short_id" }, {data: "hosts"}, {data: "pattern"},
|
||||
{data: "run_as"}, {data: "become"}, {data: "date_created"}]
|
||||
};
|
||||
jumpserver.initDataTable(options);
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -15,8 +15,14 @@
|
|||
<div class="ibox float-e-margins">
|
||||
<div class="panel-options">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active">
|
||||
<a href="" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Task replay detail' %} </a>
|
||||
<li class="active">
|
||||
<a href="{% url 'ops:task-detail' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Task detail' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'ops:task-adhoc' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Task versions' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -43,49 +49,47 @@
|
|||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="no-borders-tr">
|
||||
<td width="20%">{% trans 'UUID' %}:</td>
|
||||
<td><b>{{ object.uuid }}</b></td>
|
||||
<td width="20%">{% trans 'ID' %}:</td>
|
||||
<td><b>{{ object.id }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="20%">{% trans 'Name' %}:</td>
|
||||
<td><b>{{ object.name }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Date start' %}:</td>
|
||||
<td><b>{{ object.date_start }}</b></td>
|
||||
<td>{% trans 'Date created' %}:</td>
|
||||
<td><b>{{ object.date_created }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Date finished' %}:</td>
|
||||
<td><b>{{ object.date_finished }}</b></td>
|
||||
<td>{% trans 'Total versions' %}</td>
|
||||
<td><b>{{ object.adhoc.all |length }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Last version' %}</td>
|
||||
<td><b>{{ object.get_latest_adhoc.short_id }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Latest run' %}:</td>
|
||||
<td><b>{{ object.get_latest_history.date_start }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Time delta' %}:</td>
|
||||
<td><b>{{ object.timedelta}} s</b></td>
|
||||
<td><b>{{ object.get_latest_history.timedelta|floatformat}} s</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Is finished' %}:</td>
|
||||
<td><b>{{ object.is_finished|yesno:"Yes,No,Unkown" }}</b></td>
|
||||
<td><b>{{ object.get_latest_history.is_finished|yesno:"Yes,No,Unkown" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Is success ' %}:</td>
|
||||
{% if object.is_finished %}
|
||||
<td><b>{{ object.is_success|yesno:"Yes,No,Unkown" }}</b></td>
|
||||
{% else %}
|
||||
<td>
|
||||
<div class="progress progress-striped active">
|
||||
<div style="width: 50%" aria-valuemax="100" aria-valuemin="0" aria-valuenow="75" role="progressbar" class="progress-bar progress-bar-primary">
|
||||
<span class="sr-only">40% Complete (success)</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td><b>{{ object.get_latest_history.is_success|yesno:"Yes,No,Unkown" }}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'assets' %}:</td>
|
||||
<td>{% trans 'Conents' %}:</td>
|
||||
<td>
|
||||
<b>
|
||||
{% for asset in object.total_assets %}
|
||||
{{ asset.hostname }} <br/>
|
||||
{% for task in object.get_latest_adhoc.tasks %}
|
||||
{{ task.name }} : {{ task.action.module }} <br/>
|
||||
{% endfor %}
|
||||
</b>
|
||||
</td>
|
||||
|
@ -94,31 +98,6 @@
|
|||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span><b>Result</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">
|
||||
<pre>
|
||||
{{ object.result }}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-5" style="padding-left: 0;padding-right: 0">
|
||||
<div class="panel panel-danger">
|
||||
|
@ -154,7 +133,7 @@
|
|||
<div class="panel-body">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
{% for host in results.success %}
|
||||
{% for host in object.get_latest_history.summary.contacted %}
|
||||
{% if forloop.first %}
|
||||
<tr class="no-borders-tr">
|
||||
{% else %}
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
{% extends 'base.html' %}
|
||||
{% 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>
|
||||
<a href="{% url 'ops:task-detail' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Task detail' %} </a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'ops:task-adhoc' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Task versions' %} </a>
|
||||
</li>
|
||||
<li class="active">
|
||||
<a href="{% url 'ops:task-history' pk=object.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Run history' %} </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="col-sm-12" style="padding-left: 0">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<span style="float: left">{% trans 'Versions of ' %} <b>{{ 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 table-hover " id="task-history-list-table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">
|
||||
<input type="checkbox" id="check_all" class="ipt_check_all" >
|
||||
</th>
|
||||
<th>{% trans 'Date start' %}</th>
|
||||
<th>{% trans 'F/S/T' %}</th>
|
||||
<th>{% trans 'Is finished' %}</th>
|
||||
<th>{% trans 'Is success' %}</th>
|
||||
<th>{% trans 'Time' %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
var options = {
|
||||
ele: $('#task-history-list-table'),
|
||||
buttons: [],
|
||||
order: [],
|
||||
select: [],
|
||||
columnDefs: [
|
||||
{targets: 1, createdCell: function (td, cellData, rowData) {
|
||||
{# var detail_btn = '<a href="' + cellData + '</a>';#}
|
||||
$(td).html(cellData);
|
||||
}},
|
||||
{# {targets: 2, createdCell: function (td, cellData, rowData) {#}
|
||||
{# var dataLength = cellData.length;#}
|
||||
{# $(td).html(dataLength);#}
|
||||
{# }},#}
|
||||
{targets: 3, 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: 4, 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: 5, createdCell: function (td, cellData) {
|
||||
if (cellData) {
|
||||
$(td).html(cellData.toFixed(2) + ' s')
|
||||
} else {
|
||||
$(td).html("0" + ' s')
|
||||
}
|
||||
|
||||
}}
|
||||
],
|
||||
ajax_url: '{% url "api-ops:history-list" %}?task={{ object.pk }}',
|
||||
columns: [{data: function(){return ""}}, {data: "date_start"}, {data: "adhoc_short_id"},
|
||||
{data: "adhoc_short_id"}, {data: "adhoc_short_id"}, {data: "timedelta"}]
|
||||
};
|
||||
jumpserver.initDataTable(options);
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -22,7 +22,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control input-sm" name="keyword" placeholder="Keyword" value="{{ keyword }}">
|
||||
<input type="text" class="form-control input-sm" name="keyword" placeholder="Search" value="{{ keyword }}">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<div class="input-group-btn">
|
||||
|
@ -37,10 +37,11 @@
|
|||
{% block table_head %}
|
||||
<th class="text-center"></th>
|
||||
<th class="text-center">{% trans 'Name' %}</th>
|
||||
<th class="text-center">{% trans 'Asset' %}</th>
|
||||
<th class="text-center">{% trans 'F/S/T' %}</th>
|
||||
<th class="text-center">{% trans 'Versions' %}</th>
|
||||
<th class="text-center">{% trans 'Hosts' %}</th>
|
||||
<th class="text-center">{% trans 'Success' %}</th>
|
||||
<th class="text-center">{% trans 'Finished' %}</th>
|
||||
<th class="text-center">{% trans 'Date start' %}</th>
|
||||
<th class="text-center">{% trans 'Date' %}</th>
|
||||
<th class="text-center">{% trans 'Time' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
{% endblock %}
|
||||
|
@ -49,26 +50,23 @@
|
|||
{% for object in task_list %}
|
||||
<tr class="gradeX">
|
||||
<td class="text-center"><input type="checkbox" class="cbx-term"> </td>
|
||||
<td class="text-center"><a href="{% url 'ops:task-detail' pk=object.uuid %}">{{ object.name }}</a></td>
|
||||
<td class="text-center">{{ object.total_assets|length }}</td>
|
||||
<td class="text-center"><a href="{% url 'ops:task-detail' pk=object.id %}">{{ object.name }}</a></td>
|
||||
<td class="text-center">
|
||||
{% if object.is_success %}
|
||||
<span class="text-danger">{{ object.get_all_run_times.failed }}</span>/<span class="text-navy">{{ object.get_all_run_times.success}}</span>/{{ object.get_all_run_times.total}}
|
||||
</td>
|
||||
<td class="text-center">{{ object.adhoc.all | length}}</td>
|
||||
<td class="text-center">{{ object.get_latest_adhoc.hosts | length}}</td>
|
||||
<td class="text-center">
|
||||
{% if object.get_latest_history.is_success %}
|
||||
<i class="fa fa-check text-navy"></i>
|
||||
{% else %}
|
||||
<i class="fa fa-times text-danger"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">{{ object.get_latest_history.date_start }}</td>
|
||||
<td class="text-center">{{ object.get_latest_history.timedelta|floatformat }} s</td>
|
||||
<td class="text-center">
|
||||
{% if object.is_finished %}
|
||||
<i class="fa fa-check text-navy"></i>
|
||||
{% else %}
|
||||
<i class="fa fa-times text-danger"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">{{ object.date_start }}</td>
|
||||
<td class="text-center">{{ object.timedelta }} s</td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'ops:task-run' pk=object.uuid %}" class="btn btn-xs btn-info">{% trans "Run again" %}</a>
|
||||
<a href="{% url 'ops:task-run' pk=object.id %}" class="btn btn-xs btn-info">{% trans "Run" %}</a>
|
||||
<a data-uid="{{ object.uuid }}" class="btn btn-xs btn-danger btn-del">{% trans "Delete" %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -5,17 +5,16 @@ import sys
|
|||
import os
|
||||
from django.test import TestCase
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jumpserver.settings")
|
||||
from ops.models import AdHoc, AdHocData
|
||||
from ops.utils import run_adhoc
|
||||
from ops.models import Task, AdHoc
|
||||
from ops.utils import run_adhoc_object
|
||||
|
||||
|
||||
class TestRunAdHoc(TestCase):
|
||||
def setUp(self):
|
||||
adhoc = AdHoc(name="Test run adhoc")
|
||||
adhoc = Task(name="Test run adhoc")
|
||||
adhoc.save()
|
||||
|
||||
self.data = AdHocData(subject=adhoc, run_as_admin=True, pattern='all')
|
||||
self.data = AdHoc(subject=adhoc, run_as_admin=True, pattern='all')
|
||||
self.data.tasks = [
|
||||
{'name': 'run ls', 'action': {'module': 'shell', 'args': 'ls'}},
|
||||
{'name': 'echo ', 'action': {'module': 'shell', 'args': 'echo 123'}},
|
||||
|
|
|
@ -7,6 +7,8 @@ from .. import api
|
|||
|
||||
router = DefaultRouter()
|
||||
router.register(r'v1/tasks', api.TaskViewSet, 'task')
|
||||
router.register(r'v1/adhoc', api.AdHocViewSet, 'adhoc')
|
||||
router.register(r'v1/history', api.AdHocRunHistorySet, 'history')
|
||||
|
||||
urlpatterns = []
|
||||
|
||||
|
|
|
@ -11,5 +11,7 @@ urlpatterns = [
|
|||
# TResource Task url
|
||||
url(r'^task/$', views.TaskListView.as_view(), name='task-list'),
|
||||
url(r'^task/(?P<pk>[0-9a-zA-Z\-]+)/$', views.TaskDetailView.as_view(), name='task-detail'),
|
||||
url(r'^task/(?P<pk>[0-9a-zA-Z\-]+)/adhoc/$', views.TaskAdhocView.as_view(), name='task-adhoc'),
|
||||
url(r'^task/(?P<pk>[0-9a-zA-Z\-]+)/history/$', views.TaskHistoryView.as_view(), name='task-history'),
|
||||
url(r'^task/(?P<pk>[0-9a-zA-Z\-]+)/run/$', views.TaskRunView.as_view(), name='task-run'),
|
||||
]
|
|
@ -4,20 +4,16 @@ import re
|
|||
import time
|
||||
from django.utils import timezone
|
||||
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from .ansible import AdHocRunner
|
||||
from common.utils import get_logger, get_object_or_none, get_short_uuid_str
|
||||
from .ansible import AdHocRunner, CommandResultCallback
|
||||
from .inventory import JMSInventory
|
||||
from .ansible.exceptions import AnsibleError
|
||||
from .models import AdHocRunHistory
|
||||
from assets.utils import get_assets_by_hostname_list
|
||||
from .models import AdHocRunHistory, Task, AdHoc
|
||||
|
||||
logger = get_logger(__file__)
|
||||
UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}')
|
||||
|
||||
|
||||
def run_AdHoc():
|
||||
pass
|
||||
|
||||
|
||||
def is_uuid(s):
|
||||
if UUID_PATTERN.match(s):
|
||||
return True
|
||||
|
@ -25,97 +21,133 @@ def is_uuid(s):
|
|||
return False
|
||||
|
||||
|
||||
def asset_to_dict(asset):
|
||||
return asset.to_json()
|
||||
def record_adhoc(func):
|
||||
def _deco(adhoc, **options):
|
||||
record = AdHocRunHistory(adhoc=adhoc)
|
||||
time_start = time.time()
|
||||
try:
|
||||
result = func(adhoc, **options)
|
||||
record.is_finished = True
|
||||
if result.results_summary.get('dark'):
|
||||
record.is_success = False
|
||||
else:
|
||||
record.is_success = True
|
||||
record.result = result.results_raw
|
||||
record.summary = result.results_summary
|
||||
return result
|
||||
finally:
|
||||
record.date_finished = timezone.now()
|
||||
record.timedelta = time.time() - time_start
|
||||
record.save()
|
||||
return _deco
|
||||
|
||||
|
||||
def asset_to_dict_with_credential(asset):
|
||||
return asset._to_secret_json()
|
||||
|
||||
|
||||
def system_user_to_dict_with_credential(system_user):
|
||||
return system_user._to_secret_json()
|
||||
|
||||
|
||||
def get_hosts_with_admin(hostname_list):
|
||||
assets = get_assets_by_hostname_list(hostname_list)
|
||||
return [asset._to_secret_json for asset in assets]
|
||||
|
||||
|
||||
def get_hosts(hostname_list):
|
||||
assets = get_assets_by_hostname_list(hostname_list)
|
||||
return [asset.to_json for asset in assets]
|
||||
|
||||
|
||||
def get_run_user(name):
|
||||
from assets.models import SystemUser
|
||||
system_user = get_object_or_none(SystemUser, name=name)
|
||||
if system_user is None:
|
||||
return {}
|
||||
else:
|
||||
return system_user._to_secret_json()
|
||||
|
||||
|
||||
def get_hosts_with_run_user(hostname_list, run_as):
|
||||
hosts_dict = get_hosts(hostname_list)
|
||||
system_user_dct = get_run_user(run_as)
|
||||
|
||||
for host in hosts_dict:
|
||||
host.update(system_user_dct)
|
||||
return hosts_dict
|
||||
|
||||
|
||||
def hosts_add_become(hosts, adhoc_data):
|
||||
if adhoc_data.become:
|
||||
become_data = {
|
||||
"become": {
|
||||
"method": adhoc_data.become_method,
|
||||
"user": adhoc_data.become_user,
|
||||
"pass": adhoc_data.become_pass,
|
||||
def get_adhoc_inventory(adhoc):
|
||||
if adhoc.become:
|
||||
become_info = {
|
||||
'become': {
|
||||
adhoc.become
|
||||
}
|
||||
}
|
||||
for host in hosts:
|
||||
host.update(become_data)
|
||||
return hosts
|
||||
else:
|
||||
become_info = None
|
||||
|
||||
inventory = JMSInventory(
|
||||
adhoc.hosts, run_as_admin=adhoc.run_as_admin,
|
||||
run_as=adhoc.run_as, become_info=become_info
|
||||
)
|
||||
return inventory
|
||||
|
||||
|
||||
def run_adhoc(adhoc_data, **options):
|
||||
def get_inventory(hostname_list, run_as_admin=False, run_as=None, become_info=None):
|
||||
return JMSInventory(
|
||||
hostname_list, run_as_admin=run_as_admin,
|
||||
run_as=run_as, become_info=become_info
|
||||
)
|
||||
|
||||
|
||||
def get_adhoc_runner(hostname_list, run_as_admin=False, run_as=None, become_info=None):
|
||||
inventory = get_inventory(
|
||||
hostname_list, run_as_admin=run_as_admin,
|
||||
run_as=run_as, become_info=become_info
|
||||
)
|
||||
runner = AdHocRunner(inventory)
|
||||
return runner
|
||||
|
||||
|
||||
@record_adhoc
|
||||
def run_adhoc_object(adhoc, **options):
|
||||
"""
|
||||
:param adhoc_data: Instance of AdHocData
|
||||
:param adhoc: Instance of AdHoc
|
||||
:param options: ansible support option, like forks ...
|
||||
:return:
|
||||
"""
|
||||
name = adhoc_data.subject.name
|
||||
hostname_list = adhoc_data.hosts
|
||||
if adhoc_data.run_as_admin:
|
||||
hosts = get_hosts_with_admin(hostname_list)
|
||||
else:
|
||||
hosts = get_hosts_with_run_user(hostname_list, adhoc_data.run_as)
|
||||
hosts_add_become(hosts, adhoc_data) # admin user 自带become
|
||||
|
||||
runner = AdHocRunner(hosts)
|
||||
name = adhoc.task.name
|
||||
inventory = get_adhoc_inventory(adhoc)
|
||||
runner = AdHocRunner(inventory)
|
||||
for k, v in options:
|
||||
runner.set_option(k, v)
|
||||
|
||||
record = AdHocRunHistory(adhoc=adhoc_data)
|
||||
time_start = time.time()
|
||||
try:
|
||||
result = runner.run(adhoc_data.tasks, adhoc_data.pattern, name)
|
||||
record.is_finished = True
|
||||
if result.results_summary.get('dark'):
|
||||
record.is_success = False
|
||||
else:
|
||||
record.is_success = True
|
||||
record.result = result.results_raw
|
||||
record.summary = result.results_summary
|
||||
result = runner.run(adhoc.tasks, adhoc.pattern, name)
|
||||
return result
|
||||
except AnsibleError as e:
|
||||
logger.error("Failed run adhoc {}, {}".format(name, e))
|
||||
raise
|
||||
finally:
|
||||
record.date_finished = timezone.now()
|
||||
record.timedelta = time.time() - time_start
|
||||
record.save()
|
||||
|
||||
|
||||
def run_adhoc(hostname_list, pattern, tasks, name=None,
|
||||
run_as_admin=False, run_as=None, become_info=None):
|
||||
if name is None:
|
||||
name = "Adhoc-task-{}-{}".format(
|
||||
get_short_uuid_str(),
|
||||
timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
|
||||
inventory = get_inventory(
|
||||
hostname_list, run_as_admin=run_as_admin,
|
||||
run_as=run_as, become_info=become_info
|
||||
)
|
||||
runner = AdHocRunner(inventory)
|
||||
return runner.run(tasks, pattern, play_name=name)
|
||||
|
||||
|
||||
def create_and_run_adhoc(hostname_list, pattern, tasks, name=None,
|
||||
run_as_admin=False, run_as=None, become_info=None):
|
||||
if name is None:
|
||||
name = "Adhoc-task-{}-{}".format(
|
||||
get_short_uuid_str(),
|
||||
timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
task = Task(name=name)
|
||||
task.save()
|
||||
adhoc = AdHoc(
|
||||
task=task, pattern=pattern, name=name,
|
||||
run_as_admin=run_as_admin, run_as=run_as
|
||||
)
|
||||
adhoc.hosts = hostname_list
|
||||
adhoc.tasks = tasks
|
||||
adhoc.become = become_info
|
||||
adhoc.save()
|
||||
|
||||
|
||||
def get_task_by_name(name):
|
||||
task = get_object_or_none(Task, name=name)
|
||||
return task
|
||||
|
||||
|
||||
def create_task(name, created_by=""):
|
||||
return Task.objects.create(name=name, created_by=created_by)
|
||||
|
||||
|
||||
def create_adhoc(task, hosts, tasks, pattern='all', options=None,
|
||||
run_as_admin=False, run_as="",
|
||||
become_info=None, created_by=""):
|
||||
adhoc = AdHoc(task=task, pattern=pattern, run_as_admin=run_as_admin,
|
||||
run_as=run_as, created_by=created_by)
|
||||
adhoc.hosts = hosts
|
||||
adhoc.tasks = tasks
|
||||
adhoc.options = options
|
||||
adhoc.become = become_info
|
||||
adhoc.save()
|
||||
return adhoc
|
||||
|
|
|
@ -9,40 +9,40 @@ from django.views.generic import ListView, DetailView, View
|
|||
from django.utils import timezone
|
||||
from django.shortcuts import redirect, reverse
|
||||
|
||||
from .models import AdHoc, AdHocData, AdHocRunHistory
|
||||
from .models import Task, AdHoc, AdHocRunHistory
|
||||
from ops.tasks import rerun_task
|
||||
|
||||
|
||||
class TaskListView(ListView):
|
||||
paginate_by = settings.CONFIG.DISPLAY_PER_PAGE
|
||||
model = AdHoc
|
||||
ordering = ('-date_start',)
|
||||
model = Task
|
||||
ordering = ('-date_created',)
|
||||
context_object_name = 'task_list'
|
||||
template_name = 'ops/task_list.html'
|
||||
date_format = '%m/%d/%Y'
|
||||
keyword = date_from_s = date_to_s = ''
|
||||
|
||||
def get_queryset(self):
|
||||
date_now = timezone.localtime(timezone.now())
|
||||
date_to_default = date_now.strftime(self.date_format)
|
||||
date_from_default = (date_now - timezone.timedelta(7)) \
|
||||
.strftime(self.date_format)
|
||||
date_to_default = timezone.now()
|
||||
date_from_default = timezone.now() - timezone.timedelta(7)
|
||||
date_from_default_s = date_from_default.strftime(self.date_format)
|
||||
date_to_default_s = date_to_default.strftime(self.date_format)
|
||||
|
||||
self.queryset = super(TaskListView, self).get_queryset()
|
||||
self.queryset = super().get_queryset()
|
||||
self.keyword = self.request.GET.get('keyword', '')
|
||||
self.date_from_s = self.request.GET.get('date_from', date_from_default)
|
||||
self.date_to_s = self.request.GET.get('date_to', date_to_default)
|
||||
self.date_from_s = self.request.GET.get('date_from', date_from_default_s)
|
||||
self.date_to_s = self.request.GET.get('date_to', date_to_default_s)
|
||||
|
||||
if self.date_from_s:
|
||||
date_from = datetime.strptime(self.date_from_s, self.date_format)
|
||||
date_from = date_from.replace(tzinfo=timezone.get_current_timezone())
|
||||
self.queryset = self.queryset.filter(date_start__gt=date_from)
|
||||
self.queryset = self.queryset.filter(date_created__gt=date_from)
|
||||
|
||||
if self.date_to_s:
|
||||
date_to = timezone.datetime.strptime(
|
||||
self.date_to_s + ' 23:59:59', '%m/%d/%Y %H:%M:%S')
|
||||
date_to = date_to.replace(tzinfo=timezone.get_current_timezone())
|
||||
self.queryset = self.queryset.filter(date_finished__lt=date_to)
|
||||
self.queryset = self.queryset.filter(date_created__lt=date_to)
|
||||
|
||||
if self.keyword:
|
||||
self.queryset = self.queryset.filter(
|
||||
|
@ -63,17 +63,42 @@ class TaskListView(ListView):
|
|||
|
||||
|
||||
class TaskDetailView(DetailView):
|
||||
model = AdHocRunHistory
|
||||
model = Task
|
||||
template_name = 'ops/task_detail.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': 'Ops',
|
||||
'action': 'Playbook record detail',
|
||||
'results': json.loads(self.object.summary or '{}'),
|
||||
'action': 'Task detail',
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super(TaskDetailView, self).get_context_data(**kwargs)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class TaskAdhocView(DetailView):
|
||||
model = Task
|
||||
template_name = 'ops/task_adhoc.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': 'Ops',
|
||||
'action': 'Task versions',
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class TaskHistoryView(DetailView):
|
||||
model = Task
|
||||
template_name = 'ops/task_history.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': 'Ops',
|
||||
'action': 'Task run history',
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class TaskRunView(View):
|
||||
|
|
|
@ -3,46 +3,11 @@ from __future__ import absolute_import, unicode_literals
|
|||
|
||||
from celery import shared_task
|
||||
from common.utils import get_logger, encrypt_password
|
||||
from ops.utils import run_AdHoc
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
@shared_task(bind=True)
|
||||
def push_users(self, assets, users):
|
||||
"""
|
||||
user: {
|
||||
name: 'web',
|
||||
username: 'web',
|
||||
shell: '/bin/bash',
|
||||
password: '123123123',
|
||||
public_key: 'string',
|
||||
sudo: '/bin/whoami,/sbin/ifconfig'
|
||||
}
|
||||
"""
|
||||
if isinstance(users, dict):
|
||||
users = [users]
|
||||
if isinstance(assets, dict):
|
||||
assets = [assets]
|
||||
task_tuple = []
|
||||
|
||||
for user in users:
|
||||
# 添加用户, 设置公钥, 设置sudo
|
||||
task_tuple.extend([
|
||||
('user', 'name={} shell={} state=present password={}'.format(
|
||||
user['username'], user.get('shell', '/bin/bash'),
|
||||
encrypt_password(user.get('password', None)))),
|
||||
('authorized_key', "user={} state=present key='{}'".format(
|
||||
user['username'], user['public_key'])),
|
||||
('lineinfile',
|
||||
"dest=/etc/sudoers state=present regexp='^{0} ALL=' "
|
||||
"line='{0} ALL=(ALL) NOPASSWD: {1}' "
|
||||
"validate='visudo -cf %s'".format(
|
||||
user['username'], user.get('sudo', '/sbin/ifconfig')
|
||||
))
|
||||
])
|
||||
task_name = 'Push user {}'.format(','.join([user['name'] for user in users]))
|
||||
task = run_AdHoc(task_tuple, assets, pattern='all',
|
||||
task_name=task_name, task_id=self.request.id)
|
||||
return task
|
||||
pass
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
<i class="fa fa-coffee"></i> <span class="nav-label">{% trans 'Job Center' %}</span><span class="fa arrow"></span>
|
||||
</a>
|
||||
<ul class="nav nav-second-level">
|
||||
<li id="task"><a href="{% url 'ops:task-list' %}">{% trans 'Playbook' %}</a></li>
|
||||
<li id="task"><a href="{% url 'ops:task-list' %}">{% trans 'Tasks' %}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
|
Loading…
Reference in New Issue