mirror of https://github.com/jumpserver/jumpserver
Merge branch 'v3' of github.com:jumpserver/jumpserver into v3
commit
ef04e6ffcc
|
@ -1 +1,2 @@
|
|||
from .endpoint import ExecutionManager
|
||||
from .methods import platform_automation_methods, filter_platform_methods
|
||||
|
|
|
@ -148,7 +148,7 @@ class BasePlaybookManager:
|
|||
print(" inventory: {}".format(runner.inventory))
|
||||
print(" playbook: {}".format(runner.playbook))
|
||||
|
||||
def run(self, **kwargs):
|
||||
def run(self, *args, **kwargs):
|
||||
runners = self.get_runners()
|
||||
if len(runners) > 1:
|
||||
print("### 分批次执行开始任务, 总共 {}\n".format(len(runners)))
|
||||
|
|
|
@ -6,30 +6,26 @@ from collections import defaultdict
|
|||
from django.utils import timezone
|
||||
|
||||
from common.utils import lazyproperty, gen_key_pair
|
||||
from assets.models import ChangeSecretRecord, SecretStrategy
|
||||
from assets.models import ChangeSecretRecord
|
||||
from assets.const import (
|
||||
AutomationTypes, SecretType, SecretStrategy, DEFAULT_PASSWORD_RULES
|
||||
)
|
||||
from ..base.manager import BasePlaybookManager
|
||||
|
||||
string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~'
|
||||
DEFAULT_PASSWORD_LENGTH = 30
|
||||
DEFAULT_PASSWORD_RULES = {
|
||||
'length': DEFAULT_PASSWORD_LENGTH,
|
||||
'symbol_set': string_punctuation
|
||||
}
|
||||
|
||||
|
||||
class ChangeSecretManager(BasePlaybookManager):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.method_hosts_mapper = defaultdict(list)
|
||||
self.password_strategy = self.execution.automation.password_strategy
|
||||
self.ssh_key_strategy = self.execution.automation.ssh_key_strategy
|
||||
self.secret_strategy = self.execution.plan_snapshot['secret_strategy']
|
||||
self.ssh_key_change_strategy = self.execution.plan_snapshot['ssh_key_change_strategy']
|
||||
self._password_generated = None
|
||||
self._ssh_key_generated = None
|
||||
self.name_recorder_mapper = {} # 做个映射,方便后面处理
|
||||
|
||||
@classmethod
|
||||
def method_type(cls):
|
||||
return 'change_secret'
|
||||
return AutomationTypes.change_secret
|
||||
|
||||
@lazyproperty
|
||||
def related_accounts(self):
|
||||
|
@ -41,43 +37,52 @@ class ChangeSecretManager(BasePlaybookManager):
|
|||
return private_key
|
||||
|
||||
def generate_password(self):
|
||||
kwargs = self.automation.password_rules or {}
|
||||
kwargs = self.automation.plan_snapshot['password_rules'] or {}
|
||||
length = int(kwargs.get('length', DEFAULT_PASSWORD_RULES['length']))
|
||||
symbol_set = kwargs.get('symbol_set')
|
||||
if symbol_set is None:
|
||||
symbol_set = DEFAULT_PASSWORD_RULES['symbol_set']
|
||||
chars = string.ascii_letters + string.digits + symbol_set
|
||||
password = ''.join([random.choice(chars) for _ in range(length)])
|
||||
|
||||
no_special_chars = string.ascii_letters + string.digits
|
||||
chars = no_special_chars + symbol_set
|
||||
|
||||
first_char = random.choice(no_special_chars)
|
||||
password = ''.join([random.choice(chars) for _ in range(length - 1)])
|
||||
password = first_char + password
|
||||
return password
|
||||
|
||||
def get_ssh_key(self):
|
||||
if self.ssh_key_strategy == SecretStrategy.custom:
|
||||
return self.automation.ssh_key
|
||||
elif self.ssh_key_strategy == SecretStrategy.random_one:
|
||||
if self.secret_strategy == SecretStrategy.custom:
|
||||
ssh_key = self.automation.plan_snapshot['ssh_key']
|
||||
if not ssh_key:
|
||||
raise ValueError("Automation SSH key must be set")
|
||||
return ssh_key
|
||||
elif self.secret_strategy == SecretStrategy.random_one:
|
||||
if not self._ssh_key_generated:
|
||||
self._ssh_key_generated = self.generate_ssh_key()
|
||||
return self._ssh_key_generated
|
||||
else:
|
||||
self.generate_ssh_key()
|
||||
return self.generate_ssh_key()
|
||||
|
||||
def get_password(self):
|
||||
if self.password_strategy == SecretStrategy.custom:
|
||||
if not self.automation.password:
|
||||
if self.secret_strategy == SecretStrategy.custom:
|
||||
password = self.automation.plan_snapshot['password']
|
||||
if not password:
|
||||
raise ValueError("Automation Password must be set")
|
||||
return self.automation.password
|
||||
elif self.password_strategy == SecretStrategy.random_one:
|
||||
return password
|
||||
elif self.secret_strategy == SecretStrategy.random_one:
|
||||
if not self._password_generated:
|
||||
self._password_generated = self.generate_password()
|
||||
return self._password_generated
|
||||
else:
|
||||
self.generate_password()
|
||||
return self.generate_password()
|
||||
|
||||
def get_secret(self, account):
|
||||
if account.secret_type == 'ssh-key':
|
||||
if account.secret_type == SecretType.ssh_key:
|
||||
secret = self.get_ssh_key()
|
||||
else:
|
||||
elif account.secret_type == SecretType.password:
|
||||
secret = self.get_password()
|
||||
if not secret:
|
||||
else:
|
||||
raise ValueError("Secret must be set")
|
||||
return secret
|
||||
|
||||
|
@ -145,5 +150,3 @@ class ChangeSecretManager(BasePlaybookManager):
|
|||
|
||||
def on_runner_failed(self, runner, e):
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
@ -3,18 +3,19 @@
|
|||
#
|
||||
from .change_secret.manager import ChangeSecretManager
|
||||
from .gather_facts.manager import GatherFactsManager
|
||||
from ..const import AutomationTypes
|
||||
|
||||
|
||||
class ExecutionManager:
|
||||
manager_type_mapper = {
|
||||
'change_secret': ChangeSecretManager,
|
||||
'gather_facts': GatherFactsManager,
|
||||
AutomationTypes.change_secret: ChangeSecretManager,
|
||||
AutomationTypes.gather_facts: GatherFactsManager,
|
||||
}
|
||||
|
||||
def __init__(self, execution):
|
||||
self.execution = execution
|
||||
self._runner = self.manager_type_mapper[execution.automation.type](execution)
|
||||
self._runner = self.manager_type_mapper[execution.manager_type](execution)
|
||||
|
||||
def run(self, **kwargs):
|
||||
return self._runner.run(**kwargs)
|
||||
def run(self, *args, **kwargs):
|
||||
return self._runner.run(*args, **kwargs)
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from .types import *
|
||||
from .account import *
|
||||
from .protocol import *
|
||||
from .category import *
|
||||
from .types import *
|
||||
from .automation import *
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
from django.db.models import TextChoices
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class Connectivity(TextChoices):
|
||||
unknown = 'unknown', _('Unknown')
|
||||
ok = 'ok', _('Ok')
|
||||
failed = 'failed', _('Failed')
|
||||
|
||||
|
||||
class SecretType(TextChoices):
|
||||
password = 'password', _('Password')
|
||||
ssh_key = 'ssh_key', _('SSH key')
|
||||
access_key = 'access_key', _('Access key')
|
||||
token = 'token', _('Token')
|
|
@ -0,0 +1,30 @@
|
|||
from django.db.models import TextChoices
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~'
|
||||
DEFAULT_PASSWORD_LENGTH = 30
|
||||
DEFAULT_PASSWORD_RULES = {
|
||||
'length': DEFAULT_PASSWORD_LENGTH,
|
||||
'symbol_set': string_punctuation
|
||||
}
|
||||
|
||||
|
||||
class AutomationTypes(TextChoices):
|
||||
ping = 'ping', _('Ping')
|
||||
gather_facts = 'gather_facts', _('Gather facts')
|
||||
push_account = 'push_account', _('Create account')
|
||||
change_secret = 'change_secret', _('Change secret')
|
||||
verify_account = 'verify_account', _('Verify account')
|
||||
gather_account = 'gather_account', _('Gather account')
|
||||
|
||||
|
||||
class SecretStrategy(TextChoices):
|
||||
custom = 'specific', _('Specific')
|
||||
random_one = 'random_one', _('All assets use the same random password')
|
||||
random_all = 'random_all', _('All assets use different random password')
|
||||
|
||||
|
||||
class SSHKeyStrategy(TextChoices):
|
||||
add = 'add', _('Append SSH KEY')
|
||||
set = 'set', _('Empty and append SSH KEY')
|
||||
set_jms = 'set_jms', _('Replace (The key generated by JumpServer) ')
|
|
@ -0,0 +1,75 @@
|
|||
# Generated by Django 3.2.14 on 2022-10-19 09:06
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0107_auto_20221019_1115'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='automationexecution',
|
||||
options={'verbose_name': 'Automation task execution'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='baseautomation',
|
||||
options={'verbose_name': 'Automation task'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='changesecretrecord',
|
||||
options={'verbose_name': 'Change secret record'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='verifyaccountautomation',
|
||||
options={'verbose_name': 'Verify account automation'},
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='changesecretautomation',
|
||||
old_name='password',
|
||||
new_name='secret',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='baseautomation',
|
||||
name='updated_by',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='changesecretautomation',
|
||||
name='password_strategy',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='changesecretautomation',
|
||||
name='secret_types',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='changesecretautomation',
|
||||
name='ssh_key',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='changesecretautomation',
|
||||
name='ssh_key_strategy',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='changesecretautomation',
|
||||
name='secret_strategy',
|
||||
field=models.CharField(choices=[('specific', 'Specific'), ('random_one', 'All assets use the same random password'), ('random_all', 'All assets use different random password')], default='random_one', max_length=16, verbose_name='Secret strategy'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='changesecretautomation',
|
||||
name='secret_type',
|
||||
field=models.CharField(choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='automationexecution',
|
||||
name='automation',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='executions', to='assets.baseautomation', verbose_name='Automation task'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='changesecretautomation',
|
||||
name='ssh_key_change_strategy',
|
||||
field=models.CharField(choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (The key generated by JumpServer) ')], default='add', max_length=16, verbose_name='SSH key change strategy'),
|
||||
),
|
||||
]
|
|
@ -1,5 +1,4 @@
|
|||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from simple_history.models import HistoricalRecords
|
||||
|
||||
|
|
|
@ -4,23 +4,14 @@ from django.db import models
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.const.choices import Trigger
|
||||
from common.mixins.models import CommonModelMixin
|
||||
from common.db.fields import EncryptJsonDictTextField
|
||||
from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from ops.mixin import PeriodTaskModelMixin
|
||||
from ops.tasks import execute_automation_strategy
|
||||
from assets.models import Node, Asset
|
||||
|
||||
|
||||
class AutomationTypes(models.TextChoices):
|
||||
ping = 'ping', _('Ping')
|
||||
gather_facts = 'gather_facts', _('Gather facts')
|
||||
push_account = 'push_account', _('Create account')
|
||||
change_secret = 'change_secret', _('Change secret')
|
||||
verify_account = 'verify_account', _('Verify account')
|
||||
gather_account = 'gather_account', _('Gather account')
|
||||
|
||||
|
||||
class BaseAutomation(JMSOrgBaseModel, PeriodTaskModelMixin):
|
||||
class BaseAutomation(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin):
|
||||
accounts = models.JSONField(default=list, verbose_name=_("Accounts"))
|
||||
nodes = models.ManyToManyField(
|
||||
'assets.Node', blank=True, verbose_name=_("Nodes")
|
||||
|
@ -47,18 +38,17 @@ class BaseAutomation(JMSOrgBaseModel, PeriodTaskModelMixin):
|
|||
return assets.group_by_platform()
|
||||
|
||||
def get_register_task(self):
|
||||
name = "automation_strategy_period_{}".format(str(self.id)[:8])
|
||||
task = execute_automation_strategy.name
|
||||
args = (str(self.id), Trigger.timing)
|
||||
kwargs = {}
|
||||
return name, task, args, kwargs
|
||||
raise NotImplementedError
|
||||
|
||||
def to_attr_json(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'type': self.type,
|
||||
'org_id': self.org_id,
|
||||
'comment': self.comment,
|
||||
'accounts': self.accounts,
|
||||
'nodes': list(self.nodes.all().values_list('id', flat=True)),
|
||||
'assets': list(self.assets.all().values_list('id', flat=True)),
|
||||
'nodes': list(self.assets.all().values_list('id', flat=True)),
|
||||
}
|
||||
|
||||
def execute(self, trigger=Trigger.manual):
|
||||
|
@ -67,8 +57,9 @@ class BaseAutomation(JMSOrgBaseModel, PeriodTaskModelMixin):
|
|||
except AttributeError:
|
||||
eid = str(uuid.uuid4())
|
||||
|
||||
execution = self.executions.create(
|
||||
id=eid, trigger=trigger,
|
||||
execution = self.executions.model.objects.create(
|
||||
id=eid, trigger=trigger, automation=self,
|
||||
plan_snapshot=self.to_attr_json(),
|
||||
)
|
||||
return execution.start()
|
||||
|
||||
|
|
|
@ -2,45 +2,61 @@ from django.db import models
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.db import fields
|
||||
from common.const.choices import Trigger
|
||||
from common.db.models import JMSBaseModel
|
||||
from assets.tasks import execute_change_secret_automation
|
||||
from assets.const import AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy
|
||||
from .base import BaseAutomation
|
||||
|
||||
|
||||
__all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord', 'SecretStrategy']
|
||||
|
||||
|
||||
class SecretStrategy(models.TextChoices):
|
||||
custom = 'specific', _('Specific')
|
||||
random_one = 'random_one', _('All assets use the same random password')
|
||||
random_all = 'random_all', _('All assets use different random password')
|
||||
|
||||
|
||||
class SSHKeyStrategy(models.TextChoices):
|
||||
add = 'add', _('Append SSH KEY')
|
||||
set = 'set', _('Empty and append SSH KEY')
|
||||
set_jms = 'set_jms', _('Replace (The key generated by JumpServer) ')
|
||||
__all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord']
|
||||
|
||||
|
||||
class ChangeSecretAutomation(BaseAutomation):
|
||||
secret_types = models.JSONField(default=list, verbose_name=_('Secret types'))
|
||||
password_strategy = models.CharField(choices=SecretStrategy.choices, max_length=16,
|
||||
default=SecretStrategy.random_one, verbose_name=_('Password strategy'))
|
||||
password = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
|
||||
secret_type = models.CharField(
|
||||
choices=SecretType.choices, max_length=16,
|
||||
default=SecretType.password, verbose_name=_('Secret type')
|
||||
)
|
||||
secret_strategy = models.CharField(
|
||||
choices=SecretStrategy.choices, max_length=16,
|
||||
default=SecretStrategy.random_one, verbose_name=_('Secret strategy')
|
||||
)
|
||||
secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
|
||||
password_rules = models.JSONField(default=dict, verbose_name=_('Password rules'))
|
||||
|
||||
ssh_key_strategy = models.CharField(choices=SecretStrategy.choices, default=SecretStrategy.random_one, max_length=16)
|
||||
ssh_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH key'))
|
||||
ssh_key_change_strategy = models.CharField(choices=SSHKeyStrategy.choices, max_length=16,
|
||||
default=SSHKeyStrategy.add, verbose_name=_('SSH key strategy'))
|
||||
ssh_key_change_strategy = models.CharField(
|
||||
choices=SSHKeyStrategy.choices, max_length=16,
|
||||
default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy')
|
||||
)
|
||||
recipients = models.ManyToManyField('users.User', blank=True, verbose_name=_("Recipient"))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.type = 'change_secret'
|
||||
self.type = AutomationTypes.change_secret
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Change secret automation")
|
||||
|
||||
def get_register_task(self):
|
||||
name = "automation_change_secret_strategy_period_{}".format(str(self.id)[:8])
|
||||
task = execute_change_secret_automation.name
|
||||
args = (str(self.id), Trigger.timing)
|
||||
kwargs = {}
|
||||
return name, task, args, kwargs
|
||||
|
||||
def to_attr_json(self):
|
||||
attr_json = super().to_attr_json()
|
||||
attr_json.update({
|
||||
'secret': self.secret,
|
||||
'secret_type': self.secret_type,
|
||||
'secret_strategy': self.secret_strategy,
|
||||
'password_rules': self.password_rules,
|
||||
'ssh_key_change_strategy': self.ssh_key_change_strategy,
|
||||
'recipients': {
|
||||
str(recipient.id): (str(recipient), bool(recipient.secret_key))
|
||||
for recipient in self.recipients.all()
|
||||
}
|
||||
})
|
||||
return attr_json
|
||||
|
||||
|
||||
class ChangeSecretRecord(JMSBaseModel):
|
||||
execution = models.ForeignKey('assets.AutomationExecution', on_delete=models.CASCADE)
|
||||
|
@ -53,7 +69,7 @@ class ChangeSecretRecord(JMSBaseModel):
|
|||
error = models.TextField(blank=True, null=True, verbose_name=_('Error'))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Change secret")
|
||||
verbose_name = _("Change secret record")
|
||||
|
||||
def __str__(self):
|
||||
return self.account.__str__()
|
||||
|
|
|
@ -18,18 +18,12 @@ from common.utils import (
|
|||
random_string, ssh_pubkey_gen,
|
||||
)
|
||||
from common.db import fields
|
||||
from assets.const import Connectivity
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class Connectivity(models.TextChoices):
|
||||
unknown = 'unknown', _('Unknown')
|
||||
ok = 'ok', _('Ok')
|
||||
failed = 'failed', _('Failed')
|
||||
|
||||
|
||||
class AbsConnectivity(models.Model):
|
||||
connectivity = models.CharField(
|
||||
choices=Connectivity.choices, default=Connectivity.unknown,
|
||||
|
@ -64,7 +58,9 @@ class BaseAccount(OrgModelMixin):
|
|||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_("Name"))
|
||||
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True)
|
||||
secret_type = models.CharField(max_length=16, choices=SecretType.choices, default='password', verbose_name=_('Secret type'))
|
||||
secret_type = models.CharField(
|
||||
max_length=16, choices=SecretType.choices, default=SecretType.password, verbose_name=_('Secret type')
|
||||
)
|
||||
secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
|
||||
privileged = models.BooleanField(verbose_name=_("Privileged"), default=False)
|
||||
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
||||
|
@ -165,10 +161,7 @@ class BaseAccount(OrgModelMixin):
|
|||
'username': self.username,
|
||||
'password': self.password,
|
||||
'public_key': self.public_key,
|
||||
'private_key': self.private_key_file,
|
||||
'token': self.token
|
||||
}
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .utils import *
|
||||
from .common import *
|
||||
from .backup import *
|
||||
from .automation import *
|
||||
from .nodes_amount import *
|
||||
from .gather_asset_users import *
|
||||
from .asset_connectivity import *
|
||||
from .account_connectivity import *
|
||||
from .gather_asset_users import *
|
||||
from .gather_asset_hardware_info import *
|
||||
from .nodes_amount import *
|
||||
from .backup import *
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
from celery import shared_task
|
||||
|
||||
from orgs.utils import tmp_to_root_org, tmp_to_org
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
@shared_task
|
||||
def execute_change_secret_automation(pid, trigger):
|
||||
from assets.models import ChangeSecretAutomation
|
||||
with tmp_to_root_org():
|
||||
instance = get_object_or_none(ChangeSecretAutomation, pk=pid)
|
||||
if not instance:
|
||||
logger.error("No automation plan found: {}".format(pid))
|
||||
return
|
||||
with tmp_to_org(instance.org):
|
||||
instance.execute(trigger)
|
|
@ -126,8 +126,8 @@ class NodeAssetsUtil:
|
|||
from assets.models import Node, Asset
|
||||
|
||||
nodes = list(Node.objects.all())
|
||||
nodes_assets = Asset.nodes.through.objects.all()\
|
||||
.annotate(aid=output_as_string('asset_id'))\
|
||||
nodes_assets = Asset.nodes.through.objects.all() \
|
||||
.annotate(aid=output_as_string('asset_id')) \
|
||||
.values_list('node__key', 'aid')
|
||||
|
||||
mapping = defaultdict(set)
|
||||
|
|
|
@ -71,7 +71,7 @@ class PeriodTaskModelMixin(models.Model):
|
|||
}
|
||||
create_or_update_celery_periodic_tasks(tasks)
|
||||
|
||||
def save(self, **kwargs):
|
||||
def save(self, *args, **kwargs):
|
||||
instance = super().save(**kwargs)
|
||||
self.set_period_schedule()
|
||||
return instance
|
||||
|
|
Loading…
Reference in New Issue