perf: 优化发送结果

pull/14517/head
ibuler 2024-11-18 11:22:46 +08:00
parent e58054c441
commit ca7d2130a5
25 changed files with 437 additions and 117 deletions

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.db.models import Q, Count from django.db.models import Q, Count
from django.http import HttpResponse
from rest_framework.decorators import action from rest_framework.decorators import action
from accounts import serializers from accounts import serializers
@ -28,8 +29,9 @@ class CheckAccountExecutionViewSet(AutomationExecutionViewSet):
("list", "accounts.view_checkaccountexecution"), ("list", "accounts.view_checkaccountexecution"),
("retrieve", "accounts.view_checkaccountsexecution"), ("retrieve", "accounts.view_checkaccountsexecution"),
("create", "accounts.add_checkaccountexecution"), ("create", "accounts.add_checkaccountexecution"),
("report", "accounts.view_checkaccountsexecution"),
) )
ordering = ('-date_created',)
tp = AutomationTypes.check_account tp = AutomationTypes.check_account
def get_queryset(self): def get_queryset(self):
@ -37,6 +39,12 @@ class CheckAccountExecutionViewSet(AutomationExecutionViewSet):
queryset = queryset.filter(automation__type=self.tp) queryset = queryset.filter(automation__type=self.tp)
return queryset return queryset
@action(methods=['get'], detail=True, url_path='report')
def report(self, request, *args, **kwargs):
execution = self.get_object()
report = execution.manager.gen_report()
return HttpResponse(report)
class AccountRiskViewSet(OrgBulkModelViewSet): class AccountRiskViewSet(OrgBulkModelViewSet):
model = AccountRisk model = AccountRisk

View File

@ -1,3 +1,5 @@
from django.template.loader import render_to_string
from accounts.automations.methods import platform_automation_methods from accounts.automations.methods import platform_automation_methods
from assets.automations.base.manager import BasePlaybookManager from assets.automations.base.manager import BasePlaybookManager
from common.utils import get_logger from common.utils import get_logger
@ -6,7 +8,16 @@ logger = get_logger(__name__)
class AccountBasePlaybookManager(BasePlaybookManager): class AccountBasePlaybookManager(BasePlaybookManager):
template_path = ''
@property @property
def platform_automation_methods(self): def platform_automation_methods(self):
return platform_automation_methods return platform_automation_methods
def gen_report(self):
context = {
'execution': self.execution,
'summary': self.execution.summary,
'result': self.execution.result
}
return render_to_string(self.template_path, context)

View File

@ -15,7 +15,6 @@ from assets.const import HostTypes
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 users.models import User
from ..base.manager import AccountBasePlaybookManager from ..base.manager import AccountBasePlaybookManager
from ...utils import SecretGenerator from ...utils import SecretGenerator
@ -247,7 +246,6 @@ class ChangeSecretManager(AccountBasePlaybookManager):
] ]
recipients = self.execution.recipients recipients = self.execution.recipients
recipients = User.objects.filter(id__in=list(recipients.keys()))
if not recipients: if not recipients:
return return

View File

@ -2,9 +2,14 @@ import re
import time import time
from collections import defaultdict from collections import defaultdict
from django.template.loader import render_to_string
from django.utils import timezone from django.utils import timezone
from premailer import transform
from accounts.models import Account, AccountRisk from accounts.models import Account, AccountRisk
from common.db.utils import safe_db_connection
from common.tasks import send_mail_async
from common.utils.strings import color_fmt
def is_weak_password(password): def is_weak_password(password):
@ -30,7 +35,6 @@ def is_weak_password(password):
or not re.search(r'[0-9]', password) or not re.search(r'[0-9]', password)
or not re.search(r'[\W_]', password)): or not re.search(r'[\W_]', password)):
return True return True
return False return False
@ -38,27 +42,34 @@ def check_account_secrets(accounts, assets):
now = timezone.now().isoformat() now = timezone.now().isoformat()
risks = [] risks = []
tmpl = "Check account %s: %s" tmpl = "Check account %s: %s"
RED = "\033[31m"
GREEN = "\033[32m"
RESET = "\033[0m" # 还原默认颜色
summary = defaultdict(int) summary = defaultdict(int)
result = defaultdict(list)
summary['accounts'] = len(accounts)
summary['assets'] = len(assets)
for account in accounts: for account in accounts:
result_item = {
'asset': str(account.asset),
'username': account.username,
}
if not account.secret: if not account.secret:
print(tmpl % (account, "no secret")) print(tmpl % (account, "no secret"))
summary['no_secret'] += 1 summary['no_secret'] += 1
result['no_secret'].append(result_item)
continue continue
if is_weak_password(account.secret): if is_weak_password(account.secret):
print(tmpl % (account, f"{RED}weak{RESET}")) print(tmpl % (account, color_fmt("weak", "red")))
summary['weak'] += 1 summary['weak_password'] += 1
result['weak_password'].append(result_item)
risks.append({ risks.append({
'account': account, 'account': account,
'risk': 'weak_password', 'risk': 'weak_password',
}) })
else: else:
summary['ok'] += 1 summary['ok'] += 1
print(tmpl % (account, f"{GREEN}ok{RESET}")) result['ok'].append(result_item)
print(tmpl % (account, color_fmt("ok", "green")))
origin_risks = AccountRisk.objects.filter(asset__in=assets) origin_risks = AccountRisk.objects.filter(asset__in=assets)
origin_risks_dict = {f'{r.asset_id}_{r.username}_{r.risk}': r for r in origin_risks} origin_risks_dict = {f'{r.asset_id}_{r.username}_{r.risk}': r for r in origin_risks}
@ -77,7 +88,7 @@ def check_account_secrets(accounts, assets):
risk=d['risk'], risk=d['risk'],
details=[{'datetime': now}], details=[{'datetime': now}],
) )
return summary return summary, result
class CheckAccountManager: class CheckAccountManager:
@ -90,9 +101,12 @@ class CheckAccountManager:
self.timedelta = 0 self.timedelta = 0
self.assets = [] self.assets = []
self.summary = {} self.summary = {}
self.result = defaultdict(list)
def pre_run(self): def pre_run(self):
self.assets = self.execution.get_all_assets() self.assets = self.execution.get_all_assets()
self.execution.date_start = timezone.now()
self.execution.save(update_fields=['date_start'])
def batch_run(self, batch_size=100): def batch_run(self, batch_size=100):
for engine in self.execution.snapshot.get('engines', []): for engine in self.execution.snapshot.get('engines', []):
@ -104,18 +118,57 @@ class CheckAccountManager:
for i in range(0, len(self.assets), batch_size): for i in range(0, len(self.assets), batch_size):
_assets = self.assets[i:i + batch_size] _assets = self.assets[i:i + batch_size]
accounts = Account.objects.filter(asset__in=_assets) accounts = Account.objects.filter(asset__in=_assets)
summary = handle(accounts, _assets) summary, result = handle(accounts, _assets)
self.summary.update(summary)
def after_run(self): for k, v in summary.items():
self.summary[k] = self.summary.get(k, 0) + v
for k, v in result.items():
self.result[k].extend(v)
def _update_execution_and_summery(self):
self.date_end = timezone.now() self.date_end = timezone.now()
self.time_end = time.time() self.time_end = time.time()
self.timedelta = self.time_end - self.time_start self.duration = self.time_end - self.time_start
tmpl = "\n-\nSummary: ok: %s, weak: %s, no_secret: %s, using time: %ss" % ( self.execution.date_finished = timezone.now()
self.summary['ok'], self.summary['weak'], self.summary['no_secret'], self.timedelta self.execution.status = 'success'
self.execution.summary = self.summary
self.execution.result = self.result
with safe_db_connection():
self.execution.save(update_fields=['date_finished', 'status', 'summary', 'result'])
def after_run(self):
self._update_execution_and_summery()
self._send_report()
tmpl = "\n---\nSummary: \nok: %s, weak password: %s, no secret: %s, using time: %ss" % (
self.summary['ok'], self.summary['weak_password'], self.summary['no_secret'], int(self.timedelta)
) )
print(tmpl) print(tmpl)
def gen_report(self):
template_path = 'accounts/check_account_report.html'
context = {
'execution': self.execution,
'summary': self.execution.summary,
'result': self.execution.result
}
data = render_to_string(template_path, context)
return data
def _send_report(self):
recipients = self.execution.recipients
if not recipients:
return
report = self.gen_report()
report = transform(report)
print("Send resport to: {}".format([str(r) for r in recipients]))
subject = f'Check account automation {self.execution.id} finished'
emails = [r.email for r in recipients if r.email]
send_mail_async(subject, report, emails, html_message=report)
def run(self,): def run(self,):
self.pre_run() self.pre_run()
self.batch_run() self.batch_run()

View File

@ -18,7 +18,6 @@ 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,
# TODO 后期迁移到自动化策略中
'backup_account': AccountBackupManager, 'backup_account': AccountBackupManager,
} }
@ -28,3 +27,6 @@ class ExecutionManager:
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
return self._runner.run(*args, **kwargs) return self._runner.run(*args, **kwargs)
def __getattr__(self, item):
return getattr(self._runner, item)

View File

@ -9,7 +9,6 @@ from common.const import ConfirmOrIgnore
from common.utils import get_logger from common.utils import get_logger
from common.utils.strings import get_text_diff from common.utils.strings import get_text_diff
from orgs.utils import tmp_to_org from orgs.utils import tmp_to_org
from users.models import User
from .filter import GatherAccountsFilter from .filter import GatherAccountsFilter
from ..base.manager import AccountBasePlaybookManager from ..base.manager import AccountBasePlaybookManager
from ...notifications import GatherAccountChangeMsg from ...notifications import GatherAccountChangeMsg
@ -313,10 +312,7 @@ class GatherAccountsManager(AccountBasePlaybookManager):
if not self.asset_usernames_mapper or not recipients: if not self.asset_usernames_mapper or not recipients:
return None, None return None, None
users = User.objects.filter(id__in=recipients) users = recipients
if not users.exists():
return users, None
asset_ids = self.asset_usernames_mapper.keys() asset_ids = self.asset_usernames_mapper.keys()
assets = Asset.objects.filter(id__in=asset_ids).prefetch_related('accounts') assets = Asset.objects.filter(id__in=asset_ids).prefetch_related('accounts')
gather_accounts = GatheredAccount.objects.filter(asset_id__in=asset_ids, remote_present=True) gather_accounts = GatheredAccount.objects.filter(asset_id__in=asset_ids, remote_present=True)

View File

@ -7,68 +7,6 @@ logger = get_logger(__name__)
class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager): class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
@classmethod @classmethod
def method_type(cls): def method_type(cls):
return AutomationTypes.push_account return AutomationTypes.push_account
# @classmethod
# def trigger_by_asset_create(cls, asset):
# automations = PushAccountAutomation.objects.filter(
# triggers__contains=TriggerChoice.on_asset_create
# )
# account_automation_map = {auto.username: auto for auto in automations}
#
# util = AssetPermissionUtil()
# permissions = util.get_permissions_for_assets([asset], with_node=True)
# account_permission_map = defaultdict(list)
# for permission in permissions:
# for account in permission.accounts:
# account_permission_map[account].append(permission)
#
# username_automation_map = {}
# for username, automation in account_automation_map.items():
# if username != '@USER':
# username_automation_map[username] = automation
# continue
#
# asset_permissions = account_permission_map.get(username)
# if not asset_permissions:
# continue
# asset_permissions = util.get_permissions([p.id for p in asset_permissions])
# usernames = asset_permissions.values_list('users__username', flat=True).distinct()
# for _username in usernames:
# username_automation_map[_username] = automation
#
# asset_usernames_exists = asset.accounts.values_list('username', flat=True)
# accounts_to_create = []
# accounts_to_push = []
# for username, automation in username_automation_map.items():
# if username in asset_usernames_exists:
# continue
#
# if automation.secret_strategy != SecretStrategy.custom:
# secret_generator = SecretGenerator(
# automation.secret_strategy, automation.secret_type,
# automation.password_rules
# )
# secret = secret_generator.get_secret()
# else:
# secret = automation.secret
#
# account = Account(
# username=username, secret=secret,
# asset=asset, secret_type=automation.secret_type,
# comment='Create by account creation {}'.format(automation.name),
# )
# accounts_to_create.append(account)
# if automation.action == 'create_and_push':
# accounts_to_push.append(account)
# else:
# accounts_to_create.append(account)
#
# logger.debug(f'Create account {account} for asset {asset}')
# @classmethod
# def trigger_by_permission_accounts_change(cls):
# pass

View File

@ -48,13 +48,13 @@ class Migration(migrations.Migration):
), ),
), ),
migrations.AddField( migrations.AddField(
model_name='changesecretautomation', model_name='changesecretautomation',
name='check_conn_after_change', name='check_conn_after_change',
field=models.BooleanField(default=True, verbose_name='Check connection after change'), field=models.BooleanField(default=True, verbose_name='Check connection after change'),
), ),
migrations.AddField( migrations.AddField(
model_name='pushaccountautomation', model_name='pushaccountautomation',
name='check_conn_after_change', name='check_conn_after_change',
field=models.BooleanField(default=True, verbose_name='Check connection after change'), field=models.BooleanField(default=True, verbose_name='Check connection after change'),
), ),
] ]

View File

@ -5,7 +5,6 @@ import django.db.models.deletion
import uuid import uuid
def init_account_check_engine(apps, schema_editor): def init_account_check_engine(apps, schema_editor):
data = [ data = [
{ {
@ -26,7 +25,6 @@ def init_account_check_engine(apps, schema_editor):
model_cls.objects.create(**item) model_cls.objects.create(**item)
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [

View File

@ -0,0 +1,22 @@
# Generated by Django 4.1.13 on 2024-11-15 03:00
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("accounts", "0012_accountcheckengine_accountcheckautomation_engines"),
]
operations = [
migrations.AddField(
model_name="checkaccountautomation",
name="recipients",
field=models.ManyToManyField(
blank=True, to=settings.AUTH_USER_MODEL, verbose_name="Recipient"
),
),
]

View File

@ -42,10 +42,11 @@ class AutomationExecution(AssetAutomationExecution):
('add_pushaccountexecution', _('Can add push account execution')), ('add_pushaccountexecution', _('Can add push account execution')),
] ]
def start(self): @property
def manager(self):
from accounts.automations.endpoint import ExecutionManager from accounts.automations.endpoint import ExecutionManager
manager = ExecutionManager(execution=self) manager = ExecutionManager(execution=self)
return manager.run() return manager
class ChangeSecretMixin(SecretWithRandomMixin): class ChangeSecretMixin(SecretWithRandomMixin):

View File

@ -24,10 +24,7 @@ class ChangeSecretAutomation(ChangeSecretMixin, AccountBaseAutomation):
def to_attr_json(self): def to_attr_json(self):
attr_json = super().to_attr_json() attr_json = super().to_attr_json()
attr_json.update({ attr_json.update({
'recipients': { 'recipients': [str(r.id) for r in self.recipients.all()]
str(recipient.id): (str(recipient), bool(recipient.secret_key))
for recipient in self.recipients.all()
}
}) })
return attr_json return attr_json

View File

@ -15,11 +15,16 @@ __all__ = ['CheckAccountAutomation', 'AccountRisk', 'RiskChoice', 'CheckAccountE
class CheckAccountAutomation(AccountBaseAutomation): class CheckAccountAutomation(AccountBaseAutomation):
engines = models.ManyToManyField('CheckAccountEngine', related_name='check_automations', verbose_name=_('Engines')) engines = models.ManyToManyField('CheckAccountEngine', related_name='check_automations', verbose_name=_('Engines'))
recipients = models.ManyToManyField('users.User', verbose_name=_("Recipient"), blank=True)
def get_report_template(self):
return 'accounts/check_account_report.html'
def to_attr_json(self): def to_attr_json(self):
attr_json = super().to_attr_json() attr_json = super().to_attr_json()
attr_json.update({ attr_json.update({
'engines': [engine.slug for engine in self.engines.all()], 'engines': [engine.slug for engine in self.engines.all()],
'recipients': [str(user.id) for user in self.recipients.all()]
}) })
return attr_json return attr_json

View File

@ -9,7 +9,7 @@ from common.utils.timezone import is_date_more_than
from orgs.mixins.models import JMSOrgBaseModel from orgs.mixins.models import JMSOrgBaseModel
from .base import AccountBaseAutomation from .base import AccountBaseAutomation
__all__ = ['GatherAccountsAutomation', 'GatheredAccount', ] __all__ = ['GatherAccountsAutomation', 'GatheredAccount',]
class GatheredAccount(JMSOrgBaseModel): class GatheredAccount(JMSOrgBaseModel):

View File

@ -58,7 +58,7 @@ class CheckAccountAutomationSerializer(BaseAutomationSerializer):
model = CheckAccountAutomation model = CheckAccountAutomation
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
fields = BaseAutomationSerializer.Meta.fields \ fields = BaseAutomationSerializer.Meta.fields \
+ ['engines'] + read_only_fields + ['engines', 'recipients'] + read_only_fields
extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs
@property @property

View File

@ -0,0 +1,108 @@
{% load i18n %}
<div class='summary'>
<p>{% trans 'The following is a summary of the account check tasks. Please review and handle them' %}</p>
<table>
<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 }}</td>
</tr>
<tr>
<td>{% trans 'Date end' %}: </td>
<td>{{ execution.date_finished }}</td>
</tr>
<tr>
<td>{% trans 'Time using' %}: </td>
<td>{{ execution.duration }}s</td>
</tr>
<tr>
<td>{% trans 'Assets count' %}: </td>
<td>{{ summary.assets }}</td>
</tr>
<tr>
<td>{% trans 'Account count' %}: </td>
<td>{{ summary.accounts }}</td>
</tr>
<tr>
<td>{% trans 'Week password count' %}:</td>
<td> <span> {{ summary.weak_password }}</span></td>
</tr>
<tr>
<td>{% trans 'Ok count' %}: </td>
<td>{{ summary.ok }}</td>
</tr>
<tr>
<td>{% trans 'No password count' %}: </td>
<td>{{ summary.no_secret }}</td>
</tr>
</tbody>
</table>
</div>
<div class='result'>
<p>{% trans 'Account check details' %}:</p>
<table style="">
<thead>
<tr>
<th>{% trans 'No.' %}</th>
<th>{% trans 'Asset' %}</th>
<th>{% trans 'Username' %}</th>
<th>{% trans 'Result' %}</th>
</tr>
</thead>
<tbody>
{% for account in result.weak_password %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ account.asset }}</td>
<td>{{ account.username }}</td>
<td>{% trans 'Week password' %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<style>
table {
width: 100%;
border-collapse: collapse;
max-width: 100%;
text-align: left;
margin-top: 20px;
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 tr :first-child {
width: 10%;
}
</style>

View File

@ -1,6 +1,6 @@
from .gather_facts.manager import GatherFactsManager
from .ping.manager import PingManager from .ping.manager import PingManager
from .ping_gateway.manager import PingGatewayManager from .ping_gateway.manager import PingGatewayManager
from .gather_facts.manager import GatherFactsManager
from ..const import AutomationTypes from ..const import AutomationTypes
@ -17,3 +17,6 @@ class ExecutionManager:
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
return self._runner.run(*args, **kwargs) return self._runner.run(*args, **kwargs)
def __getattr__(self, item):
return getattr(self._runner, item)

View File

@ -0,0 +1,23 @@
# Generated by Django 4.1.13 on 2024-11-15 10:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("assets", "0007_baseautomation_date_last_run_and_more"),
]
operations = [
migrations.AddField(
model_name="automationexecution",
name="result",
field=models.JSONField(default=dict, verbose_name="Result"),
),
migrations.AddField(
model_name="automationexecution",
name="summary",
field=models.JSONField(default=dict, verbose_name="Summary"),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.13 on 2024-11-15 10:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("assets", "0008_automationexecution_result_and_more"),
]
operations = [
migrations.AddField(
model_name="automationexecution",
name="duration",
field=models.FloatField(default=0, verbose_name="Duration"),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.13 on 2024-11-18 02:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("assets", "0009_automationexecution_duration"),
]
operations = [
migrations.AlterField(
model_name="automationexecution",
name="duration",
field=models.IntegerField(default=0, verbose_name="Duration"),
),
]

View File

@ -11,6 +11,7 @@ from common.const.choices import Trigger
from common.db.fields import EncryptJsonDictTextField from common.db.fields import EncryptJsonDictTextField
from ops.mixin import PeriodTaskModelMixin from ops.mixin import PeriodTaskModelMixin
from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel
from users.models import User
class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel): class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
@ -21,6 +22,9 @@ class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
is_active = models.BooleanField(default=True, verbose_name=_("Is active")) is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
params = models.JSONField(default=dict, verbose_name=_("Parameters")) params = models.JSONField(default=dict, verbose_name=_("Parameters"))
def get_report_template(self):
raise NotImplementedError
def __str__(self): def __str__(self):
return self.name + '@' + str(self.created_by) return self.name + '@' + str(self.created_by)
@ -114,6 +118,7 @@ class AutomationExecution(OrgModelMixin):
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created'))
date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True) date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True)
date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished")) date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished"))
duration = models.IntegerField(default=0, verbose_name=_('Duration'))
snapshot = EncryptJsonDictTextField( snapshot = EncryptJsonDictTextField(
default=dict, blank=True, null=True, verbose_name=_('Automation snapshot') default=dict, blank=True, null=True, verbose_name=_('Automation snapshot')
) )
@ -121,6 +126,8 @@ class AutomationExecution(OrgModelMixin):
max_length=128, default=Trigger.manual, choices=Trigger.choices, max_length=128, default=Trigger.manual, choices=Trigger.choices,
verbose_name=_('Trigger mode') verbose_name=_('Trigger mode')
) )
summary = models.JSONField(default=dict, verbose_name=_('Summary'))
result = models.JSONField(default=dict, verbose_name=_('Result'))
class Meta: class Meta:
ordering = ('org_id', '-date_start',) ordering = ('org_id', '-date_start',)
@ -150,10 +157,14 @@ class AutomationExecution(OrgModelMixin):
def recipients(self): def recipients(self):
recipients = self.snapshot.get('recipients') recipients = self.snapshot.get('recipients')
if not recipients: if not recipients:
return {} return []
return recipients users = User.objects.filter(id__in=recipients)
return users
@property
def manager(self):
from assets.automations.endpoint import ExecutionManager
return ExecutionManager(execution=self)
def start(self): def start(self):
from assets.automations.endpoint import ExecutionManager return self.manager.run()
manager = ExecutionManager(execution=self)
return manager.run()

View File

@ -15,3 +15,30 @@ def get_text_diff(old_text, new_text):
old_text.splitlines(), new_text.splitlines(), lineterm="" old_text.splitlines(), new_text.splitlines(), lineterm=""
) )
return "\n".join(diff) return "\n".join(diff)
def color_fmt(msg, color=None):
# ANSI 颜色代码
colors = {
'red': '\033[91m',
'green': '\033[92m',
'yellow': '\033[93m',
'blue': '\033[94m',
'purple': '\033[95m',
'cyan': '\033[96m',
'default': '\033[0m' # 结束颜色的默认值
}
# 获取颜色代码,如果没有指定颜色或颜色不支持,使用默认颜色
color_code = colors.get(color, colors['default'])
# 打印带颜色的消息
return f"{color_code}{msg}{colors['default']}" # 确保在消息结束后重置颜色
def color_print(msg, color=None):
print(color_fmt(msg, color))
def color_fill_print(tmp, msg, color=None):
text = tmp.format(color_fmt(msg, color))
print(text)

View File

@ -2,17 +2,17 @@
# #
import json import json
import redis_lock
import redis import redis
import redis_lock
from django.conf import settings from django.conf import settings
from django.utils.timezone import get_current_timezone
from django.db.utils import ProgrammingError, OperationalError from django.db.utils import ProgrammingError, OperationalError
from django.utils.timezone import get_current_timezone
from django_celery_beat.models import ( from django_celery_beat.models import (
PeriodicTask, IntervalSchedule, CrontabSchedule, PeriodicTasks PeriodicTask, IntervalSchedule, CrontabSchedule, PeriodicTasks
) )
from common.utils.timezone import local_now
from common.utils import get_logger from common.utils import get_logger
from common.utils.timezone import local_now
logger = get_logger(__name__) logger = get_logger(__name__)
@ -67,7 +67,7 @@ def create_or_update_celery_periodic_tasks(tasks):
if crontab is None: if crontab is None:
crontab = CrontabSchedule.objects.create(**kwargs) crontab = CrontabSchedule.objects.create(**kwargs)
else: else:
logger.error("Schedule is not valid") logger.warning("Schedule is not valid: %s" % name)
return return
defaults = dict( defaults = dict(

86
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. # This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
[[package]] [[package]]
name = "adal" name = "adal"
@ -1613,6 +1613,45 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple" url = "https://mirrors.aliyun.com/pypi/simple"
reference = "aliyun" reference = "aliyun"
[[package]]
name = "cssselect"
version = "1.2.0"
description = "cssselect parses CSS3 Selectors and translates them to XPath 1.0"
optional = false
python-versions = ">=3.7"
files = [
{file = "cssselect-1.2.0-py2.py3-none-any.whl", hash = "sha256:da1885f0c10b60c03ed5eccbb6b68d6eff248d91976fcde348f395d54c9fd35e"},
{file = "cssselect-1.2.0.tar.gz", hash = "sha256:666b19839cfaddb9ce9d36bfe4c969132c647b92fc9088c4e23f786b30f1b3dc"},
]
[package.source]
type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "aliyun"
[[package]]
name = "cssutils"
version = "2.11.1"
description = "A CSS Cascading Style Sheets library for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "cssutils-2.11.1-py3-none-any.whl", hash = "sha256:a67bfdfdff4f3867fab43698ec4897c1a828eca5973f4073321b3bccaf1199b1"},
{file = "cssutils-2.11.1.tar.gz", hash = "sha256:0563a76513b6af6eebbe788c3bf3d01c920e46b3f90c8416738c5cfc773ff8e2"},
]
[package.dependencies]
more-itertools = "*"
[package.extras]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
test = ["cssselect", "importlib-resources", "jaraco.test (>=5.1)", "lxml", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
[package.source]
type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "aliyun"
[[package]] [[package]]
name = "daphne" name = "daphne"
version = "4.0.0" version = "4.0.0"
@ -3899,6 +3938,22 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple" url = "https://mirrors.aliyun.com/pypi/simple"
reference = "aliyun" reference = "aliyun"
[[package]]
name = "more-itertools"
version = "10.5.0"
description = "More routines for operating on iterables, beyond itertools"
optional = false
python-versions = ">=3.8"
files = [
{file = "more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6"},
{file = "more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef"},
]
[package.source]
type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "aliyun"
[[package]] [[package]]
name = "msal" name = "msal"
version = "1.29.0" version = "1.29.0"
@ -4762,6 +4817,33 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple" url = "https://mirrors.aliyun.com/pypi/simple"
reference = "aliyun" reference = "aliyun"
[[package]]
name = "premailer"
version = "3.10.0"
description = "Turns CSS blocks into style attributes"
optional = false
python-versions = "*"
files = [
{file = "premailer-3.10.0-py2.py3-none-any.whl", hash = "sha256:021b8196364d7df96d04f9ade51b794d0b77bcc19e998321c515633a2273be1a"},
{file = "premailer-3.10.0.tar.gz", hash = "sha256:d1875a8411f5dc92b53ef9f193db6c0f879dc378d618e0ad292723e388bfe4c2"},
]
[package.dependencies]
cachetools = "*"
cssselect = "*"
cssutils = "*"
lxml = "*"
requests = "*"
[package.extras]
dev = ["black", "flake8", "therapist", "tox", "twine", "wheel"]
test = ["mock", "nose"]
[package.source]
type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "aliyun"
[[package]] [[package]]
name = "prettytable" name = "prettytable"
version = "3.10.0" version = "3.10.0"
@ -7670,4 +7752,4 @@ reference = "aliyun"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "9acfafd75bf7dbb7e0dffb54b7f11f6b09aa4ceff769d193a3906d03ae796ccc" content-hash = "184c3ae62b74c9af2a61c7a1e955666da7099bd832ad3c16504b1b3012ff93bb"

View File

@ -165,6 +165,7 @@ polib = "^1.2.0"
# psycopg2 = "2.9.6" # psycopg2 = "2.9.6"
psycopg2-binary = "2.9.6" psycopg2-binary = "2.9.6"
pycountry = "^24.6.1" pycountry = "^24.6.1"
premailer = "^3.10.0"
[tool.poetry.group.xpack] [tool.poetry.group.xpack]
optional = true optional = true