mirror of https://github.com/jumpserver/jumpserver
perf: Push account recorder
parent
a121bf41fe
commit
3fca8eaead
|
@ -187,7 +187,7 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixi
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latest_change_secret_record(self) -> ChangeSecretRecord:
|
def latest_change_secret_record(self) -> ChangeSecretRecord:
|
||||||
return self.account.change_secret_records.filter(
|
return self.account.changesecretrecords.filter(
|
||||||
status=ChangeSecretRecordStatusChoice.pending
|
status=ChangeSecretRecordStatusChoice.pending
|
||||||
).order_by('-date_created').first()
|
).order_by('-date_created').first()
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.automations.methods import platform_automation_methods
|
from accounts.automations.methods import platform_automation_methods
|
||||||
from accounts.const import SSHKeyStrategy, SecretStrategy, SecretType
|
from accounts.const import SSHKeyStrategy, SecretStrategy, SecretType, ChangeSecretRecordStatusChoice
|
||||||
from accounts.models import BaseAccountQuerySet
|
from accounts.models import BaseAccountQuerySet
|
||||||
from assets.automations.base.manager import BasePlaybookManager
|
from assets.automations.base.manager import BasePlaybookManager
|
||||||
from assets.const import HostTypes
|
from assets.const import HostTypes
|
||||||
|
from common.db.utils import safe_db_connection
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
@ -32,6 +34,8 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
|
||||||
'ssh_key_change_strategy', SSHKeyStrategy.set_jms
|
'ssh_key_change_strategy', SSHKeyStrategy.set_jms
|
||||||
)
|
)
|
||||||
self.account_ids = self.execution.snapshot['accounts']
|
self.account_ids = self.execution.snapshot['accounts']
|
||||||
|
self.record_map = self.execution.snapshot.get('record_map', {}) # 这个是某个失败的记录重试
|
||||||
|
self.name_recorder_mapper = {} # 做个映射,方便后面处理
|
||||||
|
|
||||||
def gen_account_inventory(self, account, asset, h, path_dir):
|
def gen_account_inventory(self, account, asset, h, path_dir):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
@ -119,3 +123,51 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
|
||||||
inventory_hosts.append(h)
|
inventory_hosts.append(h)
|
||||||
|
|
||||||
return inventory_hosts
|
return inventory_hosts
|
||||||
|
|
||||||
|
def on_host_success(self, host, result):
|
||||||
|
recorder = self.name_recorder_mapper.get(host)
|
||||||
|
if not recorder:
|
||||||
|
return
|
||||||
|
recorder.status = ChangeSecretRecordStatusChoice.success.value
|
||||||
|
recorder.date_finished = timezone.now()
|
||||||
|
|
||||||
|
account = recorder.account
|
||||||
|
if not account:
|
||||||
|
print("Account not found, deleted ?")
|
||||||
|
return
|
||||||
|
|
||||||
|
account.secret = getattr(recorder, 'new_secret', account.secret)
|
||||||
|
account.date_updated = timezone.now()
|
||||||
|
|
||||||
|
with safe_db_connection():
|
||||||
|
recorder.save(update_fields=['status', 'date_finished'])
|
||||||
|
account.save(update_fields=['secret', 'date_updated'])
|
||||||
|
|
||||||
|
self.summary['ok_accounts'] += 1
|
||||||
|
self.result['ok_accounts'].append(
|
||||||
|
{
|
||||||
|
"asset": str(account.asset),
|
||||||
|
"username": account.username,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
super().on_host_success(host, result)
|
||||||
|
|
||||||
|
def on_host_error(self, host, error, result):
|
||||||
|
recorder = self.name_recorder_mapper.get(host)
|
||||||
|
if not recorder:
|
||||||
|
return
|
||||||
|
recorder.status = ChangeSecretRecordStatusChoice.failed.value
|
||||||
|
recorder.date_finished = timezone.now()
|
||||||
|
recorder.error = error
|
||||||
|
try:
|
||||||
|
recorder.save()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\033[31m Save {host} recorder error: {e} \033[0m\n")
|
||||||
|
self.summary['fail_accounts'] += 1
|
||||||
|
self.result['fail_accounts'].append(
|
||||||
|
{
|
||||||
|
"asset": str(recorder.asset),
|
||||||
|
"username": recorder.account.username,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
super().on_host_error(host, error, result)
|
||||||
|
|
|
@ -2,7 +2,6 @@ import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from xlsxwriter import Workbook
|
from xlsxwriter import Workbook
|
||||||
|
|
||||||
|
@ -12,7 +11,6 @@ from accounts.const import (
|
||||||
from accounts.models import ChangeSecretRecord
|
from accounts.models import ChangeSecretRecord
|
||||||
from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretReportMsg
|
from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretReportMsg
|
||||||
from accounts.serializers import ChangeSecretRecordBackUpSerializer
|
from accounts.serializers import ChangeSecretRecordBackUpSerializer
|
||||||
from common.db.utils import safe_db_connection
|
|
||||||
from common.decorators import bulk_create_decorator
|
from common.decorators import bulk_create_decorator
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from common.utils.file import encrypt_and_compress_zip_file
|
from common.utils.file import encrypt_and_compress_zip_file
|
||||||
|
@ -26,11 +24,6 @@ logger = get_logger(__name__)
|
||||||
class ChangeSecretManager(BaseChangeSecretPushManager):
|
class ChangeSecretManager(BaseChangeSecretPushManager):
|
||||||
ansible_account_prefer = ''
|
ansible_account_prefer = ''
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.record_map = self.execution.snapshot.get('record_map', {}) # 这个是某个失败的记录重试
|
|
||||||
self.name_recorder_mapper = {} # 做个映射,方便后面处理
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def method_type(cls):
|
def method_type(cls):
|
||||||
return AutomationTypes.change_secret
|
return AutomationTypes.change_secret
|
||||||
|
@ -74,54 +67,6 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
|
||||||
)
|
)
|
||||||
return recorder
|
return recorder
|
||||||
|
|
||||||
def on_host_success(self, host, result):
|
|
||||||
recorder = self.name_recorder_mapper.get(host)
|
|
||||||
if not recorder:
|
|
||||||
return
|
|
||||||
recorder.status = ChangeSecretRecordStatusChoice.success.value
|
|
||||||
recorder.date_finished = timezone.now()
|
|
||||||
|
|
||||||
account = recorder.account
|
|
||||||
if not account:
|
|
||||||
print("Account not found, deleted ?")
|
|
||||||
return
|
|
||||||
|
|
||||||
account.secret = recorder.new_secret
|
|
||||||
account.date_updated = timezone.now()
|
|
||||||
|
|
||||||
with safe_db_connection():
|
|
||||||
recorder.save(update_fields=['status', 'date_finished'])
|
|
||||||
account.save(update_fields=['secret', 'date_updated'])
|
|
||||||
|
|
||||||
self.summary['ok_accounts'] += 1
|
|
||||||
self.result['ok_accounts'].append(
|
|
||||||
{
|
|
||||||
"asset": str(account.asset),
|
|
||||||
"username": account.username,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
super().on_host_success(host, result)
|
|
||||||
|
|
||||||
def on_host_error(self, host, error, result):
|
|
||||||
recorder = self.name_recorder_mapper.get(host)
|
|
||||||
if not recorder:
|
|
||||||
return
|
|
||||||
recorder.status = ChangeSecretRecordStatusChoice.failed.value
|
|
||||||
recorder.date_finished = timezone.now()
|
|
||||||
recorder.error = error
|
|
||||||
try:
|
|
||||||
recorder.save()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\033[31m Save {host} recorder error: {e} \033[0m\n")
|
|
||||||
self.summary['fail_accounts'] += 1
|
|
||||||
self.result['fail_accounts'].append(
|
|
||||||
{
|
|
||||||
"asset": str(recorder.asset),
|
|
||||||
"username": recorder.account.username,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
super().on_host_success(host, result)
|
|
||||||
|
|
||||||
def check_secret(self):
|
def check_secret(self):
|
||||||
if self.secret_strategy == SecretStrategy.custom \
|
if self.secret_strategy == SecretStrategy.custom \
|
||||||
and not self.execution.snapshot['secret']:
|
and not self.execution.snapshot['secret']:
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.const import AutomationTypes
|
from accounts.const import AutomationTypes
|
||||||
|
from common.decorators import bulk_create_decorator
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from common.utils.timezone import local_now_filename
|
from common.utils.timezone import local_now_filename
|
||||||
from ..base.manager import BaseChangeSecretPushManager
|
from ..base.manager import BaseChangeSecretPushManager
|
||||||
|
from ...models import PushSecretRecord
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
@ -17,12 +19,33 @@ class PushAccountManager(BaseChangeSecretPushManager):
|
||||||
return account.secret
|
return account.secret
|
||||||
|
|
||||||
def gen_account_inventory(self, account, asset, h, path_dir):
|
def gen_account_inventory(self, account, asset, h, path_dir):
|
||||||
|
self.get_or_create_record(asset, account, h['name'])
|
||||||
secret = self.get_secret(account)
|
secret = self.get_secret(account)
|
||||||
secret_type = account.secret_type
|
secret_type = account.secret_type
|
||||||
new_secret, private_key_path = self.handle_ssh_secret(secret_type, secret, path_dir)
|
new_secret, private_key_path = self.handle_ssh_secret(secret_type, secret, path_dir)
|
||||||
h = self.gen_inventory(h, account, new_secret, private_key_path, asset)
|
h = self.gen_inventory(h, account, new_secret, private_key_path, asset)
|
||||||
return h
|
return h
|
||||||
|
|
||||||
|
def get_or_create_record(self, asset, account, name):
|
||||||
|
asset_account_id = f'{asset.id}-{account.id}'
|
||||||
|
|
||||||
|
if asset_account_id in self.record_map:
|
||||||
|
record_id = self.record_map[asset_account_id]
|
||||||
|
recorder = PushSecretRecord.objects.filter(id=record_id).first()
|
||||||
|
else:
|
||||||
|
recorder = self.create_record(asset, account)
|
||||||
|
|
||||||
|
self.name_recorder_mapper[name] = recorder
|
||||||
|
return recorder
|
||||||
|
|
||||||
|
@bulk_create_decorator(PushSecretRecord)
|
||||||
|
def create_record(self, asset, account):
|
||||||
|
recorder = PushSecretRecord(
|
||||||
|
asset=asset, account=account, execution=self.execution,
|
||||||
|
comment=f'{account.username}@{asset.address}'
|
||||||
|
)
|
||||||
|
return recorder
|
||||||
|
|
||||||
def print_summary(self):
|
def print_summary(self):
|
||||||
print('\n\n' + '-' * 80)
|
print('\n\n' + '-' * 80)
|
||||||
plan_execution_end = _('Plan execution end')
|
plan_execution_end = _('Plan execution end')
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Generated by Django 4.1.13 on 2025-01-23 07:22
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0011_auto_20241204_1516'),
|
||||||
|
('accounts', '0028_remove_checkaccountengine_is_active_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='changesecretrecord',
|
||||||
|
name='account',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)ss', to='accounts.account'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='changesecretrecord',
|
||||||
|
name='asset',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='asset_%(class)ss', to='assets.asset'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='changesecretrecord',
|
||||||
|
name='execution',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='execution_%(class)ss', to='accounts.automationexecution'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PushSecretRecord',
|
||||||
|
fields=[
|
||||||
|
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
|
||||||
|
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
|
||||||
|
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||||
|
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||||
|
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||||
|
('date_finished', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Date finished')),
|
||||||
|
('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.SET_NULL, related_name='%(class)ss', to='accounts.account')),
|
||||||
|
('asset', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='asset_%(class)ss', to='assets.asset')),
|
||||||
|
('execution', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='execution_%(class)ss', to='accounts.automationexecution')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Push secret record',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -9,7 +9,7 @@ from common.db import fields
|
||||||
from common.db.models import JMSBaseModel
|
from common.db.models import JMSBaseModel
|
||||||
from .base import AccountBaseAutomation, ChangeSecretMixin
|
from .base import AccountBaseAutomation, ChangeSecretMixin
|
||||||
|
|
||||||
__all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord', ]
|
__all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord', 'BaseSecretRecord']
|
||||||
|
|
||||||
|
|
||||||
class ChangeSecretAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
class ChangeSecretAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
||||||
|
@ -30,36 +30,42 @@ class ChangeSecretAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
||||||
return attr_json
|
return attr_json
|
||||||
|
|
||||||
|
|
||||||
class ChangeSecretRecord(JMSBaseModel):
|
class BaseSecretRecord(JMSBaseModel):
|
||||||
account = models.ForeignKey(
|
account = models.ForeignKey(
|
||||||
'accounts.Account', on_delete=models.SET_NULL,
|
'accounts.Account', on_delete=models.SET_NULL,
|
||||||
null=True, related_name='change_secret_records'
|
null=True, related_name='%(class)ss'
|
||||||
)
|
)
|
||||||
asset = models.ForeignKey(
|
asset = models.ForeignKey(
|
||||||
'assets.Asset', on_delete=models.SET_NULL,
|
'assets.Asset', on_delete=models.SET_NULL,
|
||||||
null=True, related_name='asset_change_secret_records'
|
null=True, related_name='asset_%(class)ss'
|
||||||
)
|
)
|
||||||
execution = models.ForeignKey(
|
execution = models.ForeignKey(
|
||||||
'accounts.AutomationExecution', on_delete=models.SET_NULL,
|
'accounts.AutomationExecution', on_delete=models.SET_NULL,
|
||||||
null=True, related_name='execution_change_secret_records',
|
null=True, related_name='execution_%(class)ss',
|
||||||
)
|
)
|
||||||
old_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Old secret'))
|
|
||||||
new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('New secret'))
|
|
||||||
date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('Date finished'), db_index=True)
|
date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('Date finished'), db_index=True)
|
||||||
ignore_fail = models.BooleanField(default=False, verbose_name=_('Ignore fail'))
|
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=16, verbose_name=_('Status'), default=ChangeSecretRecordStatusChoice.pending.value
|
max_length=16, verbose_name=_('Status'), default=ChangeSecretRecordStatusChoice.pending.value
|
||||||
)
|
)
|
||||||
error = models.TextField(blank=True, null=True, verbose_name=_('Error'))
|
error = models.TextField(blank=True, null=True, verbose_name=_('Error'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Change secret record")
|
abstract = True
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.account.username}@{self.asset}'
|
return f'{self.account.username}@{self.asset}'
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def get_valid_records():
|
def get_valid_records(cls):
|
||||||
return ChangeSecretRecord.objects.exclude(
|
return cls.objects.exclude(
|
||||||
Q(execution__isnull=True) | Q(asset__isnull=True) | Q(account__isnull=True)
|
Q(execution__isnull=True) | Q(asset__isnull=True) | Q(account__isnull=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeSecretRecord(BaseSecretRecord):
|
||||||
|
old_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Old secret'))
|
||||||
|
new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('New secret'))
|
||||||
|
ignore_fail = models.BooleanField(default=False, verbose_name=_('Ignore fail'))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Change secret record")
|
||||||
|
|
|
@ -3,10 +3,10 @@ from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.const import AutomationTypes
|
from accounts.const import AutomationTypes
|
||||||
from accounts.models import Account
|
from accounts.models import Account
|
||||||
from .base import AccountBaseAutomation
|
from .base import AccountBaseAutomation, ChangeSecretMixin
|
||||||
from .change_secret import ChangeSecretMixin
|
from .change_secret import BaseSecretRecord
|
||||||
|
|
||||||
__all__ = ['PushAccountAutomation']
|
__all__ = ['PushAccountAutomation', 'PushSecretRecord']
|
||||||
|
|
||||||
|
|
||||||
class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
||||||
|
@ -36,3 +36,8 @@ class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Push asset account")
|
verbose_name = _("Push asset account")
|
||||||
|
|
||||||
|
|
||||||
|
class PushSecretRecord(BaseSecretRecord):
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Push secret record")
|
||||||
|
|
Loading…
Reference in New Issue