diff --git a/apps/accounts/api/automations/check_account.py b/apps/accounts/api/automations/check_account.py index 7c975b586..702899811 100644 --- a/apps/accounts/api/automations/check_account.py +++ b/apps/accounts/api/automations/check_account.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # from django.db.models import Q, Count +from django.http import HttpResponse from rest_framework.decorators import action from accounts import serializers @@ -28,8 +29,9 @@ class CheckAccountExecutionViewSet(AutomationExecutionViewSet): ("list", "accounts.view_checkaccountexecution"), ("retrieve", "accounts.view_checkaccountsexecution"), ("create", "accounts.add_checkaccountexecution"), + ("report", "accounts.view_checkaccountsexecution"), ) - + ordering = ('-date_created',) tp = AutomationTypes.check_account def get_queryset(self): @@ -37,6 +39,12 @@ class CheckAccountExecutionViewSet(AutomationExecutionViewSet): queryset = queryset.filter(automation__type=self.tp) 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): model = AccountRisk diff --git a/apps/accounts/automations/base/manager.py b/apps/accounts/automations/base/manager.py index 401304597..652a8aa89 100644 --- a/apps/accounts/automations/base/manager.py +++ b/apps/accounts/automations/base/manager.py @@ -1,3 +1,5 @@ +from django.template.loader import render_to_string + from accounts.automations.methods import platform_automation_methods from assets.automations.base.manager import BasePlaybookManager from common.utils import get_logger @@ -6,7 +8,16 @@ logger = get_logger(__name__) class AccountBasePlaybookManager(BasePlaybookManager): + template_path = '' @property def platform_automation_methods(self): 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) diff --git a/apps/accounts/automations/change_secret/manager.py b/apps/accounts/automations/change_secret/manager.py index d4ca1c2f3..294826802 100644 --- a/apps/accounts/automations/change_secret/manager.py +++ b/apps/accounts/automations/change_secret/manager.py @@ -15,7 +15,6 @@ from assets.const import HostTypes from common.utils import get_logger from common.utils.file import encrypt_and_compress_zip_file from common.utils.timezone import local_now_filename -from users.models import User from ..base.manager import AccountBasePlaybookManager from ...utils import SecretGenerator @@ -247,7 +246,6 @@ class ChangeSecretManager(AccountBasePlaybookManager): ] recipients = self.execution.recipients - recipients = User.objects.filter(id__in=list(recipients.keys())) if not recipients: return diff --git a/apps/accounts/automations/check_account/manager.py b/apps/accounts/automations/check_account/manager.py index fee2db3e5..1a5cc44b0 100644 --- a/apps/accounts/automations/check_account/manager.py +++ b/apps/accounts/automations/check_account/manager.py @@ -2,9 +2,14 @@ import re import time from collections import defaultdict +from django.template.loader import render_to_string from django.utils import timezone +from premailer import transform 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): @@ -30,7 +35,6 @@ def is_weak_password(password): or not re.search(r'[0-9]', password) or not re.search(r'[\W_]', password)): return True - return False @@ -38,27 +42,34 @@ def check_account_secrets(accounts, assets): now = timezone.now().isoformat() risks = [] tmpl = "Check account %s: %s" - RED = "\033[31m" - GREEN = "\033[32m" - RESET = "\033[0m" # 还原默认颜色 - summary = defaultdict(int) + result = defaultdict(list) + summary['accounts'] = len(accounts) + summary['assets'] = len(assets) + for account in accounts: + result_item = { + 'asset': str(account.asset), + 'username': account.username, + } if not account.secret: print(tmpl % (account, "no secret")) summary['no_secret'] += 1 + result['no_secret'].append(result_item) continue if is_weak_password(account.secret): - print(tmpl % (account, f"{RED}weak{RESET}")) - summary['weak'] += 1 + print(tmpl % (account, color_fmt("weak", "red"))) + summary['weak_password'] += 1 + result['weak_password'].append(result_item) risks.append({ 'account': account, 'risk': 'weak_password', }) else: 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_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'], details=[{'datetime': now}], ) - return summary + return summary, result class CheckAccountManager: @@ -90,9 +101,12 @@ class CheckAccountManager: self.timedelta = 0 self.assets = [] self.summary = {} + self.result = defaultdict(list) def pre_run(self): 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): for engine in self.execution.snapshot.get('engines', []): @@ -104,18 +118,57 @@ class CheckAccountManager: for i in range(0, len(self.assets), batch_size): _assets = self.assets[i:i + batch_size] accounts = Account.objects.filter(asset__in=_assets) - summary = handle(accounts, _assets) - self.summary.update(summary) + summary, result = handle(accounts, _assets) - 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.time_end = time.time() - self.timedelta = self.time_end - self.time_start - tmpl = "\n-\nSummary: ok: %s, weak: %s, no_secret: %s, using time: %ss" % ( - self.summary['ok'], self.summary['weak'], self.summary['no_secret'], self.timedelta + self.duration = self.time_end - self.time_start + self.execution.date_finished = timezone.now() + 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) + 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,): self.pre_run() self.batch_run() diff --git a/apps/accounts/automations/endpoint.py b/apps/accounts/automations/endpoint.py index 348db03d3..db600e791 100644 --- a/apps/accounts/automations/endpoint.py +++ b/apps/accounts/automations/endpoint.py @@ -18,7 +18,6 @@ class ExecutionManager: AutomationTypes.gather_accounts: GatherAccountsManager, AutomationTypes.verify_gateway_account: VerifyGatewayAccountManager, AutomationTypes.check_account: CheckAccountManager, - # TODO 后期迁移到自动化策略中 'backup_account': AccountBackupManager, } @@ -28,3 +27,6 @@ class ExecutionManager: def run(self, *args, **kwargs): return self._runner.run(*args, **kwargs) + + def __getattr__(self, item): + return getattr(self._runner, item) diff --git a/apps/accounts/automations/gather_account/manager.py b/apps/accounts/automations/gather_account/manager.py index 956a3f5c8..1b18d5dd8 100644 --- a/apps/accounts/automations/gather_account/manager.py +++ b/apps/accounts/automations/gather_account/manager.py @@ -9,7 +9,6 @@ from common.const import ConfirmOrIgnore from common.utils import get_logger from common.utils.strings import get_text_diff from orgs.utils import tmp_to_org -from users.models import User from .filter import GatherAccountsFilter from ..base.manager import AccountBasePlaybookManager from ...notifications import GatherAccountChangeMsg @@ -313,10 +312,7 @@ class GatherAccountsManager(AccountBasePlaybookManager): if not self.asset_usernames_mapper or not recipients: return None, None - users = User.objects.filter(id__in=recipients) - if not users.exists(): - return users, None - + users = recipients asset_ids = self.asset_usernames_mapper.keys() assets = Asset.objects.filter(id__in=asset_ids).prefetch_related('accounts') gather_accounts = GatheredAccount.objects.filter(asset_id__in=asset_ids, remote_present=True) diff --git a/apps/accounts/automations/push_account/manager.py b/apps/accounts/automations/push_account/manager.py index 12f630923..18aca5134 100644 --- a/apps/accounts/automations/push_account/manager.py +++ b/apps/accounts/automations/push_account/manager.py @@ -7,68 +7,6 @@ logger = get_logger(__name__) class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager): - @classmethod def method_type(cls): 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 diff --git a/apps/accounts/migrations/0008_remove_accountrisk_confirmed_accountrisk_status_and_more.py b/apps/accounts/migrations/0008_remove_accountrisk_confirmed_accountrisk_status_and_more.py index c4c18f232..fc737791f 100644 --- a/apps/accounts/migrations/0008_remove_accountrisk_confirmed_accountrisk_status_and_more.py +++ b/apps/accounts/migrations/0008_remove_accountrisk_confirmed_accountrisk_status_and_more.py @@ -48,13 +48,13 @@ class Migration(migrations.Migration): ), ), migrations.AddField( - model_name='changesecretautomation', - name='check_conn_after_change', - field=models.BooleanField(default=True, verbose_name='Check connection after change'), - ), - migrations.AddField( - model_name='pushaccountautomation', - name='check_conn_after_change', - field=models.BooleanField(default=True, verbose_name='Check connection after change'), - ), + model_name='changesecretautomation', + name='check_conn_after_change', + field=models.BooleanField(default=True, verbose_name='Check connection after change'), + ), + migrations.AddField( + model_name='pushaccountautomation', + name='check_conn_after_change', + field=models.BooleanField(default=True, verbose_name='Check connection after change'), + ), ] diff --git a/apps/accounts/migrations/0012_checkaccountautomation_checkaccountengine_and_more.py b/apps/accounts/migrations/0012_accountcheckengine_accountcheckautomation_engines.py similarity index 99% rename from apps/accounts/migrations/0012_checkaccountautomation_checkaccountengine_and_more.py rename to apps/accounts/migrations/0012_accountcheckengine_accountcheckautomation_engines.py index 3ec3e4e50..375469b9d 100644 --- a/apps/accounts/migrations/0012_checkaccountautomation_checkaccountengine_and_more.py +++ b/apps/accounts/migrations/0012_accountcheckengine_accountcheckautomation_engines.py @@ -5,7 +5,6 @@ import django.db.models.deletion import uuid - def init_account_check_engine(apps, schema_editor): data = [ { @@ -26,7 +25,6 @@ def init_account_check_engine(apps, schema_editor): model_cls.objects.create(**item) - class Migration(migrations.Migration): dependencies = [ diff --git a/apps/accounts/migrations/0013_checkaccountautomation_recipients.py b/apps/accounts/migrations/0013_checkaccountautomation_recipients.py new file mode 100644 index 000000000..4197379b2 --- /dev/null +++ b/apps/accounts/migrations/0013_checkaccountautomation_recipients.py @@ -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" + ), + ), + ] diff --git a/apps/accounts/models/automations/base.py b/apps/accounts/models/automations/base.py index 837830759..8a2464c7e 100644 --- a/apps/accounts/models/automations/base.py +++ b/apps/accounts/models/automations/base.py @@ -42,10 +42,11 @@ class AutomationExecution(AssetAutomationExecution): ('add_pushaccountexecution', _('Can add push account execution')), ] - def start(self): + @property + def manager(self): from accounts.automations.endpoint import ExecutionManager manager = ExecutionManager(execution=self) - return manager.run() + return manager class ChangeSecretMixin(SecretWithRandomMixin): diff --git a/apps/accounts/models/automations/change_secret.py b/apps/accounts/models/automations/change_secret.py index 6d1c22715..dff4d5cc4 100644 --- a/apps/accounts/models/automations/change_secret.py +++ b/apps/accounts/models/automations/change_secret.py @@ -24,10 +24,7 @@ class ChangeSecretAutomation(ChangeSecretMixin, AccountBaseAutomation): def to_attr_json(self): attr_json = super().to_attr_json() attr_json.update({ - 'recipients': { - str(recipient.id): (str(recipient), bool(recipient.secret_key)) - for recipient in self.recipients.all() - } + 'recipients': [str(r.id) for r in self.recipients.all()] }) return attr_json diff --git a/apps/accounts/models/automations/check_account.py b/apps/accounts/models/automations/check_account.py index 71b7c543d..2f3657764 100644 --- a/apps/accounts/models/automations/check_account.py +++ b/apps/accounts/models/automations/check_account.py @@ -15,11 +15,16 @@ __all__ = ['CheckAccountAutomation', 'AccountRisk', 'RiskChoice', 'CheckAccountE class CheckAccountAutomation(AccountBaseAutomation): 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): attr_json = super().to_attr_json() attr_json.update({ 'engines': [engine.slug for engine in self.engines.all()], + 'recipients': [str(user.id) for user in self.recipients.all()] }) return attr_json diff --git a/apps/accounts/models/automations/gather_account.py b/apps/accounts/models/automations/gather_account.py index b575d4f42..dca398b92 100644 --- a/apps/accounts/models/automations/gather_account.py +++ b/apps/accounts/models/automations/gather_account.py @@ -9,7 +9,7 @@ from common.utils.timezone import is_date_more_than from orgs.mixins.models import JMSOrgBaseModel from .base import AccountBaseAutomation -__all__ = ['GatherAccountsAutomation', 'GatheredAccount', ] +__all__ = ['GatherAccountsAutomation', 'GatheredAccount',] class GatheredAccount(JMSOrgBaseModel): diff --git a/apps/accounts/serializers/automations/check_account.py b/apps/accounts/serializers/automations/check_account.py index 1f0e10aa6..32e50e88b 100644 --- a/apps/accounts/serializers/automations/check_account.py +++ b/apps/accounts/serializers/automations/check_account.py @@ -58,7 +58,7 @@ class CheckAccountAutomationSerializer(BaseAutomationSerializer): model = CheckAccountAutomation read_only_fields = BaseAutomationSerializer.Meta.read_only_fields fields = BaseAutomationSerializer.Meta.fields \ - + ['engines'] + read_only_fields + + ['engines', 'recipients'] + read_only_fields extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs @property diff --git a/apps/accounts/templates/accounts/check_account_report.html b/apps/accounts/templates/accounts/check_account_report.html new file mode 100644 index 000000000..820cecd97 --- /dev/null +++ b/apps/accounts/templates/accounts/check_account_report.html @@ -0,0 +1,108 @@ +{% load i18n %} + +
+

{% trans 'The following is a summary of the account check tasks. Please review and handle them' %}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
任务汇总:
{% trans 'Task name' %}: {{ execution.automation.name }}
{% trans 'Date start' %}: {{ execution.date_start }}
{% trans 'Date end' %}: {{ execution.date_finished }}
{% trans 'Time using' %}: {{ execution.duration }}s
{% trans 'Assets count' %}: {{ summary.assets }}
{% trans 'Account count' %}: {{ summary.accounts }}
{% trans 'Week password count' %}: {{ summary.weak_password }}
{% trans 'Ok count' %}: {{ summary.ok }}
{% trans 'No password count' %}: {{ summary.no_secret }}
+
+ +
+

{% trans 'Account check details' %}:

+ + + + + + + + + + + {% for account in result.weak_password %} + + + + + + + {% endfor %} + +
{% trans 'No.' %}{% trans 'Asset' %}{% trans 'Username' %}{% trans 'Result' %}
{{ forloop.counter }}{{ account.asset }}{{ account.username }}{% trans 'Week password' %}
+
+ + diff --git a/apps/assets/automations/endpoint.py b/apps/assets/automations/endpoint.py index 99feebc49..c29efbd48 100644 --- a/apps/assets/automations/endpoint.py +++ b/apps/assets/automations/endpoint.py @@ -1,6 +1,6 @@ +from .gather_facts.manager import GatherFactsManager from .ping.manager import PingManager from .ping_gateway.manager import PingGatewayManager -from .gather_facts.manager import GatherFactsManager from ..const import AutomationTypes @@ -17,3 +17,6 @@ class ExecutionManager: def run(self, *args, **kwargs): return self._runner.run(*args, **kwargs) + + def __getattr__(self, item): + return getattr(self._runner, item) diff --git a/apps/assets/migrations/0008_automationexecution_result_and_more.py b/apps/assets/migrations/0008_automationexecution_result_and_more.py new file mode 100644 index 000000000..48a371488 --- /dev/null +++ b/apps/assets/migrations/0008_automationexecution_result_and_more.py @@ -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"), + ), + ] diff --git a/apps/assets/migrations/0009_automationexecution_duration.py b/apps/assets/migrations/0009_automationexecution_duration.py new file mode 100644 index 000000000..ab905469c --- /dev/null +++ b/apps/assets/migrations/0009_automationexecution_duration.py @@ -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"), + ), + ] diff --git a/apps/assets/migrations/0010_alter_automationexecution_duration.py b/apps/assets/migrations/0010_alter_automationexecution_duration.py new file mode 100644 index 000000000..b9c6708c2 --- /dev/null +++ b/apps/assets/migrations/0010_alter_automationexecution_duration.py @@ -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"), + ), + ] diff --git a/apps/assets/models/automations/base.py b/apps/assets/models/automations/base.py index 660552b8f..f1c67582e 100644 --- a/apps/assets/models/automations/base.py +++ b/apps/assets/models/automations/base.py @@ -11,6 +11,7 @@ from common.const.choices import Trigger from common.db.fields import EncryptJsonDictTextField from ops.mixin import PeriodTaskModelMixin from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel +from users.models import User class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel): @@ -21,6 +22,9 @@ class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel): is_active = models.BooleanField(default=True, verbose_name=_("Is active")) params = models.JSONField(default=dict, verbose_name=_("Parameters")) + def get_report_template(self): + raise NotImplementedError + def __str__(self): 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_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True) date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished")) + duration = models.IntegerField(default=0, verbose_name=_('Duration')) snapshot = EncryptJsonDictTextField( 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, verbose_name=_('Trigger mode') ) + summary = models.JSONField(default=dict, verbose_name=_('Summary')) + result = models.JSONField(default=dict, verbose_name=_('Result')) class Meta: ordering = ('org_id', '-date_start',) @@ -150,10 +157,14 @@ class AutomationExecution(OrgModelMixin): def recipients(self): recipients = self.snapshot.get('recipients') if not recipients: - return {} - return recipients + return [] + 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): - from assets.automations.endpoint import ExecutionManager - manager = ExecutionManager(execution=self) - return manager.run() + return self.manager.run() diff --git a/apps/common/utils/strings.py b/apps/common/utils/strings.py index 30ac27621..440e931c6 100644 --- a/apps/common/utils/strings.py +++ b/apps/common/utils/strings.py @@ -15,3 +15,30 @@ def get_text_diff(old_text, new_text): old_text.splitlines(), new_text.splitlines(), lineterm="" ) 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) diff --git a/apps/ops/celery/utils.py b/apps/ops/celery/utils.py index 1db7b4675..5f76860df 100644 --- a/apps/ops/celery/utils.py +++ b/apps/ops/celery/utils.py @@ -2,17 +2,17 @@ # import json -import redis_lock import redis +import redis_lock from django.conf import settings -from django.utils.timezone import get_current_timezone from django.db.utils import ProgrammingError, OperationalError +from django.utils.timezone import get_current_timezone from django_celery_beat.models import ( PeriodicTask, IntervalSchedule, CrontabSchedule, PeriodicTasks ) -from common.utils.timezone import local_now from common.utils import get_logger +from common.utils.timezone import local_now logger = get_logger(__name__) @@ -67,7 +67,7 @@ def create_or_update_celery_periodic_tasks(tasks): if crontab is None: crontab = CrontabSchedule.objects.create(**kwargs) else: - logger.error("Schedule is not valid") + logger.warning("Schedule is not valid: %s" % name) return defaults = dict( diff --git a/poetry.lock b/poetry.lock index 051a97173..78cbcdc6f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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]] name = "adal" @@ -1613,6 +1613,45 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" 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]] name = "daphne" version = "4.0.0" @@ -3899,6 +3938,22 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" 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]] name = "msal" version = "1.29.0" @@ -4762,6 +4817,33 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" 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]] name = "prettytable" version = "3.10.0" @@ -7670,4 +7752,4 @@ reference = "aliyun" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "9acfafd75bf7dbb7e0dffb54b7f11f6b09aa4ceff769d193a3906d03ae796ccc" +content-hash = "184c3ae62b74c9af2a61c7a1e955666da7099bd832ad3c16504b1b3012ff93bb" diff --git a/pyproject.toml b/pyproject.toml index 9b2047f6c..7ddace257 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -165,6 +165,7 @@ polib = "^1.2.0" # psycopg2 = "2.9.6" psycopg2-binary = "2.9.6" pycountry = "^24.6.1" +premailer = "^3.10.0" [tool.poetry.group.xpack] optional = true