diff --git a/apps/assets/api.py b/apps/assets/api.py index da363dee4..3cc9e0773 100644 --- a/apps/assets/api.py +++ b/apps/assets/api.py @@ -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: diff --git a/apps/assets/models.py b/apps/assets/models.py deleted file mode 100644 index ec51c5a2b..000000000 --- a/apps/assets/models.py +++ /dev/null @@ -1,2 +0,0 @@ -# -*- coding: utf-8 -*- -# diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index 939a1307b..38ed90721 100644 --- a/apps/assets/models/__init__.py +++ b/apps/assets/models/__init__.py @@ -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 * diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index 00c5b8582..e5656ca97 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -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 diff --git a/apps/assets/models/group.py b/apps/assets/models/group.py index 49f25e51f..31a1cd2d5 100644 --- a/apps/assets/models/group.py +++ b/apps/assets/models/group.py @@ -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')) diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 5b2388cf0..47bc41ce5 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -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, diff --git a/apps/assets/serializers.py b/apps/assets/serializers.py index 5a41cc386..0a51adfb7 100644 --- a/apps/assets/serializers.py +++ b/apps/assets/serializers.py @@ -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): diff --git a/apps/assets/tasks.py b/apps/assets/tasks.py index 2c5267281..5ac899abd 100644 --- a/apps/assets/tasks.py +++ b/apps/assets/tasks.py @@ -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 diff --git a/apps/assets/templates/assets/admin_user_assets.html b/apps/assets/templates/assets/admin_user_assets.html new file mode 100644 index 000000000..62244b5f0 --- /dev/null +++ b/apps/assets/templates/assets/admin_user_assets.html @@ -0,0 +1,387 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} + +{% block custom_head_css_js %} + + +{% endblock %} + +{% block content %} +
+
+
+
+ +
+
+
+
+ {% trans 'Asset list of ' %} {{ admin_user.name }} {{ total_amount }} {{ unreachable_amount }} +
+ + + + + + + + + + +
+
+
+ + + + + + + + + + + {% for asset in page_obj %} + + + + + {% if asset.is_connective == '1' %} + + {% else %} + + {% endif %} + + {% endfor %} + +
{% trans 'Hostname' %}{% trans 'IP' %}{% trans 'Port' %}{% trans 'Alive' %}
{{ asset.hostname }}{{ asset.ip }}{{ asset.port }} + + + +
+
+ {% include '_pagination.html' %} +
+
+
+
+
+
+
+ {% trans 'Quick update' %} +
+
+ + + + + + + +
{% trans 'Retest connectivity' %}: + + + +
+
+
+
+
+ {% trans 'Replace asset admin user with this' %} +
+
+ + + + + + + + + + + +
+ +
+ +
+
+
+ +
+
+ {% trans 'Replace asset group admin user with this' %} +
+
+ + + + + + + + + + + +
+ +
+ +
+
+
+
+
+
+
+
+
+ + +{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/assets/templates/assets/admin_user_detail.html b/apps/assets/templates/assets/admin_user_detail.html index 7e5e5970d..f11834880 100644 --- a/apps/assets/templates/assets/admin_user_detail.html +++ b/apps/assets/templates/assets/admin_user_detail.html @@ -15,7 +15,10 @@
- -
-
- {% trans 'Asset list of ' %} {{ admin_user.name }} {{ paginator.count }} -
- - - - - - - - - - -
-
-
- - - - - - - - - - - -{# {% for asset in page_obj %}#} -{# #} -{# #} -{# #} -{# #} -{# #} -{# #} -{# {% endfor %}#} - -
{% trans 'Hostname' %}{% trans 'IP' %}{% trans 'Port' %}{% trans 'Alive' %}{% trans 'Action' %}
{{ asset.hostname }}{{ asset.ip }}{{ asset.port }}Alive
-{#
#} -{# {% include '_pagination.html' %}#} -{#
#} -
-
@@ -128,24 +86,6 @@ - - - - - - - - - - - -
{% trans 'Get install script' %}: - - - -
{% trans 'Retest asset connectivity' %}: - - - -
{% trans 'Reset private key' %}: @@ -153,62 +93,14 @@
-
-
- -
-
- {% trans 'Replace asset admin user with this' %} -
-
- - - - - - - - - - - -
- -
- -
-
-
- -
-
- {% trans 'Replace asset admin user with this admin user' %} -
-
- - - - - - - - - - + + + +
- -
- -
{% trans 'Reset password' %}: + + + +
@@ -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{% trans 'Name' %} {% trans 'Username' %} {% trans 'Asset num' %} + {% trans 'Unreachable' %} {% trans 'Comment' %} {% trans 'Action' %} @@ -34,11 +35,7 @@ $(document).ready(function(){ var detail_btn = '' + cellData + ''; $(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('' + innerHtml + ''); - }}, - {targets: 5, createdCell: function (td, cellData, rowData) { + {targets: 6, createdCell: function (td, cellData, rowData) { {# var script_btn = '{% trans "Script" %}'.replace('99991937', cellData);#} var update_btn = '{% trans "Update" %}'.replace('99991937', cellData); var del_btn = '{% trans "Delete" %}'.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); }) diff --git a/apps/assets/templates/assets/system_user_asset.html b/apps/assets/templates/assets/system_user_asset.html index 9795b4667..fda20d19f 100644 --- a/apps/assets/templates/assets/system_user_asset.html +++ b/apps/assets/templates/assets/system_user_asset.html @@ -58,18 +58,16 @@ {{ asset.ip }} {{ asset.port }} + - + {% endfor %} -{#
#} -{# {% include '_pagination.html' %}#} -{#
#}
diff --git a/apps/assets/templates/assets/system_user_list.html b/apps/assets/templates/assets/system_user_list.html index 5e8804a86..9192daefb 100644 --- a/apps/assets/templates/assets/system_user_list.html +++ b/apps/assets/templates/assets/system_user_list.html @@ -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() }; diff --git a/apps/assets/urls/views_urls.py b/apps/assets/urls/views_urls.py index 0353b5b36..b83a00120 100644 --- a/apps/assets/urls/views_urls.py +++ b/apps/assets/urls/views_urls.py @@ -41,6 +41,7 @@ urlpatterns = [ url(r'^admin-user/(?P[0-9a-zA-Z\-]+)/$', views.AdminUserDetailView.as_view(), name='admin-user-detail'), url(r'^admin-user/(?P[0-9a-zA-Z\-]+)/update/$', views.AdminUserUpdateView.as_view(), name='admin-user-update'), url(r'^admin-user/(?P[0-9a-zA-Z\-]+)/delete/$', views.AdminUserDeleteView.as_view(), name='admin-user-delete'), + url(r'^admin-user/(?P[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'), diff --git a/apps/assets/utils.py b/apps/assets/utils.py index 6e9493f3f..f7efb50ff 100644 --- a/apps/assets/utils.py +++ b/apps/assets/utils.py @@ -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 diff --git a/apps/assets/views/admin_user.py b/apps/assets/views/admin_user.py index b45437d3a..0cc4f66f2 100644 --- a/apps/assets/views/admin_user.py +++ b/apps/assets/views/admin_user.py @@ -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' diff --git a/apps/assets/views/system_user.py b/apps/assets/views/system_user.py index 801bc78f4..0d3ccfe8b 100644 --- a/apps/assets/views/system_user.py +++ b/apps/assets/views/system_user.py @@ -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) diff --git a/apps/common/celery.py b/apps/common/celery.py index 18b5f755d..98270a12c 100644 --- a/apps/common/celery.py +++ b/apps/common/celery.py @@ -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': (), }, diff --git a/apps/common/utils.py b/apps/common/utils.py index fb9bc6937..afbfa06cb 100644 --- a/apps/common/utils.py +++ b/apps/common/utils.py @@ -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() diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index 795223d84..596563981 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -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 = [ diff --git a/apps/ops/ansible/callback.py b/apps/ops/ansible/callback.py index ac46d82cf..cc4bc6215 100644 --- a/apps/ops/ansible/callback.py +++ b/apps/ops/ansible/callback.py @@ -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) diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index fb58b4ffd..d3d342368 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -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) + + diff --git a/apps/ops/ansible/runner.py b/apps/ops/ansible/runner.py index 6fa765a67..37a818fe0 100644 --- a/apps/ops/ansible/runner.py +++ b/apps/ops/ansible/runner.py @@ -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) diff --git a/apps/ops/ansible/test_inventory.bak b/apps/ops/ansible/test_inventory.py similarity index 93% rename from apps/ops/ansible/test_inventory.bak rename to apps/ops/ansible/test_inventory.py index d4ff43f24..00c7fa459 100644 --- a/apps/ops/ansible/test_inventory.bak +++ b/apps/ops/ansible/test_inventory.py @@ -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) diff --git a/apps/ops/ansible/test_runner.bak b/apps/ops/ansible/test_runner.py similarity index 84% rename from apps/ops/ansible/test_runner.bak rename to apps/ops/ansible/test_runner.py index ec1da5fa5..07c9051f3 100644 --- a/apps/ops/ansible/test_runner.bak +++ b/apps/ops/ansible/test_runner.py @@ -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') diff --git a/apps/ops/api.py b/apps/ops/api.py index 35eadd2ac..7e16974d7 100644 --- a/apps/ops/api.py +++ b/apps/ops/api.py @@ -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 diff --git a/apps/ops/inventory.py b/apps/ops/inventory.py new file mode 100644 index 000000000..ce3dd96de --- /dev/null +++ b/apps/ops/inventory.py @@ -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() diff --git a/apps/ops/models.py b/apps/ops/models.py index a91b0cfa9..e242a0867 100644 --- a/apps/ops/models.py +++ b/apps/ops/models.py @@ -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 diff --git a/apps/ops/serializers.py b/apps/ops/serializers.py index 35727be20..2ffb21c06 100644 --- a/apps/ops/serializers.py +++ b/apps/ops/serializers.py @@ -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 diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py index 669b3741e..c5298377d 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -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) diff --git a/apps/ops/templates/ops/task_adhoc.html b/apps/ops/templates/ops/task_adhoc.html new file mode 100644 index 000000000..21232ad82 --- /dev/null +++ b/apps/ops/templates/ops/task_adhoc.html @@ -0,0 +1,117 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} + +{% block custom_head_css_js %} + + + + +{% endblock %} +{% block content %} +
+
+
+
+ +
+
+
+
+ {% trans 'Versions of ' %} {{ object.name }} +
+ + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + +
+ + {% trans 'Version' %}{% trans 'Hosts' %}{% trans 'Pattern' %}{% trans 'Run as' %}{% trans 'Become' %}{% trans 'Datetime' %}
+
+
+
+
+
+
+
+
+{% endblock %} + +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/ops/templates/ops/task_detail.html b/apps/ops/templates/ops/task_detail.html index ad3a01624..bc98f9ff1 100644 --- a/apps/ops/templates/ops/task_detail.html +++ b/apps/ops/templates/ops/task_detail.html @@ -15,8 +15,14 @@
@@ -43,49 +49,47 @@ - - + + - - + + - - + + + + + + + + + + - + - + - {% if object.is_finished %} - - {% else %} - - {% endif %} + - + @@ -94,31 +98,6 @@
{% trans 'UUID' %}:{{ object.uuid }}{% trans 'ID' %}:{{ object.id }}
{% trans 'Name' %}: {{ object.name }}
{% trans 'Date start' %}:{{ object.date_start }}{% trans 'Date created' %}:{{ object.date_created }}
{% trans 'Date finished' %}:{{ object.date_finished }}{% trans 'Total versions' %}{{ object.adhoc.all |length }}
{% trans 'Last version' %}{{ object.get_latest_adhoc.short_id }}
{% trans 'Latest run' %}:{{ object.get_latest_history.date_start }}
{% trans 'Time delta' %}:{{ object.timedelta}} s{{ object.get_latest_history.timedelta|floatformat}} s
{% trans 'Is finished' %}:{{ object.is_finished|yesno:"Yes,No,Unkown" }}{{ object.get_latest_history.is_finished|yesno:"Yes,No,Unkown" }}
{% trans 'Is success ' %}:{{ object.is_success|yesno:"Yes,No,Unkown" }} -
-
- 40% Complete (success) -
-
-
{{ object.get_latest_history.is_success|yesno:"Yes,No,Unkown" }}
{% trans 'assets' %}:{% trans 'Conents' %}: - {% for asset in object.total_assets %} - {{ asset.hostname }}
+ {% for task in object.get_latest_adhoc.tasks %} + {{ task.name }} : {{ task.action.module }}
{% endfor %}
- -
-
- Result -
- - - - - - - - - - -
-
-
-
-{{ object.result }}
-                                    
-
-
-
@@ -154,7 +133,7 @@
- {% for host in results.success %} + {% for host in object.get_latest_history.summary.contacted %} {% if forloop.first %} {% else %} diff --git a/apps/ops/templates/ops/task_history.html b/apps/ops/templates/ops/task_history.html new file mode 100644 index 000000000..6fde3b742 --- /dev/null +++ b/apps/ops/templates/ops/task_history.html @@ -0,0 +1,123 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} + +{% block custom_head_css_js %} + + + + +{% endblock %} +{% block content %} +
+
+
+
+ +
+
+
+
+ {% trans 'Versions of ' %} {{ object.name }} +
+ + + + + + + + + + +
+
+
+
+ + + + + + + + + + + + + +
+ + {% trans 'Date start' %}{% trans 'F/S/T' %}{% trans 'Is finished' %}{% trans 'Is success' %}{% trans 'Time' %}
+
+
+
+ + + + + +{% endblock %} + +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/ops/templates/ops/task_list.html b/apps/ops/templates/ops/task_list.html index dac614805..765525318 100644 --- a/apps/ops/templates/ops/task_list.html +++ b/apps/ops/templates/ops/task_list.html @@ -22,7 +22,7 @@
- +
@@ -37,10 +37,11 @@ {% block table_head %} {% trans 'Name' %} - {% trans 'Asset' %} + {% trans 'F/S/T' %} + {% trans 'Versions' %} + {% trans 'Hosts' %} {% trans 'Success' %} - {% trans 'Finished' %} - {% trans 'Date start' %} + {% trans 'Date' %} {% trans 'Time' %} {% trans 'Action' %} {% endblock %} @@ -49,26 +50,23 @@ {% for object in task_list %} - {{ object.name }} - {{ object.total_assets|length }} + {{ object.name }} - {% if object.is_success %} + {{ object.get_all_run_times.failed }}/{{ object.get_all_run_times.success}}/{{ object.get_all_run_times.total}} + + {{ object.adhoc.all | length}} + {{ object.get_latest_adhoc.hosts | length}} + + {% if object.get_latest_history.is_success %} {% else %} {% endif %} + {{ object.get_latest_history.date_start }} + {{ object.get_latest_history.timedelta|floatformat }} s - {% if object.is_finished %} - - {% else %} - - {% endif %} - - {{ object.date_start }} - {{ object.timedelta }} s - - {% trans "Run again" %} + {% trans "Run" %} {% trans "Delete" %} diff --git a/apps/ops/test_utils.py b/apps/ops/test_utils.py index 668ae1f6f..5e5905519 100644 --- a/apps/ops/test_utils.py +++ b/apps/ops/test_utils.py @@ -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'}}, diff --git a/apps/ops/urls/api_urls.py b/apps/ops/urls/api_urls.py index c96bfb926..af16c3de1 100644 --- a/apps/ops/urls/api_urls.py +++ b/apps/ops/urls/api_urls.py @@ -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 = [] diff --git a/apps/ops/urls/view_urls.py b/apps/ops/urls/view_urls.py index 940a7c303..e24f4031d 100644 --- a/apps/ops/urls/view_urls.py +++ b/apps/ops/urls/view_urls.py @@ -11,5 +11,7 @@ urlpatterns = [ # TResource Task url url(r'^task/$', views.TaskListView.as_view(), name='task-list'), url(r'^task/(?P[0-9a-zA-Z\-]+)/$', views.TaskDetailView.as_view(), name='task-detail'), + url(r'^task/(?P[0-9a-zA-Z\-]+)/adhoc/$', views.TaskAdhocView.as_view(), name='task-adhoc'), + url(r'^task/(?P[0-9a-zA-Z\-]+)/history/$', views.TaskHistoryView.as_view(), name='task-history'), url(r'^task/(?P[0-9a-zA-Z\-]+)/run/$', views.TaskRunView.as_view(), name='task-run'), ] \ No newline at end of file diff --git a/apps/ops/utils.py b/apps/ops/utils.py index 76a6255e8..1bace8351 100644 --- a/apps/ops/utils.py +++ b/apps/ops/utils.py @@ -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 diff --git a/apps/ops/views.py b/apps/ops/views.py index 8f2afbb0e..a740890a6 100644 --- a/apps/ops/views.py +++ b/apps/ops/views.py @@ -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): diff --git a/apps/perms/tasks.py b/apps/perms/tasks.py index 6bef200cd..4b66b5727 100644 --- a/apps/perms/tasks.py +++ b/apps/perms/tasks.py @@ -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 diff --git a/apps/templates/_nav.html b/apps/templates/_nav.html index 748aa2640..cf7877f6f 100644 --- a/apps/templates/_nav.html +++ b/apps/templates/_nav.html @@ -52,7 +52,7 @@ {% trans 'Job Center' %}