Merge branch 'pam' of github.com:jumpserver/jumpserver into pam

pull/14586/head
ibuler 2024-12-04 18:33:31 +08:00
commit aa52ab13ce
22 changed files with 361 additions and 325 deletions

View File

@ -1,41 +1,36 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from rest_framework import status, viewsets
from rest_framework.response import Response
from accounts import serializers from accounts import serializers
from accounts.const import AutomationTypes
from accounts.models import ( from accounts.models import (
AccountBackupAutomation, AccountBackupExecution BackupAccountAutomation
) )
from accounts.tasks import execute_account_backup_task
from common.const.choices import Trigger
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from .base import AutomationExecutionViewSet
__all__ = [ __all__ = [
'AccountBackupPlanViewSet', 'AccountBackupPlanExecutionViewSet' 'BackupAccountViewSet', 'BackupAccountExecutionViewSet'
] ]
class AccountBackupPlanViewSet(OrgBulkModelViewSet): class BackupAccountViewSet(OrgBulkModelViewSet):
model = AccountBackupAutomation model = BackupAccountAutomation
filterset_fields = ('name',) filterset_fields = ('name',)
search_fields = filterset_fields search_fields = filterset_fields
serializer_class = serializers.AccountBackupSerializer serializer_class = serializers.BackupAccountSerializer
class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet): class BackupAccountExecutionViewSet(AutomationExecutionViewSet):
serializer_class = serializers.AccountBackupPlanExecutionSerializer rbac_perms = (
search_fields = ('trigger', 'plan__name') ("list", "accounts.view_backupaccountexecution"),
filterset_fields = ('trigger', 'plan_id', 'plan__name') ("retrieve", "accounts.view_backupaccountexecution"),
http_method_names = ['get', 'post', 'options'] ("create", "accounts.add_backupaccountexecution"),
("report", "accounts.view_backupaccountexecution"),
)
tp = AutomationTypes.backup_account
def get_queryset(self): def get_queryset(self):
queryset = AccountBackupExecution.objects.all() queryset = super().get_queryset()
queryset = queryset.filter(automation__type=self.tp)
return queryset return queryset
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
pid = serializer.data.get('plan')
task = execute_account_backup_task.delay(pid=str(pid), trigger=Trigger.manual)
return Response({'task': task.id}, status=status.HTTP_201_CREATED)

View File

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.db import transaction
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework import status from rest_framework import status
@ -13,7 +12,6 @@ from accounts.filters import GatheredAccountFilterSet
from accounts.models import GatherAccountsAutomation, AutomationExecution from accounts.models import GatherAccountsAutomation, AutomationExecution
from accounts.models import GatheredAccount from accounts.models import GatheredAccount
from assets.models import Asset from assets.models import Asset
from accounts.tasks.common import quickstart_automation_by_snapshot
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from .base import AutomationExecutionViewSet from .base import AutomationExecutionViewSet

View File

@ -3,15 +3,17 @@ import time
from collections import defaultdict, OrderedDict from collections import defaultdict, OrderedDict
from django.conf import settings from django.conf import settings
from django.db.models import F
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from xlsxwriter import Workbook from xlsxwriter import Workbook
from accounts.const import AccountBackupType from accounts.const import AccountBackupType
from accounts.models.automations.backup_account import AccountBackupAutomation from accounts.models import BackupAccountAutomation, Account
from accounts.notifications import AccountBackupExecutionTaskMsg, AccountBackupByObjStorageExecutionTaskMsg from accounts.notifications import AccountBackupExecutionTaskMsg, AccountBackupByObjStorageExecutionTaskMsg
from accounts.serializers import AccountSecretSerializer from accounts.serializers import AccountSecretSerializer
from assets.const import AllTypes from assets.const import AllTypes
from common.const import Status
from common.utils.file import encrypt_and_compress_zip_file, zip_files from common.utils.file import encrypt_and_compress_zip_file, zip_files
from common.utils.timezone import local_now_filename, local_now_display from common.utils.timezone import local_now_filename, local_now_display
from terminal.models.component.storage import ReplayStorage from terminal.models.component.storage import ReplayStorage
@ -20,6 +22,7 @@ from users.models import User
PATH = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp') PATH = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
split_help_text = _('The account key will be split into two parts and sent') split_help_text = _('The account key will be split into two parts and sent')
class RecipientsNotFound(Exception): class RecipientsNotFound(Exception):
pass pass
@ -73,9 +76,9 @@ class BaseAccountHandler:
class AssetAccountHandler(BaseAccountHandler): class AssetAccountHandler(BaseAccountHandler):
@staticmethod @staticmethod
def get_filename(plan_name): def get_filename(name):
filename = os.path.join( filename = os.path.join(
PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.xlsx' PATH, f'{name}-{local_now_filename()}-{time.time()}.xlsx'
) )
return filename return filename
@ -117,32 +120,41 @@ class AssetAccountHandler(BaseAccountHandler):
cls.handler_secret(data, section) cls.handler_secret(data, section)
data_map.update(cls.add_rows(data, header_fields, sheet_name)) data_map.update(cls.add_rows(data, header_fields, sheet_name))
number_of_backup_accounts = _('Number of backup accounts') number_of_backup_accounts = _('Number of backup accounts')
print('\n\033[33m- {}: {}\033[0m'.format(number_of_backup_accounts, accounts.count())) print('\033[33m- {}: {}\033[0m'.format(number_of_backup_accounts, accounts.count()))
return data_map return data_map
class AccountBackupHandler: class AccountBackupHandler:
def __init__(self, execution): def __init__(self, manager, execution):
self.manager = manager
self.execution = execution self.execution = execution
self.plan_name = self.execution.plan.name self.name = self.execution.snapshot.get('name', '-')
self.is_frozen = False # 任务状态冻结标志
def get_accounts(self):
# TODO 可以优化一下查询 在账号上做 category 的缓存 避免数据量大时连表操作
types = self.execution.snapshot.get('types', [])
self.manager.summary['total_types'] = len(types)
qs = Account.objects.filter(
asset__platform__type__in=types
).annotate(type=F('asset__platform__type'))
return qs
def create_excel(self, section='complete'): def create_excel(self, section='complete'):
hint = _('Generating asset or application related backup information files') hint = _('Generating asset related backup information files')
print( print(
'\n'
f'\033[32m>>> {hint}\033[0m' f'\033[32m>>> {hint}\033[0m'
'' ''
) )
# Print task start date
time_start = time.time() time_start = time.time()
files = [] files = []
accounts = self.execution.backup_accounts accounts = self.get_accounts()
self.manager.summary['total_accounts'] = accounts.count()
data_map = AssetAccountHandler.create_data_map(accounts, section) data_map = AssetAccountHandler.create_data_map(accounts, section)
if not data_map: if not data_map:
return files return files
filename = AssetAccountHandler.get_filename(self.plan_name) filename = AssetAccountHandler.get_filename(self.name)
wb = Workbook(filename) wb = Workbook(filename)
for sheet, data in data_map.items(): for sheet, data in data_map.items():
@ -163,21 +175,19 @@ class AccountBackupHandler:
return return
recipients = User.objects.filter(id__in=list(recipients)) recipients = User.objects.filter(id__in=list(recipients))
print( print(
'\n'
f'\033[32m>>> {_("Start sending backup emails")}\033[0m' f'\033[32m>>> {_("Start sending backup emails")}\033[0m'
'' ''
) )
plan_name = self.plan_name name = self.name
for user in recipients: for user in recipients:
if not user.secret_key: if not user.secret_key:
attachment_list = [] attachment_list = []
else: else:
attachment = os.path.join(PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.zip') attachment = os.path.join(PATH, f'{name}-{local_now_filename()}-{time.time()}.zip')
encrypt_and_compress_zip_file(attachment, user.secret_key, files) encrypt_and_compress_zip_file(attachment, user.secret_key, files)
attachment_list = [attachment, ] attachment_list = [attachment]
AccountBackupExecutionTaskMsg(plan_name, user).publish(attachment_list) AccountBackupExecutionTaskMsg(name, user).publish(attachment_list)
email_sent_to = _('Email sent to')
print('{} {}({})'.format(email_sent_to, user, user.email))
for file in files: for file in files:
os.remove(file) os.remove(file)
@ -186,63 +196,41 @@ class AccountBackupHandler:
return return
recipients = ReplayStorage.objects.filter(id__in=list(recipients)) recipients = ReplayStorage.objects.filter(id__in=list(recipients))
print( print(
'\n'
'\033[32m>>> 📃 ---> sftp \033[0m' '\033[32m>>> 📃 ---> sftp \033[0m'
'' ''
) )
plan_name = self.plan_name name = self.name
encrypt_file = _('Encrypting files using encryption password') encrypt_file = _('Encrypting files using encryption password')
for rec in recipients: for rec in recipients:
attachment = os.path.join(PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.zip') attachment = os.path.join(PATH, f'{name}-{local_now_filename()}-{time.time()}.zip')
if password: if password:
print(f'\033[32m>>> {encrypt_file}\033[0m') print(f'\033[32m>>> {encrypt_file}\033[0m')
encrypt_and_compress_zip_file(attachment, password, files) encrypt_and_compress_zip_file(attachment, password, files)
else: else:
zip_files(attachment, files) zip_files(attachment, files)
attachment_list = attachment attachment_list = attachment
AccountBackupByObjStorageExecutionTaskMsg(plan_name, rec).publish(attachment_list) AccountBackupByObjStorageExecutionTaskMsg(name, rec).publish(attachment_list)
file_sent_to = _('The backup file will be sent to') file_sent_to = _('The backup file will be sent to')
print('{}: {}({})'.format(file_sent_to, rec.name, rec.id)) print('{}: {}({})'.format(file_sent_to, rec.name, rec.id))
for file in files: for file in files:
os.remove(file) os.remove(file)
def step_perform_task_update(self, is_success, reason):
self.execution.reason = reason[:1024]
self.execution.is_success = is_success
self.execution.save()
finish = _('Finish')
print(f'\n{finish}\n')
@staticmethod
def step_finished(is_success):
if is_success:
print(_('Success'))
else:
print(_('Failed'))
def _run(self): def _run(self):
is_success = False
error = '-'
try: try:
backup_type = self.execution.snapshot.get('backup_type', AccountBackupType.email.value) backup_type = self.execution.snapshot.get('backup_type', AccountBackupType.email)
if backup_type == AccountBackupType.email.value: if backup_type == AccountBackupType.email:
self.backup_by_email() self.backup_by_email()
elif backup_type == AccountBackupType.object_storage.value: elif backup_type == AccountBackupType.object_storage:
self.backup_by_obj_storage() self.backup_by_obj_storage()
except Exception as e: except Exception as e:
self.is_frozen = True
print(e)
error = str(e) error = str(e)
else: print(f'\033[31m>>> {error}\033[0m')
is_success = True self.execution.status = Status.error
finally: self.execution.summary['error'] = error
reason = error
self.step_perform_task_update(is_success, reason)
self.step_finished(is_success)
def backup_by_obj_storage(self): def backup_by_obj_storage(self):
object_id = self.execution.snapshot.get('id') object_id = self.execution.snapshot.get('id')
zip_encrypt_password = AccountBackupAutomation.objects.get(id=object_id).zip_encrypt_password zip_encrypt_password = BackupAccountAutomation.objects.get(id=object_id).zip_encrypt_password
obj_recipients_part_one = self.execution.snapshot.get('obj_recipients_part_one', []) obj_recipients_part_one = self.execution.snapshot.get('obj_recipients_part_one', [])
obj_recipients_part_two = self.execution.snapshot.get('obj_recipients_part_two', []) obj_recipients_part_two = self.execution.snapshot.get('obj_recipients_part_two', [])
no_assigned_sftp_server = _('The backup task has no assigned sftp server') no_assigned_sftp_server = _('The backup task has no assigned sftp server')
@ -266,7 +254,6 @@ class AccountBackupHandler:
self.send_backup_obj_storage(files, recipients, zip_encrypt_password) self.send_backup_obj_storage(files, recipients, zip_encrypt_password)
def backup_by_email(self): def backup_by_email(self):
warn_text = _('The backup task has no assigned recipient') warn_text = _('The backup task has no assigned recipient')
recipients_part_one = self.execution.snapshot.get('recipients_part_one', []) recipients_part_one = self.execution.snapshot.get('recipients_part_one', [])
recipients_part_two = self.execution.snapshot.get('recipients_part_two', []) recipients_part_two = self.execution.snapshot.get('recipients_part_two', [])
@ -276,7 +263,7 @@ class AccountBackupHandler:
f'\033[31m>>> {warn_text}\033[0m' f'\033[31m>>> {warn_text}\033[0m'
'' ''
) )
raise RecipientsNotFound('Not Found Recipients') return
if recipients_part_one and recipients_part_two: if recipients_part_one and recipients_part_two:
print(f'\033[32m>>> {split_help_text}\033[0m') print(f'\033[32m>>> {split_help_text}\033[0m')
files = self.create_excel(section='front') files = self.create_excel(section='front')
@ -290,18 +277,5 @@ class AccountBackupHandler:
self.send_backup_mail(files, recipients) self.send_backup_mail(files, recipients)
def run(self): def run(self):
plan_start = _('Plan start') print('{}: {}'.format(_('Plan start'), local_now_display()))
plan_end = _('Plan end') self._run()
time_cost = _('Duration')
error = _('An exception occurred during task execution')
print('{}: {}'.format(plan_start, local_now_display()))
time_start = time.time()
try:
self._run()
except Exception as e:
print(error)
print(e)
finally:
print('\n{}: {}'.format(plan_end, local_now_display()))
timedelta = round((time.time() - time_start), 2)
print('{}: {}s'.format(time_cost, timedelta))

View File

@ -1,11 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import time
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from assets.automations.base.manager import BaseManager from assets.automations.base.manager import BaseManager
from common.db.utils import safe_db_connection
from common.utils.timezone import local_now_display from common.utils.timezone import local_now_display
from .handlers import AccountBackupHandler from .handlers import AccountBackupHandler
@ -14,23 +12,19 @@ class AccountBackupManager(BaseManager):
def do_run(self): def do_run(self):
execution = self.execution execution = self.execution
account_backup_execution_being_executed = _('The account backup plan is being executed') account_backup_execution_being_executed = _('The account backup plan is being executed')
print(f'\n\033[33m# {account_backup_execution_being_executed}\033[0m') print(f'\033[33m# {account_backup_execution_being_executed}\033[0m')
handler = AccountBackupHandler(execution) handler = AccountBackupHandler(self, execution)
handler.run() handler.run()
def send_report_if_need(self): def send_report_if_need(self):
pass pass
def update_execution(self):
timedelta = int(time.time() - self.time_start)
self.execution.timedelta = timedelta
with safe_db_connection():
self.execution.save(update_fields=['timedelta', ])
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')
print('{} {}\n'.format(plan_execution_end, local_now_display())) print('{} {}\n'.format(plan_execution_end, local_now_display()))
time_cost = _('Duration') time_cost = _('Duration')
print('{}: {}s'.format(time_cost, self.duration)) print('{}: {}s'.format(time_cost, self.duration))
def get_report_template(self):
return "accounts/backup_account_report.html"

View File

@ -18,7 +18,7 @@ class ExecutionManager:
AutomationTypes.gather_accounts: GatherAccountsManager, AutomationTypes.gather_accounts: GatherAccountsManager,
AutomationTypes.verify_gateway_account: VerifyGatewayAccountManager, AutomationTypes.verify_gateway_account: VerifyGatewayAccountManager,
AutomationTypes.check_account: CheckAccountManager, AutomationTypes.check_account: CheckAccountManager,
'backup_account': AccountBackupManager, AutomationTypes.backup_account: AccountBackupManager,
} }
def __init__(self, execution): def __init__(self, execution):

View File

@ -28,13 +28,14 @@ class AutomationTypes(models.TextChoices):
gather_accounts = 'gather_accounts', _('Gather accounts') gather_accounts = 'gather_accounts', _('Gather accounts')
verify_gateway_account = 'verify_gateway_account', _('Verify gateway account') verify_gateway_account = 'verify_gateway_account', _('Verify gateway account')
check_account = 'check_account', _('Check account') check_account = 'check_account', _('Check account')
backup_account = 'backup_account', _('Backup account')
@classmethod @classmethod
def get_type_model(cls, tp): def get_type_model(cls, tp):
from accounts.models import ( from accounts.models import (
PushAccountAutomation, ChangeSecretAutomation, PushAccountAutomation, ChangeSecretAutomation,
VerifyAccountAutomation, GatherAccountsAutomation, VerifyAccountAutomation, GatherAccountsAutomation,
CheckAccountAutomation, CheckAccountAutomation, BackupAccountAutomation
) )
type_model_dict = { type_model_dict = {
cls.push_account: PushAccountAutomation, cls.push_account: PushAccountAutomation,
@ -42,6 +43,7 @@ class AutomationTypes(models.TextChoices):
cls.verify_account: VerifyAccountAutomation, cls.verify_account: VerifyAccountAutomation,
cls.gather_accounts: GatherAccountsAutomation, cls.gather_accounts: GatherAccountsAutomation,
cls.check_account: CheckAccountAutomation, cls.check_account: CheckAccountAutomation,
cls.backup_account: BackupAccountAutomation,
} }
return type_model_dict.get(tp) return type_model_dict.get(tp)

View File

@ -0,0 +1,116 @@
# Generated by Django 4.1.13 on 2024-12-03 09:23
from datetime import timedelta as dt_timedelta
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import common.db.fields
def migrate_account_backup(apps, schema_editor):
old_backup_model = apps.get_model('accounts', 'AccountBackupAutomation')
account_backup_model = apps.get_model('accounts', 'BackupAccountAutomation')
backup_id_old_new_map = {}
for backup in old_backup_model.objects.all():
data = {
'comment': backup.comment,
'created_by': backup.created_by,
'updated_by': backup.updated_by,
'date_created': backup.date_created,
'date_updated': backup.date_updated,
'name': backup.name,
'interval': backup.interval,
'crontab': backup.crontab,
'is_periodic': backup.is_periodic,
'start_time': backup.start_time,
'date_last_run': backup.date_last_run,
'org_id': backup.org_id,
'type': 'backup_account',
'types': backup.types,
'backup_type': backup.backup_type,
'is_password_divided_by_email': backup.is_password_divided_by_email,
'is_password_divided_by_obj_storage': backup.is_password_divided_by_obj_storage,
'zip_encrypt_password': backup.zip_encrypt_password
}
obj = account_backup_model.objects.create(**data)
backup_id_old_new_map[str(backup.id)] = str(obj.id)
obj.recipients_part_one.set(backup.recipients_part_one.all())
obj.recipients_part_two.set(backup.recipients_part_two.all())
obj.obj_recipients_part_one.set(backup.obj_recipients_part_one.all())
obj.obj_recipients_part_two.set(backup.obj_recipients_part_two.all())
old_execution_model = apps.get_model('accounts', 'AccountBackupExecution')
backup_execution_model = apps.get_model('accounts', 'AutomationExecution')
for execution in old_execution_model.objects.all():
automation_id = backup_id_old_new_map.get(str(execution.plan_id))
if not automation_id:
continue
data = {
'automation_id': automation_id,
'date_start': execution.date_start,
'duration': int(execution.timedelta),
'date_finished': execution.date_start + dt_timedelta(seconds=int(execution.timedelta)),
'snapshot': execution.snapshot,
'trigger': execution.trigger,
'status': 'error' if execution.reason == '-' else 'success',
'org_id': execution.org_id
}
backup_execution_model.objects.create(**data)
class Migration(migrations.Migration):
dependencies = [
('assets', '0010_alter_automationexecution_duration'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('terminal', '0003_auto_20171230_0308'),
('accounts', '0018_changesecretrecord_ignore_fail_and_more'),
]
operations = [
migrations.CreateModel(
name='BackupAccountAutomation',
fields=[
('baseautomation_ptr',
models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True,
primary_key=True, serialize=False, to='assets.baseautomation')),
('types', models.JSONField(default=list)),
('backup_type',
models.CharField(choices=[('email', 'Email'), ('object_storage', 'SFTP')], default='email',
max_length=128, verbose_name='Backup type')),
('is_password_divided_by_email', models.BooleanField(default=True, verbose_name='Password divided')),
('is_password_divided_by_obj_storage',
models.BooleanField(default=True, verbose_name='Password divided')),
('zip_encrypt_password', common.db.fields.EncryptCharField(blank=True, max_length=4096, null=True,
verbose_name='Zip encrypt password')),
('obj_recipients_part_one',
models.ManyToManyField(blank=True, related_name='obj_recipient_part_one_plans',
to='terminal.replaystorage', verbose_name='Object storage recipient part one')),
('obj_recipients_part_two',
models.ManyToManyField(blank=True, related_name='obj_recipient_part_two_plans',
to='terminal.replaystorage', verbose_name='Object storage recipient part two')),
('recipients_part_one', models.ManyToManyField(blank=True, related_name='recipient_part_one_plans',
to=settings.AUTH_USER_MODEL,
verbose_name='Recipient part one')),
('recipients_part_two', models.ManyToManyField(blank=True, related_name='recipient_part_two_plans',
to=settings.AUTH_USER_MODEL,
verbose_name='Recipient part two')),
],
options={
'verbose_name': 'Account backup plan',
},
bases=('accounts.accountbaseautomation',),
),
migrations.RunPython(migrate_account_backup),
migrations.RemoveField(
model_name='accountbackupexecution',
name='plan',
),
migrations.DeleteModel(
name='AccountBackupAutomation',
),
migrations.DeleteModel(
name='AccountBackupExecution',
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 4.1.13 on 2024-12-04 08:36
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('accounts', '0019_backupaccountautomation_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='automationexecution',
options={'permissions': [('view_changesecretexecution', 'Can view change secret execution'), ('add_changesecretexecution', 'Can add change secret execution'), ('view_gatheraccountsexecution', 'Can view gather accounts execution'), ('add_gatheraccountsexecution', 'Can add gather accounts execution'), ('view_pushaccountexecution', 'Can view push account execution'), ('add_pushaccountexecution', 'Can add push account execution'), ('view_backupaccountexecution', 'Can view backup account execution'), ('add_backupaccountexecution', 'Can add backup account execution')], 'verbose_name': 'Automation execution', 'verbose_name_plural': 'Automation executions'},
),
]

View File

@ -1,30 +1,26 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import uuid
from celery import current_task
from django.db import models from django.db import models
from django.db.models import F
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from accounts.const import AccountBackupType from accounts.const import AccountBackupType, AutomationTypes
from common.const.choices import Trigger
from common.db import fields from common.db import fields
from common.db.encoder import ModelJSONFieldEncoder from common.utils import get_logger
from common.utils import get_logger, lazyproperty from .base import AccountBaseAutomation
from ops.mixin import PeriodTaskModelMixin
from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel
__all__ = ['AccountBackupAutomation', 'AccountBackupExecution'] __all__ = ['BackupAccountAutomation']
logger = get_logger(__file__) logger = get_logger(__file__)
class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel): class BackupAccountAutomation(AccountBaseAutomation):
types = models.JSONField(default=list) types = models.JSONField(default=list)
backup_type = models.CharField(max_length=128, choices=AccountBackupType.choices, backup_type = models.CharField(
default=AccountBackupType.email.value, verbose_name=_('Backup type')) max_length=128, choices=AccountBackupType.choices,
default=AccountBackupType.email, verbose_name=_('Backup type')
)
is_password_divided_by_email = models.BooleanField(default=True, verbose_name=_('Password divided')) is_password_divided_by_email = models.BooleanField(default=True, verbose_name=_('Password divided'))
is_password_divided_by_obj_storage = models.BooleanField(default=True, verbose_name=_('Password divided')) is_password_divided_by_obj_storage = models.BooleanField(default=True, verbose_name=_('Password divided'))
recipients_part_one = models.ManyToManyField( recipients_part_one = models.ManyToManyField(
@ -51,27 +47,11 @@ class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
return f'{self.name}({self.org_id})' return f'{self.name}({self.org_id})'
class Meta: class Meta:
ordering = ['name']
unique_together = [('name', 'org_id')]
verbose_name = _('Account backup plan') verbose_name = _('Account backup plan')
def get_register_task(self):
from ...tasks import execute_account_backup_task
name = "account_backup_plan_period_{}".format(str(self.id)[:8])
task = execute_account_backup_task.name
args = (str(self.id), Trigger.timing)
kwargs = {}
return name, task, args, kwargs
def to_attr_json(self): def to_attr_json(self):
return { attr_json = super().to_attr_json()
'id': self.id, attr_json.update({
'name': self.name,
'is_periodic': self.is_periodic,
'interval': self.interval,
'crontab': self.crontab,
'org_id': self.org_id,
'created_by': self.created_by,
'types': self.types, 'types': self.types,
'backup_type': self.backup_type, 'backup_type': self.backup_type,
'is_password_divided_by_email': self.is_password_divided_by_email, 'is_password_divided_by_email': self.is_password_divided_by_email,
@ -93,75 +73,9 @@ class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
str(obj_storage.id): (str(obj_storage.name), str(obj_storage.type)) str(obj_storage.id): (str(obj_storage.name), str(obj_storage.type))
for obj_storage in self.obj_recipients_part_two.all() for obj_storage in self.obj_recipients_part_two.all()
}, },
} })
return attr_json
@property def save(self, *args, **kwargs):
def executed_amount(self): self.type = AutomationTypes.backup_account
return self.execution.count() super().save(*args, **kwargs)
def execute(self, trigger):
try:
hid = current_task.request.id
except AttributeError:
hid = str(uuid.uuid4())
execution = AccountBackupExecution.objects.create(
id=hid, plan=self, snapshot=self.to_attr_json(), trigger=trigger
)
return execution.start()
@lazyproperty
def latest_execution(self):
return self.execution.first()
class AccountBackupExecution(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
date_start = models.DateTimeField(
auto_now_add=True, verbose_name=_('Date start')
)
timedelta = models.FloatField(
default=0.0, verbose_name=_('Time'), null=True
)
snapshot = models.JSONField(
encoder=ModelJSONFieldEncoder, default=dict,
blank=True, null=True, verbose_name=_('Account backup snapshot')
)
trigger = models.CharField(
max_length=128, default=Trigger.manual, choices=Trigger.choices,
verbose_name=_('Trigger mode')
)
reason = models.CharField(
max_length=1024, blank=True, null=True, verbose_name=_('Reason')
)
is_success = models.BooleanField(default=False, verbose_name=_('Is success'))
plan = models.ForeignKey(
'AccountBackupAutomation', related_name='execution', on_delete=models.CASCADE,
verbose_name=_('Account backup plan')
)
class Meta:
ordering = ('-date_start',)
verbose_name = _('Account backup execution')
@property
def types(self):
types = self.snapshot.get('types')
return types
@lazyproperty
def backup_accounts(self):
from accounts.models import Account
# TODO 可以优化一下查询 在账号上做 category 的缓存 避免数据量大时连表操作
qs = Account.objects.filter(
asset__platform__type__in=self.types
).annotate(type=F('asset__platform__type'))
return qs
@property
def manager_type(self):
return 'backup_account'
def start(self):
from accounts.automations.endpoint import ExecutionManager
manager = ExecutionManager(execution=self)
return manager.run()

View File

@ -40,6 +40,9 @@ class AutomationExecution(AssetAutomationExecution):
('view_pushaccountexecution', _('Can view push account execution')), ('view_pushaccountexecution', _('Can view push account execution')),
('add_pushaccountexecution', _('Can add push account execution')), ('add_pushaccountexecution', _('Can add push account execution')),
('view_backupaccountexecution', _('Can view backup account execution')),
('add_backupaccountexecution', _('Can add backup account execution')),
] ]
@property @property

View File

@ -9,7 +9,7 @@ from terminal.models.component.storage import ReplayStorage
from users.models import User from users.models import User
class AccountBackupExecutionTaskMsg(object): class AccountBackupExecutionTaskMsg:
subject = _('Notification of account backup route task results') subject = _('Notification of account backup route task results')
def __init__(self, name: str, user: User): def __init__(self, name: str, user: User):
@ -34,7 +34,7 @@ class AccountBackupExecutionTaskMsg(object):
) )
class AccountBackupByObjStorageExecutionTaskMsg(object): class AccountBackupByObjStorageExecutionTaskMsg:
subject = _('Notification of account backup route task results') subject = _('Notification of account backup route task results')
def __init__(self, name: str, obj_storage: ReplayStorage): def __init__(self, name: str, obj_storage: ReplayStorage):
@ -53,7 +53,7 @@ class AccountBackupByObjStorageExecutionTaskMsg(object):
) )
class ChangeSecretExecutionTaskMsg(object): class ChangeSecretExecutionTaskMsg:
subject = _('Notification of implementation result of encryption change plan') subject = _('Notification of implementation result of encryption change plan')
def __init__(self, name: str, user: User, summary): def __init__(self, name: str, user: User, summary):

View File

@ -1,6 +1,5 @@
from .account import * from .account import *
from .backup import *
from .base import * from .base import *
from .service import *
from .template import * from .template import *
from .virtual import * from .virtual import *
from .service import *

View File

@ -1,56 +0,0 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from accounts.models import AccountBackupAutomation, AccountBackupExecution
from common.const.choices import Trigger
from common.serializers.fields import LabeledChoiceField, EncryptedField
from common.utils import get_logger
from ops.mixin import PeriodTaskSerializerMixin
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
logger = get_logger(__file__)
__all__ = ['AccountBackupSerializer', 'AccountBackupPlanExecutionSerializer']
class AccountBackupSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer):
zip_encrypt_password = EncryptedField(
label=_('Zip Encrypt Password'), required=False, max_length=40960, allow_blank=True,
allow_null=True, write_only=True,
)
class Meta:
model = AccountBackupAutomation
read_only_fields = [
'date_created', 'date_updated', 'created_by',
'periodic_display', 'executed_amount'
]
fields = read_only_fields + [
'id', 'name', 'is_periodic', 'interval', 'crontab',
'comment', 'types', 'recipients_part_one', 'recipients_part_two', 'backup_type',
'is_password_divided_by_email', 'is_password_divided_by_obj_storage', 'obj_recipients_part_one',
'obj_recipients_part_two', 'zip_encrypt_password'
]
extra_kwargs = {
'name': {'required': True},
'executed_amount': {'label': _('Executions')},
'recipients': {
'label': _('Recipient'),
'help_text': _('Currently only mail sending is supported')
},
'types': {'label': _('Asset type')}
}
class AccountBackupPlanExecutionSerializer(serializers.ModelSerializer):
trigger = LabeledChoiceField(choices=Trigger.choices, label=_("Trigger mode"), read_only=True)
class Meta:
model = AccountBackupExecution
read_only_fields = [
'id', 'date_start', 'timedelta', 'snapshot',
'trigger', 'reason', 'is_success', 'org_id'
]
fields = read_only_fields + ['plan']

View File

@ -1,3 +1,4 @@
from .backup import *
from .base import * from .base import *
from .change_secret import * from .change_secret import *
from .check_account import * from .check_account import *

View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import gettext_lazy as _
from accounts.const import AutomationTypes
from accounts.models import BackupAccountAutomation
from common.serializers.fields import EncryptedField
from common.utils import get_logger
from .base import BaseAutomationSerializer
logger = get_logger(__file__)
__all__ = ['BackupAccountSerializer']
class BackupAccountSerializer(BaseAutomationSerializer):
zip_encrypt_password = EncryptedField(
label=_('Zip Encrypt Password'), required=False, max_length=40960, allow_blank=True,
allow_null=True, write_only=True,
)
class Meta:
model = BackupAccountAutomation
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
fields = BaseAutomationSerializer.Meta.fields + read_only_fields + [
'types', 'recipients_part_one', 'recipients_part_two', 'backup_type',
'is_password_divided_by_email', 'is_password_divided_by_obj_storage',
'obj_recipients_part_one', 'obj_recipients_part_two', 'zip_encrypt_password'
]
extra_kwargs = {
'name': {'required': True},
'obj_recipients_part_one': {
'label': _('Recipient part one'), 'help_text': _(
"Currently only mail sending is supported"
)},
'obj_recipients_part_two': {
'label': _('Recipient part two'), 'help_text': _(
"Currently only mail sending is supported"
)},
'types': {'label': _('Asset type')}
}
@property
def model_type(self):
return AutomationTypes.backup_account

View File

@ -26,15 +26,14 @@ class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSe
class Meta: class Meta:
read_only_fields = [ read_only_fields = [
'date_created', 'date_updated', 'created_by', 'date_created', 'date_updated', 'created_by',
'periodic_display', 'executed_amount' 'periodic_display', 'executed_amount', 'type'
] ]
fields = read_only_fields + [ fields = read_only_fields + [
'id', 'name', 'is_periodic', 'interval', 'crontab', 'comment', 'id', 'name', 'is_periodic', 'interval', 'crontab', 'comment',
'type', 'accounts', 'nodes', 'assets', 'is_active', 'accounts', 'nodes', 'assets', 'is_active',
] ]
extra_kwargs = { extra_kwargs = {
'name': {'required': True}, 'name': {'required': True},
'type': {'read_only': True},
'executed_amount': {'label': _('Executions')}, 'executed_amount': {'label': _('Executions')},
} }
@ -58,7 +57,7 @@ class AutomationExecutionSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = AutomationExecution model = AutomationExecution
read_only_fields = [ read_only_fields = [
'trigger', 'date_start', 'date_finished', 'snapshot', 'status' 'trigger', 'date_start', 'date_finished', 'snapshot', 'status', 'duration'
] ]
fields = ['id', 'automation', 'type'] + read_only_fields fields = ['id', 'automation', 'type'] + read_only_fields

View File

@ -1,5 +1,4 @@
from .automation import * from .automation import *
from .backup_account import *
from .gather_accounts import * from .gather_accounts import *
from .push_account import * from .push_account import *
from .remove_account import * from .remove_account import *

View File

@ -1,42 +0,0 @@
# -*- coding: utf-8 -*-
#
from celery import shared_task
from django.utils.translation import gettext_lazy as _
from common.utils import get_object_or_none, get_logger
from orgs.utils import tmp_to_org, tmp_to_root_org
logger = get_logger(__file__)
def task_activity_callback(self, pid, trigger, *args, **kwargs):
from accounts.models import AccountBackupAutomation
with tmp_to_root_org():
plan = get_object_or_none(AccountBackupAutomation, pk=pid)
if not plan:
return
if not plan.latest_execution:
return
resource_ids = plan.latest_execution.backup_accounts
org_id = plan.org_id
return resource_ids, org_id
@shared_task(
verbose_name=_('Execute account backup plan'),
activity_callback=task_activity_callback,
description=_(
"""
When performing scheduled or manual account backups, this task is used
"""
)
)
def execute_account_backup_task(pid, trigger, **kwargs):
from accounts.models import AccountBackupAutomation
with tmp_to_root_org():
plan = get_object_or_none(AccountBackupAutomation, pk=pid)
if not plan:
logger.error("No account backup route plan found: {}".format(pid))
return
with tmp_to_org(plan.org):
plan.execute(trigger)

View File

@ -0,0 +1,78 @@
{% load i18n %}
<div class='summary'>
<p>{% trans 'The following is a summary of account backup tasks, please review and handle them' %}</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 'Account count' %}:</td>
<td>{{ summary.total_accounts }}</td>
</tr>
<tr>
<td>{% trans 'Type count' %}:</td>
<td>{{ summary.total_types }}</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;
}
.result {
margin-top: 20px;
}
.result tr :first-child {
width: 10%;
}
</style>

View File

@ -14,8 +14,8 @@ router.register(r'gathered-accounts', api.GatheredAccountViewSet, 'gathered-acco
router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret') router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret')
router.register(r'account-templates', api.AccountTemplateViewSet, 'account-template') router.register(r'account-templates', api.AccountTemplateViewSet, 'account-template')
router.register(r'account-template-secrets', api.AccountTemplateSecretsViewSet, 'account-template-secret') router.register(r'account-template-secrets', api.AccountTemplateSecretsViewSet, 'account-template-secret')
router.register(r'account-backup-plans', api.AccountBackupPlanViewSet, 'account-backup') router.register(r'account-backup-plans', api.BackupAccountViewSet, 'account-backup')
router.register(r'account-backup-plan-executions', api.AccountBackupPlanExecutionViewSet, 'account-backup-execution') router.register(r'account-backup-plan-executions', api.BackupAccountExecutionViewSet, 'account-backup-execution')
router.register(r'change-secret-automations', api.ChangeSecretAutomationViewSet, 'change-secret-automation') router.register(r'change-secret-automations', api.ChangeSecretAutomationViewSet, 'change-secret-automation')
router.register(r'change-secret-executions', api.ChangSecretExecutionViewSet, 'change-secret-execution') router.register(r'change-secret-executions', api.ChangSecretExecutionViewSet, 'change-secret-execution')
router.register(r'change-secret-records', api.ChangeSecretRecordViewSet, 'change-secret-record') router.register(r'change-secret-records', api.ChangeSecretRecordViewSet, 'change-secret-record')

View File

@ -98,7 +98,7 @@ class BaseManager:
self.summary = defaultdict(int) self.summary = defaultdict(int)
self.result = defaultdict(list) self.result = defaultdict(list)
self.duration = 0 self.duration = 0
self.status = 'success' self.status = Status.success
def get_assets_group_by_platform(self): def get_assets_group_by_platform(self):
return self.execution.all_assets_group_by_platform() return self.execution.all_assets_group_by_platform()

View File

@ -42,17 +42,17 @@ class AutomationExecutionSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = AutomationExecution model = AutomationExecution
read_only_fields = [ read_only_fields = [
'trigger', 'date_start', 'date_finished', 'snapshot', 'status' 'trigger', 'date_start', 'date_finished', 'snapshot', 'status', 'duration'
] ]
fields = ['id', 'automation'] + read_only_fields fields = ['id', 'automation'] + read_only_fields
@staticmethod @staticmethod
def get_status(obj): def get_status(obj):
if obj.status == 'success': from common.const import Status
return _("Success") status = Status._member_map_.get(obj.status)
elif obj.status == 'pending': if status is None:
return _("Pending") return obj.status
return obj.status return status.label
@staticmethod @staticmethod
def get_snapshot(obj): def get_snapshot(obj):