diff --git a/apps/assets/automations/__init__.py b/apps/assets/automations/__init__.py index 7c63c916a..30fb03cda 100644 --- a/apps/assets/automations/__init__.py +++ b/apps/assets/automations/__init__.py @@ -1 +1,2 @@ +from .endpoint import ExecutionManager from .methods import platform_automation_methods, filter_platform_methods diff --git a/apps/assets/automations/base/manager.py b/apps/assets/automations/base/manager.py index b1d140492..ad8104127 100644 --- a/apps/assets/automations/base/manager.py +++ b/apps/assets/automations/base/manager.py @@ -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))) diff --git a/apps/assets/automations/change_secret/manager.py b/apps/assets/automations/change_secret/manager.py index 072278d0d..fcb663ae8 100644 --- a/apps/assets/automations/change_secret/manager.py +++ b/apps/assets/automations/change_secret/manager.py @@ -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 - - diff --git a/apps/assets/automations/endpoint.py b/apps/assets/automations/endpoint.py index 3e69d2953..0efbc55ea 100644 --- a/apps/assets/automations/endpoint.py +++ b/apps/assets/automations/endpoint.py @@ -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) diff --git a/apps/assets/const/__init__.py b/apps/assets/const/__init__.py index e3e822fb3..81115b412 100644 --- a/apps/assets/const/__init__.py +++ b/apps/assets/const/__init__.py @@ -1,3 +1,5 @@ +from .types import * +from .account import * from .protocol import * from .category import * -from .types import * +from .automation import * diff --git a/apps/assets/const/account.py b/apps/assets/const/account.py new file mode 100644 index 000000000..5ec872134 --- /dev/null +++ b/apps/assets/const/account.py @@ -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') diff --git a/apps/assets/const/automation.py b/apps/assets/const/automation.py new file mode 100644 index 000000000..54b3cc019 --- /dev/null +++ b/apps/assets/const/automation.py @@ -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) ') diff --git a/apps/assets/migrations/0108_auto_20221019_1706.py b/apps/assets/migrations/0108_auto_20221019_1706.py new file mode 100644 index 000000000..f59a5b7a9 --- /dev/null +++ b/apps/assets/migrations/0108_auto_20221019_1706.py @@ -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'), + ), + ] diff --git a/apps/assets/models/account.py b/apps/assets/models/account.py index 8bb19bb39..b0ff6c482 100644 --- a/apps/assets/models/account.py +++ b/apps/assets/models/account.py @@ -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 diff --git a/apps/assets/models/automations/base.py b/apps/assets/models/automations/base.py index 17d681477..b0fbd80a2 100644 --- a/apps/assets/models/automations/base.py +++ b/apps/assets/models/automations/base.py @@ -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() diff --git a/apps/assets/models/automations/change_secret.py b/apps/assets/models/automations/change_secret.py index 0d34a4840..47462320d 100644 --- a/apps/assets/models/automations/change_secret.py +++ b/apps/assets/models/automations/change_secret.py @@ -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__() diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 56de5fbac..f293b9a93 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -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 - diff --git a/apps/assets/tasks/__init__.py b/apps/assets/tasks/__init__.py index fcd5fbe46..1f26cde3f 100644 --- a/apps/assets/tasks/__init__.py +++ b/apps/assets/tasks/__init__.py @@ -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 * diff --git a/apps/assets/tasks/automation.py b/apps/assets/tasks/automation.py new file mode 100644 index 000000000..0859381a8 --- /dev/null +++ b/apps/assets/tasks/automation.py @@ -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) diff --git a/apps/assets/utils.py b/apps/assets/utils.py index 478b2187d..6c39fffa5 100644 --- a/apps/assets/utils.py +++ b/apps/assets/utils.py @@ -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) diff --git a/apps/ops/mixin.py b/apps/ops/mixin.py index 4d2fd52a7..7c0b473ec 100644 --- a/apps/ops/mixin.py +++ b/apps/ops/mixin.py @@ -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