mirror of https://github.com/jumpserver/jumpserver
perf: 优化发送结果
parent
e58054c441
commit
ca7d2130a5
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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 = [
|
|
@ -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"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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)
|
||||
|
|
|
@ -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"),
|
||||
),
|
||||
]
|
|
@ -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"),
|
||||
),
|
||||
]
|
|
@ -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"),
|
||||
),
|
||||
]
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue