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

pull/9008/head
Jiangjie.Bai 2022-11-01 14:46:48 +08:00
commit 0c15ac71f6
24 changed files with 186 additions and 108 deletions

View File

@ -8,7 +8,6 @@ from collections import defaultdict
from django.conf import settings
from django.utils import timezone
from django.db.models import Model
from django.utils.translation import gettext as _
from common.utils import get_logger
@ -115,9 +114,9 @@ class BasePlaybookManager:
method_attr = '{}_method'.format(self.__class__.method_type())
method_enabled = automation and \
getattr(automation, enabled_attr) and \
getattr(automation, method_attr) and \
getattr(automation, method_attr) in self.method_id_meta_mapper
getattr(automation, enabled_attr) and \
getattr(automation, method_attr) and \
getattr(automation, method_attr) in self.method_id_meta_mapper
if not method_enabled:
host['error'] = _('{} disabled'.format(self.__class__.method_type()))
@ -132,6 +131,7 @@ class BasePlaybookManager:
def generate_private_key_path(secret, path_dir):
key_name = '.' + md5(secret.encode('utf-8')).hexdigest()
key_path = os.path.join(path_dir, key_name)
if not os.path.exists(key_path):
ssh_key_string_to_obj(secret, password=None).write_private_key_file(key_path)
os.chmod(key_path, 0o400)

View File

@ -154,7 +154,7 @@ class ChangeSecretManager(BasePlaybookManager):
recorder = self.name_recorder_mapper.get(host)
if not recorder:
return
recorder.status = 'succeed'
recorder.status = 'success'
recorder.date_finished = timezone.now()
recorder.save()

View File

@ -106,7 +106,7 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
return '{0.name}({0.address})'.format(self)
@property
def category_property(self):
def specific(self):
if not hasattr(self, self.category):
return {}
instance = getattr(self, self.category)

View File

@ -15,7 +15,7 @@ class Database(Asset):
return self.address
@property
def category_property(self):
def specific(self):
return {
'db_name': self.db_name,
}

View File

@ -3,7 +3,7 @@ from celery import current_task
from django.db import models
from django.utils.translation import ugettext_lazy as _
from common.const.choices import Trigger
from common.const.choices import Trigger, Status
from common.mixins.models import CommonModelMixin
from common.db.fields import EncryptJsonDictTextField
from orgs.mixins.models import OrgModelMixin

View File

@ -77,7 +77,7 @@ class AssetSerializer(OrgResourceSerializerMixin, WritableNestedModelSerializer)
'nodes', 'labels', 'accounts', 'protocols', 'nodes_display',
]
read_only_fields = [
'category', 'type', 'category_property',
'category', 'type', 'specific',
'connectivity', 'date_verified',
'created_by', 'date_created',
]
@ -90,7 +90,7 @@ class AssetSerializer(OrgResourceSerializerMixin, WritableNestedModelSerializer)
def get_field_names(self, declared_fields, info):
names = super().get_field_names(declared_fields, info)
if self.__class__.__name__ != 'AssetSerializer':
names.remove('category_property')
names.remove('specific')
return names
@classmethod

View File

@ -9,3 +9,12 @@ AUDITOR = 'Auditor'
class Trigger(models.TextChoices):
manual = 'manual', _('Manual trigger')
timing = 'timing', _('Timing trigger')
class Status(models.TextChoices):
pending = 'pending', _("Pending")
running = 'running', _("Running")
success = 'success', _("Success")
failed = 'failed', _("Failed")
error = 'error', _("Error")
canceled = 'canceled', _("Canceled")

View File

@ -2,6 +2,14 @@ from collections import defaultdict
class DefaultCallback:
STATUS_MAPPER = {
'successful': 'success',
'failure': 'failed',
'running': 'running',
'pending': 'pending',
'unknown': 'unknown'
}
def __init__(self):
self.result = dict(
ok=defaultdict(dict),
@ -27,7 +35,7 @@ class DefaultCallback:
return results
def is_success(self):
return self.status != 'successful'
return self.status != 'success'
def event_handler(self, data, **kwargs):
event = data.get('event', None)
@ -131,4 +139,5 @@ class DefaultCallback:
pass
def status_handler(self, data, **kwargs):
self.status = data.get('status', 'unknown')
status = data.get('status', '')
self.status = self.STATUS_MAPPER.get(status, 'unknown')

View File

@ -9,13 +9,12 @@ __all__ = ['JMSInventory']
class JMSInventory:
def __init__(self, assets, account_policy='smart',
account_prefer='root,administrator',
host_callback=None):
def __init__(self, assets, account_policy='privileged_first',
account_prefer='root,Administrator', host_callback=None):
"""
:param assets:
:param account_prefer: account username name if not set use account_policy
:param account_policy: smart, privileged_must, privileged_first
:param account_policy: privileged_only, privileged_first, skip
"""
self.assets = self.clean_assets(assets)
self.account_prefer = account_prefer
@ -105,7 +104,7 @@ class JMSInventory:
'id': str(asset.id), 'name': asset.name, 'address': asset.address,
'type': asset.type, 'category': asset.category,
'protocol': asset.protocol, 'port': asset.port,
'category_property': asset.category_property,
'specific': asset.specific,
'protocols': [{'name': p.name, 'port': p.port} for p in protocols],
},
'jms_account': {
@ -137,24 +136,27 @@ class JMSInventory:
def select_account(self, asset):
accounts = list(asset.accounts.all())
account_selected = None
account_username = self.account_prefer
account_usernames = self.account_prefer
if isinstance(self.account_prefer, str):
account_username = self.account_prefer.split(',')
account_usernames = self.account_prefer.split(',')
if account_username:
for username in account_username:
account_matched = list(filter(lambda account: account.username == username, accounts))
if account_matched:
account_selected = account_matched[0]
break
# 优先使用提供的名称
if account_usernames:
account_matched = list(filter(lambda account: account.username in account_usernames, accounts))
account_selected = account_matched[0] if account_matched else None
if not account_selected:
if self.account_policy in ['privileged_must', 'privileged_first']:
account_matched = list(filter(lambda account: account.privileged, accounts))
account_selected = account_matched[0] if account_matched else None
if account_selected or self.account_policy == 'skip':
return account_selected
if not account_selected and self.account_policy == 'privileged_first':
if self.account_policy in ['privileged_only', 'privileged_first']:
account_matched = list(filter(lambda account: account.privileged, accounts))
account_selected = account_matched[0] if account_matched else None
if account_selected:
return account_selected
if self.account_policy == 'privileged_first':
account_selected = accounts[0] if accounts else None
return account_selected

View File

@ -5,7 +5,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from common.utils import get_logger
from .base import BaseAnsibleTask, BaseAnsibleExecution
from .base import BaseAnsibleJob, BaseAnsibleExecution
from ..ansible import AdHocRunner
__all__ = ["AdHoc", "AdHocExecution"]
@ -14,7 +14,7 @@ __all__ = ["AdHoc", "AdHocExecution"]
logger = get_logger(__file__)
class AdHoc(BaseAnsibleTask):
class AdHoc(BaseAnsibleJob):
pattern = models.CharField(max_length=1024, verbose_name=_("Pattern"), default='all')
module = models.CharField(max_length=128, default='shell', verbose_name=_('Module'))
args = models.CharField(max_length=1024, default='', verbose_name=_('Args'))

View File

@ -12,7 +12,7 @@ from ..ansible.inventory import JMSInventory
from ..mixin import PeriodTaskModelMixin
class BaseAnsibleTask(PeriodTaskModelMixin, JMSOrgBaseModel):
class BaseAnsibleJob(PeriodTaskModelMixin, JMSOrgBaseModel):
owner = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True)
assets = models.ManyToManyField('assets.Asset', verbose_name=_("Assets"))
account = models.CharField(max_length=128, default='root', verbose_name=_('Account'))
@ -46,7 +46,7 @@ class BaseAnsibleTask(PeriodTaskModelMixin, JMSOrgBaseModel):
class BaseAnsibleExecution(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
status = models.CharField(max_length=16, verbose_name=_('Status'), default='running')
task = models.ForeignKey(BaseAnsibleTask, on_delete=models.CASCADE, related_name='executions', null=True)
task = models.ForeignKey(BaseAnsibleJob, on_delete=models.CASCADE, related_name='executions', null=True)
result = models.JSONField(blank=True, null=True, verbose_name=_('Result'))
summary = models.JSONField(default=dict, verbose_name=_('Summary'))
creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True)
@ -86,7 +86,7 @@ class BaseAnsibleExecution(models.Model):
def set_result(self, cb):
status_mapper = {
'successful': 'succeeded',
'successful': 'success',
}
this = self.__class__.objects.get(id=self.id)
this.status = status_mapper.get(cb.status, cb.status)
@ -112,11 +112,11 @@ class BaseAnsibleExecution(models.Model):
@property
def is_finished(self):
return self.status in ['succeeded', 'failed']
return self.status in ['success', 'failed']
@property
def is_success(self):
return self.status == 'succeeded'
return self.status == 'success'
@property
def time_cost(self):

View File

@ -23,6 +23,7 @@ class CeleryTask(models.Model):
"comment": getattr(task, 'comment', None),
"queue": getattr(task, 'queue', 'default')
}
@property
def state(self):
last_five_executions = CeleryTaskExecution.objects.filter(name=self.name).order_by('-date_published')[:5]

View File

@ -2,7 +2,7 @@ from django.db import models
from django.utils.translation import gettext_lazy as _
from orgs.mixins.models import JMSOrgBaseModel
from .base import BaseAnsibleExecution, BaseAnsibleTask
from .base import BaseAnsibleExecution, BaseAnsibleJob
class PlaybookTemplate(JMSOrgBaseModel):
@ -19,7 +19,7 @@ class PlaybookTemplate(JMSOrgBaseModel):
unique_together = [('org_id', 'name')]
class Playbook(BaseAnsibleTask):
class Playbook(BaseAnsibleJob):
path = models.FilePathField(max_length=1024, verbose_name=_("Playbook"))
owner = models.ForeignKey('users.User', verbose_name=_("Owner"), on_delete=models.SET_NULL, null=True)
comment = models.TextField(blank=True, verbose_name=_("Comment"))

View File

@ -20,16 +20,15 @@ __all__ = [
class OrgManager(models.Manager):
def all_group_by_org(self):
from ..models import Organization
orgs = list(Organization.objects.all())
querysets = {}
org_queryset = {}
for org in orgs:
org_id = org.id
queryset = super(OrgManager, self).get_queryset().filter(org_id=org_id)
querysets[org] = queryset
return querysets
org_queryset[org] = queryset
return org_queryset
def get_queryset(self):
queryset = super(OrgManager, self).get_queryset()
@ -46,7 +45,7 @@ class OrgManager(models.Manager):
for obj in objs:
if org.is_root():
if not obj.org_id:
raise ValidationError('Please save in a organization')
raise ValidationError('Please save in a org')
else:
obj.org_id = org.id
return super().bulk_create(objs, batch_size, ignore_conflicts)
@ -54,20 +53,24 @@ class OrgManager(models.Manager):
class OrgModelMixin(models.Model):
org_id = models.CharField(
max_length=36, blank=True, default='', verbose_name=_("Organization"), db_index=True
max_length=36, blank=True, default='',
verbose_name=_("Organization"), db_index=True
)
objects = OrgManager()
sep = '@'
def save(self, *args, **kwargs):
org = get_current_org()
locking_org = getattr(self, 'locking_org', None)
if locking_org:
org = Organization.get_instance(locking_org)
else:
org = get_current_org()
# 这里不可以优化成, 因为 root 组织下可以设置组织 id 来保存
# if org.is_root() and not self.org_id:
# raise ...
if org.is_root():
if not self.org_id:
raise ValidationError('Please save in a organization')
raise ValidationError('Please save in a org')
else:
self.org_id = org.id
return super().save(*args, **kwargs)
@ -87,8 +90,6 @@ class OrgModelMixin(models.Model):
name = getattr(self, attr)
elif hasattr(self, 'name'):
name = self.name
elif hasattr(self, 'name'):
name = self.hostname
return name + self.sep + self.org_name
def validate_unique(self, exclude=None):

View File

@ -103,22 +103,20 @@ def tmp_to_builtin_org(system=0, default=0):
set_current_org(ori_org)
def get_org_filters():
kwargs = {}
_current_org = get_current_org()
if _current_org is None:
return kwargs
if _current_org.is_root():
return kwargs
kwargs['org_id'] = _current_org.id
return kwargs
def filter_org_queryset(queryset):
kwargs = get_org_filters()
locking_org = getattr(queryset.model, 'LOCKING_ORG', None)
if locking_org:
org = Organization.get_instance(locking_org)
else:
org = get_current_org()
if org is None:
kwargs = {}
elif org.is_root():
kwargs = {}
else:
kwargs = {'org_id': org.id}
#
# lines = traceback.format_stack()
# print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>")
# for line in lines[-10:-1]:

View File

@ -2,29 +2,17 @@ from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from orgs.utils import tmp_to_builtin_org
from terminal import serializers
from terminal.models import AppletHost, Applet
from terminal.models import AppletHost, Applet, AppletHostDeployment
from terminal.tasks import run_applet_host_deployment
__all__ = ['AppletHostViewSet']
__all__ = ['AppletHostViewSet', 'AppletHostDeploymentViewSet']
class AppletHostViewSet(viewsets.ModelViewSet):
serializer_class = serializers.AppletHostSerializer
def get_queryset(self):
return AppletHost.objects.all()
def dispatch(self, request, *args, **kwargs):
with tmp_to_builtin_org(system=1):
return super().dispatch(request, *args, **kwargs)
@action(methods=['post'], detail=True)
def deploy(self, request):
from terminal.automations.deploy_applet_host.manager import DeployAppletHostManager
manager = DeployAppletHostManager(self)
manager.run()
queryset = AppletHost.objects.all()
@action(methods=['get'], detail=True, url_path='')
def not_published_applets(self, request, *args, **kwargs):
@ -33,3 +21,15 @@ class AppletHostViewSet(viewsets.ModelViewSet):
serializer = serializers.AppletSerializer(applets, many=True)
return Response(serializer.data)
class AppletHostDeploymentViewSet(viewsets.ModelViewSet):
serializer_class = serializers.AppletHostDeploymentSerializer
queryset = AppletHostDeployment.objects.all()
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
instance = serializer.save()
task = run_applet_host_deployment.delay(instance.id)
return Response({'task': str(task.id)}, status=201)

View File

@ -1,16 +1,21 @@
import os
import datetime
import shutil
from django.utils import timezone
from django.conf import settings
from common.utils import get_logger
from common.db.utils import safe_db_connection
from ops.ansible import PlaybookRunner, JMSInventory
logger = get_logger(__name__)
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
class DeployAppletHostManager:
def __init__(self, applet_host):
self.applet_host = applet_host
def __init__(self, deployment):
self.deployment = deployment
self.run_dir = self.get_run_dir()
@staticmethod
@ -28,16 +33,32 @@ class DeployAppletHostManager:
return playbook_dst
def generate_inventory(self):
inventory = JMSInventory([self.applet_host], account_policy='privileged_only')
inventory = JMSInventory([self.deployment.host], account_policy='privileged_only')
inventory_dir = os.path.join(self.run_dir, 'inventory')
inventory_path = os.path.join(inventory_dir, 'hosts.yml')
inventory.write_to_file(inventory_path)
return inventory_path
def run(self, **kwargs):
def _run(self, **kwargs):
inventory = self.generate_inventory()
playbook = self.generate_playbook()
runner = PlaybookRunner(
inventory=inventory, playbook=playbook, project_dir=self.run_dir
)
return runner.run(**kwargs)
def run(self, **kwargs):
try:
self.deployment.date_start = timezone.now()
cb = self._run(**kwargs)
self.deployment.status = cb.status
except Exception as e:
logger.error("Error: {}".format(e))
self.deployment.status = 'error'
finally:
self.deployment.date_finished = timezone.now()
with safe_db_connection():
self.deployment.save()

View File

@ -73,7 +73,7 @@ class Migration(migrations.Migration):
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('status', models.CharField(max_length=16, verbose_name='Status')),
('status', models.CharField(max_length=16, default='', verbose_name='Status')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='terminal.applethost', verbose_name='Hosting')),
],

View File

@ -1,4 +1,4 @@
# Generated by Django 3.2.14 on 2022-10-28 07:44
# Generated by Django 3.2.14 on 2022-10-31 10:48
from django.db import migrations, models
import django.db.models.deletion
@ -19,12 +19,22 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='applethost',
name='date_inited',
field=models.DateTimeField(blank=True, null=True, verbose_name='Date initialized'),
field=models.DateTimeField(blank=True, null=True, verbose_name='Date inited'),
),
migrations.AddField(
model_name='applethost',
name='initialized',
field=models.BooleanField(default=False, verbose_name='Initialized'),
name='inited',
field=models.BooleanField(default=False, verbose_name='Inited'),
),
migrations.AddField(
model_name='applethostdeployment',
name='date_finished',
field=models.DateTimeField(null=True, verbose_name='Date finished'),
),
migrations.AddField(
model_name='applethostdeployment',
name='date_start',
field=models.DateTimeField(db_index=True, null=True, verbose_name='Date start'),
),
migrations.AlterField(
model_name='appletpublication',
@ -36,7 +46,4 @@ class Migration(migrations.Migration):
name='host',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='publications', to='terminal.applethost', verbose_name='Host'),
),
migrations.DeleteModel(
name='AppletHostDeployment',
),
]

View File

@ -55,6 +55,7 @@ class AppletPublication(JMSBaseModel):
applet = models.ForeignKey('Applet', on_delete=models.PROTECT, related_name='publications', verbose_name=_('Applet'))
host = models.ForeignKey('AppletHost', on_delete=models.PROTECT, related_name='publications', verbose_name=_('Host'))
status = models.CharField(max_length=16, verbose_name=_('Status'))
published = models.BooleanField(default=False, verbose_name=_('Published'))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
class Meta:

View File

@ -1,17 +1,19 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from common.db.models import JMSBaseModel
from assets.models import Host
from ops.ansible import PlaybookRunner, JMSInventory
__all__ = ['AppletHost']
__all__ = ['AppletHost', 'AppletHostDeployment']
class AppletHost(Host):
LOCKING_ORG = 'SYSTEM'
account_automation = models.BooleanField(default=False, verbose_name=_('Account automation'))
initialized = models.BooleanField(default=False, verbose_name=_('Initialized'))
date_inited = models.DateTimeField(null=True, blank=True, verbose_name=_('Date initialized'))
inited = models.BooleanField(default=False, verbose_name=_('Inited'))
date_inited = models.DateTimeField(null=True, blank=True, verbose_name=_('Date inited'))
date_synced = models.DateTimeField(null=True, blank=True, verbose_name=_('Date synced'))
status = models.CharField(max_length=16, verbose_name=_('Status'))
applets = models.ManyToManyField(
@ -19,11 +21,18 @@ class AppletHost(Host):
through='AppletPublication', through_fields=('host', 'applet'),
)
def deploy(self):
inventory = JMSInventory([self])
playbook = PlaybookRunner(inventory, 'applets.yml')
playbook.run()
def __str__(self):
return self.name
class AppletHostDeployment(JMSBaseModel):
host = models.ForeignKey('AppletHost', on_delete=models.CASCADE, verbose_name=_('Hosting'))
status = models.CharField(max_length=16, default='', verbose_name=_('Status'))
date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True)
date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished"))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
def start(self, **kwargs):
from ...automations.deploy_applet_host import DeployAppletHostManager
manager = DeployAppletHostManager(self)
manager.run(**kwargs)

View File

@ -5,13 +5,13 @@ from common.drf.fields import ObjectRelatedField, LabeledChoiceField
from common.validators import ProjectUniqueValidator
from assets.models import Platform
from assets.serializers import HostSerializer
from ..models import Applet, AppletPublication, AppletHost
from ..models import Applet, AppletPublication, AppletHost, AppletHostDeployment
__all__ = [
'AppletSerializer', 'AppletPublicationSerializer',
'AppletHostSerializer',
'AppletUploadSerializer'
'AppletHostSerializer', 'AppletHostDeploymentSerializer',
'AppletUploadSerializer',
]
@ -85,3 +85,13 @@ class AppletHostSerializer(HostSerializer):
validators.append(uniq_validator)
return validators
class AppletHostDeploymentSerializer(serializers.ModelSerializer):
class Meta:
model = AppletHostDeployment
fields_mini = ['id', 'host', 'status']
read_only_fields = [
'status', 'date_created', 'date_updated',
'date_start', 'date_finished'
]
fields = fields_mini + ['comment'] + read_only_fields

View File

@ -12,9 +12,14 @@ from django.core.files.storage import default_storage
from common.utils import get_log_keep_day
from ops.celery.decorator import (
register_as_period_task, after_app_ready_start, after_app_shutdown_clean_periodic
register_as_period_task, after_app_ready_start,
after_app_shutdown_clean_periodic
)
from .models import Status, Session, Command, Task, AppletHost
from .models import (
Status, Session, Command, Task, AppletHost,
AppletHostDeployment
)
from orgs.utils import tmp_to_builtin_org
from .backends import server_replay_storage
from .utils import find_session_replay_local
@ -84,16 +89,19 @@ def upload_session_replay_to_external_storage(session_id):
if not session:
logger.error(f'Session db item not found: {session_id}')
return
local_path, foobar = find_session_replay_local(session)
if not local_path:
logger.error(f'Session replay not found, may be upload error: {local_path}')
return
abs_path = default_storage.path(local_path)
remote_path = session.get_relative_path_by_local_path(abs_path)
ok, err = server_replay_storage.upload(abs_path, remote_path)
if not ok:
logger.error(f'Session replay upload to external error: {err}')
return
try:
default_storage.delete(local_path)
except:
@ -103,5 +111,6 @@ def upload_session_replay_to_external_storage(session_id):
@shared_task
def run_applet_host_deployment(did):
host = AppletHost.objects.get(id=did)
host.deploy()
with tmp_to_builtin_org(system=1):
deployment = AppletHostDeployment.objects.get(id=did)
deployment.start()

View File

@ -27,6 +27,7 @@ router.register(r'endpoint-rules', api.EndpointRuleViewSet, 'endpoint-rule')
router.register(r'applets', api.AppletViewSet, 'applet')
router.register(r'applet-hosts', api.AppletHostViewSet, 'applet-host')
router.register(r'applet-publications', api.AppletPublicationViewSet, 'applet-publication')
router.register(r'applet-host-deployments', api.AppletHostDeploymentViewSet, 'applet-host-deployment')
urlpatterns = [