perf: Change secret

pull/14631/head
feng 2024-12-10 16:37:48 +08:00 committed by feng626
parent c95c3099b7
commit a904e59252
5 changed files with 217 additions and 107 deletions

View File

@ -1,7 +1,13 @@
from django.template.loader import render_to_string from copy import deepcopy
from django.conf import settings
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.models import BaseAccountQuerySet
from assets.automations.base.manager import BasePlaybookManager from assets.automations.base.manager import BasePlaybookManager
from assets.const import HostTypes
from common.utils import get_logger from common.utils import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
@ -13,3 +19,103 @@ class AccountBasePlaybookManager(BasePlaybookManager):
@property @property
def platform_automation_methods(self): def platform_automation_methods(self):
return platform_automation_methods return platform_automation_methods
class BaseChangeSecretPushManager(AccountBasePlaybookManager):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.secret_type = self.execution.snapshot.get('secret_type')
self.secret_strategy = self.execution.snapshot.get(
'secret_strategy', SecretStrategy.custom
)
self.ssh_key_change_strategy = self.execution.snapshot.get(
'ssh_key_change_strategy', SSHKeyStrategy.set_jms
)
self.account_ids = self.execution.snapshot['accounts']
def gen_account_inventory(self, account, asset, h, path_dir):
raise NotImplementedError
def get_ssh_params(self, secret, secret_type):
kwargs = {}
if secret_type != SecretType.SSH_KEY:
return kwargs
kwargs['strategy'] = self.ssh_key_change_strategy
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
if kwargs['strategy'] == SSHKeyStrategy.set_jms:
kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip())
return kwargs
def get_accounts(self, privilege_account) -> BaseAccountQuerySet | None:
if not privilege_account:
print('Not privilege account')
return
asset = privilege_account.asset
accounts = asset.accounts.all()
accounts = accounts.filter(id__in=self.account_ids)
if self.secret_type:
accounts = accounts.filter(secret_type=self.secret_type)
if settings.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED:
accounts = accounts.filter(privileged=False).exclude(
username__in=['root', 'administrator', privilege_account.username]
)
return accounts
def handle_ssh_secret(self, secret_type, new_secret, path_dir):
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)
return new_secret, private_key_path
def gen_inventory(self, h, account, new_secret, private_key_path, asset):
secret_type = account.secret_type
h['ssh_params'].update(self.get_ssh_params(new_secret, secret_type))
h['account'] = {
'name': account.name,
'username': account.username,
'secret_type': secret_type,
'secret': account.escape_jinja2_syntax(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
return h
def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs):
host = super().host_callback(
host, asset=asset, account=account, automation=automation,
path_dir=path_dir, **kwargs
)
if host.get('error'):
return host
host['check_conn_after_change'] = self.execution.snapshot.get('check_conn_after_change', True)
host['ssh_params'] = {}
accounts = self.get_accounts(account)
error_msg = _("No pending accounts found")
if not accounts:
print(f'{asset}: {error_msg}')
return []
if asset.type == HostTypes.WINDOWS:
accounts = accounts.filter(secret_type=SecretType.PASSWORD)
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
for account in accounts:
h = deepcopy(host)
h['name'] += '(' + account.username + ')' # To distinguish different accounts
h = self.gen_account_inventory(account, asset, h, path_dir)
inventory_hosts.append(h)
return inventory_hosts

View File

@ -1,6 +1,5 @@
import os import os
import time import time
from copy import deepcopy
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
@ -8,73 +7,34 @@ from django.utils.translation import gettext_lazy as _
from xlsxwriter import Workbook from xlsxwriter import Workbook
from accounts.const import ( from accounts.const import (
AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy, ChangeSecretRecordStatusChoice AutomationTypes, SecretStrategy, ChangeSecretRecordStatusChoice
) )
from accounts.models import ChangeSecretRecord, BaseAccountQuerySet 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 assets.const import HostTypes
from common.db.utils import safe_db_connection 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
from common.utils.timezone import local_now_filename from common.utils.timezone import local_now_filename
from ..base.manager import AccountBasePlaybookManager from ..base.manager import BaseChangeSecretPushManager
from ...utils import SecretGenerator from ...utils import SecretGenerator
logger = get_logger(__name__) logger = get_logger(__name__)
class ChangeSecretManager(AccountBasePlaybookManager): class ChangeSecretManager(BaseChangeSecretPushManager):
ansible_account_prefer = '' ansible_account_prefer = ''
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.record_map = self.execution.snapshot.get('record_map', {}) # 这个是某个失败的记录重试 self.record_map = self.execution.snapshot.get('record_map', {}) # 这个是某个失败的记录重试
self.secret_type = self.execution.snapshot.get('secret_type')
self.secret_strategy = self.execution.snapshot.get(
'secret_strategy', SecretStrategy.custom
)
self.ssh_key_change_strategy = self.execution.snapshot.get(
'ssh_key_change_strategy', SSHKeyStrategy.set_jms
)
self.account_ids = self.execution.snapshot['accounts']
self.name_recorder_mapper = {} # 做个映射,方便后面处理 self.name_recorder_mapper = {} # 做个映射,方便后面处理
self.pending_add_records = []
@classmethod @classmethod
def method_type(cls): def method_type(cls):
return AutomationTypes.change_secret return AutomationTypes.change_secret
def get_ssh_params(self, secret, secret_type):
kwargs = {}
if secret_type != SecretType.SSH_KEY:
return kwargs
kwargs['strategy'] = self.ssh_key_change_strategy
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
if kwargs['strategy'] == SSHKeyStrategy.set_jms:
kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip())
return kwargs
def get_accounts(self, privilege_account) -> BaseAccountQuerySet | None:
if not privilege_account:
print('Not privilege account')
return
asset = privilege_account.asset
accounts = asset.accounts.all()
accounts = accounts.filter(id__in=self.account_ids)
if self.secret_type:
accounts = accounts.filter(secret_type=self.secret_type)
if settings.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED:
accounts = accounts.filter(privileged=False).exclude(
username__in=['root', 'administrator', privilege_account.username]
)
return accounts
def get_secret(self, account): def get_secret(self, account):
if self.secret_strategy == SecretStrategy.custom: if self.secret_strategy == SecretStrategy.custom:
new_secret = self.execution.snapshot['secret'] new_secret = self.execution.snapshot['secret']
@ -86,12 +46,11 @@ class ChangeSecretManager(AccountBasePlaybookManager):
new_secret = generator.get_secret() new_secret = generator.get_secret()
return new_secret return new_secret
def gen_new_secret(self, account, new_secret, path_dir): def gen_account_inventory(self, account, asset, h, path_dir):
private_key_path = None record = self.get_or_create_record(asset, account, h['name'])
if account.secret_type == SecretType.SSH_KEY: new_secret, private_key_path = self.handle_ssh_secret(account.secret_type, record.new_secret, path_dir)
private_key_path = self.generate_private_key_path(new_secret, path_dir) h = self.gen_inventory(h, account, new_secret, private_key_path, asset)
new_secret = self.generate_public_key(new_secret) return h
return new_secret, private_key_path
def get_or_create_record(self, asset, account, name): def get_or_create_record(self, asset, account, name):
asset_account_id = f'{asset.id}-{account.id}' asset_account_id = f'{asset.id}-{account.id}'
@ -115,56 +74,6 @@ class ChangeSecretManager(AccountBasePlaybookManager):
) )
return recorder return recorder
def gen_change_secret_inventory(self, h, account, new_secret, private_key_path, asset):
secret_type = account.secret_type
h['ssh_params'].update(self.get_ssh_params(new_secret, secret_type))
h['account'] = {
'name': account.name,
'username': account.username,
'secret_type': secret_type,
'secret': account.escape_jinja2_syntax(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
return h
def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs):
host = super().host_callback(
host, asset=asset, account=account, automation=automation,
path_dir=path_dir, **kwargs
)
if host.get('error'):
return host
host['check_conn_after_change'] = self.execution.snapshot.get('check_conn_after_change', True)
host['ssh_params'] = {}
accounts = self.get_accounts(account)
error_msg = _("No pending accounts found")
if not accounts:
print(f'{asset}: {error_msg}')
return []
if asset.type == HostTypes.WINDOWS:
accounts = accounts.filter(secret_type=SecretType.PASSWORD)
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
for account in accounts:
h = deepcopy(host)
h['name'] += '(' + account.username + ')' # To distinguish different accounts
record = self.get_or_create_record(asset, account, h['name'])
new_secret, private_key_path = self.gen_new_secret(account, record.new_secret, path_dir)
h = self.gen_change_secret_inventory(h, account, new_secret, private_key_path, asset)
inventory_hosts.append(h)
return inventory_hosts
def on_host_success(self, host, result): def on_host_success(self, host, result):
recorder = self.name_recorder_mapper.get(host) recorder = self.name_recorder_mapper.get(host)
if not recorder: if not recorder:

View File

@ -1,15 +1,34 @@
from django.utils.translation import gettext_lazy as _
from accounts.const import AutomationTypes from accounts.const import AutomationTypes
from common.utils import get_logger from common.utils import get_logger
from ..base.manager import AccountBasePlaybookManager from common.utils.timezone import local_now_filename
from ..change_secret.manager import ChangeSecretManager from ..base.manager import BaseChangeSecretPushManager
logger = get_logger(__name__) logger = get_logger(__name__)
class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager): class PushAccountManager(BaseChangeSecretPushManager):
@classmethod @classmethod
def method_type(cls): def method_type(cls):
return AutomationTypes.push_account return AutomationTypes.push_account
def get_secret(self, account): def get_secret(self, account):
return account.secret return account.secret
def gen_account_inventory(self, account, asset, h, path_dir):
secret = self.get_secret(account)
secret_type = account.secret_type
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)
return h
def print_summary(self):
print('\n\n' + '-' * 80)
plan_execution_end = _('Plan execution end')
print('{} {}\n'.format(plan_execution_end, local_now_filename()))
time_cost = _('Duration')
print('{}: {}s'.format(time_cost, self.duration))
def get_report_template(self):
return "accounts/push_account_report.html"

View File

@ -1,7 +1,7 @@
{% load i18n %} {% load i18n %}
<div class='summary'> <div class='summary'>
<p>{% trans 'The following is a summary of account change secret or push tasks, please read and process' %}</p> <p>{% trans 'The following is a summary of account change secret tasks, please read and process' %}</p>
<table> <table>
<caption></caption> <caption></caption>
<thead> <thead>
@ -71,7 +71,7 @@
{% endif %} {% endif %}
</div> </div>
<div class='result'> <div class='result'>
{% if summary.lost_accounts %} {% if summary.fail_accounts %}
<p>{% trans 'Failed accounts' %}: {{ summary.fail_accounts }}</p> <p>{% trans 'Failed accounts' %}: {{ summary.fail_accounts }}</p>
<table> <table>
<caption></caption> <caption></caption>
@ -130,5 +130,4 @@
width: 10%; width: 10%;
} }
</style> </style>

View File

@ -0,0 +1,77 @@
{% load i18n %}
<div class='summary'>
<p>{% trans 'The following is a summary of account push tasks, please read and process' %}</p>
<table>
<caption></caption>
<thead>
<tr>
<th colspan='2'>任务汇总:</th>
</tr>
</thead>
<tbody>
<tr>
<td>{% trans 'Task name' %}:</td>
<td>{{ execution.automation.name }} </td>
</tr>
<tr>
<td>{% trans 'Date start' %}:</td>
<td>{{ execution.date_start | date:"Y/m/d H:i:s" }}</td>
</tr>
<tr>
<td>{% trans 'Date end' %}:</td>
<td>{{ execution.date_finished | date:"Y/m/d H:i:s" }}</td>
</tr>
<tr>
<td>{% trans 'Time using' %}:</td>
<td>{{ execution.duration }}s</td>
</tr>
<tr>
<td>{% trans 'Assets count' %}:</td>
<td>{{ summary.total_assets }}</td>
</tr>
<tr>
<td>{% trans 'Asset success count' %}:</td>
<td>{{ summary.ok_assets }}</td>
</tr>
<tr>
<td>{% trans 'Asset failed count' %}:</td>
<td>{{ summary.fail_assets }}</td>
</tr>
<tr>
<td>{% trans 'Asset not support count' %}:</td>
<td>{{ summary.error_assets }}</td>
</tr>
</tbody>
</table>
</div>
<style>
table {
width: 100%;
border-collapse: collapse;
max-width: 100%;
text-align: left;
margin-top: 10px;
padding: 20px;
}
th {
background: #f2f2f2;
font-size: 14px;
padding: 5px;
border: 1px solid #ddd;
}
tr :first-child {
width: 30%;
}
td {
border: 1px solid #ddd;
padding: 5px;
font-size: 12px;
}
</style>