|
|
@ -7,11 +7,15 @@ 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
|
|
|
|
|
|
|
|
|
|
|
|
from accounts.const import AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy, ChangeSecretRecordStatusChoice
|
|
|
|
from accounts.const import (
|
|
|
|
|
|
|
|
AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy, ChangeSecretRecordStatusChoice
|
|
|
|
|
|
|
|
)
|
|
|
|
from accounts.models import ChangeSecretRecord, BaseAccountQuerySet
|
|
|
|
from accounts.models import ChangeSecretRecord, BaseAccountQuerySet
|
|
|
|
from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretFailedMsg
|
|
|
|
from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretFailedMsg
|
|
|
|
from accounts.serializers import ChangeSecretRecordBackUpSerializer
|
|
|
|
from accounts.serializers import ChangeSecretRecordBackUpSerializer
|
|
|
|
from assets.const import HostTypes
|
|
|
|
from assets.const import HostTypes
|
|
|
|
|
|
|
|
from common.db.utils import safe_db_connection
|
|
|
|
|
|
|
|
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
|
|
|
@ -26,7 +30,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|
|
|
|
|
|
|
|
|
|
|
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_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
|
|
|
@ -36,6 +40,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|
|
|
)
|
|
|
|
)
|
|
|
|
self.account_ids = self.execution.snapshot['accounts']
|
|
|
|
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):
|
|
|
@ -52,18 +57,6 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|
|
|
kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip())
|
|
|
|
kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip())
|
|
|
|
return kwargs
|
|
|
|
return kwargs
|
|
|
|
|
|
|
|
|
|
|
|
def secret_generator(self, secret_type):
|
|
|
|
|
|
|
|
return SecretGenerator(
|
|
|
|
|
|
|
|
self.secret_strategy, secret_type,
|
|
|
|
|
|
|
|
self.execution.snapshot.get('password_rules')
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_secret(self, secret_type):
|
|
|
|
|
|
|
|
if self.secret_strategy == SecretStrategy.custom:
|
|
|
|
|
|
|
|
return self.execution.snapshot['secret']
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
return self.secret_generator(secret_type).get_secret()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_accounts(self, privilege_account) -> BaseAccountQuerySet | None:
|
|
|
|
def get_accounts(self, privilege_account) -> BaseAccountQuerySet | None:
|
|
|
|
if not privilege_account:
|
|
|
|
if not privilege_account:
|
|
|
|
print('Not privilege account')
|
|
|
|
print('Not privilege account')
|
|
|
@ -81,10 +74,65 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return accounts
|
|
|
|
return accounts
|
|
|
|
|
|
|
|
|
|
|
|
def host_callback(
|
|
|
|
def gen_new_secret(self, account, path_dir):
|
|
|
|
self, host, asset=None, account=None,
|
|
|
|
private_key_path = None
|
|
|
|
automation=None, path_dir=None, **kwargs
|
|
|
|
if self.secret_type is None:
|
|
|
|
):
|
|
|
|
new_secret = account.secret
|
|
|
|
|
|
|
|
return new_secret, private_key_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self.secret_strategy == SecretStrategy.custom:
|
|
|
|
|
|
|
|
new_secret = self.execution.snapshot['secret']
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
generator = SecretGenerator(
|
|
|
|
|
|
|
|
self.secret_strategy, self.secret_type,
|
|
|
|
|
|
|
|
self.execution.snapshot.get('password_rules')
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
new_secret = generator.get_secret()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if account.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 get_or_create_record(self, asset, account, new_secret, 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 = ChangeSecretRecord.objects.filter(id=record_id).first()
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
recorder = self.create_record(asset, account, new_secret)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if recorder:
|
|
|
|
|
|
|
|
self.name_recorder_mapper[name] = recorder
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@bulk_create_decorator(ChangeSecretRecord)
|
|
|
|
|
|
|
|
def create_record(self, asset, account, new_secret):
|
|
|
|
|
|
|
|
recorder = ChangeSecretRecord(
|
|
|
|
|
|
|
|
asset=asset, account=account, execution=self.execution,
|
|
|
|
|
|
|
|
old_secret=account.secret, new_secret=new_secret,
|
|
|
|
|
|
|
|
comment=f'{account.username}@{asset.address}'
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
return recorder
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def gen_change_secret_inventory(self, host, account, new_secret, private_key_path, asset):
|
|
|
|
|
|
|
|
h = deepcopy(host)
|
|
|
|
|
|
|
|
secret_type = account.secret_type
|
|
|
|
|
|
|
|
h['name'] += '(' + account.username + ')'
|
|
|
|
|
|
|
|
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': 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 = super().host_callback(
|
|
|
|
host, asset=asset, account=account, automation=automation,
|
|
|
|
host, asset=asset, account=account, automation=automation,
|
|
|
|
path_dir=path_dir, **kwargs
|
|
|
|
path_dir=path_dir, **kwargs
|
|
|
@ -93,72 +141,29 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|
|
|
return host
|
|
|
|
return host
|
|
|
|
|
|
|
|
|
|
|
|
host['check_conn_after_change'] = self.execution.snapshot.get('check_conn_after_change', True)
|
|
|
|
host['check_conn_after_change'] = self.execution.snapshot.get('check_conn_after_change', True)
|
|
|
|
|
|
|
|
host['ssh_params'] = {}
|
|
|
|
|
|
|
|
|
|
|
|
accounts = self.get_accounts(account)
|
|
|
|
accounts = self.get_accounts(account)
|
|
|
|
error_msg = _("No pending accounts found")
|
|
|
|
error_msg = _("! No pending accounts found")
|
|
|
|
if not accounts:
|
|
|
|
if not accounts:
|
|
|
|
print(f'{asset}: {error_msg}')
|
|
|
|
print(f'{asset}: {error_msg}')
|
|
|
|
return []
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
records = []
|
|
|
|
if asset.type == HostTypes.WINDOWS:
|
|
|
|
|
|
|
|
accounts = accounts.filter(secret_type=SecretType.PASSWORD)
|
|
|
|
|
|
|
|
|
|
|
|
inventory_hosts = []
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
if asset.type == HostTypes.WINDOWS:
|
|
|
|
|
|
|
|
accounts = accounts.filter(secret_type=SecretType.PASSWORD)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
host['ssh_params'] = {}
|
|
|
|
|
|
|
|
for account in accounts:
|
|
|
|
for account in accounts:
|
|
|
|
h = deepcopy(host)
|
|
|
|
new_secret, private_key_path = self.gen_new_secret(account, path_dir)
|
|
|
|
secret_type = account.secret_type
|
|
|
|
h = self.gen_change_secret_inventory(host, account, new_secret, private_key_path, asset)
|
|
|
|
h['name'] += '(' + account.username + ')'
|
|
|
|
self.get_or_create_record(asset, account, new_secret, h['name'])
|
|
|
|
if self.secret_type is None:
|
|
|
|
|
|
|
|
new_secret = account.secret
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
new_secret = self.get_secret(secret_type)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if new_secret is None:
|
|
|
|
|
|
|
|
print(f'new_secret is None, account: {account}')
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
asset_account_id = f'{asset.id}-{account.id}'
|
|
|
|
|
|
|
|
if asset_account_id not in self.record_map:
|
|
|
|
|
|
|
|
recorder = ChangeSecretRecord(
|
|
|
|
|
|
|
|
asset=asset, account=account, execution=self.execution,
|
|
|
|
|
|
|
|
old_secret=account.secret, new_secret=new_secret,
|
|
|
|
|
|
|
|
comment=f'{account.username}@{asset.address}'
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
records.append(recorder)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
record_id = self.record_map[asset_account_id]
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
recorder = ChangeSecretRecord.objects.get(id=record_id)
|
|
|
|
|
|
|
|
except ChangeSecretRecord.DoesNotExist:
|
|
|
|
|
|
|
|
print(f"Record {record_id} not found")
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.name_recorder_mapper[h['name']] = recorder
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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': 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
|
|
|
|
|
|
|
|
inventory_hosts.append(h)
|
|
|
|
inventory_hosts.append(h)
|
|
|
|
ChangeSecretRecord.objects.bulk_create(records)
|
|
|
|
|
|
|
|
|
|
|
|
self.create_record.finish()
|
|
|
|
return inventory_hosts
|
|
|
|
return inventory_hosts
|
|
|
|
|
|
|
|
|
|
|
|
def on_host_success(self, host, result):
|
|
|
|
def on_host_success(self, host, result):
|
|
|
@ -172,24 +177,13 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|
|
|
if not account:
|
|
|
|
if not account:
|
|
|
|
print("Account not found, deleted ?")
|
|
|
|
print("Account not found, deleted ?")
|
|
|
|
return
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
account.secret = recorder.new_secret
|
|
|
|
account.secret = recorder.new_secret
|
|
|
|
account.date_updated = timezone.now()
|
|
|
|
account.date_updated = timezone.now()
|
|
|
|
|
|
|
|
|
|
|
|
max_retries = 3
|
|
|
|
with safe_db_connection():
|
|
|
|
retry_count = 0
|
|
|
|
recorder.save(update_fields=['status', 'date_finished'])
|
|
|
|
|
|
|
|
account.save(update_fields=['secret', 'version', 'date_updated'])
|
|
|
|
while retry_count < max_retries:
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
recorder.save()
|
|
|
|
|
|
|
|
account.save(update_fields=['secret', 'version', 'date_updated'])
|
|
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
|
|
retry_count += 1
|
|
|
|
|
|
|
|
if retry_count == max_retries:
|
|
|
|
|
|
|
|
self.on_host_error(host, str(e), result)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
print(f'retry {retry_count} times for {host} recorder save error: {e}')
|
|
|
|
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def on_host_error(self, host, error, result):
|
|
|
|
def on_host_error(self, host, error, result):
|
|
|
|
recorder = self.name_recorder_mapper.get(host)
|
|
|
|
recorder = self.name_recorder_mapper.get(host)
|
|
|
@ -222,21 +216,24 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
failed += 1
|
|
|
|
failed += 1
|
|
|
|
total += 1
|
|
|
|
total += 1
|
|
|
|
|
|
|
|
|
|
|
|
summary = _('Success: %s, Failed: %s, Total: %s') % (succeed, failed, total)
|
|
|
|
summary = _('Success: %s, Failed: %s, Total: %s') % (succeed, failed, total)
|
|
|
|
return summary
|
|
|
|
return summary
|
|
|
|
|
|
|
|
|
|
|
|
def run(self, *args, **kwargs):
|
|
|
|
def print_summary(self):
|
|
|
|
if self.secret_type and not self.check_secret():
|
|
|
|
|
|
|
|
self.execution.status = 'success'
|
|
|
|
|
|
|
|
self.execution.date_finished = timezone.now()
|
|
|
|
|
|
|
|
self.execution.save()
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
super().run(*args, **kwargs)
|
|
|
|
|
|
|
|
recorders = list(self.name_recorder_mapper.values())
|
|
|
|
recorders = list(self.name_recorder_mapper.values())
|
|
|
|
summary = self.get_summary(recorders)
|
|
|
|
summary = self.get_summary(recorders)
|
|
|
|
print(summary, end='')
|
|
|
|
print('\n\n' + '-' * 80)
|
|
|
|
|
|
|
|
plan_execution_end = _('Plan execution end')
|
|
|
|
|
|
|
|
print('{} {}\n'.format(plan_execution_end, local_now_filename()))
|
|
|
|
|
|
|
|
time_cost = _('Time cost')
|
|
|
|
|
|
|
|
print('{}: {}s'.format(time_cost, self.duration))
|
|
|
|
|
|
|
|
print(summary)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def send_report_if_need(self, *args, **kwargs):
|
|
|
|
|
|
|
|
if self.secret_type and not self.check_secret():
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
recorders = list(self.name_recorder_mapper.values())
|
|
|
|
if self.record_map:
|
|
|
|
if self.record_map:
|
|
|
|
return
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
@ -262,6 +259,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|
|
|
if not recorders:
|
|
|
|
if not recorders:
|
|
|
|
return
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
summary = self.get_summary(recorders)
|
|
|
|
self.send_recorder_mail(recipients, recorders, summary)
|
|
|
|
self.send_recorder_mail(recipients, recorders, summary)
|
|
|
|
|
|
|
|
|
|
|
|
def send_recorder_mail(self, recipients, recorders, summary):
|
|
|
|
def send_recorder_mail(self, recipients, recorders, summary):
|
|
|
|