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 from .methods import platform_automation_methods, filter_platform_methods

View File

@ -148,7 +148,7 @@ class BasePlaybookManager:
print(" inventory: {}".format(runner.inventory)) print(" inventory: {}".format(runner.inventory))
print(" playbook: {}".format(runner.playbook)) print(" playbook: {}".format(runner.playbook))
def run(self, **kwargs): def run(self, *args, **kwargs):
runners = self.get_runners() runners = self.get_runners()
if len(runners) > 1: if len(runners) > 1:
print("### 分批次执行开始任务, 总共 {}\n".format(len(runners))) print("### 分批次执行开始任务, 总共 {}\n".format(len(runners)))

View File

@ -6,30 +6,26 @@ from collections import defaultdict
from django.utils import timezone from django.utils import timezone
from common.utils import lazyproperty, gen_key_pair 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 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): class ChangeSecretManager(BasePlaybookManager):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.method_hosts_mapper = defaultdict(list) self.method_hosts_mapper = defaultdict(list)
self.password_strategy = self.execution.automation.password_strategy self.secret_strategy = self.execution.plan_snapshot['secret_strategy']
self.ssh_key_strategy = self.execution.automation.ssh_key_strategy self.ssh_key_change_strategy = self.execution.plan_snapshot['ssh_key_change_strategy']
self._password_generated = None self._password_generated = None
self._ssh_key_generated = None self._ssh_key_generated = None
self.name_recorder_mapper = {} # 做个映射,方便后面处理 self.name_recorder_mapper = {} # 做个映射,方便后面处理
@classmethod @classmethod
def method_type(cls): def method_type(cls):
return 'change_secret' return AutomationTypes.change_secret
@lazyproperty @lazyproperty
def related_accounts(self): def related_accounts(self):
@ -41,43 +37,52 @@ class ChangeSecretManager(BasePlaybookManager):
return private_key return private_key
def generate_password(self): 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'])) length = int(kwargs.get('length', DEFAULT_PASSWORD_RULES['length']))
symbol_set = kwargs.get('symbol_set') symbol_set = kwargs.get('symbol_set')
if symbol_set is None: if symbol_set is None:
symbol_set = DEFAULT_PASSWORD_RULES['symbol_set'] 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 return password
def get_ssh_key(self): def get_ssh_key(self):
if self.ssh_key_strategy == SecretStrategy.custom: if self.secret_strategy == SecretStrategy.custom:
return self.automation.ssh_key ssh_key = self.automation.plan_snapshot['ssh_key']
elif self.ssh_key_strategy == SecretStrategy.random_one: 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: if not self._ssh_key_generated:
self._ssh_key_generated = self.generate_ssh_key() self._ssh_key_generated = self.generate_ssh_key()
return self._ssh_key_generated return self._ssh_key_generated
else: else:
self.generate_ssh_key() return self.generate_ssh_key()
def get_password(self): def get_password(self):
if self.password_strategy == SecretStrategy.custom: if self.secret_strategy == SecretStrategy.custom:
if not self.automation.password: password = self.automation.plan_snapshot['password']
if not password:
raise ValueError("Automation Password must be set") raise ValueError("Automation Password must be set")
return self.automation.password return password
elif self.password_strategy == SecretStrategy.random_one: elif self.secret_strategy == SecretStrategy.random_one:
if not self._password_generated: if not self._password_generated:
self._password_generated = self.generate_password() self._password_generated = self.generate_password()
return self._password_generated return self._password_generated
else: else:
self.generate_password() return self.generate_password()
def get_secret(self, account): def get_secret(self, account):
if account.secret_type == 'ssh-key': if account.secret_type == SecretType.ssh_key:
secret = self.get_ssh_key() secret = self.get_ssh_key()
else: elif account.secret_type == SecretType.password:
secret = self.get_password() secret = self.get_password()
if not secret: else:
raise ValueError("Secret must be set") raise ValueError("Secret must be set")
return secret return secret
@ -145,5 +150,3 @@ class ChangeSecretManager(BasePlaybookManager):
def on_runner_failed(self, runner, e): def on_runner_failed(self, runner, e):
pass pass

View File

@ -3,18 +3,19 @@
# #
from .change_secret.manager import ChangeSecretManager from .change_secret.manager import ChangeSecretManager
from .gather_facts.manager import GatherFactsManager from .gather_facts.manager import GatherFactsManager
from ..const import AutomationTypes
class ExecutionManager: class ExecutionManager:
manager_type_mapper = { manager_type_mapper = {
'change_secret': ChangeSecretManager, AutomationTypes.change_secret: ChangeSecretManager,
'gather_facts': GatherFactsManager, AutomationTypes.gather_facts: GatherFactsManager,
} }
def __init__(self, execution): def __init__(self, execution):
self.execution = 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): def run(self, *args, **kwargs):
return self._runner.run(**kwargs) return self._runner.run(*args, **kwargs)

View File

@ -1,3 +1,5 @@
from .types import *
from .account import *
from .protocol import * from .protocol import *
from .category 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 import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from simple_history.models import HistoricalRecords 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 django.utils.translation import ugettext_lazy as _
from common.const.choices import Trigger from common.const.choices import Trigger
from common.mixins.models import CommonModelMixin
from common.db.fields import EncryptJsonDictTextField 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.mixin import PeriodTaskModelMixin
from ops.tasks import execute_automation_strategy
from assets.models import Node, Asset from assets.models import Node, Asset
class AutomationTypes(models.TextChoices): class BaseAutomation(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin):
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):
accounts = models.JSONField(default=list, verbose_name=_("Accounts")) accounts = models.JSONField(default=list, verbose_name=_("Accounts"))
nodes = models.ManyToManyField( nodes = models.ManyToManyField(
'assets.Node', blank=True, verbose_name=_("Nodes") 'assets.Node', blank=True, verbose_name=_("Nodes")
@ -47,18 +38,17 @@ class BaseAutomation(JMSOrgBaseModel, PeriodTaskModelMixin):
return assets.group_by_platform() return assets.group_by_platform()
def get_register_task(self): def get_register_task(self):
name = "automation_strategy_period_{}".format(str(self.id)[:8]) raise NotImplementedError
task = execute_automation_strategy.name
args = (str(self.id), Trigger.timing)
kwargs = {}
return name, task, args, kwargs
def to_attr_json(self): def to_attr_json(self):
return { return {
'name': self.name, 'name': self.name,
'type': self.type,
'org_id': self.org_id,
'comment': self.comment,
'accounts': self.accounts, 'accounts': self.accounts,
'nodes': list(self.nodes.all().values_list('id', flat=True)),
'assets': list(self.assets.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): def execute(self, trigger=Trigger.manual):
@ -67,8 +57,9 @@ class BaseAutomation(JMSOrgBaseModel, PeriodTaskModelMixin):
except AttributeError: except AttributeError:
eid = str(uuid.uuid4()) eid = str(uuid.uuid4())
execution = self.executions.create( execution = self.executions.model.objects.create(
id=eid, trigger=trigger, id=eid, trigger=trigger, automation=self,
plan_snapshot=self.to_attr_json(),
) )
return execution.start() return execution.start()

View File

@ -2,45 +2,61 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.db import fields from common.db import fields
from common.const.choices import Trigger
from common.db.models import JMSBaseModel 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 from .base import BaseAutomation
__all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord']
__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) ')
class ChangeSecretAutomation(BaseAutomation): class ChangeSecretAutomation(BaseAutomation):
secret_types = models.JSONField(default=list, verbose_name=_('Secret types')) secret_type = models.CharField(
password_strategy = models.CharField(choices=SecretStrategy.choices, max_length=16, choices=SecretType.choices, max_length=16,
default=SecretStrategy.random_one, verbose_name=_('Password strategy')) default=SecretType.password, verbose_name=_('Secret type')
password = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret')) )
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')) password_rules = models.JSONField(default=dict, verbose_name=_('Password rules'))
ssh_key_change_strategy = models.CharField(
ssh_key_strategy = models.CharField(choices=SecretStrategy.choices, default=SecretStrategy.random_one, max_length=16) choices=SSHKeyStrategy.choices, max_length=16,
ssh_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH key')) default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy')
ssh_key_change_strategy = models.CharField(choices=SSHKeyStrategy.choices, max_length=16, )
default=SSHKeyStrategy.add, verbose_name=_('SSH key strategy'))
recipients = models.ManyToManyField('users.User', blank=True, verbose_name=_("Recipient")) recipients = models.ManyToManyField('users.User', blank=True, verbose_name=_("Recipient"))
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.type = 'change_secret' self.type = AutomationTypes.change_secret
super().save(*args, **kwargs) super().save(*args, **kwargs)
class Meta: class Meta:
verbose_name = _("Change secret automation") 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): class ChangeSecretRecord(JMSBaseModel):
execution = models.ForeignKey('assets.AutomationExecution', on_delete=models.CASCADE) 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')) error = models.TextField(blank=True, null=True, verbose_name=_('Error'))
class Meta: class Meta:
verbose_name = _("Change secret") verbose_name = _("Change secret record")
def __str__(self): def __str__(self):
return self.account.__str__() return self.account.__str__()

View File

@ -18,18 +18,12 @@ from common.utils import (
random_string, ssh_pubkey_gen, random_string, ssh_pubkey_gen,
) )
from common.db import fields from common.db import fields
from assets.const import Connectivity
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
logger = get_logger(__file__) logger = get_logger(__file__)
class Connectivity(models.TextChoices):
unknown = 'unknown', _('Unknown')
ok = 'ok', _('Ok')
failed = 'failed', _('Failed')
class AbsConnectivity(models.Model): class AbsConnectivity(models.Model):
connectivity = models.CharField( connectivity = models.CharField(
choices=Connectivity.choices, default=Connectivity.unknown, choices=Connectivity.choices, default=Connectivity.unknown,
@ -64,7 +58,9 @@ class BaseAccount(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_("Name")) name = models.CharField(max_length=128, verbose_name=_("Name"))
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True) 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')) secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
privileged = models.BooleanField(verbose_name=_("Privileged"), default=False) privileged = models.BooleanField(verbose_name=_("Privileged"), default=False)
comment = models.TextField(blank=True, verbose_name=_('Comment')) comment = models.TextField(blank=True, verbose_name=_('Comment'))
@ -165,10 +161,7 @@ class BaseAccount(OrgModelMixin):
'username': self.username, 'username': self.username,
'password': self.password, 'password': self.password,
'public_key': self.public_key, 'public_key': self.public_key,
'private_key': self.private_key_file,
'token': self.token
} }
class Meta: class Meta:
abstract = True abstract = True

View File

@ -1,10 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .utils import * from .utils import *
from .common import * from .common import *
from .backup import *
from .automation import *
from .nodes_amount import *
from .gather_asset_users import *
from .asset_connectivity import * from .asset_connectivity import *
from .account_connectivity import * from .account_connectivity import *
from .gather_asset_users import *
from .gather_asset_hardware_info 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 from assets.models import Node, Asset
nodes = list(Node.objects.all()) nodes = list(Node.objects.all())
nodes_assets = Asset.nodes.through.objects.all()\ nodes_assets = Asset.nodes.through.objects.all() \
.annotate(aid=output_as_string('asset_id'))\ .annotate(aid=output_as_string('asset_id')) \
.values_list('node__key', 'aid') .values_list('node__key', 'aid')
mapping = defaultdict(set) mapping = defaultdict(set)

View File

@ -71,7 +71,7 @@ class PeriodTaskModelMixin(models.Model):
} }
create_or_update_celery_periodic_tasks(tasks) create_or_update_celery_periodic_tasks(tasks)
def save(self, **kwargs): def save(self, *args, **kwargs):
instance = super().save(**kwargs) instance = super().save(**kwargs)
self.set_period_schedule() self.set_period_schedule()
return instance return instance