mirror of https://github.com/jumpserver/jumpserver
Merge branch 'v3' of https://github.com/jumpserver/jumpserver into pr@v3@feat_db_automations
commit
be875638ed
|
@ -6,5 +6,6 @@ from .label import *
|
||||||
from .account import *
|
from .account import *
|
||||||
from .node import *
|
from .node import *
|
||||||
from .domain import *
|
from .domain import *
|
||||||
|
from .automations import *
|
||||||
from .gathered_user import *
|
from .gathered_user import *
|
||||||
from .favorite_asset import *
|
from .favorite_asset import *
|
||||||
|
|
|
@ -82,10 +82,11 @@ class AssetsTaskMixin:
|
||||||
def perform_assets_task(self, serializer):
|
def perform_assets_task(self, serializer):
|
||||||
data = serializer.validated_data
|
data = serializer.validated_data
|
||||||
assets = data.get('assets', [])
|
assets = data.get('assets', [])
|
||||||
|
asset_ids = [asset.id for asset in assets]
|
||||||
if data['action'] == "refresh":
|
if data['action'] == "refresh":
|
||||||
task = update_assets_hardware_info_manual.delay(assets)
|
task = update_assets_hardware_info_manual.delay(asset_ids)
|
||||||
else:
|
else:
|
||||||
task = test_assets_connectivity_manual.delay(assets)
|
task = test_assets_connectivity_manual.delay(asset_ids)
|
||||||
return task
|
return task
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .base import *
|
||||||
|
from .change_secret import *
|
||||||
|
from .gather_accounts import *
|
|
@ -0,0 +1,118 @@
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status, mixins, viewsets
|
||||||
|
|
||||||
|
from orgs.mixins import generics
|
||||||
|
from assets import serializers
|
||||||
|
from assets.const import AutomationTypes
|
||||||
|
from assets.tasks import execute_automation
|
||||||
|
from assets.models import BaseAutomation, AutomationExecution
|
||||||
|
from common.const.choices import Trigger
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'AutomationAssetsListApi', 'AutomationRemoveAssetApi',
|
||||||
|
'AutomationAddAssetApi', 'AutomationNodeAddRemoveApi', 'AutomationExecutionViewSet'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class AutomationAssetsListApi(generics.ListAPIView):
|
||||||
|
serializer_class = serializers.AutomationAssetsSerializer
|
||||||
|
filter_fields = ("name", "address")
|
||||||
|
search_fields = filter_fields
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
pk = self.kwargs.get('pk')
|
||||||
|
return get_object_or_404(BaseAutomation, pk=pk)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
instance = self.get_object()
|
||||||
|
assets = instance.get_all_assets().only(
|
||||||
|
*self.serializer_class.Meta.only_fields
|
||||||
|
)
|
||||||
|
return assets
|
||||||
|
|
||||||
|
|
||||||
|
class AutomationRemoveAssetApi(generics.RetrieveUpdateAPIView):
|
||||||
|
model = BaseAutomation
|
||||||
|
serializer_class = serializers.UpdateAssetSerializer
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
instance = self.get_object()
|
||||||
|
serializer = self.serializer_class(data=request.data)
|
||||||
|
|
||||||
|
if not serializer.is_valid():
|
||||||
|
return Response({'error': serializer.errors})
|
||||||
|
|
||||||
|
assets = serializer.validated_data.get('assets')
|
||||||
|
if assets:
|
||||||
|
instance.assets.remove(*tuple(assets))
|
||||||
|
return Response({'msg': 'ok'})
|
||||||
|
|
||||||
|
|
||||||
|
class AutomationAddAssetApi(generics.RetrieveUpdateAPIView):
|
||||||
|
model = BaseAutomation
|
||||||
|
serializer_class = serializers.UpdateAssetSerializer
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
instance = self.get_object()
|
||||||
|
serializer = self.serializer_class(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
assets = serializer.validated_data.get('assets')
|
||||||
|
if assets:
|
||||||
|
instance.assets.add(*tuple(assets))
|
||||||
|
return Response({"msg": "ok"})
|
||||||
|
else:
|
||||||
|
return Response({"error": serializer.errors})
|
||||||
|
|
||||||
|
|
||||||
|
class AutomationNodeAddRemoveApi(generics.RetrieveUpdateAPIView):
|
||||||
|
model = BaseAutomation
|
||||||
|
serializer_class = serializers.UpdateAssetSerializer
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
action_params = ['add', 'remove']
|
||||||
|
action = request.query_params.get('action')
|
||||||
|
if action not in action_params:
|
||||||
|
err_info = _("The parameter 'action' must be [{}]".format(','.join(action_params)))
|
||||||
|
return Response({"error": err_info})
|
||||||
|
|
||||||
|
instance = self.get_object()
|
||||||
|
serializer = self.serializer_class(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
nodes = serializer.validated_data.get('nodes')
|
||||||
|
if nodes:
|
||||||
|
# eg: plan.nodes.add(*tuple(assets))
|
||||||
|
getattr(instance.nodes, action)(*tuple(nodes))
|
||||||
|
return Response({"msg": "ok"})
|
||||||
|
else:
|
||||||
|
return Response({"error": serializer.errors})
|
||||||
|
|
||||||
|
|
||||||
|
class AutomationExecutionViewSet(
|
||||||
|
mixins.CreateModelMixin, mixins.ListModelMixin,
|
||||||
|
mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||||
|
):
|
||||||
|
search_fields = ('trigger',)
|
||||||
|
filterset_fields = ('trigger', 'automation_id')
|
||||||
|
serializer_class = serializers.AutomationExecutionSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = AutomationExecution.objects.all()
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def filter_queryset(self, queryset):
|
||||||
|
queryset = super().filter_queryset(queryset)
|
||||||
|
queryset = queryset.order_by('-date_start')
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
automation = serializer.validated_data.get('automation')
|
||||||
|
tp = serializer.validated_data.get('type')
|
||||||
|
model = AutomationTypes.get_type_model(tp)
|
||||||
|
task = execute_automation.delay(
|
||||||
|
pid=automation.pk, trigger=Trigger.manual, model=model
|
||||||
|
)
|
||||||
|
return Response({'task': task.id}, status=status.HTTP_201_CREATED)
|
|
@ -0,0 +1,40 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
|
||||||
|
from rest_framework import mixins
|
||||||
|
|
||||||
|
from common.utils import get_object_or_none
|
||||||
|
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
|
||||||
|
|
||||||
|
from assets.models import ChangeSecretAutomation, ChangeSecretRecord, AutomationExecution
|
||||||
|
from assets import serializers
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeSecretAutomationViewSet(OrgBulkModelViewSet):
|
||||||
|
model = ChangeSecretAutomation
|
||||||
|
filter_fields = ('name', 'secret_type', 'secret_strategy')
|
||||||
|
search_fields = filter_fields
|
||||||
|
ordering_fields = ('name',)
|
||||||
|
serializer_class = serializers.ChangeSecretAutomationSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
|
||||||
|
serializer_class = serializers.ChangeSecretRecordSerializer
|
||||||
|
filter_fields = ['asset', 'execution_id']
|
||||||
|
search_fields = ['asset__hostname']
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return ChangeSecretRecord.objects.all()
|
||||||
|
|
||||||
|
def filter_queryset(self, queryset):
|
||||||
|
queryset = super().filter_queryset(queryset)
|
||||||
|
eid = self.request.GET.get('execution_id')
|
||||||
|
execution = get_object_or_none(AutomationExecution, pk=eid)
|
||||||
|
if execution:
|
||||||
|
queryset = queryset.filter(execution=execution)
|
||||||
|
queryset = queryset.order_by('is_success', '-date_start')
|
||||||
|
return queryset
|
|
@ -0,0 +1,18 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
|
|
||||||
|
from assets.models import GatherAccountsAutomation
|
||||||
|
from assets import serializers
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'GatherAccountsAutomationViewSet',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class GatherAccountsAutomationViewSet(OrgBulkModelViewSet):
|
||||||
|
model = GatherAccountsAutomation
|
||||||
|
filter_fields = ('name',)
|
||||||
|
search_fields = filter_fields
|
||||||
|
ordering_fields = ('name',)
|
||||||
|
serializer_class = serializers.GatherAccountAutomationSerializer
|
|
@ -47,7 +47,7 @@ class PushOrVerifyHostCallbackMixin:
|
||||||
secret = account.secret
|
secret = account.secret
|
||||||
|
|
||||||
private_key_path = None
|
private_key_path = None
|
||||||
if account.secret_type == SecretType.ssh_key:
|
if account.secret_type == SecretType.SSH_KEY:
|
||||||
private_key_path = self.generate_private_key_path(secret, path_dir)
|
private_key_path = self.generate_private_key_path(secret, path_dir)
|
||||||
secret = self.generate_public_key(secret)
|
secret = self.generate_public_key(secret)
|
||||||
|
|
||||||
|
@ -221,6 +221,7 @@ class BasePlaybookManager:
|
||||||
else:
|
else:
|
||||||
print(">>> 开始执行任务\n")
|
print(">>> 开始执行任务\n")
|
||||||
|
|
||||||
|
self.execution.date_start = timezone.now()
|
||||||
for i, runner in enumerate(runners, start=1):
|
for i, runner in enumerate(runners, start=1):
|
||||||
if len(runners) > 1:
|
if len(runners) > 1:
|
||||||
print(">>> 开始执行第 {} 批任务".format(i))
|
print(">>> 开始执行第 {} 批任务".format(i))
|
||||||
|
@ -231,3 +232,6 @@ class BasePlaybookManager:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.on_runner_failed(runner, e)
|
self.on_runner_failed(runner, e)
|
||||||
print('\n')
|
print('\n')
|
||||||
|
self.execution.status = 'success'
|
||||||
|
self.execution.date_finished = timezone.now()
|
||||||
|
self.execution.save()
|
||||||
|
|
|
@ -89,9 +89,9 @@ class ChangeSecretManager(BasePlaybookManager):
|
||||||
return self.generate_password()
|
return self.generate_password()
|
||||||
|
|
||||||
def get_secret(self):
|
def get_secret(self):
|
||||||
if self.secret_type == SecretType.ssh_key:
|
if self.secret_type == SecretType.SSH_KEY:
|
||||||
secret = self.get_ssh_key()
|
secret = self.get_ssh_key()
|
||||||
elif self.secret_type == SecretType.password:
|
elif self.secret_type == SecretType.PASSWORD:
|
||||||
secret = self.get_password()
|
secret = self.get_password()
|
||||||
else:
|
else:
|
||||||
raise ValueError("Secret must be set")
|
raise ValueError("Secret must be set")
|
||||||
|
@ -99,7 +99,7 @@ class ChangeSecretManager(BasePlaybookManager):
|
||||||
|
|
||||||
def get_kwargs(self, account, secret):
|
def get_kwargs(self, account, secret):
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
if self.secret_type != SecretType.ssh_key:
|
if self.secret_type != SecretType.SSH_KEY:
|
||||||
return kwargs
|
return kwargs
|
||||||
kwargs['strategy'] = self.execution.snapshot['ssh_key_change_strategy']
|
kwargs['strategy'] = self.execution.snapshot['ssh_key_change_strategy']
|
||||||
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
|
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
|
||||||
|
@ -143,7 +143,7 @@ class ChangeSecretManager(BasePlaybookManager):
|
||||||
self.name_recorder_mapper[h['name']] = recorder
|
self.name_recorder_mapper[h['name']] = recorder
|
||||||
|
|
||||||
private_key_path = None
|
private_key_path = None
|
||||||
if self.secret_type == SecretType.ssh_key:
|
if self.secret_type == SecretType.SSH_KEY:
|
||||||
private_key_path = self.generate_private_key_path(new_secret, path_dir)
|
private_key_path = self.generate_private_key_path(new_secret, path_dir)
|
||||||
new_secret = self.generate_public_key(new_secret)
|
new_secret = self.generate_public_key(new_secret)
|
||||||
|
|
||||||
|
|
|
@ -4,16 +4,18 @@ from .gather_accounts.manager import GatherAccountsManager
|
||||||
from .verify_account.manager import VerifyAccountManager
|
from .verify_account.manager import VerifyAccountManager
|
||||||
from .push_account.manager import PushAccountManager
|
from .push_account.manager import PushAccountManager
|
||||||
from .backup_account.manager import AccountBackupManager
|
from .backup_account.manager import AccountBackupManager
|
||||||
|
from .ping.manager import PingManager
|
||||||
from ..const import AutomationTypes
|
from ..const import AutomationTypes
|
||||||
|
|
||||||
|
|
||||||
class ExecutionManager:
|
class ExecutionManager:
|
||||||
manager_type_mapper = {
|
manager_type_mapper = {
|
||||||
AutomationTypes.change_secret: ChangeSecretManager,
|
AutomationTypes.ping: PingManager,
|
||||||
AutomationTypes.gather_facts: GatherFactsManager,
|
|
||||||
AutomationTypes.gather_accounts: GatherAccountsManager,
|
|
||||||
AutomationTypes.verify_account: VerifyAccountManager,
|
|
||||||
AutomationTypes.push_account: PushAccountManager,
|
AutomationTypes.push_account: PushAccountManager,
|
||||||
|
AutomationTypes.gather_facts: GatherFactsManager,
|
||||||
|
AutomationTypes.change_secret: ChangeSecretManager,
|
||||||
|
AutomationTypes.verify_account: VerifyAccountManager,
|
||||||
|
AutomationTypes.gather_accounts: GatherAccountsManager,
|
||||||
# TODO 后期迁移到自动化策略中
|
# TODO 后期迁移到自动化策略中
|
||||||
'backup_account': AccountBackupManager,
|
'backup_account': AccountBackupManager,
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,14 +21,14 @@ class PingManager(BasePlaybookManager):
|
||||||
|
|
||||||
def on_host_success(self, host, result):
|
def on_host_success(self, host, result):
|
||||||
asset, account = self.host_asset_and_account_mapper.get(host)
|
asset, account = self.host_asset_and_account_mapper.get(host)
|
||||||
asset.set_connectivity(Connectivity.ok)
|
asset.set_connectivity(Connectivity.OK)
|
||||||
if not account:
|
if not account:
|
||||||
return
|
return
|
||||||
account.set_connectivity(Connectivity.ok)
|
account.set_connectivity(Connectivity.OK)
|
||||||
|
|
||||||
def on_host_error(self, host, error, result):
|
def on_host_error(self, host, error, result):
|
||||||
asset, account = self.host_asset_and_account_mapper.get(host)
|
asset, account = self.host_asset_and_account_mapper.get(host)
|
||||||
asset.set_connectivity(Connectivity.failed)
|
asset.set_connectivity(Connectivity.FAILED)
|
||||||
if not account:
|
if not account:
|
||||||
return
|
return
|
||||||
account.set_connectivity(Connectivity.failed)
|
account.set_connectivity(Connectivity.FAILED)
|
||||||
|
|
|
@ -18,8 +18,8 @@ class VerifyAccountManager(PushOrVerifyHostCallbackMixin, BasePlaybookManager):
|
||||||
|
|
||||||
def on_host_success(self, host, result):
|
def on_host_success(self, host, result):
|
||||||
account = self.host_account_mapper.get(host)
|
account = self.host_account_mapper.get(host)
|
||||||
account.set_connectivity(Connectivity.ok)
|
account.set_connectivity(Connectivity.OK)
|
||||||
|
|
||||||
def on_host_error(self, host, error, result):
|
def on_host_error(self, host, error, result):
|
||||||
account = self.host_account_mapper.get(host)
|
account = self.host_account_mapper.get(host)
|
||||||
account.set_connectivity(Connectivity.failed)
|
account.set_connectivity(Connectivity.FAILED)
|
||||||
|
|
|
@ -3,13 +3,13 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class Connectivity(TextChoices):
|
class Connectivity(TextChoices):
|
||||||
unknown = 'unknown', _('Unknown')
|
UNKNOWN = 'unknown', _('Unknown')
|
||||||
ok = 'ok', _('Ok')
|
OK = 'ok', _('Ok')
|
||||||
failed = 'failed', _('Failed')
|
FAILED = 'failed', _('Failed')
|
||||||
|
|
||||||
|
|
||||||
class SecretType(TextChoices):
|
class SecretType(TextChoices):
|
||||||
password = 'password', _('Password')
|
PASSWORD = 'password', _('Password')
|
||||||
ssh_key = 'ssh_key', _('SSH key')
|
SSH_KEY = 'ssh_key', _('SSH key')
|
||||||
access_key = 'access_key', _('Access key')
|
ACCESS_KEY = 'access_key', _('Access key')
|
||||||
token = 'token', _('Token')
|
TOKEN = 'token', _('Token')
|
||||||
|
|
|
@ -17,6 +17,22 @@ class AutomationTypes(TextChoices):
|
||||||
verify_account = 'verify_account', _('Verify account')
|
verify_account = 'verify_account', _('Verify account')
|
||||||
gather_accounts = 'gather_accounts', _('Gather accounts')
|
gather_accounts = 'gather_accounts', _('Gather accounts')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_type_model(cls, tp):
|
||||||
|
from assets.models import (
|
||||||
|
PingAutomation, GatherFactsAutomation, PushAccountAutomation,
|
||||||
|
ChangeSecretAutomation, VerifyAccountAutomation, GatherAccountsAutomation,
|
||||||
|
)
|
||||||
|
type_model_dict = {
|
||||||
|
cls.ping: PingAutomation,
|
||||||
|
cls.gather_facts: GatherFactsAutomation,
|
||||||
|
cls.push_account: PushAccountAutomation,
|
||||||
|
cls.change_secret: ChangeSecretAutomation,
|
||||||
|
cls.verify_account: VerifyAccountAutomation,
|
||||||
|
cls.gather_accounts: GatherAccountsAutomation,
|
||||||
|
}
|
||||||
|
return type_model_dict.get(tp)
|
||||||
|
|
||||||
|
|
||||||
class SecretStrategy(TextChoices):
|
class SecretStrategy(TextChoices):
|
||||||
custom = 'specific', _('Specific')
|
custom = 'specific', _('Specific')
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from .change_secret import *
|
from .ping import *
|
||||||
from .discovery_account import *
|
from .base import *
|
||||||
from .push_account import *
|
from .push_account import *
|
||||||
from .gather_facts import *
|
from .gather_facts import *
|
||||||
from .gather_accounts import *
|
from .change_secret import *
|
||||||
from .verify_account import *
|
from .verify_account import *
|
||||||
from .ping import *
|
from .gather_accounts import *
|
||||||
|
from .discovery_account import *
|
||||||
|
|
|
@ -3,7 +3,7 @@ from celery import current_task
|
||||||
from django.db import models
|
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, Status
|
from common.const.choices import Trigger
|
||||||
from common.mixins.models import CommonModelMixin
|
from common.mixins.models import CommonModelMixin
|
||||||
from common.db.fields import EncryptJsonDictTextField
|
from common.db.fields import EncryptJsonDictTextField
|
||||||
from orgs.mixins.models import OrgModelMixin
|
from orgs.mixins.models import OrgModelMixin
|
||||||
|
@ -15,12 +15,8 @@ from assets.const import AutomationTypes
|
||||||
|
|
||||||
class BaseAutomation(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin):
|
class BaseAutomation(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin):
|
||||||
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")
|
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
|
||||||
)
|
|
||||||
assets = models.ManyToManyField(
|
|
||||||
'assets.Asset', blank=True, verbose_name=_("Assets")
|
|
||||||
)
|
|
||||||
type = models.CharField(max_length=16, choices=AutomationTypes.choices, verbose_name=_('Type'))
|
type = models.CharField(max_length=16, choices=AutomationTypes.choices, verbose_name=_('Type'))
|
||||||
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
||||||
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
||||||
|
@ -92,7 +88,7 @@ class AutomationExecution(OrgModelMixin):
|
||||||
'BaseAutomation', related_name='executions', on_delete=models.CASCADE,
|
'BaseAutomation', related_name='executions', on_delete=models.CASCADE,
|
||||||
verbose_name=_('Automation task')
|
verbose_name=_('Automation task')
|
||||||
)
|
)
|
||||||
status = models.CharField(max_length=16, default='pending')
|
status = models.CharField(max_length=16, default='pending', verbose_name=_('Status'))
|
||||||
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created'))
|
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created'))
|
||||||
date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True)
|
date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True)
|
||||||
date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished"))
|
date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished"))
|
||||||
|
|
|
@ -12,7 +12,7 @@ __all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord']
|
||||||
class ChangeSecretAutomation(BaseAutomation):
|
class ChangeSecretAutomation(BaseAutomation):
|
||||||
secret_type = models.CharField(
|
secret_type = models.CharField(
|
||||||
choices=SecretType.choices, max_length=16,
|
choices=SecretType.choices, max_length=16,
|
||||||
default=SecretType.password, verbose_name=_('Secret type')
|
default=SecretType.PASSWORD, verbose_name=_('Secret type')
|
||||||
)
|
)
|
||||||
secret_strategy = models.CharField(
|
secret_strategy = models.CharField(
|
||||||
choices=SecretStrategy.choices, max_length=16,
|
choices=SecretStrategy.choices, max_length=16,
|
||||||
|
@ -24,7 +24,7 @@ class ChangeSecretAutomation(BaseAutomation):
|
||||||
choices=SSHKeyStrategy.choices, max_length=16,
|
choices=SSHKeyStrategy.choices, max_length=16,
|
||||||
default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy')
|
default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy')
|
||||||
)
|
)
|
||||||
recipients = models.ManyToManyField('users.User', blank=True, verbose_name=_("Recipient"))
|
recipients = models.ManyToManyField('users.User', verbose_name=_("Recipient"), blank=True)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
self.type = AutomationTypes.change_secret
|
self.type = AutomationTypes.change_secret
|
||||||
|
|
|
@ -13,3 +13,7 @@ class GatherAccountsAutomation(BaseAutomation):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Gather asset accounts")
|
verbose_name = _("Gather asset accounts")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def executed_amount(self):
|
||||||
|
return self.executions.count()
|
||||||
|
|
|
@ -24,7 +24,7 @@ logger = get_logger(__file__)
|
||||||
|
|
||||||
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,
|
||||||
max_length=16, verbose_name=_('Connectivity')
|
max_length=16, verbose_name=_('Connectivity')
|
||||||
)
|
)
|
||||||
date_verified = models.DateTimeField(null=True, verbose_name=_("Date verified"))
|
date_verified = models.DateTimeField(null=True, verbose_name=_("Date verified"))
|
||||||
|
@ -50,7 +50,7 @@ class BaseAccount(JMSOrgBaseModel):
|
||||||
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(
|
secret_type = models.CharField(
|
||||||
max_length=16, choices=SecretType.choices, default=SecretType.password, verbose_name=_('Secret type')
|
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)
|
||||||
|
@ -65,25 +65,25 @@ class BaseAccount(JMSOrgBaseModel):
|
||||||
@property
|
@property
|
||||||
def specific(self):
|
def specific(self):
|
||||||
data = {}
|
data = {}
|
||||||
if self.secret_type != SecretType.ssh_key:
|
if self.secret_type != SecretType.SSH_KEY:
|
||||||
return data
|
return data
|
||||||
data['ssh_key_fingerprint'] = self.ssh_key_fingerprint
|
data['ssh_key_fingerprint'] = self.ssh_key_fingerprint
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def private_key(self):
|
def private_key(self):
|
||||||
if self.secret_type == SecretType.ssh_key:
|
if self.secret_type == SecretType.SSH_KEY:
|
||||||
return self.secret
|
return self.secret
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@private_key.setter
|
@private_key.setter
|
||||||
def private_key(self, value):
|
def private_key(self, value):
|
||||||
self.secret = value
|
self.secret = value
|
||||||
self.secret_type = SecretType.ssh_key
|
self.secret_type = SecretType.SSH_KEY
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def public_key(self):
|
def public_key(self):
|
||||||
if self.secret_type == SecretType.ssh_key:
|
if self.secret_type == SecretType.SSH_KEY:
|
||||||
return ssh_pubkey_gen(private_key=self.private_key)
|
return ssh_pubkey_gen(private_key=self.private_key)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ class BaseAccount(JMSOrgBaseModel):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def private_key_path(self):
|
def private_key_path(self):
|
||||||
if not self.secret_type != SecretType.ssh_key or not self.secret:
|
if not self.secret_type != SecretType.SSH_KEY or not self.secret:
|
||||||
return None
|
return None
|
||||||
project_dir = settings.PROJECT_DIR
|
project_dir = settings.PROJECT_DIR
|
||||||
tmp_dir = os.path.join(project_dir, 'tmp')
|
tmp_dir = os.path.join(project_dir, 'tmp')
|
||||||
|
|
|
@ -201,7 +201,7 @@ class CommandFilterRule(OrgModelMixin):
|
||||||
q |= Q(user_groups__in=set(user_groups))
|
q |= Q(user_groups__in=set(user_groups))
|
||||||
if account:
|
if account:
|
||||||
org_id = account.org_id
|
org_id = account.org_id
|
||||||
q |= Q(accounts__contains=list(account)) |\
|
q |= Q(accounts__contains=account.username) | \
|
||||||
Q(accounts__contains=SpecialAccount.ALL.value)
|
Q(accounts__contains=SpecialAccount.ALL.value)
|
||||||
if asset:
|
if asset:
|
||||||
org_id = asset.org_id
|
org_id = asset.org_id
|
||||||
|
|
|
@ -6,7 +6,6 @@ from common.db.fields import JsonDictTextField
|
||||||
|
|
||||||
from assets.const import Protocol
|
from assets.const import Protocol
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Platform', 'PlatformProtocol', 'PlatformAutomation']
|
__all__ = ['Platform', 'PlatformProtocol', 'PlatformAutomation']
|
||||||
|
|
||||||
|
|
||||||
|
@ -49,11 +48,15 @@ class PlatformAutomation(models.Model):
|
||||||
push_account_enabled = models.BooleanField(default=False, verbose_name=_("Push account enabled"))
|
push_account_enabled = models.BooleanField(default=False, verbose_name=_("Push account enabled"))
|
||||||
push_account_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Push account method"))
|
push_account_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Push account method"))
|
||||||
change_secret_enabled = models.BooleanField(default=False, verbose_name=_("Change password enabled"))
|
change_secret_enabled = models.BooleanField(default=False, verbose_name=_("Change password enabled"))
|
||||||
change_secret_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Change password method"))
|
change_secret_method = models.TextField(
|
||||||
|
max_length=32, blank=True, null=True, verbose_name=_("Change password method"))
|
||||||
verify_account_enabled = models.BooleanField(default=False, verbose_name=_("Verify account enabled"))
|
verify_account_enabled = models.BooleanField(default=False, verbose_name=_("Verify account enabled"))
|
||||||
verify_account_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Verify account method"))
|
verify_account_method = models.TextField(
|
||||||
|
max_length=32, blank=True, null=True, verbose_name=_("Verify account method"))
|
||||||
gather_accounts_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled"))
|
gather_accounts_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled"))
|
||||||
gather_accounts_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Gather facts method"))
|
gather_accounts_method = models.TextField(
|
||||||
|
max_length=32, blank=True, null=True, verbose_name=_("Gather facts method")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Platform(models.Model):
|
class Platform(models.Model):
|
||||||
|
@ -61,10 +64,11 @@ class Platform(models.Model):
|
||||||
对资产提供 约束和默认值
|
对资产提供 约束和默认值
|
||||||
对资产进行抽象
|
对资产进行抽象
|
||||||
"""
|
"""
|
||||||
CHARSET_CHOICES = (
|
|
||||||
('utf8', 'UTF-8'),
|
class CharsetChoices(models.TextChoices):
|
||||||
('gbk', 'GBK'),
|
utf8 = 'utf8', 'UTF-8'
|
||||||
)
|
gbk = 'gbk', 'GBK'
|
||||||
|
|
||||||
name = models.SlugField(verbose_name=_("Name"), unique=True, allow_unicode=True)
|
name = models.SlugField(verbose_name=_("Name"), unique=True, allow_unicode=True)
|
||||||
category = models.CharField(default='host', max_length=32, verbose_name=_("Category"))
|
category = models.CharField(default='host', max_length=32, verbose_name=_("Category"))
|
||||||
type = models.CharField(max_length=32, default='linux', verbose_name=_("Type"))
|
type = models.CharField(max_length=32, default='linux', verbose_name=_("Type"))
|
||||||
|
@ -72,7 +76,9 @@ class Platform(models.Model):
|
||||||
internal = models.BooleanField(default=False, verbose_name=_("Internal"))
|
internal = models.BooleanField(default=False, verbose_name=_("Internal"))
|
||||||
comment = models.TextField(blank=True, null=True, verbose_name=_("Comment"))
|
comment = models.TextField(blank=True, null=True, verbose_name=_("Comment"))
|
||||||
# 资产有关的
|
# 资产有关的
|
||||||
charset = models.CharField(default='utf8', choices=CHARSET_CHOICES, max_length=8, verbose_name=_("Charset"))
|
charset = models.CharField(
|
||||||
|
default=CharsetChoices.utf8, choices=CharsetChoices.choices, max_length=8, verbose_name=_("Charset")
|
||||||
|
)
|
||||||
domain_enabled = models.BooleanField(default=True, verbose_name=_("Domain enabled"))
|
domain_enabled = models.BooleanField(default=True, verbose_name=_("Domain enabled"))
|
||||||
protocols_enabled = models.BooleanField(default=True, verbose_name=_("Protocols enabled"))
|
protocols_enabled = models.BooleanField(default=True, verbose_name=_("Protocols enabled"))
|
||||||
# 账号有关的
|
# 账号有关的
|
||||||
|
@ -103,4 +109,3 @@ class Platform(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Platform")
|
verbose_name = _("Platform")
|
||||||
# ordering = ('name',)
|
# ordering = ('name',)
|
||||||
|
|
||||||
|
|
|
@ -11,4 +11,4 @@ from .account import *
|
||||||
from assets.serializers.account.backup import *
|
from assets.serializers.account.backup import *
|
||||||
from .platform import *
|
from .platform import *
|
||||||
from .cagegory import *
|
from .cagegory import *
|
||||||
from .automation import *
|
from .automations import *
|
||||||
|
|
|
@ -2,10 +2,11 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from common.drf.serializers import SecretReadableMixin
|
from common.drf.serializers import SecretReadableMixin
|
||||||
from common.drf.fields import ObjectRelatedField
|
from common.drf.fields import ObjectRelatedField, LabeledChoiceField
|
||||||
from assets.tasks import push_accounts_to_assets
|
from assets.tasks import push_accounts_to_assets
|
||||||
from assets.models import Account, AccountTemplate, Asset
|
from assets.models import Account, AccountTemplate, Asset
|
||||||
from .base import BaseAccountSerializer
|
from .base import BaseAccountSerializer
|
||||||
|
from assets.const import SecretType
|
||||||
|
|
||||||
|
|
||||||
class AccountSerializerCreateMixin(serializers.ModelSerializer):
|
class AccountSerializerCreateMixin(serializers.ModelSerializer):
|
||||||
|
@ -91,6 +92,8 @@ class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
||||||
|
|
||||||
|
|
||||||
class AccountHistorySerializer(serializers.ModelSerializer):
|
class AccountHistorySerializer(serializers.ModelSerializer):
|
||||||
|
secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Account.history.model
|
model = Account.history.model
|
||||||
fields = ['id', 'secret', 'secret_type', 'version', 'history_date', 'history_user']
|
fields = ['id', 'secret', 'secret_type', 'version', 'history_date', 'history_user']
|
||||||
|
|
|
@ -6,6 +6,8 @@ from rest_framework import serializers
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
from ops.mixin import PeriodTaskSerializerMixin
|
from ops.mixin import PeriodTaskSerializerMixin
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
|
from common.const.choices import Trigger
|
||||||
|
from common.drf.fields import LabeledChoiceField
|
||||||
|
|
||||||
from assets.models import AccountBackupPlan, AccountBackupPlanExecution
|
from assets.models import AccountBackupPlan, AccountBackupPlanExecution
|
||||||
|
|
||||||
|
@ -20,7 +22,7 @@ class AccountBackupPlanSerializer(PeriodTaskSerializerMixin, BulkOrgResourceMode
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'is_periodic', 'interval', 'crontab', 'date_created',
|
'id', 'name', 'is_periodic', 'interval', 'crontab', 'date_created',
|
||||||
'date_updated', 'created_by', 'periodic_display', 'comment',
|
'date_updated', 'created_by', 'periodic_display', 'comment',
|
||||||
'recipients', 'categories'
|
'recipients', 'types'
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'name': {'required': True},
|
'name': {'required': True},
|
||||||
|
@ -32,17 +34,12 @@ class AccountBackupPlanSerializer(PeriodTaskSerializerMixin, BulkOrgResourceMode
|
||||||
|
|
||||||
|
|
||||||
class AccountBackupPlanExecutionSerializer(serializers.ModelSerializer):
|
class AccountBackupPlanExecutionSerializer(serializers.ModelSerializer):
|
||||||
trigger_display = serializers.ReadOnlyField(
|
trigger = LabeledChoiceField(choices=Trigger.choices, label=_('Trigger mode'))
|
||||||
source='get_trigger_display', label=_('Trigger mode')
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AccountBackupPlanExecution
|
model = AccountBackupPlanExecution
|
||||||
fields = [
|
read_only_fields = [
|
||||||
'id', 'date_start', 'timedelta', 'plan_snapshot', 'trigger', 'reason',
|
|
||||||
'is_success', 'plan', 'org_id', 'recipients', 'trigger_display'
|
|
||||||
]
|
|
||||||
read_only_fields = (
|
|
||||||
'id', 'date_start', 'timedelta', 'plan_snapshot', 'trigger', 'reason',
|
'id', 'date_start', 'timedelta', 'plan_snapshot', 'trigger', 'reason',
|
||||||
'is_success', 'org_id', 'recipients'
|
'is_success', 'org_id', 'recipients'
|
||||||
)
|
]
|
||||||
|
fields = read_only_fields + ['plan']
|
||||||
|
|
|
@ -1,27 +1,21 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from io import StringIO
|
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from common.utils import validate_ssh_private_key, ssh_private_key_gen
|
|
||||||
from common.drf.fields import EncryptedField
|
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
|
||||||
from assets.models import BaseAccount
|
from assets.models import BaseAccount
|
||||||
|
from assets.serializers.base import AuthValidateMixin
|
||||||
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
|
|
||||||
__all__ = ['BaseAccountSerializer']
|
__all__ = ['BaseAccountSerializer']
|
||||||
|
|
||||||
|
|
||||||
class BaseAccountSerializer(BulkOrgResourceModelSerializer):
|
class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
|
||||||
secret = EncryptedField(
|
|
||||||
label=_('Secret'), required=False, allow_blank=True,
|
|
||||||
allow_null=True, max_length=40960
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = BaseAccount
|
model = BaseAccount
|
||||||
fields_mini = ['id', 'name', 'username']
|
fields_mini = ['id', 'name', 'username']
|
||||||
fields_small = fields_mini + ['privileged', 'secret_type', 'secret', 'has_secret', 'specific']
|
fields_small = fields_mini + [
|
||||||
|
'secret_type', 'secret', 'has_secret', 'passphrase',
|
||||||
|
'privileged', 'is_active', 'specific',
|
||||||
|
]
|
||||||
fields_other = ['created_by', 'date_created', 'date_updated', 'comment']
|
fields_other = ['created_by', 'date_created', 'date_updated', 'comment']
|
||||||
fields = fields_small + fields_other
|
fields = fields_small + fields_other
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
|
@ -29,28 +23,5 @@ class BaseAccountSerializer(BulkOrgResourceModelSerializer):
|
||||||
'date_verified', 'created_by', 'date_created',
|
'date_verified', 'created_by', 'date_created',
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'secret': {'write_only': True},
|
'specific': {'label': _('Specific')},
|
||||||
'passphrase': {'write_only': True},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def validate_private_key(self, private_key):
|
|
||||||
if not private_key:
|
|
||||||
return ''
|
|
||||||
passphrase = self.initial_data.get('passphrase')
|
|
||||||
passphrase = passphrase if passphrase else None
|
|
||||||
valid = validate_ssh_private_key(private_key, password=passphrase)
|
|
||||||
if not valid:
|
|
||||||
raise serializers.ValidationError(_("private key invalid or passphrase error"))
|
|
||||||
|
|
||||||
private_key = ssh_private_key_gen(private_key, password=passphrase)
|
|
||||||
string_io = StringIO()
|
|
||||||
private_key.write_private_key(string_io)
|
|
||||||
private_key = string_io.getvalue()
|
|
||||||
return private_key
|
|
||||||
|
|
||||||
def validate_secret(self, value):
|
|
||||||
secret_type = self.initial_data.get('secret_type')
|
|
||||||
if secret_type == 'ssh_key':
|
|
||||||
value = self.validate_private_key(value)
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@ class AssetSerializer(OrgResourceSerializerMixin, WritableNestedModelSerializer)
|
||||||
'nodes', 'labels', 'protocols', 'accounts', 'nodes_display',
|
'nodes', 'labels', 'protocols', 'accounts', 'nodes_display',
|
||||||
]
|
]
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
'category', 'type', 'specific',
|
'category', 'type', 'specific', 'info',
|
||||||
'connectivity', 'date_verified',
|
'connectivity', 'date_verified',
|
||||||
'created_by', 'date_created',
|
'created_by', 'date_created',
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
from django.utils.translation import ugettext as _
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from common.utils import get_logger
|
|
||||||
|
|
||||||
from assets.models import ChangeSecretRecord
|
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
|
||||||
|
|
||||||
|
|
||||||
class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
|
|
||||||
asset = serializers.SerializerMethodField(label=_('Asset'))
|
|
||||||
account = serializers.SerializerMethodField(label=_('Account'))
|
|
||||||
is_success = serializers.SerializerMethodField(label=_('Is success'))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ChangeSecretRecord
|
|
||||||
fields = [
|
|
||||||
'id', 'asset', 'account', 'old_secret', 'new_secret',
|
|
||||||
'status', 'error', 'is_success'
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_asset(instance):
|
|
||||||
return str(instance.asset)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_account(instance):
|
|
||||||
return str(instance.account)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_is_success(obj):
|
|
||||||
if obj.status == 'success':
|
|
||||||
return _("Success")
|
|
||||||
return _("Failed")
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .base import *
|
||||||
|
from .change_secret import *
|
||||||
|
from .gather_accounts import *
|
|
@ -0,0 +1,81 @@
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from ops.mixin import PeriodTaskSerializerMixin
|
||||||
|
from assets.const import AutomationTypes
|
||||||
|
from assets.models import Asset, Node, BaseAutomation, AutomationExecution
|
||||||
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
|
from common.utils import get_logger
|
||||||
|
from common.drf.fields import ObjectRelatedField
|
||||||
|
|
||||||
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'BaseAutomationSerializer', 'AutomationExecutionSerializer',
|
||||||
|
'UpdateAssetSerializer', 'UpdateNodeSerializer', 'AutomationAssetsSerializer',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer):
|
||||||
|
assets = ObjectRelatedField(many=True, required=False, queryset=Asset.objects, label=_('Assets'))
|
||||||
|
nodes = ObjectRelatedField(many=True, required=False, queryset=Node.objects, label=_('Nodes'))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
read_only_fields = [
|
||||||
|
'date_created', 'date_updated', 'created_by', 'periodic_display'
|
||||||
|
]
|
||||||
|
fields = read_only_fields + [
|
||||||
|
'id', 'name', 'is_periodic', 'interval', 'crontab', 'comment',
|
||||||
|
'type', 'accounts', 'nodes', 'assets', 'is_active'
|
||||||
|
]
|
||||||
|
extra_kwargs = {
|
||||||
|
'name': {'required': True},
|
||||||
|
'type': {'read_only': True},
|
||||||
|
'periodic_display': {'label': _('Periodic perform')},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AutomationExecutionSerializer(serializers.ModelSerializer):
|
||||||
|
snapshot = serializers.SerializerMethodField(label=_('Automation snapshot'))
|
||||||
|
type = serializers.ChoiceField(choices=AutomationTypes.choices, write_only=True, label=_('Type'))
|
||||||
|
trigger_display = serializers.ReadOnlyField(source='get_trigger_display', label=_('Trigger mode'))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = AutomationExecution
|
||||||
|
read_only_fields = [
|
||||||
|
'trigger_display', 'date_start', 'date_finished', 'snapshot', 'status'
|
||||||
|
]
|
||||||
|
fields = ['id', 'automation', 'trigger', 'type'] + read_only_fields
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_snapshot(obj):
|
||||||
|
tp = obj.snapshot['type']
|
||||||
|
snapshot = {
|
||||||
|
'type': tp,
|
||||||
|
'name': obj.snapshot['name'],
|
||||||
|
'comment': obj.snapshot['comment'],
|
||||||
|
'accounts': obj.snapshot['accounts'],
|
||||||
|
'node_amount': len(obj.snapshot['nodes']),
|
||||||
|
'asset_amount': len(obj.snapshot['assets']),
|
||||||
|
'type_display': getattr(AutomationTypes, tp).label,
|
||||||
|
}
|
||||||
|
return snapshot
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateAssetSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = BaseAutomation
|
||||||
|
fields = ['id', 'assets']
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateNodeSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = BaseAutomation
|
||||||
|
fields = ['id', 'nodes']
|
||||||
|
|
||||||
|
|
||||||
|
class AutomationAssetsSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Asset
|
||||||
|
only_fields = ['id', 'name', 'address']
|
||||||
|
fields = tuple(only_fields)
|
|
@ -0,0 +1,133 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from common.utils import get_logger
|
||||||
|
from common.drf.fields import LabeledChoiceField, ObjectRelatedField
|
||||||
|
from assets.serializers.base import AuthValidateMixin
|
||||||
|
from assets.const import DEFAULT_PASSWORD_RULES, SecretType, SecretStrategy, SSHKeyStrategy
|
||||||
|
from assets.models import Asset, Account, ChangeSecretAutomation, ChangeSecretRecord, AutomationExecution
|
||||||
|
|
||||||
|
from .base import BaseAutomationSerializer
|
||||||
|
|
||||||
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'ChangeSecretAutomationSerializer',
|
||||||
|
'ChangeSecretRecordSerializer',
|
||||||
|
'ChangeSecretRecordBackUpSerializer'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializer):
|
||||||
|
secret_strategy = LabeledChoiceField(
|
||||||
|
choices=SecretStrategy.choices, required=True, label=_('Secret strategy')
|
||||||
|
)
|
||||||
|
ssh_key_change_strategy = LabeledChoiceField(
|
||||||
|
choices=SSHKeyStrategy.choices, required=False, label=_('SSH Key strategy')
|
||||||
|
)
|
||||||
|
password_rules = serializers.DictField(default=DEFAULT_PASSWORD_RULES)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ChangeSecretAutomation
|
||||||
|
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
|
||||||
|
fields = BaseAutomationSerializer.Meta.fields + read_only_fields + [
|
||||||
|
'secret_type', 'secret_strategy', 'secret', 'password_rules',
|
||||||
|
'ssh_key_change_strategy', 'passphrase', 'recipients',
|
||||||
|
]
|
||||||
|
extra_kwargs = {**BaseAutomationSerializer.Meta.extra_kwargs, **{
|
||||||
|
'recipients': {'label': _('Recipient'), 'help_text': _(
|
||||||
|
"Currently only mail sending is supported"
|
||||||
|
)},
|
||||||
|
}}
|
||||||
|
|
||||||
|
def validate_password_rules(self, password_rules):
|
||||||
|
secret_type = self.initial_secret_type
|
||||||
|
if secret_type != SecretType.PASSWORD:
|
||||||
|
return password_rules
|
||||||
|
|
||||||
|
length = password_rules.get('length')
|
||||||
|
symbol_set = password_rules.get('symbol_set', '')
|
||||||
|
|
||||||
|
try:
|
||||||
|
length = int(length)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
msg = _("* Please enter the correct password length")
|
||||||
|
raise serializers.ValidationError(msg)
|
||||||
|
if length < 6 or length > 30:
|
||||||
|
msg = _('* Password length range 6-30 bits')
|
||||||
|
raise serializers.ValidationError(msg)
|
||||||
|
|
||||||
|
if not isinstance(symbol_set, str):
|
||||||
|
symbol_set = str(symbol_set)
|
||||||
|
|
||||||
|
password_rules = {'length': length, 'symbol_set': ''.join(symbol_set)}
|
||||||
|
return password_rules
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
secret_type = attrs.get('secret_type')
|
||||||
|
secret_strategy = attrs.get('secret_strategy')
|
||||||
|
if secret_type == SecretType.PASSWORD:
|
||||||
|
attrs.pop('ssh_key_change_strategy', None)
|
||||||
|
if secret_strategy == SecretStrategy.custom:
|
||||||
|
attrs.pop('password_rules', None)
|
||||||
|
else:
|
||||||
|
attrs.pop('secret', None)
|
||||||
|
elif secret_type == SecretType.SSH_KEY:
|
||||||
|
attrs.pop('password_rules', None)
|
||||||
|
if secret_strategy != SecretStrategy.custom:
|
||||||
|
attrs.pop('secret', None)
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeSecretRecordSerializer(serializers.ModelSerializer):
|
||||||
|
is_success = serializers.SerializerMethodField(label=_('Is success'))
|
||||||
|
asset = ObjectRelatedField(queryset=Asset.objects, label=_('Asset'))
|
||||||
|
account = ObjectRelatedField(queryset=Account.objects, label=_('Account'))
|
||||||
|
execution = ObjectRelatedField(
|
||||||
|
queryset=AutomationExecution.objects, label=_('Automation task execution')
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ChangeSecretRecord
|
||||||
|
fields = [
|
||||||
|
'id', 'asset', 'account', 'date_started',
|
||||||
|
'date_finished', 'is_success', 'error', 'execution',
|
||||||
|
]
|
||||||
|
read_only_fields = fields
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_is_success(obj):
|
||||||
|
if obj.status == 'success':
|
||||||
|
return _("Success")
|
||||||
|
return _("Failed")
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
|
||||||
|
asset = serializers.SerializerMethodField(label=_('Asset'))
|
||||||
|
account = serializers.SerializerMethodField(label=_('Account'))
|
||||||
|
is_success = serializers.SerializerMethodField(label=_('Is success'))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ChangeSecretRecord
|
||||||
|
fields = [
|
||||||
|
'id', 'asset', 'account', 'old_secret', 'new_secret',
|
||||||
|
'status', 'error', 'is_success'
|
||||||
|
]
|
||||||
|
read_only_fields = fields
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_asset(instance):
|
||||||
|
return str(instance.asset)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_account(instance):
|
||||||
|
return str(instance.account)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_is_success(obj):
|
||||||
|
if obj.status == 'success':
|
||||||
|
return _("Success")
|
||||||
|
return _("Failed")
|
|
@ -0,0 +1,24 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from assets.models import GatherAccountsAutomation
|
||||||
|
from common.utils import get_logger
|
||||||
|
|
||||||
|
from .base import BaseAutomationSerializer
|
||||||
|
|
||||||
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'GatherAccountAutomationSerializer',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class GatherAccountAutomationSerializer(BaseAutomationSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = GatherAccountsAutomation
|
||||||
|
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields + ['executed_amount']
|
||||||
|
fields = BaseAutomationSerializer.Meta.fields + read_only_fields
|
||||||
|
|
||||||
|
extra_kwargs = {**BaseAutomationSerializer.Meta.extra_kwargs, **{
|
||||||
|
'executed_amount': {'label': _('Executed amount')}
|
||||||
|
}}
|
|
@ -1,68 +1,53 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from io import StringIO
|
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from common.utils import ssh_pubkey_gen, ssh_private_key_gen, validate_ssh_private_key
|
from assets.const import SecretType
|
||||||
from common.drf.fields import EncryptedField
|
from common.drf.fields import EncryptedField, LabeledChoiceField
|
||||||
from .utils import validate_password_for_ansible
|
from .utils import validate_password_for_ansible, validate_ssh_key
|
||||||
|
|
||||||
|
|
||||||
class AuthValidateMixin(serializers.Serializer):
|
class AuthValidateMixin(serializers.Serializer):
|
||||||
password = EncryptedField(
|
secret_type = LabeledChoiceField(
|
||||||
label=_('Password'), required=False, allow_blank=True, allow_null=True,
|
choices=SecretType.choices, required=True, label=_('Secret type')
|
||||||
max_length=1024, validators=[validate_password_for_ansible]
|
|
||||||
)
|
)
|
||||||
private_key = EncryptedField(
|
secret = EncryptedField(
|
||||||
label=_('SSH private key'), required=False, allow_blank=True,
|
label=_('Secret'), required=False, max_length=40960, allow_blank=True,
|
||||||
allow_null=True, max_length=16384
|
allow_null=True, write_only=True,
|
||||||
)
|
)
|
||||||
passphrase = serializers.CharField(
|
passphrase = serializers.CharField(
|
||||||
allow_blank=True, allow_null=True, required=False, max_length=512,
|
allow_blank=True, allow_null=True, required=False, max_length=512,
|
||||||
write_only=True, label=_('Key password')
|
write_only=True, label=_('Key password')
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_private_key(self, private_key):
|
@property
|
||||||
if not private_key:
|
def initial_secret_type(self):
|
||||||
return
|
secret_type = self.initial_data.get('secret_type')
|
||||||
passphrase = self.initial_data.get('passphrase')
|
return secret_type
|
||||||
passphrase = passphrase if passphrase else None
|
|
||||||
valid = validate_ssh_private_key(private_key, password=passphrase)
|
|
||||||
if not valid:
|
|
||||||
raise serializers.ValidationError(_("private key invalid or passphrase error"))
|
|
||||||
|
|
||||||
private_key = ssh_private_key_gen(private_key, password=passphrase)
|
def validate_secret(self, secret):
|
||||||
string_io = StringIO()
|
if not secret:
|
||||||
private_key.write_private_key(string_io)
|
return
|
||||||
private_key = string_io.getvalue()
|
secret_type = self.initial_secret_type
|
||||||
return private_key
|
if secret_type == SecretType.PASSWORD:
|
||||||
|
validate_password_for_ansible(secret)
|
||||||
|
return secret
|
||||||
|
elif secret_type == SecretType.SSH_KEY:
|
||||||
|
passphrase = self.initial_data.get('passphrase')
|
||||||
|
passphrase = passphrase if passphrase else None
|
||||||
|
return validate_ssh_key(secret, passphrase)
|
||||||
|
else:
|
||||||
|
return secret
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clean_auth_fields(validated_data):
|
def clean_auth_fields(validated_data):
|
||||||
for field in ('password', 'private_key', 'public_key'):
|
for field in ('secret',):
|
||||||
value = validated_data.get(field)
|
value = validated_data.get(field)
|
||||||
if not value:
|
if not value:
|
||||||
validated_data.pop(field, None)
|
validated_data.pop(field, None)
|
||||||
validated_data.pop('passphrase', None)
|
validated_data.pop('passphrase', None)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _validate_gen_key(attrs):
|
|
||||||
private_key = attrs.get('private_key')
|
|
||||||
if not private_key:
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
password = attrs.get('passphrase')
|
|
||||||
username = attrs.get('username')
|
|
||||||
public_key = ssh_pubkey_gen(private_key, password=password, username=username)
|
|
||||||
attrs['public_key'] = public_key
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
def validate(self, attrs):
|
|
||||||
attrs = self._validate_gen_key(attrs)
|
|
||||||
return super().validate(attrs)
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
self.clean_auth_fields(validated_data)
|
self.clean_auth_fields(validated_data)
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||||
from ..models import GatheredUser
|
from common.drf.fields import ObjectRelatedField
|
||||||
|
from ..models import GatheredUser, Asset
|
||||||
|
|
||||||
|
|
||||||
class GatheredUserSerializer(OrgResourceModelSerializerMixin):
|
class GatheredUserSerializer(OrgResourceModelSerializerMixin):
|
||||||
|
asset = ObjectRelatedField(queryset=Asset.objects, label=_('Asset'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GatheredUser
|
model = GatheredUser
|
||||||
fields_mini = ['id']
|
fields_mini = ['id']
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from common.utils import ssh_private_key_gen, validate_ssh_private_key
|
||||||
|
|
||||||
|
|
||||||
def validate_password_for_ansible(password):
|
def validate_password_for_ansible(password):
|
||||||
""" 校验 Ansible 不支持的特殊字符 """
|
""" 校验 Ansible 不支持的特殊字符 """
|
||||||
|
@ -15,3 +19,14 @@ def validate_password_for_ansible(password):
|
||||||
if '"' in password:
|
if '"' in password:
|
||||||
raise serializers.ValidationError(_('Password can not contains `"` '))
|
raise serializers.ValidationError(_('Password can not contains `"` '))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_ssh_key(ssh_key, passphrase=None):
|
||||||
|
valid = validate_ssh_private_key(ssh_key, password=passphrase)
|
||||||
|
if not valid:
|
||||||
|
raise serializers.ValidationError(_("private key invalid or passphrase error"))
|
||||||
|
|
||||||
|
ssh_key = ssh_private_key_gen(ssh_key, password=passphrase)
|
||||||
|
string_io = StringIO()
|
||||||
|
ssh_key.write_private_key(string_io)
|
||||||
|
ssh_key = string_io.getvalue()
|
||||||
|
return ssh_key
|
||||||
|
|
|
@ -9,3 +9,4 @@ from .gather_facts import *
|
||||||
from .nodes_amount import *
|
from .nodes_amount import *
|
||||||
from .push_account import *
|
from .push_account import *
|
||||||
from .verify_account import *
|
from .verify_account import *
|
||||||
|
from .gather_accounts import *
|
||||||
|
|
|
@ -7,9 +7,9 @@ logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
@shared_task(queue='ansible')
|
@shared_task(queue='ansible')
|
||||||
def execute_automation(pid, trigger, mode):
|
def execute_automation(pid, trigger, model):
|
||||||
with tmp_to_root_org():
|
with tmp_to_root_org():
|
||||||
instance = get_object_or_none(mode, pk=pid)
|
instance = get_object_or_none(model, pk=pid)
|
||||||
if not instance:
|
if not instance:
|
||||||
logger.error("No automation task found: {}".format(pid))
|
logger.error("No automation task found: {}".format(pid))
|
||||||
return
|
return
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
# ~*~ coding: utf-8 ~*~
|
||||||
|
from celery import shared_task
|
||||||
|
from django.utils.translation import gettext_noop
|
||||||
|
|
||||||
|
from orgs.utils import tmp_to_root_org, org_aware_func
|
||||||
|
from common.utils import get_logger
|
||||||
|
from assets.models import Node
|
||||||
|
|
||||||
|
__all__ = ['gather_asset_accounts']
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@org_aware_func("nodes")
|
||||||
|
def gather_asset_accounts_util(nodes, task_name):
|
||||||
|
from assets.models import GatherAccountsAutomation
|
||||||
|
task_name = GatherAccountsAutomation.generate_unique_name(task_name)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'name': task_name,
|
||||||
|
'comment': ', '.join([str(i) for i in nodes])
|
||||||
|
}
|
||||||
|
instance = GatherAccountsAutomation.objects.create(**data)
|
||||||
|
instance.nodes.add(*nodes)
|
||||||
|
instance.execute()
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(queue="ansible")
|
||||||
|
def gather_asset_accounts(node_ids, task_name=None):
|
||||||
|
if task_name is None:
|
||||||
|
task_name = gettext_noop("Gather assets accounts")
|
||||||
|
|
||||||
|
with tmp_to_root_org():
|
||||||
|
nodes = Node.objects.filter(id__in=node_ids)
|
||||||
|
gather_asset_accounts_util(nodes=nodes, task_name=task_name)
|
|
@ -27,17 +27,26 @@ router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset')
|
||||||
router.register(r'account-backup-plans', api.AccountBackupPlanViewSet, 'account-backup')
|
router.register(r'account-backup-plans', api.AccountBackupPlanViewSet, 'account-backup')
|
||||||
router.register(r'account-backup-plan-executions', api.AccountBackupPlanExecutionViewSet, 'account-backup-execution')
|
router.register(r'account-backup-plan-executions', api.AccountBackupPlanExecutionViewSet, 'account-backup-execution')
|
||||||
|
|
||||||
|
router.register(r'change-secret-automations', api.ChangeSecretAutomationViewSet, 'change-secret-automation')
|
||||||
|
router.register(r'automation-executions', api.AutomationExecutionViewSet, 'automation-execution')
|
||||||
|
router.register(r'change-secret-records', api.ChangeSecretRecordViewSet, 'change-secret-record')
|
||||||
|
router.register(r'gather-account-automations', api.GatherAccountsAutomationViewSet, 'gather-account-automation')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# path('assets/<uuid:pk>/gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'),
|
# path('assets/<uuid:pk>/gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'),
|
||||||
path('assets/<uuid:pk>/tasks/', api.AssetTaskCreateApi.as_view(), name='asset-task-create'),
|
path('assets/<uuid:pk>/tasks/', api.AssetTaskCreateApi.as_view(), name='asset-task-create'),
|
||||||
path('assets/tasks/', api.AssetsTaskCreateApi.as_view(), name='assets-task-create'),
|
path('assets/tasks/', api.AssetsTaskCreateApi.as_view(), name='assets-task-create'),
|
||||||
path('assets/<uuid:pk>/perm-users/', api.AssetPermUserListApi.as_view(), name='asset-perm-user-list'),
|
path('assets/<uuid:pk>/perm-users/', api.AssetPermUserListApi.as_view(), name='asset-perm-user-list'),
|
||||||
path('assets/<uuid:pk>/perm-users/<uuid:perm_user_id>/permissions/', api.AssetPermUserPermissionsListApi.as_view(), name='asset-perm-user-permission-list'),
|
path('assets/<uuid:pk>/perm-users/<uuid:perm_user_id>/permissions/', api.AssetPermUserPermissionsListApi.as_view(),
|
||||||
path('assets/<uuid:pk>/perm-user-groups/', api.AssetPermUserGroupListApi.as_view(), name='asset-perm-user-group-list'),
|
name='asset-perm-user-permission-list'),
|
||||||
path('assets/<uuid:pk>/perm-user-groups/<uuid:perm_user_group_id>/permissions/', api.AssetPermUserGroupPermissionsListApi.as_view(), name='asset-perm-user-group-permission-list'),
|
path('assets/<uuid:pk>/perm-user-groups/', api.AssetPermUserGroupListApi.as_view(),
|
||||||
|
name='asset-perm-user-group-list'),
|
||||||
|
path('assets/<uuid:pk>/perm-user-groups/<uuid:perm_user_group_id>/permissions/',
|
||||||
|
api.AssetPermUserGroupPermissionsListApi.as_view(), name='asset-perm-user-group-permission-list'),
|
||||||
|
|
||||||
path('accounts/tasks/', api.AccountTaskCreateAPI.as_view(), name='account-task-create'),
|
path('accounts/tasks/', api.AccountTaskCreateAPI.as_view(), name='account-task-create'),
|
||||||
path('account-secrets/<uuid:pk>/histories/', api.AccountHistoriesSecretAPI.as_view(), name='account-secret-history'),
|
path('account-secrets/<uuid:pk>/histories/', api.AccountHistoriesSecretAPI.as_view(),
|
||||||
|
name='account-secret-history'),
|
||||||
|
|
||||||
path('nodes/category/tree/', api.CategoryTreeApi.as_view(), name='asset-category-tree'),
|
path('nodes/category/tree/', api.CategoryTreeApi.as_view(), name='asset-category-tree'),
|
||||||
path('nodes/tree/', api.NodeListAsTreeApi.as_view(), name='node-tree'),
|
path('nodes/tree/', api.NodeListAsTreeApi.as_view(), name='node-tree'),
|
||||||
|
@ -52,7 +61,11 @@ urlpatterns = [
|
||||||
path('nodes/<uuid:pk>/tasks/', api.NodeTaskCreateApi.as_view(), name='node-task-create'),
|
path('nodes/<uuid:pk>/tasks/', api.NodeTaskCreateApi.as_view(), name='node-task-create'),
|
||||||
|
|
||||||
path('gateways/<uuid:pk>/test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
|
path('gateways/<uuid:pk>/test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
|
||||||
|
|
||||||
|
path('automation/<uuid:pk>/asset/remove/', api.AutomationRemoveAssetApi.as_view(), name='automation-remove-asset'),
|
||||||
|
path('automation/<uuid:pk>/asset/add/', api.AutomationAddAssetApi.as_view(), name='automation-add-asset'),
|
||||||
|
path('automation/<uuid:pk>/nodes/', api.AutomationNodeAddRemoveApi.as_view(), name='automation-add-or-remove-node'),
|
||||||
|
path('automation/<uuid:pk>/assets/', api.AutomationAssetsListApi.as_view(), name='automation-assets'),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += router.urls
|
urlpatterns += router.urls
|
||||||
|
|
||||||
|
|
|
@ -178,8 +178,6 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
|
||||||
get_object: callable
|
get_object: callable
|
||||||
get_serializer: callable
|
get_serializer: callable
|
||||||
perform_create: callable
|
perform_create: callable
|
||||||
check_token_permission: callable
|
|
||||||
create_connection_token: callable
|
|
||||||
|
|
||||||
@action(methods=['POST'], detail=False, url_path='secret-info/detail')
|
@action(methods=['POST'], detail=False, url_path='secret-info/detail')
|
||||||
def get_secret_detail(self, request, *args, **kwargs):
|
def get_secret_detail(self, request, *args, **kwargs):
|
||||||
|
@ -277,10 +275,10 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
|
||||||
from perms.utils.account import PermAccountUtil
|
from perms.utils.account import PermAccountUtil
|
||||||
actions, expire_at = PermAccountUtil().validate_permission(user, asset, account_username)
|
actions, expire_at = PermAccountUtil().validate_permission(user, asset, account_username)
|
||||||
if not actions:
|
if not actions:
|
||||||
error = ''
|
error = 'No actions'
|
||||||
raise PermissionDenied(error)
|
raise PermissionDenied(error)
|
||||||
if expire_at < time.time():
|
if expire_at < time.time():
|
||||||
error = ''
|
error = 'Expired'
|
||||||
raise PermissionDenied(error)
|
raise PermissionDenied(error)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import base64
|
import base64
|
||||||
|
import time
|
||||||
|
|
||||||
from django.shortcuts import redirect, reverse, render
|
from django.shortcuts import redirect, reverse, render
|
||||||
from django.utils.deprecation import MiddlewareMixin
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
|
|
|
@ -85,7 +85,7 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
|
||||||
is_valid = False
|
is_valid = False
|
||||||
error = _('No user or invalid user')
|
error = _('No user or invalid user')
|
||||||
return is_valid, error
|
return is_valid, error
|
||||||
if not self.asset or self.asset.is_active:
|
if not self.asset or not self.asset.is_active:
|
||||||
is_valid = False
|
is_valid = False
|
||||||
error = _('No asset or inactive asset')
|
error = _('No asset or inactive asset')
|
||||||
return is_valid, error
|
return is_valid, error
|
||||||
|
|
|
@ -159,7 +159,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
|
||||||
domain = ConnectionTokenDomainSerializer(read_only=True)
|
domain = ConnectionTokenDomainSerializer(read_only=True)
|
||||||
cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True)
|
cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True)
|
||||||
actions = ActionsField()
|
actions = ActionsField()
|
||||||
expired_at = serializers.IntegerField()
|
expire_at = serializers.IntegerField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConnectionToken
|
model = ConnectionToken
|
||||||
|
@ -167,5 +167,5 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
|
||||||
'id', 'secret',
|
'id', 'secret',
|
||||||
'user', 'asset', 'account_username', 'account', 'protocol',
|
'user', 'asset', 'account_username', 'account', 'protocol',
|
||||||
'domain', 'gateway', 'cmd_filter_rules',
|
'domain', 'gateway', 'cmd_filter_rules',
|
||||||
'actions', 'expired_at',
|
'actions', 'expire_at',
|
||||||
]
|
]
|
||||||
|
|
|
@ -44,7 +44,9 @@ class BaseService(object):
|
||||||
if self.is_running:
|
if self.is_running:
|
||||||
msg = f'{self.name} is running: {self.pid}.'
|
msg = f'{self.name} is running: {self.pid}.'
|
||||||
else:
|
else:
|
||||||
msg = f'{self.name} is stopped.'
|
msg = '\033[31m{} is stopped.\033[0m\nYou can manual start it to find the error: \n' \
|
||||||
|
' $ cd {}\n' \
|
||||||
|
' $ {}'.format(self.name, self.cwd, ' '.join(self.cmd))
|
||||||
print(msg)
|
print(msg)
|
||||||
|
|
||||||
# -- log --
|
# -- log --
|
||||||
|
|
|
@ -76,7 +76,6 @@ class ServicesUtil(object):
|
||||||
def clean_up(self):
|
def clean_up(self):
|
||||||
if not self.EXIT_EVENT.is_set():
|
if not self.EXIT_EVENT.is_set():
|
||||||
self.EXIT_EVENT.set()
|
self.EXIT_EVENT.set()
|
||||||
|
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
def show_status(self):
|
def show_status(self):
|
||||||
|
|
|
@ -16,14 +16,13 @@ class IsValidUser(permissions.IsAuthenticated, permissions.BasePermission):
|
||||||
"""Allows access to valid user, is active and not expired"""
|
"""Allows access to valid user, is active and not expired"""
|
||||||
|
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
return super(IsValidUser, self).has_permission(request, view) \
|
return super().has_permission(request, view) \
|
||||||
and request.user.is_valid
|
and request.user.is_valid
|
||||||
|
|
||||||
|
|
||||||
class IsValidUserOrConnectionToken(IsValidUser):
|
class IsValidUserOrConnectionToken(IsValidUser):
|
||||||
|
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
return super(IsValidUserOrConnectionToken, self).has_permission(request, view) \
|
return super().has_permission(request, view) \
|
||||||
or self.is_valid_connection_token(request)
|
or self.is_valid_connection_token(request)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -42,6 +41,12 @@ class OnlySuperUser(IsValidUser):
|
||||||
and request.user.is_superuser
|
and request.user.is_superuser
|
||||||
|
|
||||||
|
|
||||||
|
class IsServiceAccount(IsValidUser):
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
return super().has_permission(request, view) \
|
||||||
|
and request.user.is_service_account
|
||||||
|
|
||||||
|
|
||||||
class WithBootstrapToken(permissions.BasePermission):
|
class WithBootstrapToken(permissions.BasePermission):
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
authorization = request.META.get('HTTP_AUTHORIZATION', '')
|
authorization = request.META.get('HTTP_AUTHORIZATION', '')
|
||||||
|
|
|
@ -308,14 +308,14 @@ class HealthCheckView(HealthApiMixin):
|
||||||
def get_db_status():
|
def get_db_status():
|
||||||
t1 = time.time()
|
t1 = time.time()
|
||||||
try:
|
try:
|
||||||
User.objects.first()
|
ok = User.objects.first() is not None
|
||||||
t2 = time.time()
|
t2 = time.time()
|
||||||
return True, t2 - t1
|
return ok, t2 - t1
|
||||||
except:
|
except Exception as e:
|
||||||
t2 = time.time()
|
return False, str(e)
|
||||||
return False, t2 - t1
|
|
||||||
|
|
||||||
def get_redis_status(self):
|
@staticmethod
|
||||||
|
def get_redis_status():
|
||||||
key = 'HEALTH_CHECK'
|
key = 'HEALTH_CHECK'
|
||||||
|
|
||||||
t1 = time.time()
|
t1 = time.time()
|
||||||
|
@ -324,12 +324,12 @@ class HealthCheckView(HealthApiMixin):
|
||||||
cache.set(key, '1', 10)
|
cache.set(key, '1', 10)
|
||||||
got = cache.get(key)
|
got = cache.get(key)
|
||||||
t2 = time.time()
|
t2 = time.time()
|
||||||
|
|
||||||
if value == got:
|
if value == got:
|
||||||
return True, t2 -t1
|
return True, t2 - t1
|
||||||
return False, t2 -t1
|
return False, 'Value not match'
|
||||||
except:
|
except Exception as e:
|
||||||
t2 = time.time()
|
return False, str(e)
|
||||||
return False, t2 - t1
|
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
redis_status, redis_time = self.get_redis_status()
|
redis_status, redis_time = self.get_redis_status()
|
||||||
|
@ -341,7 +341,7 @@ class HealthCheckView(HealthApiMixin):
|
||||||
'db_time': db_time,
|
'db_time': db_time,
|
||||||
'redis_status': redis_status,
|
'redis_status': redis_status,
|
||||||
'redis_time': redis_time,
|
'redis_time': redis_time,
|
||||||
'time': int(time.time())
|
'time': int(time.time()),
|
||||||
}
|
}
|
||||||
return Response(data)
|
return Response(data)
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import pytz
|
import pytz
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.shortcuts import HttpResponse
|
from django.shortcuts import HttpResponse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -92,3 +95,37 @@ class RefererCheckMiddleware:
|
||||||
return HttpResponseForbidden('CSRF CHECK ERROR')
|
return HttpResponseForbidden('CSRF CHECK ERROR')
|
||||||
response = self.get_response(request)
|
response = self.get_response(request)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class StartMiddleware:
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
if not settings.DEBUG_DEV:
|
||||||
|
raise MiddlewareNotUsed
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
request._s_time_start = time.time()
|
||||||
|
response = self.get_response(request)
|
||||||
|
request._s_time_end = time.time()
|
||||||
|
if request.path == '/api/health/':
|
||||||
|
data = response.data
|
||||||
|
data['pre_middleware_time'] = request._e_time_start - request._s_time_start
|
||||||
|
data['api_time'] = request._e_time_end - request._e_time_start
|
||||||
|
data['post_middleware_time'] = request._s_time_end - request._e_time_end
|
||||||
|
response.content = json.dumps(data)
|
||||||
|
response.headers['Content-Length'] = str(len(response.content))
|
||||||
|
return response
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class EndMiddleware:
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
if not settings.DEBUG_DEV:
|
||||||
|
raise MiddlewareNotUsed
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
request._e_time_start = time.time()
|
||||||
|
response = self.get_response(request)
|
||||||
|
request._e_time_end = time.time()
|
||||||
|
return response
|
||||||
|
|
|
@ -86,6 +86,7 @@ INSTALLED_APPS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
'jumpserver.middleware.StartMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.locale.LocaleMiddleware',
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
|
@ -105,6 +106,7 @@ MIDDLEWARE = [
|
||||||
'authentication.middleware.ThirdPartyLoginMiddleware',
|
'authentication.middleware.ThirdPartyLoginMiddleware',
|
||||||
'authentication.middleware.SessionCookieMiddleware',
|
'authentication.middleware.SessionCookieMiddleware',
|
||||||
'simple_history.middleware.HistoryRequestMiddleware',
|
'simple_history.middleware.HistoryRequestMiddleware',
|
||||||
|
'jumpserver.middleware.EndMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'jumpserver.urls'
|
ROOT_URLCONF = 'jumpserver.urls'
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0b396cc9a485f6474d14ca30a1a7ba4f954b07754148b964efbb21519c55b280
|
oid sha256:314c29cb8b10aaddbb030bf49af293be23f0153ff1f1c7562946879574ce6de8
|
||||||
size 102849
|
size 102801
|
||||||
|
|
|
@ -858,7 +858,7 @@ msgstr "校验日期"
|
||||||
|
|
||||||
#: assets/models/base.py:63
|
#: assets/models/base.py:63
|
||||||
msgid "Privileged"
|
msgid "Privileged"
|
||||||
msgstr "特权的"
|
msgstr "特权账号"
|
||||||
|
|
||||||
#: assets/models/cmd_filter.py:32 perms/models/asset_permission.py:61
|
#: assets/models/cmd_filter.py:32 perms/models/asset_permission.py:61
|
||||||
#: users/models/group.py:31 users/models/user.py:671
|
#: users/models/group.py:31 users/models/user.py:671
|
||||||
|
|
|
@ -144,7 +144,7 @@ def check_server_performance_period():
|
||||||
ServerPerformanceCheckUtil().check_and_publish()
|
ServerPerformanceCheckUtil().check_and_publish()
|
||||||
|
|
||||||
|
|
||||||
@shared_task(queue="ansible", verbose_name=_("Hello"), comment="an test shared task")
|
@shared_task(verbose_name=_("Hello"), comment="an test shared task")
|
||||||
def hello(name, callback=None):
|
def hello(name, callback=None):
|
||||||
from users.models import User
|
from users.models import User
|
||||||
import time
|
import time
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
import uuid
|
import uuid
|
||||||
import logging
|
import logging
|
||||||
from functools import reduce
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F, Q, TextChoices
|
from django.db.models import F, Q, TextChoices
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
|
from common.utils import lazyproperty, date_expired_default
|
||||||
|
from common.db.models import BaseCreateUpdateModel, UnionQuerySet
|
||||||
from assets.models import Asset, Node, FamilyMixin, Account
|
from assets.models import Asset, Node, FamilyMixin, Account
|
||||||
from orgs.mixins.models import OrgModelMixin
|
from orgs.mixins.models import OrgModelMixin
|
||||||
from orgs.mixins.models import OrgManager
|
from orgs.mixins.models import OrgManager
|
||||||
from common.utils import lazyproperty, date_expired_default
|
|
||||||
from common.db.models import BaseCreateUpdateModel, UnionQuerySet
|
|
||||||
from .const import Action, SpecialAccount
|
from .const import Action, SpecialAccount
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|
|
@ -6,10 +6,11 @@ from rest_framework.fields import empty
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from common.drf.fields import ObjectRelatedField
|
||||||
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
from assets.models import Asset, Node
|
from assets.models import Asset, Node
|
||||||
from users.models import User, UserGroup
|
from users.models import User, UserGroup
|
||||||
from perms.models import AssetPermission, Action
|
from perms.models import AssetPermission, Action
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
|
||||||
|
|
||||||
__all__ = ['AssetPermissionSerializer', 'ActionsField']
|
__all__ = ['AssetPermissionSerializer', 'ActionsField']
|
||||||
|
|
||||||
|
@ -44,18 +45,10 @@ class ActionsDisplayField(ActionsField):
|
||||||
|
|
||||||
|
|
||||||
class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
|
class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
|
||||||
users_display = serializers.ListField(
|
users = ObjectRelatedField(queryset=User.objects, many=True, required=False)
|
||||||
child=serializers.CharField(), label=_('Users display'), required=False
|
user_groups = ObjectRelatedField(queryset=UserGroup.objects, many=True, required=False)
|
||||||
)
|
assets = ObjectRelatedField(queryset=Asset.objects, many=True, required=False)
|
||||||
user_groups_display = serializers.ListField(
|
nodes = ObjectRelatedField(queryset=Node.objects, many=True, required=False)
|
||||||
child=serializers.CharField(), label=_('User groups display'), required=False
|
|
||||||
)
|
|
||||||
assets_display = serializers.ListField(
|
|
||||||
child=serializers.CharField(), label=_('Assets display'), required=False
|
|
||||||
)
|
|
||||||
nodes_display = serializers.ListField(
|
|
||||||
child=serializers.CharField(), label=_('Nodes display'), required=False
|
|
||||||
)
|
|
||||||
actions = ActionsField(required=False, allow_null=True, label=_("Actions"))
|
actions = ActionsField(required=False, allow_null=True, label=_("Actions"))
|
||||||
is_valid = serializers.BooleanField(read_only=True, label=_("Is valid"))
|
is_valid = serializers.BooleanField(read_only=True, label=_("Is valid"))
|
||||||
is_expired = serializers.BooleanField(read_only=True, label=_('Is expired'))
|
is_expired = serializers.BooleanField(read_only=True, label=_('Is expired'))
|
||||||
|
@ -64,24 +57,16 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
|
||||||
model = AssetPermission
|
model = AssetPermission
|
||||||
fields_mini = ['id', 'name']
|
fields_mini = ['id', 'name']
|
||||||
fields_small = fields_mini + [
|
fields_small = fields_mini + [
|
||||||
'is_active', 'is_expired', 'is_valid', 'actions',
|
'accounts', 'is_active', 'is_expired', 'is_valid',
|
||||||
'accounts',
|
'actions', 'created_by', 'date_created', 'date_expired',
|
||||||
'created_by', 'date_created', 'date_expired',
|
|
||||||
'date_start', 'comment', 'from_ticket'
|
'date_start', 'comment', 'from_ticket'
|
||||||
]
|
]
|
||||||
fields_m2m = [
|
fields_m2m = [
|
||||||
'users', 'users_display', 'user_groups', 'user_groups_display', 'assets',
|
'users', 'user_groups', 'assets', 'nodes',
|
||||||
'assets_display', 'nodes', 'nodes_display',
|
|
||||||
'users_amount', 'user_groups_amount', 'assets_amount',
|
|
||||||
'nodes_amount',
|
|
||||||
]
|
]
|
||||||
fields = fields_small + fields_m2m
|
fields = fields_small + fields_m2m
|
||||||
read_only_fields = ['created_by', 'date_created', 'from_ticket']
|
read_only_fields = ['created_by', 'date_created', 'from_ticket']
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'users_amount': {'label': _('Users amount')},
|
|
||||||
'user_groups_amount': {'label': _('User groups amount')},
|
|
||||||
'assets_amount': {'label': _('Assets amount')},
|
|
||||||
'nodes_amount': {'label': _('Nodes amount')},
|
|
||||||
'actions': {'label': _('Actions')},
|
'actions': {'label': _('Actions')},
|
||||||
'is_expired': {'label': _('Is expired')},
|
'is_expired': {'label': _('Is expired')},
|
||||||
'is_valid': {'label': _('Is valid')},
|
'is_valid': {'label': _('Is valid')},
|
||||||
|
|
|
@ -53,7 +53,9 @@ class PermAccountUtil(AssetPermissionUtil):
|
||||||
user, asset, with_actions=True, with_perms=True
|
user, asset, with_actions=True, with_perms=True
|
||||||
)
|
)
|
||||||
perm = perms.first()
|
perm = perms.first()
|
||||||
account = accounts.filter(username=account_username).first()
|
actions = []
|
||||||
actions = account.actions if account else []
|
for account in accounts:
|
||||||
expire_at = perm.date_expired if perm else time.time()
|
if account.username == account_username:
|
||||||
|
actions = account.actions
|
||||||
|
expire_at = perm.date_expired.timestamp() if perm else time.time()
|
||||||
return actions, expire_at
|
return actions, expire_at
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
from .applet import *
|
from .applet import *
|
||||||
from .host import *
|
from .host import *
|
||||||
|
from .relation import *
|
||||||
|
|
|
@ -2,10 +2,14 @@ from rest_framework import viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from common.permissions import IsServiceAccount
|
||||||
from common.drf.api import JMSModelViewSet
|
from common.drf.api import JMSModelViewSet
|
||||||
from orgs.utils import tmp_to_builtin_org
|
from orgs.utils import tmp_to_builtin_org
|
||||||
from terminal import serializers
|
from terminal.serializers import (
|
||||||
from terminal.models import AppletHost, Applet, AppletHostDeployment
|
AppletHostSerializer, AppletHostDeploymentSerializer,
|
||||||
|
AppletHostStartupSerializer
|
||||||
|
)
|
||||||
|
from terminal.models import AppletHost, AppletHostDeployment
|
||||||
from terminal.tasks import run_applet_host_deployment
|
from terminal.tasks import run_applet_host_deployment
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,37 +17,29 @@ __all__ = ['AppletHostViewSet', 'AppletHostDeploymentViewSet']
|
||||||
|
|
||||||
|
|
||||||
class AppletHostViewSet(JMSModelViewSet):
|
class AppletHostViewSet(JMSModelViewSet):
|
||||||
serializer_class = serializers.AppletHostSerializer
|
serializer_class = AppletHostSerializer
|
||||||
queryset = AppletHost.objects.all()
|
queryset = AppletHost.objects.all()
|
||||||
rbac_perms = {
|
|
||||||
'accounts': 'terminal.view_applethost',
|
|
||||||
'reports': '*'
|
|
||||||
}
|
|
||||||
|
|
||||||
@action(methods=['post'], detail=True, serializer_class=serializers.AppletHostReportSerializer)
|
def dispatch(self, request, *args, **kwargs):
|
||||||
def reports(self, request, *args, **kwargs):
|
with tmp_to_builtin_org(system=1):
|
||||||
# 1. Host 和 Terminal 关联
|
return super().dispatch(request, *args, **kwargs)
|
||||||
# 2. 上报 安装的 Applets 每小时
|
|
||||||
|
def get_permissions(self):
|
||||||
|
if self.action == 'startup':
|
||||||
|
return [IsServiceAccount()]
|
||||||
|
return super().get_permissions()
|
||||||
|
|
||||||
|
@action(methods=['post'], detail=True, serializer_class=AppletHostStartupSerializer)
|
||||||
|
def startup(self, request, *args, **kwargs):
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
data = serializer.validated_data
|
|
||||||
instance.check_terminal_binding(request)
|
instance.check_terminal_binding(request)
|
||||||
instance.check_applets_state(data['applets'])
|
|
||||||
return Response({'msg': 'ok'})
|
return Response({'msg': 'ok'})
|
||||||
|
|
||||||
@action(methods=['get'], detail=True, serializer_class=serializers.AppletHostAccountSerializer)
|
|
||||||
def accounts(self, request, *args, **kwargs):
|
|
||||||
host = self.get_object()
|
|
||||||
with tmp_to_builtin_org(system=1):
|
|
||||||
accounts = host.accounts.all().filter(privileged=False)
|
|
||||||
response = self.get_paginated_response_from_queryset(accounts)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class AppletHostDeploymentViewSet(viewsets.ModelViewSet):
|
class AppletHostDeploymentViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = serializers.AppletHostDeploymentSerializer
|
serializer_class = AppletHostDeploymentSerializer
|
||||||
queryset = AppletHostDeployment.objects.all()
|
queryset = AppletHostDeployment.objects.all()
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.conf import settings
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from common.drf.api import JMSModelViewSet
|
||||||
|
from common.permissions import IsServiceAccount
|
||||||
|
from common.utils import is_uuid
|
||||||
|
from orgs.utils import tmp_to_builtin_org
|
||||||
|
from rbac.permissions import RBACPermission
|
||||||
|
from terminal.models import AppletHost
|
||||||
|
from terminal.serializers import (
|
||||||
|
AppletHostAccountSerializer,
|
||||||
|
AppletPublicationSerializer,
|
||||||
|
AppletHostAppletReportSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HostMixin:
|
||||||
|
request: Request
|
||||||
|
permission_denied: Callable
|
||||||
|
kwargs: dict
|
||||||
|
rbac_perms = (
|
||||||
|
('list', 'terminal.view_applethost'),
|
||||||
|
('retrieve', 'terminal.view_applethost'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
if self.kwargs.get('host') and settings.DEBUG:
|
||||||
|
return [RBACPermission()]
|
||||||
|
else:
|
||||||
|
return [IsServiceAccount()]
|
||||||
|
|
||||||
|
def self_host(self):
|
||||||
|
try:
|
||||||
|
return self.request.user.terminal.applet_host
|
||||||
|
except AttributeError:
|
||||||
|
raise self.permission_denied(self.request, 'User has no applet host')
|
||||||
|
|
||||||
|
def pk_host(self):
|
||||||
|
return get_object_or_404(AppletHost, id=self.kwargs.get('host'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def host(self):
|
||||||
|
if self.kwargs.get('host'):
|
||||||
|
return self.pk_host()
|
||||||
|
else:
|
||||||
|
return self.self_host()
|
||||||
|
|
||||||
|
|
||||||
|
class AppletHostAccountsViewSet(HostMixin, JMSModelViewSet):
|
||||||
|
serializer_class = AppletHostAccountSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
with tmp_to_builtin_org(system=1):
|
||||||
|
queryset = self.host.accounts.all()
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class AppletHostAppletViewSet(HostMixin, JMSModelViewSet):
|
||||||
|
host: AppletHost
|
||||||
|
serializer_class = AppletPublicationSerializer
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
pk = self.kwargs.get('pk')
|
||||||
|
if not is_uuid(pk):
|
||||||
|
return self.host.publications.get(applet__name=pk)
|
||||||
|
else:
|
||||||
|
return self.host.publications.get(pk=pk)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = self.host.publications.all()
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
@action(methods=['post'], detail=False)
|
||||||
|
def reports(self, request, *args, **kwargs):
|
||||||
|
serializer = AppletHostAppletReportSerializer(data=request.data, many=True)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
data = serializer.validated_data
|
||||||
|
self.host.check_applets_state(data)
|
||||||
|
publications = self.host.publications.all()
|
||||||
|
serializer = AppletPublicationSerializer(publications, many=True)
|
||||||
|
return Response(serializer.data)
|
|
@ -14,7 +14,7 @@
|
||||||
RDS_fSingleSessionPerUser: 1
|
RDS_fSingleSessionPerUser: 1
|
||||||
RDS_MaxDisconnectionTime: 60000
|
RDS_MaxDisconnectionTime: 60000
|
||||||
RDS_RemoteAppLogoffTimeLimit: 0
|
RDS_RemoteAppLogoffTimeLimit: 0
|
||||||
TinkerInstaller: JumpServer-Remoteapp_v0.0.1.exe
|
TinkerInstaller: Tinker_Installer_v0.0.1.exe
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Install RDS-Licensing (RDS)
|
- name: Install RDS-Licensing (RDS)
|
||||||
|
@ -31,12 +31,12 @@
|
||||||
include_management_tools: yes
|
include_management_tools: yes
|
||||||
register: rds_install
|
register: rds_install
|
||||||
|
|
||||||
- name: Download JumpServer Remoteapp installer (jumpserver)
|
- name: Download JumpServer Tinker installer (jumpserver)
|
||||||
ansible.windows.win_get_url:
|
ansible.windows.win_get_url:
|
||||||
url: "{{ DownloadHost }}/{{ TinkerInstaller }}"
|
url: "{{ DownloadHost }}/{{ TinkerInstaller }}"
|
||||||
dest: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}"
|
dest: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}"
|
||||||
|
|
||||||
- name: Install JumpServer Remoteapp agent (jumpserver)
|
- name: Install JumpServer Tinker (jumpserver)
|
||||||
ansible.windows.win_package:
|
ansible.windows.win_package:
|
||||||
path: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}"
|
path: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}"
|
||||||
arguments:
|
arguments:
|
||||||
|
@ -48,7 +48,7 @@
|
||||||
- name: Set remote-server on the global system path (remote-server)
|
- name: Set remote-server on the global system path (remote-server)
|
||||||
ansible.windows.win_path:
|
ansible.windows.win_path:
|
||||||
elements:
|
elements:
|
||||||
- '%USERPROFILE%\AppData\Local\Programs\JumpServer-Remoteapp\'
|
- '%USERPROFILE%\AppData\Local\Programs\Tinker\'
|
||||||
scope: user
|
scope: user
|
||||||
|
|
||||||
- name: Download python-3.10.8
|
- name: Download python-3.10.8
|
||||||
|
@ -153,18 +153,18 @@
|
||||||
arguments:
|
arguments:
|
||||||
- /quiet
|
- /quiet
|
||||||
|
|
||||||
- name: Generate component config
|
- name: Generate tinkerd component config
|
||||||
ansible.windows.win_shell:
|
ansible.windows.win_shell:
|
||||||
"remoteapp-server config --hostname {{ HOST_NAME }} --core_host {{ CORE_HOST }}
|
"tinkerd config --hostname {{ HOST_NAME }} --core_host {{ CORE_HOST }}
|
||||||
--token {{ BOOTSTRAP_TOKEN }} --host_id {{ HOST_ID }}"
|
--token {{ BOOTSTRAP_TOKEN }} --host_id {{ HOST_ID }}"
|
||||||
|
|
||||||
- name: Install remoteapp-server service
|
- name: Install tinkerd service
|
||||||
ansible.windows.win_shell:
|
ansible.windows.win_shell:
|
||||||
"remoteapp-server service install"
|
"tinkerd service install"
|
||||||
|
|
||||||
- name: Start remoteapp-server service
|
- name: Start tinkerd service
|
||||||
ansible.windows.win_shell:
|
ansible.windows.win_shell:
|
||||||
"remoteapp-server service start"
|
"tinkerd service start"
|
||||||
|
|
||||||
- name: Wait Tinker api health
|
- name: Wait Tinker api health
|
||||||
ansible.windows.win_uri:
|
ansible.windows.win_uri:
|
||||||
|
|
|
@ -11,7 +11,6 @@ from common.db.models import JMSBaseModel
|
||||||
from common.utils import random_string
|
from common.utils import random_string
|
||||||
from assets.models import Host
|
from assets.models import Host
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['AppletHost', 'AppletHostDeployment']
|
__all__ = ['AppletHost', 'AppletHostDeployment']
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,7 +25,7 @@ class AppletHost(Host):
|
||||||
)
|
)
|
||||||
applets = models.ManyToManyField(
|
applets = models.ManyToManyField(
|
||||||
'Applet', verbose_name=_('Applet'),
|
'Applet', verbose_name=_('Applet'),
|
||||||
through='AppletPublication', through_fields=('host', 'applet'),
|
through='AppletPublication', through_fields=('host', 'applet'),
|
||||||
)
|
)
|
||||||
LOCKING_ORG = '00000000-0000-0000-0000-000000000004'
|
LOCKING_ORG = '00000000-0000-0000-0000-000000000004'
|
||||||
|
|
||||||
|
@ -34,10 +33,10 @@ class AppletHost(Host):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self):
|
def load(self):
|
||||||
if self.terminal:
|
if not self.terminal:
|
||||||
return 'online'
|
return 'offline'
|
||||||
return self.terminal.status
|
return self.terminal.load
|
||||||
|
|
||||||
def check_terminal_binding(self, request):
|
def check_terminal_binding(self, request):
|
||||||
request_terminal = getattr(request.user, 'terminal', None)
|
request_terminal = getattr(request.user, 'terminal', None)
|
||||||
|
@ -70,8 +69,8 @@ class AppletHost(Host):
|
||||||
status_applets['published'].append(applet)
|
status_applets['published'].append(applet)
|
||||||
|
|
||||||
for status, applets in status_applets.items():
|
for status, applets in status_applets.items():
|
||||||
self.publications.filter(applet__in=applets)\
|
self.publications.filter(applet__in=applets) \
|
||||||
.exclude(status=status)\
|
.exclude(status=status) \
|
||||||
.update(status=status)
|
.update(status=status)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -95,7 +94,7 @@ class AppletHost(Host):
|
||||||
account = account_model(
|
account = account_model(
|
||||||
username=username, secret=password, name=username,
|
username=username, secret=password, name=username,
|
||||||
asset_id=self.id, secret_type='password', version=1,
|
asset_id=self.id, secret_type='password', version=1,
|
||||||
org_id=self.LOCKING_ORG
|
org_id=self.LOCKING_ORG, is_active=False,
|
||||||
)
|
)
|
||||||
accounts.append(account)
|
accounts.append(account)
|
||||||
bulk_create_with_history(accounts, account_model, batch_size=20)
|
bulk_create_with_history(accounts, account_model, batch_size=20)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import uuid
|
import uuid
|
||||||
|
import time
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -34,7 +35,7 @@ class TerminalStatusMixin:
|
||||||
def is_alive(self):
|
def is_alive(self):
|
||||||
if not self.last_stat:
|
if not self.last_stat:
|
||||||
return False
|
return False
|
||||||
return self.last_stat.date_created > timezone.now() - timezone.timedelta(seconds=120)
|
return time.time() - self.last_stat.date_created.timestamp() < 150
|
||||||
|
|
||||||
|
|
||||||
class StorageMixin:
|
class StorageMixin:
|
||||||
|
|
|
@ -17,7 +17,7 @@ class AppletPublicationSerializer(serializers.ModelSerializer):
|
||||||
UNPUBLISHED = 'unpublished', _('Unpublished')
|
UNPUBLISHED = 'unpublished', _('Unpublished')
|
||||||
NOT_MATCH = 'not_match', _('Not match')
|
NOT_MATCH = 'not_match', _('Not match')
|
||||||
|
|
||||||
applet = ObjectRelatedField(attrs=('id', 'display_name', 'icon', 'version'), queryset=Applet.objects.all())
|
applet = ObjectRelatedField(attrs=('id', 'name', 'display_name', 'icon', 'version'), queryset=Applet.objects.all())
|
||||||
host = ObjectRelatedField(queryset=AppletHost.objects.all())
|
host = ObjectRelatedField(queryset=AppletHost.objects.all())
|
||||||
status = LabeledChoiceField(choices=Status.choices, label=_("Status"))
|
status = LabeledChoiceField(choices=Status.choices, label=_("Status"))
|
||||||
|
|
||||||
|
|
|
@ -2,16 +2,18 @@ from rest_framework import serializers
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from common.validators import ProjectUniqueValidator
|
from common.validators import ProjectUniqueValidator
|
||||||
from common.drf.fields import ObjectRelatedField
|
from common.drf.fields import ObjectRelatedField, LabeledChoiceField
|
||||||
from assets.models import Platform, Account
|
from assets.models import Platform, Account
|
||||||
from assets.serializers import HostSerializer
|
from assets.serializers import HostSerializer
|
||||||
from ..models import AppletHost, AppletHostDeployment, Applet
|
from ..models import AppletHost, AppletHostDeployment, Applet
|
||||||
from .applet import AppletSerializer
|
from .applet import AppletSerializer
|
||||||
|
from .. import const
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'AppletHostSerializer', 'AppletHostDeploymentSerializer',
|
'AppletHostSerializer', 'AppletHostDeploymentSerializer',
|
||||||
'AppletHostAccountSerializer', 'AppletHostReportSerializer'
|
'AppletHostAccountSerializer', 'AppletHostAppletReportSerializer',
|
||||||
|
'AppletHostStartupSerializer',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,14 +36,16 @@ class DeployOptionsSerializer(serializers.Serializer):
|
||||||
|
|
||||||
class AppletHostSerializer(HostSerializer):
|
class AppletHostSerializer(HostSerializer):
|
||||||
deploy_options = DeployOptionsSerializer(required=False, label=_("Deploy options"))
|
deploy_options = DeployOptionsSerializer(required=False, label=_("Deploy options"))
|
||||||
|
load = LabeledChoiceField(
|
||||||
|
read_only=True, label=_('Load status'), choices=const.ComponentLoad.choices,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(HostSerializer.Meta):
|
class Meta(HostSerializer.Meta):
|
||||||
model = AppletHost
|
model = AppletHost
|
||||||
fields = HostSerializer.Meta.fields + [
|
fields = HostSerializer.Meta.fields + [
|
||||||
'status', 'date_synced', 'deploy_options'
|
'load', 'date_synced', 'deploy_options'
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'status': {'read_only': True},
|
|
||||||
'date_synced': {'read_only': True}
|
'date_synced': {'read_only': True}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,8 +97,14 @@ class AppletHostDeploymentSerializer(serializers.ModelSerializer):
|
||||||
class AppletHostAccountSerializer(serializers.ModelSerializer):
|
class AppletHostAccountSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Account
|
model = Account
|
||||||
fields = ['id', 'username', 'secret', 'date_updated']
|
fields = ['id', 'username', 'secret', 'is_active', 'date_updated']
|
||||||
|
|
||||||
|
|
||||||
class AppletHostReportSerializer(serializers.Serializer):
|
class AppletHostAppletReportSerializer(serializers.Serializer):
|
||||||
applets = ObjectRelatedField(attrs=('id', 'name', 'version'), queryset=Applet.objects.all(), many=True)
|
id = serializers.UUIDField(read_only=True)
|
||||||
|
name = serializers.CharField()
|
||||||
|
version = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
|
class AppletHostStartupSerializer(serializers.Serializer):
|
||||||
|
pass
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from orgs.utils import tmp_to_builtin_org
|
||||||
from .models import Applet, AppletHost
|
from .models import Applet, AppletHost
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,7 +14,8 @@ def on_applet_host_create(sender, instance, created=False, **kwargs):
|
||||||
return
|
return
|
||||||
applets = Applet.objects.all()
|
applets = Applet.objects.all()
|
||||||
instance.applets.set(applets)
|
instance.applets.set(applets)
|
||||||
instance.generate_accounts()
|
with tmp_to_builtin_org(system=1):
|
||||||
|
instance.generate_accounts()
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Applet)
|
@receiver(post_save, sender=Applet)
|
||||||
|
|
|
@ -59,7 +59,6 @@ class BaseTerminal(object):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
status = status_serializer.save()
|
status = status_serializer.save()
|
||||||
print("Save status ok: ", status)
|
|
||||||
time.sleep(self.interval)
|
time.sleep(self.interval)
|
||||||
except OperationalError:
|
except OperationalError:
|
||||||
print("Save status error, close old connections")
|
print("Save status error, close old connections")
|
||||||
|
|
|
@ -12,8 +12,8 @@ app_name = 'terminal'
|
||||||
|
|
||||||
router = BulkRouter()
|
router = BulkRouter()
|
||||||
router.register(r'sessions', api.SessionViewSet, 'session')
|
router.register(r'sessions', api.SessionViewSet, 'session')
|
||||||
router.register(r'terminals/(?P<terminal>[a-zA-Z0-9\-]{36})?/?status', api.StatusViewSet, 'terminal-status')
|
router.register(r'terminals/((?P<terminal>[^/.]{36})/)?status', api.StatusViewSet, 'terminal-status')
|
||||||
router.register(r'terminals/(?P<terminal>[a-zA-Z0-9\-]{36})?/?sessions', api.SessionViewSet, 'terminal-sessions')
|
router.register(r'terminals/((?P<terminal>[^/.]{36})/)?sessions', api.SessionViewSet, 'terminal-sessions')
|
||||||
router.register(r'terminals', api.TerminalViewSet, 'terminal')
|
router.register(r'terminals', api.TerminalViewSet, 'terminal')
|
||||||
router.register(r'tasks', api.TaskViewSet, 'tasks')
|
router.register(r'tasks', api.TaskViewSet, 'tasks')
|
||||||
router.register(r'commands', api.CommandViewSet, 'command')
|
router.register(r'commands', api.CommandViewSet, 'command')
|
||||||
|
@ -25,6 +25,8 @@ router.register(r'session-join-records', api.SessionJoinRecordsViewSet, 'session
|
||||||
router.register(r'endpoints', api.EndpointViewSet, 'endpoint')
|
router.register(r'endpoints', api.EndpointViewSet, 'endpoint')
|
||||||
router.register(r'endpoint-rules', api.EndpointRuleViewSet, 'endpoint-rule')
|
router.register(r'endpoint-rules', api.EndpointRuleViewSet, 'endpoint-rule')
|
||||||
router.register(r'applets', api.AppletViewSet, 'applet')
|
router.register(r'applets', api.AppletViewSet, 'applet')
|
||||||
|
router.register(r'applet-hosts/((?P<host>[^/.]+)/)?accounts', api.AppletHostAccountsViewSet, 'applet-host-account')
|
||||||
|
router.register(r'applet-hosts/((?P<host>[^/.]+)/)?applets', api.AppletHostAppletViewSet, 'applet-host-applet')
|
||||||
router.register(r'applet-hosts', api.AppletHostViewSet, 'applet-host')
|
router.register(r'applet-hosts', api.AppletHostViewSet, 'applet-host')
|
||||||
router.register(r'applet-publications', api.AppletPublicationViewSet, 'applet-publication')
|
router.register(r'applet-publications', api.AppletPublicationViewSet, 'applet-publication')
|
||||||
router.register(r'applet-host-deployments', api.AppletHostDeploymentViewSet, 'applet-host-deployment')
|
router.register(r'applet-host-deployments', api.AppletHostDeploymentViewSet, 'applet-host-deployment')
|
||||||
|
|
|
@ -5,21 +5,24 @@ from rest_framework import serializers
|
||||||
from orgs.models import Organization
|
from orgs.models import Organization
|
||||||
from orgs.utils import get_current_org_id
|
from orgs.utils import get_current_org_id
|
||||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||||
|
from common.drf.fields import LabeledChoiceField
|
||||||
from tickets.models import TicketFlow, ApprovalRule
|
from tickets.models import TicketFlow, ApprovalRule
|
||||||
from tickets.const import TicketApprovalStrategy
|
from tickets.const import TicketApprovalStrategy, TicketType
|
||||||
|
|
||||||
__all__ = ['TicketFlowSerializer']
|
__all__ = ['TicketFlowSerializer']
|
||||||
|
|
||||||
|
|
||||||
class TicketFlowApproveSerializer(serializers.ModelSerializer):
|
class TicketFlowApproveSerializer(serializers.ModelSerializer):
|
||||||
strategy_display = serializers.ReadOnlyField(source='get_strategy_display', label=_('Approve strategy'))
|
strategy = LabeledChoiceField(
|
||||||
|
choices=TicketApprovalStrategy.choices, required=True, label=_('Approve strategy')
|
||||||
|
)
|
||||||
assignees_read_only = serializers.SerializerMethodField(label=_('Assignees'))
|
assignees_read_only = serializers.SerializerMethodField(label=_('Assignees'))
|
||||||
assignees_display = serializers.SerializerMethodField(label=_('Assignees display'))
|
assignees_display = serializers.SerializerMethodField(label=_('Assignees display'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ApprovalRule
|
model = ApprovalRule
|
||||||
fields_small = [
|
fields_small = [
|
||||||
'level', 'strategy', 'assignees_read_only', 'assignees_display', 'strategy_display'
|
'level', 'strategy', 'assignees_read_only', 'assignees_display',
|
||||||
]
|
]
|
||||||
fields_m2m = ['assignees', ]
|
fields_m2m = ['assignees', ]
|
||||||
fields = fields_small + fields_m2m
|
fields = fields_small + fields_m2m
|
||||||
|
@ -46,14 +49,16 @@ class TicketFlowApproveSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class TicketFlowSerializer(OrgResourceModelSerializerMixin):
|
class TicketFlowSerializer(OrgResourceModelSerializerMixin):
|
||||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
|
type = LabeledChoiceField(
|
||||||
|
choices=TicketType.choices, required=True, label=_('Type')
|
||||||
|
)
|
||||||
rules = TicketFlowApproveSerializer(many=True, required=True)
|
rules = TicketFlowApproveSerializer(many=True, required=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TicketFlow
|
model = TicketFlow
|
||||||
fields_mini = ['id', ]
|
fields_mini = ['id', ]
|
||||||
fields_small = fields_mini + [
|
fields_small = fields_mini + [
|
||||||
'type', 'type_display', 'approval_level', 'created_by', 'date_created', 'date_updated',
|
'type', 'approval_level', 'created_by', 'date_created', 'date_updated',
|
||||||
'org_id', 'org_name'
|
'org_id', 'org_name'
|
||||||
]
|
]
|
||||||
fields = fields_small + ['rules', ]
|
fields = fields_small + ['rules', ]
|
||||||
|
|
Loading…
Reference in New Issue