Merge branch 'v3' of github.com:jumpserver/jumpserver into v3

pull/8991/head
ibuler 2022-10-20 16:39:55 +08:00
commit ef04e6ffcc
16 changed files with 244 additions and 98 deletions

View File

@ -1 +1,2 @@
from .endpoint import ExecutionManager
from .methods import platform_automation_methods, filter_platform_methods

View File

@ -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)))

View File

@ -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

View File

@ -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)

View File

@ -1,3 +1,5 @@
from .types import *
from .account import *
from .protocol import *
from .category import *
from .types import *
from .automation import *

View File

@ -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')

View File

@ -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) ')

View File

@ -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'),
),
]

View File

@ -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

View File

@ -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()

View File

@ -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__()

View File

@ -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

View File

@ -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 *

View File

@ -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)

View File

@ -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)

View File

@ -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