From bc54685a319cd14684c61c8cf42a0309f98049cf Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Mon, 30 Oct 2023 14:24:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=B9=E5=AF=86=E8=AE=B0=E5=BD=95=20?= =?UTF-8?q?=E6=8E=A8=E9=80=81=E8=AE=B0=E5=BD=95=E5=8F=AF=E5=8D=95=E7=8B=AC?= =?UTF-8?q?=E6=89=A7=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../accounts/api/automations/change_secret.py | 32 +++++--- apps/accounts/api/automations/push_account.py | 1 + .../automations/change_secret/manager.py | 38 ++++----- .../automations/push_account/manager.py | 78 +------------------ apps/accounts/filters.py | 9 ++- apps/accounts/migrations/0003_automation.py | 2 +- .../models/automations/change_secret.py | 8 +- .../serializers/automations/change_secret.py | 4 +- apps/accounts/tasks/automation.py | 39 +++++++++- apps/assets/automations/base/manager.py | 3 - 10 files changed, 94 insertions(+), 120 deletions(-) diff --git a/apps/accounts/api/automations/change_secret.py b/apps/accounts/api/automations/change_secret.py index e334403f5..79df60c55 100644 --- a/apps/accounts/api/automations/change_secret.py +++ b/apps/accounts/api/automations/change_secret.py @@ -1,16 +1,19 @@ # -*- coding: utf-8 -*- # - -from rest_framework import mixins +from rest_framework import status, mixins +from rest_framework.decorators import action +from rest_framework.response import Response from accounts import serializers from accounts.const import AutomationTypes from accounts.models import ChangeSecretAutomation, ChangeSecretRecord +from accounts.tasks import execute_automation_record_task from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet from .base import ( AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi, AutomationNodeAddRemoveApi, AutomationExecutionViewSet ) +from ...filters import ChangeSecretRecordFilterSet __all__ = [ 'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet', @@ -29,18 +32,27 @@ class ChangeSecretAutomationViewSet(OrgBulkModelViewSet): class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet): serializer_class = serializers.ChangeSecretRecordSerializer - filter_fields = ('asset', 'execution_id') + filterset_class = ChangeSecretRecordFilterSet search_fields = ('asset__address',) + tp = AutomationTypes.change_secret + rbac_perms = { + 'execute': 'accounts.add_changesecretexecution', + } def get_queryset(self): - return ChangeSecretRecord.objects.filter( - execution__automation__type=AutomationTypes.change_secret - ) + return ChangeSecretRecord.objects.all() - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - eid = self.request.query_params.get('execution_id') - return queryset.filter(execution_id=eid) + @action(methods=['post'], detail=False, url_path='execute') + def execute(self, request, *args, **kwargs): + record_id = request.data.get('record_id') + record = self.get_queryset().filter(pk=record_id) + if not record: + return Response( + {'detail': 'record not found'}, + status=status.HTTP_404_NOT_FOUND + ) + task = execute_automation_record_task.delay(record_id, self.tp) + return Response({'task': task.id}, status=status.HTTP_200_OK) class ChangSecretExecutionViewSet(AutomationExecutionViewSet): diff --git a/apps/accounts/api/automations/push_account.py b/apps/accounts/api/automations/push_account.py index 94cf64d51..e8832815b 100644 --- a/apps/accounts/api/automations/push_account.py +++ b/apps/accounts/api/automations/push_account.py @@ -42,6 +42,7 @@ class PushAccountExecutionViewSet(AutomationExecutionViewSet): class PushAccountRecordViewSet(ChangeSecretRecordViewSet): serializer_class = serializers.ChangeSecretRecordSerializer + tp = AutomationTypes.push_account def get_queryset(self): return ChangeSecretRecord.objects.filter( diff --git a/apps/accounts/automations/change_secret/manager.py b/apps/accounts/automations/change_secret/manager.py index 18ee771ed..b25852189 100644 --- a/apps/accounts/automations/change_secret/manager.py +++ b/apps/accounts/automations/change_secret/manager.py @@ -1,6 +1,5 @@ import os import time -from collections import defaultdict from copy import deepcopy from django.conf import settings @@ -27,7 +26,7 @@ class ChangeSecretManager(AccountBasePlaybookManager): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.method_hosts_mapper = defaultdict(list) + self.record_id = self.execution.snapshot.get('record_id') self.secret_type = self.execution.snapshot.get('secret_type') self.secret_strategy = self.execution.snapshot.get( 'secret_strategy', SecretStrategy.custom @@ -96,17 +95,13 @@ class ChangeSecretManager(AccountBasePlaybookManager): accounts = self.get_accounts(account) if not accounts: - print('没有发现待改密账号: %s 用户ID: %s 类型: %s' % ( + print('没有发现待处理的账号: %s 用户ID: %s 类型: %s' % ( asset.name, self.account_ids, self.secret_type )) return [] - method_attr = getattr(automation, self.method_type() + '_method') - method_hosts = self.method_hosts_mapper[method_attr] - method_hosts = [h for h in method_hosts if h != host['name']] - inventory_hosts = [] records = [] - + inventory_hosts = [] if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY: print(f'Windows {asset} does not support ssh key push') return inventory_hosts @@ -116,13 +111,20 @@ class ChangeSecretManager(AccountBasePlaybookManager): h = deepcopy(host) secret_type = account.secret_type h['name'] += '(' + account.username + ')' - new_secret = self.get_secret(secret_type) + if self.secret_type is None: + new_secret = account.secret + else: + new_secret = self.get_secret(secret_type) + + if self.record_id is None: + recorder = ChangeSecretRecord( + asset=asset, account=account, execution=self.execution, + old_secret=account.secret, new_secret=new_secret, + ) + records.append(recorder) + else: + recorder = ChangeSecretRecord.objects.get(id=self.record_id) - recorder = ChangeSecretRecord( - asset=asset, account=account, execution=self.execution, - old_secret=account.secret, new_secret=new_secret, - ) - records.append(recorder) self.name_recorder_mapper[h['name']] = recorder private_key_path = None @@ -142,8 +144,6 @@ class ChangeSecretManager(AccountBasePlaybookManager): if asset.platform.type == 'oracle': h['account']['mode'] = 'sysdba' if account.privileged else None inventory_hosts.append(h) - method_hosts.append(h['name']) - self.method_hosts_mapper[method_attr] = method_hosts ChangeSecretRecord.objects.bulk_create(records) return inventory_hosts @@ -171,7 +171,7 @@ class ChangeSecretManager(AccountBasePlaybookManager): recorder.save() def on_runner_failed(self, runner, e): - logger.error("Change secret error: ", e) + logger.error("Account error: ", e) def check_secret(self): if self.secret_strategy == SecretStrategy.custom \ @@ -181,9 +181,11 @@ class ChangeSecretManager(AccountBasePlaybookManager): return True def run(self, *args, **kwargs): - if not self.check_secret(): + if self.secret_type and not self.check_secret(): return super().run(*args, **kwargs) + if self.record_id: + return recorders = self.name_recorder_mapper.values() recorders = list(recorders) self.send_recorder_mail(recorders) diff --git a/apps/accounts/automations/push_account/manager.py b/apps/accounts/automations/push_account/manager.py index 7f9afc796..12f630923 100644 --- a/apps/accounts/automations/push_account/manager.py +++ b/apps/accounts/automations/push_account/manager.py @@ -1,7 +1,4 @@ -from copy import deepcopy - -from accounts.const import AutomationTypes, SecretType, Connectivity -from assets.const import HostTypes +from accounts.const import AutomationTypes from common.utils import get_logger from ..base.manager import AccountBasePlaybookManager from ..change_secret.manager import ChangeSecretManager @@ -10,84 +7,11 @@ logger = get_logger(__name__) class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager): - ansible_account_prefer = '' @classmethod def method_type(cls): return AutomationTypes.push_account - def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs): - host = super(ChangeSecretManager, self).host_callback( - host, asset=asset, account=account, automation=automation, - path_dir=path_dir, **kwargs - ) - if host.get('error'): - return host - - accounts = self.get_accounts(account) - inventory_hosts = [] - if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY: - msg = f'Windows {asset} does not support ssh key push' - print(msg) - return inventory_hosts - - host['ssh_params'] = {} - for account in accounts: - h = deepcopy(host) - secret_type = account.secret_type - h['name'] += '(' + account.username + ')' - if self.secret_type is None: - new_secret = account.secret - else: - new_secret = self.get_secret(secret_type) - - self.name_recorder_mapper[h['name']] = { - 'account': account, 'new_secret': new_secret, - } - - private_key_path = None - if secret_type == SecretType.SSH_KEY: - private_key_path = self.generate_private_key_path(new_secret, path_dir) - new_secret = self.generate_public_key(new_secret) - - h['ssh_params'].update(self.get_ssh_params(account, new_secret, secret_type)) - h['account'] = { - 'name': account.name, - 'username': account.username, - 'secret_type': secret_type, - 'secret': new_secret, - 'private_key_path': private_key_path, - 'become': account.get_ansible_become_auth(), - } - if asset.platform.type == 'oracle': - h['account']['mode'] = 'sysdba' if account.privileged else None - inventory_hosts.append(h) - return inventory_hosts - - def on_host_success(self, host, result): - account_info = self.name_recorder_mapper.get(host) - if not account_info: - return - - account = account_info['account'] - new_secret = account_info['new_secret'] - if not account: - return - account.secret = new_secret - account.save(update_fields=['secret']) - account.set_connectivity(Connectivity.OK) - - def on_host_error(self, host, error, result): - pass - - def on_runner_failed(self, runner, e): - logger.error("Pust account error: {}".format(e)) - - def run(self, *args, **kwargs): - if self.secret_type and not self.check_secret(): - return - super(ChangeSecretManager, self).run(*args, **kwargs) - # @classmethod # def trigger_by_asset_create(cls, asset): # automations = PushAccountAutomation.objects.filter( diff --git a/apps/accounts/filters.py b/apps/accounts/filters.py index e71a5b9fb..fe05c78dd 100644 --- a/apps/accounts/filters.py +++ b/apps/accounts/filters.py @@ -5,7 +5,7 @@ from django_filters import rest_framework as drf_filters from assets.models import Node from common.drf.filters import BaseFilterSet -from .models import Account, GatheredAccount +from .models import Account, GatheredAccount, ChangeSecretRecord class AccountFilterSet(BaseFilterSet): @@ -59,3 +59,10 @@ class GatheredAccountFilterSet(BaseFilterSet): class Meta: model = GatheredAccount fields = ['id', 'asset_id', 'username'] + + +class ChangeSecretRecordFilterSet(BaseFilterSet): + + class Meta: + model = ChangeSecretRecord + fields = ['asset_id', 'execution_id'] diff --git a/apps/accounts/migrations/0003_automation.py b/apps/accounts/migrations/0003_automation.py index 98f5f01f2..4840cc2ad 100644 --- a/apps/accounts/migrations/0003_automation.py +++ b/apps/accounts/migrations/0003_automation.py @@ -116,7 +116,7 @@ class Migration(migrations.Migration): ('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='New secret')), ('date_started', models.DateTimeField(blank=True, null=True, verbose_name='Date started')), ('date_finished', models.DateTimeField(blank=True, null=True, verbose_name='Date finished')), - ('status', models.CharField(default='pending', max_length=16)), + ('status', models.CharField(default='pending', max_length=16, verbose_name='Status')), ('error', models.TextField(blank=True, null=True, verbose_name='Error')), ('account', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.account')), diff --git a/apps/accounts/models/automations/change_secret.py b/apps/accounts/models/automations/change_secret.py index 9c2086142..c575ac161 100644 --- a/apps/accounts/models/automations/change_secret.py +++ b/apps/accounts/models/automations/change_secret.py @@ -40,7 +40,7 @@ class ChangeSecretRecord(JMSBaseModel): new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('New secret')) date_started = models.DateTimeField(blank=True, null=True, verbose_name=_('Date started')) date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('Date finished')) - status = models.CharField(max_length=16, default='pending') + status = models.CharField(max_length=16, default='pending', verbose_name=_('Status')) error = models.TextField(blank=True, null=True, verbose_name=_('Error')) class Meta: @@ -49,9 +49,3 @@ class ChangeSecretRecord(JMSBaseModel): def __str__(self): return self.account.__str__() - - @property - def timedelta(self): - if self.date_started and self.date_finished: - return self.date_finished - self.date_started - return None diff --git a/apps/accounts/serializers/automations/change_secret.py b/apps/accounts/serializers/automations/change_secret.py index 23effe738..0ef0c4b53 100644 --- a/apps/accounts/serializers/automations/change_secret.py +++ b/apps/accounts/serializers/automations/change_secret.py @@ -112,8 +112,8 @@ class ChangeSecretRecordSerializer(serializers.ModelSerializer): class Meta: model = ChangeSecretRecord fields = [ - 'id', 'asset', 'account', 'date_started', 'date_finished', - 'timedelta', 'is_success', 'error', 'execution', + 'id', 'asset', 'account', 'date_finished', + 'status', 'is_success', 'error', 'execution', ] read_only_fields = fields diff --git a/apps/accounts/tasks/automation.py b/apps/accounts/tasks/automation.py index 359a15913..7c81e417f 100644 --- a/apps/accounts/tasks/automation.py +++ b/apps/accounts/tasks/automation.py @@ -1,7 +1,8 @@ from celery import shared_task -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import gettext_lazy as _, gettext_noop from accounts.const import AutomationTypes +from accounts.tasks.common import quickstart_automation_by_snapshot from common.utils import get_logger, get_object_or_none from orgs.utils import tmp_to_org, tmp_to_root_org @@ -33,3 +34,39 @@ def execute_account_automation_task(pid, trigger, tp): return with tmp_to_org(instance.org): instance.execute(trigger) + + +def record_task_activity_callback(self, record_id, *args, **kwargs): + from accounts.models import ChangeSecretRecord + with tmp_to_root_org(): + record = get_object_or_none(ChangeSecretRecord, id=record_id) + if not record: + return + resource_ids = [record.id] + org_id = record.execution.org_id + return resource_ids, org_id + + +@shared_task( + queue='ansible', verbose_name=_('Execute automation record'), + activity_callback=record_task_activity_callback +) +def execute_automation_record_task(record_id, tp): + from accounts.models import ChangeSecretRecord + with tmp_to_root_org(): + instance = get_object_or_none(ChangeSecretRecord, pk=record_id) + if not instance: + logger.error("No automation record found: {}".format(record_id)) + return + + task_name = gettext_noop('Execute automation record') + task_snapshot = { + 'secret': instance.new_secret, + 'secret_type': instance.execution.snapshot.get('secret_type'), + 'accounts': [str(instance.account_id)], + 'assets': [str(instance.asset_id)], + 'params': {}, + 'record_id': record_id, + } + with tmp_to_org(instance.execution.org_id): + quickstart_automation_by_snapshot(task_name, tp, task_snapshot) diff --git a/apps/assets/automations/base/manager.py b/apps/assets/automations/base/manager.py index 42664a72a..817952d46 100644 --- a/apps/assets/automations/base/manager.py +++ b/apps/assets/automations/base/manager.py @@ -2,7 +2,6 @@ import hashlib import json import os import shutil -from collections import defaultdict from socket import gethostname import yaml @@ -37,8 +36,6 @@ class BasePlaybookManager: } # 根据执行方式就行分组, 不同资产的改密、推送等操作可能会使用不同的执行方式 # 然后根据执行方式分组, 再根据 bulk_size 分组, 生成不同的 playbook - # 避免一个 playbook 中包含太多的主机 - self.method_hosts_mapper = defaultdict(list) self.playbooks = [] self.gateway_servers = dict() params = self.execution.snapshot.get('params')