feat: 改密记录 推送记录可单独执行

pull/12021/head
feng 2023-10-30 14:24:34 +08:00 committed by Bryan
parent ee586954f8
commit bc54685a31
10 changed files with 94 additions and 120 deletions

View File

@ -1,16 +1,19 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from rest_framework import status, mixins
from rest_framework import mixins from rest_framework.decorators import action
from rest_framework.response import Response
from accounts import serializers from accounts import serializers
from accounts.const import AutomationTypes from accounts.const import AutomationTypes
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord from accounts.models import ChangeSecretAutomation, ChangeSecretRecord
from accounts.tasks import execute_automation_record_task
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
from .base import ( from .base import (
AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi, AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi,
AutomationNodeAddRemoveApi, AutomationExecutionViewSet AutomationNodeAddRemoveApi, AutomationExecutionViewSet
) )
from ...filters import ChangeSecretRecordFilterSet
__all__ = [ __all__ = [
'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet', 'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet',
@ -29,18 +32,27 @@ class ChangeSecretAutomationViewSet(OrgBulkModelViewSet):
class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet): class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
serializer_class = serializers.ChangeSecretRecordSerializer serializer_class = serializers.ChangeSecretRecordSerializer
filter_fields = ('asset', 'execution_id') filterset_class = ChangeSecretRecordFilterSet
search_fields = ('asset__address',) search_fields = ('asset__address',)
tp = AutomationTypes.change_secret
rbac_perms = {
'execute': 'accounts.add_changesecretexecution',
}
def get_queryset(self): def get_queryset(self):
return ChangeSecretRecord.objects.filter( return ChangeSecretRecord.objects.all()
execution__automation__type=AutomationTypes.change_secret
)
def filter_queryset(self, queryset): @action(methods=['post'], detail=False, url_path='execute')
queryset = super().filter_queryset(queryset) def execute(self, request, *args, **kwargs):
eid = self.request.query_params.get('execution_id') record_id = request.data.get('record_id')
return queryset.filter(execution_id=eid) 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): class ChangSecretExecutionViewSet(AutomationExecutionViewSet):

View File

@ -42,6 +42,7 @@ class PushAccountExecutionViewSet(AutomationExecutionViewSet):
class PushAccountRecordViewSet(ChangeSecretRecordViewSet): class PushAccountRecordViewSet(ChangeSecretRecordViewSet):
serializer_class = serializers.ChangeSecretRecordSerializer serializer_class = serializers.ChangeSecretRecordSerializer
tp = AutomationTypes.push_account
def get_queryset(self): def get_queryset(self):
return ChangeSecretRecord.objects.filter( return ChangeSecretRecord.objects.filter(

View File

@ -1,6 +1,5 @@
import os import os
import time import time
from collections import defaultdict
from copy import deepcopy from copy import deepcopy
from django.conf import settings from django.conf import settings
@ -27,7 +26,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*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_type = self.execution.snapshot.get('secret_type')
self.secret_strategy = self.execution.snapshot.get( self.secret_strategy = self.execution.snapshot.get(
'secret_strategy', SecretStrategy.custom 'secret_strategy', SecretStrategy.custom
@ -96,17 +95,13 @@ class ChangeSecretManager(AccountBasePlaybookManager):
accounts = self.get_accounts(account) accounts = self.get_accounts(account)
if not accounts: if not accounts:
print('没有发现待改密账号: %s 用户ID: %s 类型: %s' % ( print('没有发现待处理的账号: %s 用户ID: %s 类型: %s' % (
asset.name, self.account_ids, self.secret_type asset.name, self.account_ids, self.secret_type
)) ))
return [] 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 = [] records = []
inventory_hosts = []
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY: if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
print(f'Windows {asset} does not support ssh key push') print(f'Windows {asset} does not support ssh key push')
return inventory_hosts return inventory_hosts
@ -116,13 +111,20 @@ class ChangeSecretManager(AccountBasePlaybookManager):
h = deepcopy(host) h = deepcopy(host)
secret_type = account.secret_type secret_type = account.secret_type
h['name'] += '(' + account.username + ')' 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 self.name_recorder_mapper[h['name']] = recorder
private_key_path = None private_key_path = None
@ -142,8 +144,6 @@ class ChangeSecretManager(AccountBasePlaybookManager):
if asset.platform.type == 'oracle': if asset.platform.type == 'oracle':
h['account']['mode'] = 'sysdba' if account.privileged else None h['account']['mode'] = 'sysdba' if account.privileged else None
inventory_hosts.append(h) inventory_hosts.append(h)
method_hosts.append(h['name'])
self.method_hosts_mapper[method_attr] = method_hosts
ChangeSecretRecord.objects.bulk_create(records) ChangeSecretRecord.objects.bulk_create(records)
return inventory_hosts return inventory_hosts
@ -171,7 +171,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
recorder.save() recorder.save()
def on_runner_failed(self, runner, e): def on_runner_failed(self, runner, e):
logger.error("Change secret error: ", e) logger.error("Account error: ", e)
def check_secret(self): def check_secret(self):
if self.secret_strategy == SecretStrategy.custom \ if self.secret_strategy == SecretStrategy.custom \
@ -181,9 +181,11 @@ class ChangeSecretManager(AccountBasePlaybookManager):
return True return True
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
if not self.check_secret(): if self.secret_type and not self.check_secret():
return return
super().run(*args, **kwargs) super().run(*args, **kwargs)
if self.record_id:
return
recorders = self.name_recorder_mapper.values() recorders = self.name_recorder_mapper.values()
recorders = list(recorders) recorders = list(recorders)
self.send_recorder_mail(recorders) self.send_recorder_mail(recorders)

View File

@ -1,7 +1,4 @@
from copy import deepcopy from accounts.const import AutomationTypes
from accounts.const import AutomationTypes, SecretType, Connectivity
from assets.const import HostTypes
from common.utils import get_logger from common.utils import get_logger
from ..base.manager import AccountBasePlaybookManager from ..base.manager import AccountBasePlaybookManager
from ..change_secret.manager import ChangeSecretManager from ..change_secret.manager import ChangeSecretManager
@ -10,84 +7,11 @@ logger = get_logger(__name__)
class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager): class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
ansible_account_prefer = ''
@classmethod @classmethod
def method_type(cls): def method_type(cls):
return AutomationTypes.push_account 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 # @classmethod
# def trigger_by_asset_create(cls, asset): # def trigger_by_asset_create(cls, asset):
# automations = PushAccountAutomation.objects.filter( # automations = PushAccountAutomation.objects.filter(

View File

@ -5,7 +5,7 @@ from django_filters import rest_framework as drf_filters
from assets.models import Node from assets.models import Node
from common.drf.filters import BaseFilterSet from common.drf.filters import BaseFilterSet
from .models import Account, GatheredAccount from .models import Account, GatheredAccount, ChangeSecretRecord
class AccountFilterSet(BaseFilterSet): class AccountFilterSet(BaseFilterSet):
@ -59,3 +59,10 @@ class GatheredAccountFilterSet(BaseFilterSet):
class Meta: class Meta:
model = GatheredAccount model = GatheredAccount
fields = ['id', 'asset_id', 'username'] fields = ['id', 'asset_id', 'username']
class ChangeSecretRecordFilterSet(BaseFilterSet):
class Meta:
model = ChangeSecretRecord
fields = ['asset_id', 'execution_id']

View File

@ -116,7 +116,7 @@ class Migration(migrations.Migration):
('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='New secret')), ('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_started', models.DateTimeField(blank=True, null=True, verbose_name='Date started')),
('date_finished', models.DateTimeField(blank=True, null=True, verbose_name='Date finished')), ('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')), ('error', models.TextField(blank=True, null=True, verbose_name='Error')),
('account', ('account',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.account')), models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.account')),

View File

@ -40,7 +40,7 @@ class ChangeSecretRecord(JMSBaseModel):
new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('New secret')) 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_started = models.DateTimeField(blank=True, null=True, verbose_name=_('Date started'))
date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('Date finished')) 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')) error = models.TextField(blank=True, null=True, verbose_name=_('Error'))
class Meta: class Meta:
@ -49,9 +49,3 @@ class ChangeSecretRecord(JMSBaseModel):
def __str__(self): def __str__(self):
return self.account.__str__() 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

View File

@ -112,8 +112,8 @@ class ChangeSecretRecordSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = ChangeSecretRecord model = ChangeSecretRecord
fields = [ fields = [
'id', 'asset', 'account', 'date_started', 'date_finished', 'id', 'asset', 'account', 'date_finished',
'timedelta', 'is_success', 'error', 'execution', 'status', 'is_success', 'error', 'execution',
] ]
read_only_fields = fields read_only_fields = fields

View File

@ -1,7 +1,8 @@
from celery import shared_task 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.const import AutomationTypes
from accounts.tasks.common import quickstart_automation_by_snapshot
from common.utils import get_logger, get_object_or_none from common.utils import get_logger, get_object_or_none
from orgs.utils import tmp_to_org, tmp_to_root_org from orgs.utils import tmp_to_org, tmp_to_root_org
@ -33,3 +34,39 @@ def execute_account_automation_task(pid, trigger, tp):
return return
with tmp_to_org(instance.org): with tmp_to_org(instance.org):
instance.execute(trigger) 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)

View File

@ -2,7 +2,6 @@ import hashlib
import json import json
import os import os
import shutil import shutil
from collections import defaultdict
from socket import gethostname from socket import gethostname
import yaml import yaml
@ -37,8 +36,6 @@ class BasePlaybookManager:
} }
# 根据执行方式就行分组, 不同资产的改密、推送等操作可能会使用不同的执行方式 # 根据执行方式就行分组, 不同资产的改密、推送等操作可能会使用不同的执行方式
# 然后根据执行方式分组, 再根据 bulk_size 分组, 生成不同的 playbook # 然后根据执行方式分组, 再根据 bulk_size 分组, 生成不同的 playbook
# 避免一个 playbook 中包含太多的主机
self.method_hosts_mapper = defaultdict(list)
self.playbooks = [] self.playbooks = []
self.gateway_servers = dict() self.gateway_servers = dict()
params = self.execution.snapshot.get('params') params = self.execution.snapshot.get('params')