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 .node import *
|
||||
from .domain import *
|
||||
from .automations import *
|
||||
from .gathered_user import *
|
||||
from .favorite_asset import *
|
||||
|
|
|
@ -82,10 +82,11 @@ class AssetsTaskMixin:
|
|||
def perform_assets_task(self, serializer):
|
||||
data = serializer.validated_data
|
||||
assets = data.get('assets', [])
|
||||
asset_ids = [asset.id for asset in assets]
|
||||
if data['action'] == "refresh":
|
||||
task = update_assets_hardware_info_manual.delay(assets)
|
||||
task = update_assets_hardware_info_manual.delay(asset_ids)
|
||||
else:
|
||||
task = test_assets_connectivity_manual.delay(assets)
|
||||
task = test_assets_connectivity_manual.delay(asset_ids)
|
||||
return task
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
secret = self.generate_public_key(secret)
|
||||
|
||||
|
@ -221,6 +221,7 @@ class BasePlaybookManager:
|
|||
else:
|
||||
print(">>> 开始执行任务\n")
|
||||
|
||||
self.execution.date_start = timezone.now()
|
||||
for i, runner in enumerate(runners, start=1):
|
||||
if len(runners) > 1:
|
||||
print(">>> 开始执行第 {} 批任务".format(i))
|
||||
|
@ -231,3 +232,6 @@ class BasePlaybookManager:
|
|||
except Exception as e:
|
||||
self.on_runner_failed(runner, e)
|
||||
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()
|
||||
|
||||
def get_secret(self):
|
||||
if self.secret_type == SecretType.ssh_key:
|
||||
if self.secret_type == SecretType.SSH_KEY:
|
||||
secret = self.get_ssh_key()
|
||||
elif self.secret_type == SecretType.password:
|
||||
elif self.secret_type == SecretType.PASSWORD:
|
||||
secret = self.get_password()
|
||||
else:
|
||||
raise ValueError("Secret must be set")
|
||||
|
@ -99,7 +99,7 @@ class ChangeSecretManager(BasePlaybookManager):
|
|||
|
||||
def get_kwargs(self, account, secret):
|
||||
kwargs = {}
|
||||
if self.secret_type != SecretType.ssh_key:
|
||||
if self.secret_type != SecretType.SSH_KEY:
|
||||
return kwargs
|
||||
kwargs['strategy'] = self.execution.snapshot['ssh_key_change_strategy']
|
||||
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
|
||||
|
@ -143,7 +143,7 @@ class ChangeSecretManager(BasePlaybookManager):
|
|||
self.name_recorder_mapper[h['name']] = recorder
|
||||
|
||||
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)
|
||||
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 .push_account.manager import PushAccountManager
|
||||
from .backup_account.manager import AccountBackupManager
|
||||
from .ping.manager import PingManager
|
||||
from ..const import AutomationTypes
|
||||
|
||||
|
||||
class ExecutionManager:
|
||||
manager_type_mapper = {
|
||||
AutomationTypes.change_secret: ChangeSecretManager,
|
||||
AutomationTypes.gather_facts: GatherFactsManager,
|
||||
AutomationTypes.gather_accounts: GatherAccountsManager,
|
||||
AutomationTypes.verify_account: VerifyAccountManager,
|
||||
AutomationTypes.ping: PingManager,
|
||||
AutomationTypes.push_account: PushAccountManager,
|
||||
AutomationTypes.gather_facts: GatherFactsManager,
|
||||
AutomationTypes.change_secret: ChangeSecretManager,
|
||||
AutomationTypes.verify_account: VerifyAccountManager,
|
||||
AutomationTypes.gather_accounts: GatherAccountsManager,
|
||||
# TODO 后期迁移到自动化策略中
|
||||
'backup_account': AccountBackupManager,
|
||||
}
|
||||
|
|
|
@ -21,14 +21,14 @@ class PingManager(BasePlaybookManager):
|
|||
|
||||
def on_host_success(self, host, result):
|
||||
asset, account = self.host_asset_and_account_mapper.get(host)
|
||||
asset.set_connectivity(Connectivity.ok)
|
||||
asset.set_connectivity(Connectivity.OK)
|
||||
if not account:
|
||||
return
|
||||
account.set_connectivity(Connectivity.ok)
|
||||
account.set_connectivity(Connectivity.OK)
|
||||
|
||||
def on_host_error(self, host, error, result):
|
||||
asset, account = self.host_asset_and_account_mapper.get(host)
|
||||
asset.set_connectivity(Connectivity.failed)
|
||||
asset.set_connectivity(Connectivity.FAILED)
|
||||
if not account:
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
unknown = 'unknown', _('Unknown')
|
||||
ok = 'ok', _('Ok')
|
||||
failed = 'failed', _('Failed')
|
||||
UNKNOWN = 'unknown', _('Unknown')
|
||||
OK = 'ok', _('Ok')
|
||||
FAILED = 'failed', _('Failed')
|
||||
|
||||
|
||||
class SecretType(TextChoices):
|
||||
password = 'password', _('Password')
|
||||
ssh_key = 'ssh_key', _('SSH key')
|
||||
access_key = 'access_key', _('Access key')
|
||||
token = 'token', _('Token')
|
||||
PASSWORD = 'password', _('Password')
|
||||
SSH_KEY = 'ssh_key', _('SSH key')
|
||||
ACCESS_KEY = 'access_key', _('Access key')
|
||||
TOKEN = 'token', _('Token')
|
||||
|
|
|
@ -17,6 +17,22 @@ class AutomationTypes(TextChoices):
|
|||
verify_account = 'verify_account', _('Verify account')
|
||||
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):
|
||||
custom = 'specific', _('Specific')
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
from .change_secret import *
|
||||
from .discovery_account import *
|
||||
from .ping import *
|
||||
from .base import *
|
||||
from .push_account import *
|
||||
from .gather_facts import *
|
||||
from .gather_accounts import *
|
||||
from .change_secret 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.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.db.fields import EncryptJsonDictTextField
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
|
@ -15,12 +15,8 @@ from assets.const import AutomationTypes
|
|||
|
||||
class BaseAutomation(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin):
|
||||
accounts = models.JSONField(default=list, verbose_name=_("Accounts"))
|
||||
nodes = models.ManyToManyField(
|
||||
'assets.Node', blank=True, verbose_name=_("Nodes")
|
||||
)
|
||||
assets = models.ManyToManyField(
|
||||
'assets.Asset', blank=True, verbose_name=_("Assets")
|
||||
)
|
||||
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
|
||||
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
|
||||
type = models.CharField(max_length=16, choices=AutomationTypes.choices, verbose_name=_('Type'))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
||||
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
||||
|
@ -92,7 +88,7 @@ class AutomationExecution(OrgModelMixin):
|
|||
'BaseAutomation', related_name='executions', on_delete=models.CASCADE,
|
||||
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_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True)
|
||||
date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished"))
|
||||
|
|
|
@ -12,7 +12,7 @@ __all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord']
|
|||
class ChangeSecretAutomation(BaseAutomation):
|
||||
secret_type = models.CharField(
|
||||
choices=SecretType.choices, max_length=16,
|
||||
default=SecretType.password, verbose_name=_('Secret type')
|
||||
default=SecretType.PASSWORD, verbose_name=_('Secret type')
|
||||
)
|
||||
secret_strategy = models.CharField(
|
||||
choices=SecretStrategy.choices, max_length=16,
|
||||
|
@ -24,7 +24,7 @@ class ChangeSecretAutomation(BaseAutomation):
|
|||
choices=SSHKeyStrategy.choices, max_length=16,
|
||||
default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy')
|
||||
)
|
||||
recipients = models.ManyToManyField('users.User', blank=True, verbose_name=_("Recipient"))
|
||||
recipients = models.ManyToManyField('users.User', verbose_name=_("Recipient"), blank=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.type = AutomationTypes.change_secret
|
||||
|
|
|
@ -13,3 +13,7 @@ class GatherAccountsAutomation(BaseAutomation):
|
|||
|
||||
class Meta:
|
||||
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):
|
||||
connectivity = models.CharField(
|
||||
choices=Connectivity.choices, default=Connectivity.unknown,
|
||||
choices=Connectivity.choices, default=Connectivity.UNKNOWN,
|
||||
max_length=16, verbose_name=_('Connectivity')
|
||||
)
|
||||
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"))
|
||||
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True)
|
||||
secret_type = models.CharField(
|
||||
max_length=16, choices=SecretType.choices, default=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'))
|
||||
privileged = models.BooleanField(verbose_name=_("Privileged"), default=False)
|
||||
|
@ -65,25 +65,25 @@ class BaseAccount(JMSOrgBaseModel):
|
|||
@property
|
||||
def specific(self):
|
||||
data = {}
|
||||
if self.secret_type != SecretType.ssh_key:
|
||||
if self.secret_type != SecretType.SSH_KEY:
|
||||
return data
|
||||
data['ssh_key_fingerprint'] = self.ssh_key_fingerprint
|
||||
return data
|
||||
|
||||
@property
|
||||
def private_key(self):
|
||||
if self.secret_type == SecretType.ssh_key:
|
||||
if self.secret_type == SecretType.SSH_KEY:
|
||||
return self.secret
|
||||
return None
|
||||
|
||||
@private_key.setter
|
||||
def private_key(self, value):
|
||||
self.secret = value
|
||||
self.secret_type = SecretType.ssh_key
|
||||
self.secret_type = SecretType.SSH_KEY
|
||||
|
||||
@lazyproperty
|
||||
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 None
|
||||
|
||||
|
@ -113,7 +113,7 @@ class BaseAccount(JMSOrgBaseModel):
|
|||
|
||||
@property
|
||||
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
|
||||
project_dir = settings.PROJECT_DIR
|
||||
tmp_dir = os.path.join(project_dir, 'tmp')
|
||||
|
|
|
@ -201,7 +201,7 @@ class CommandFilterRule(OrgModelMixin):
|
|||
q |= Q(user_groups__in=set(user_groups))
|
||||
if account:
|
||||
org_id = account.org_id
|
||||
q |= Q(accounts__contains=list(account)) |\
|
||||
q |= Q(accounts__contains=account.username) | \
|
||||
Q(accounts__contains=SpecialAccount.ALL.value)
|
||||
if asset:
|
||||
org_id = asset.org_id
|
||||
|
|
|
@ -6,7 +6,6 @@ from common.db.fields import JsonDictTextField
|
|||
|
||||
from assets.const import Protocol
|
||||
|
||||
|
||||
__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_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_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_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_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):
|
||||
|
@ -61,10 +64,11 @@ class Platform(models.Model):
|
|||
对资产提供 约束和默认值
|
||||
对资产进行抽象
|
||||
"""
|
||||
CHARSET_CHOICES = (
|
||||
('utf8', 'UTF-8'),
|
||||
('gbk', 'GBK'),
|
||||
)
|
||||
|
||||
class CharsetChoices(models.TextChoices):
|
||||
utf8 = 'utf8', 'UTF-8'
|
||||
gbk = 'gbk', 'GBK'
|
||||
|
||||
name = models.SlugField(verbose_name=_("Name"), unique=True, allow_unicode=True)
|
||||
category = models.CharField(default='host', max_length=32, verbose_name=_("Category"))
|
||||
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"))
|
||||
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"))
|
||||
protocols_enabled = models.BooleanField(default=True, verbose_name=_("Protocols enabled"))
|
||||
# 账号有关的
|
||||
|
@ -103,4 +109,3 @@ class Platform(models.Model):
|
|||
class Meta:
|
||||
verbose_name = _("Platform")
|
||||
# ordering = ('name',)
|
||||
|
||||
|
|
|
@ -11,4 +11,4 @@ from .account import *
|
|||
from assets.serializers.account.backup import *
|
||||
from .platform 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 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.models import Account, AccountTemplate, Asset
|
||||
from .base import BaseAccountSerializer
|
||||
from assets.const import SecretType
|
||||
|
||||
|
||||
class AccountSerializerCreateMixin(serializers.ModelSerializer):
|
||||
|
@ -91,6 +92,8 @@ class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
|||
|
||||
|
||||
class AccountHistorySerializer(serializers.ModelSerializer):
|
||||
secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type'))
|
||||
|
||||
class Meta:
|
||||
model = Account.history.model
|
||||
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 ops.mixin import PeriodTaskSerializerMixin
|
||||
from common.utils import get_logger
|
||||
from common.const.choices import Trigger
|
||||
from common.drf.fields import LabeledChoiceField
|
||||
|
||||
from assets.models import AccountBackupPlan, AccountBackupPlanExecution
|
||||
|
||||
|
@ -20,7 +22,7 @@ class AccountBackupPlanSerializer(PeriodTaskSerializerMixin, BulkOrgResourceMode
|
|||
fields = [
|
||||
'id', 'name', 'is_periodic', 'interval', 'crontab', 'date_created',
|
||||
'date_updated', 'created_by', 'periodic_display', 'comment',
|
||||
'recipients', 'categories'
|
||||
'recipients', 'types'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'name': {'required': True},
|
||||
|
@ -32,17 +34,12 @@ class AccountBackupPlanSerializer(PeriodTaskSerializerMixin, BulkOrgResourceMode
|
|||
|
||||
|
||||
class AccountBackupPlanExecutionSerializer(serializers.ModelSerializer):
|
||||
trigger_display = serializers.ReadOnlyField(
|
||||
source='get_trigger_display', label=_('Trigger mode')
|
||||
)
|
||||
trigger = LabeledChoiceField(choices=Trigger.choices, label=_('Trigger mode'))
|
||||
|
||||
class Meta:
|
||||
model = AccountBackupPlanExecution
|
||||
fields = [
|
||||
'id', 'date_start', 'timedelta', 'plan_snapshot', 'trigger', 'reason',
|
||||
'is_success', 'plan', 'org_id', 'recipients', 'trigger_display'
|
||||
]
|
||||
read_only_fields = (
|
||||
read_only_fields = [
|
||||
'id', 'date_start', 'timedelta', 'plan_snapshot', 'trigger', 'reason',
|
||||
'is_success', 'org_id', 'recipients'
|
||||
)
|
||||
]
|
||||
fields = read_only_fields + ['plan']
|
||||
|
|
|
@ -1,27 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from io import StringIO
|
||||
|
||||
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.serializers.base import AuthValidateMixin
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
|
||||
__all__ = ['BaseAccountSerializer']
|
||||
|
||||
|
||||
class BaseAccountSerializer(BulkOrgResourceModelSerializer):
|
||||
secret = EncryptedField(
|
||||
label=_('Secret'), required=False, allow_blank=True,
|
||||
allow_null=True, max_length=40960
|
||||
)
|
||||
|
||||
class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
|
||||
class Meta:
|
||||
model = BaseAccount
|
||||
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 = fields_small + fields_other
|
||||
read_only_fields = [
|
||||
|
@ -29,28 +23,5 @@ class BaseAccountSerializer(BulkOrgResourceModelSerializer):
|
|||
'date_verified', 'created_by', 'date_created',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'secret': {'write_only': True},
|
||||
'passphrase': {'write_only': True},
|
||||
'specific': {'label': _('Specific')},
|
||||
}
|
||||
|
||||
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',
|
||||
]
|
||||
read_only_fields = [
|
||||
'category', 'type', 'specific',
|
||||
'category', 'type', 'specific', 'info',
|
||||
'connectivity', 'date_verified',
|
||||
'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 -*-
|
||||
#
|
||||
from io import StringIO
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.utils import ssh_pubkey_gen, ssh_private_key_gen, validate_ssh_private_key
|
||||
from common.drf.fields import EncryptedField
|
||||
from .utils import validate_password_for_ansible
|
||||
from assets.const import SecretType
|
||||
from common.drf.fields import EncryptedField, LabeledChoiceField
|
||||
from .utils import validate_password_for_ansible, validate_ssh_key
|
||||
|
||||
|
||||
class AuthValidateMixin(serializers.Serializer):
|
||||
password = EncryptedField(
|
||||
label=_('Password'), required=False, allow_blank=True, allow_null=True,
|
||||
max_length=1024, validators=[validate_password_for_ansible]
|
||||
secret_type = LabeledChoiceField(
|
||||
choices=SecretType.choices, required=True, label=_('Secret type')
|
||||
)
|
||||
private_key = EncryptedField(
|
||||
label=_('SSH private key'), required=False, allow_blank=True,
|
||||
allow_null=True, max_length=16384
|
||||
secret = EncryptedField(
|
||||
label=_('Secret'), required=False, max_length=40960, allow_blank=True,
|
||||
allow_null=True, write_only=True,
|
||||
)
|
||||
passphrase = serializers.CharField(
|
||||
allow_blank=True, allow_null=True, required=False, max_length=512,
|
||||
write_only=True, label=_('Key password')
|
||||
)
|
||||
|
||||
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"))
|
||||
@property
|
||||
def initial_secret_type(self):
|
||||
secret_type = self.initial_data.get('secret_type')
|
||||
return secret_type
|
||||
|
||||
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, secret):
|
||||
if not secret:
|
||||
return
|
||||
secret_type = self.initial_secret_type
|
||||
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
|
||||
def clean_auth_fields(validated_data):
|
||||
for field in ('password', 'private_key', 'public_key'):
|
||||
for field in ('secret',):
|
||||
value = validated_data.get(field)
|
||||
if not value:
|
||||
validated_data.pop(field, 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):
|
||||
self.clean_auth_fields(validated_data)
|
||||
return super().create(validated_data)
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||
from ..models import GatheredUser
|
||||
from common.drf.fields import ObjectRelatedField
|
||||
from ..models import GatheredUser, Asset
|
||||
|
||||
|
||||
class GatheredUserSerializer(OrgResourceModelSerializerMixin):
|
||||
asset = ObjectRelatedField(queryset=Asset.objects, label=_('Asset'))
|
||||
|
||||
class Meta:
|
||||
model = GatheredUser
|
||||
fields_mini = ['id']
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
from io import StringIO
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.utils import ssh_private_key_gen, validate_ssh_private_key
|
||||
|
||||
|
||||
def validate_password_for_ansible(password):
|
||||
""" 校验 Ansible 不支持的特殊字符 """
|
||||
|
@ -15,3 +19,14 @@ def validate_password_for_ansible(password):
|
|||
if '"' in password:
|
||||
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 .push_account import *
|
||||
from .verify_account import *
|
||||
from .gather_accounts import *
|
||||
|
|
|
@ -7,9 +7,9 @@ logger = get_logger(__file__)
|
|||
|
||||
|
||||
@shared_task(queue='ansible')
|
||||
def execute_automation(pid, trigger, mode):
|
||||
def execute_automation(pid, trigger, model):
|
||||
with tmp_to_root_org():
|
||||
instance = get_object_or_none(mode, pk=pid)
|
||||
instance = get_object_or_none(model, pk=pid)
|
||||
if not instance:
|
||||
logger.error("No automation task found: {}".format(pid))
|
||||
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-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 = [
|
||||
# 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/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/<uuid:perm_user_id>/permissions/', api.AssetPermUserPermissionsListApi.as_view(), name='asset-perm-user-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('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-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('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/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('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
|
||||
|
||||
|
|
|
@ -178,8 +178,6 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
|
|||
get_object: callable
|
||||
get_serializer: callable
|
||||
perform_create: callable
|
||||
check_token_permission: callable
|
||||
create_connection_token: callable
|
||||
|
||||
@action(methods=['POST'], detail=False, url_path='secret-info/detail')
|
||||
def get_secret_detail(self, request, *args, **kwargs):
|
||||
|
@ -277,10 +275,10 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
|
|||
from perms.utils.account import PermAccountUtil
|
||||
actions, expire_at = PermAccountUtil().validate_permission(user, asset, account_username)
|
||||
if not actions:
|
||||
error = ''
|
||||
error = 'No actions'
|
||||
raise PermissionDenied(error)
|
||||
if expire_at < time.time():
|
||||
error = ''
|
||||
error = 'Expired'
|
||||
raise PermissionDenied(error)
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import base64
|
||||
import time
|
||||
|
||||
from django.shortcuts import redirect, reverse, render
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
|
|
|
@ -85,7 +85,7 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
|
|||
is_valid = False
|
||||
error = _('No user or invalid user')
|
||||
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
|
||||
error = _('No asset or inactive asset')
|
||||
return is_valid, error
|
||||
|
|
|
@ -159,7 +159,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
|
|||
domain = ConnectionTokenDomainSerializer(read_only=True)
|
||||
cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True)
|
||||
actions = ActionsField()
|
||||
expired_at = serializers.IntegerField()
|
||||
expire_at = serializers.IntegerField()
|
||||
|
||||
class Meta:
|
||||
model = ConnectionToken
|
||||
|
@ -167,5 +167,5 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
|
|||
'id', 'secret',
|
||||
'user', 'asset', 'account_username', 'account', 'protocol',
|
||||
'domain', 'gateway', 'cmd_filter_rules',
|
||||
'actions', 'expired_at',
|
||||
'actions', 'expire_at',
|
||||
]
|
||||
|
|
|
@ -44,7 +44,9 @@ class BaseService(object):
|
|||
if self.is_running:
|
||||
msg = f'{self.name} is running: {self.pid}.'
|
||||
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)
|
||||
|
||||
# -- log --
|
||||
|
|
|
@ -76,7 +76,6 @@ class ServicesUtil(object):
|
|||
def clean_up(self):
|
||||
if not self.EXIT_EVENT.is_set():
|
||||
self.EXIT_EVENT.set()
|
||||
|
||||
self.stop()
|
||||
|
||||
def show_status(self):
|
||||
|
|
|
@ -16,14 +16,13 @@ class IsValidUser(permissions.IsAuthenticated, permissions.BasePermission):
|
|||
"""Allows access to valid user, is active and not expired"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
class IsValidUserOrConnectionToken(IsValidUser):
|
||||
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
|
@ -42,6 +41,12 @@ class OnlySuperUser(IsValidUser):
|
|||
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):
|
||||
def has_permission(self, request, view):
|
||||
authorization = request.META.get('HTTP_AUTHORIZATION', '')
|
||||
|
|
|
@ -308,14 +308,14 @@ class HealthCheckView(HealthApiMixin):
|
|||
def get_db_status():
|
||||
t1 = time.time()
|
||||
try:
|
||||
User.objects.first()
|
||||
ok = User.objects.first() is not None
|
||||
t2 = time.time()
|
||||
return True, t2 - t1
|
||||
except:
|
||||
t2 = time.time()
|
||||
return False, t2 - t1
|
||||
return ok, t2 - t1
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
def get_redis_status(self):
|
||||
@staticmethod
|
||||
def get_redis_status():
|
||||
key = 'HEALTH_CHECK'
|
||||
|
||||
t1 = time.time()
|
||||
|
@ -324,12 +324,12 @@ class HealthCheckView(HealthApiMixin):
|
|||
cache.set(key, '1', 10)
|
||||
got = cache.get(key)
|
||||
t2 = time.time()
|
||||
|
||||
if value == got:
|
||||
return True, t2 -t1
|
||||
return False, t2 -t1
|
||||
except:
|
||||
t2 = time.time()
|
||||
return False, t2 - t1
|
||||
return True, t2 - t1
|
||||
return False, 'Value not match'
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
def get(self, request):
|
||||
redis_status, redis_time = self.get_redis_status()
|
||||
|
@ -341,7 +341,7 @@ class HealthCheckView(HealthApiMixin):
|
|||
'db_time': db_time,
|
||||
'redis_status': redis_status,
|
||||
'redis_time': redis_time,
|
||||
'time': int(time.time())
|
||||
'time': int(time.time()),
|
||||
}
|
||||
return Response(data)
|
||||
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
import os
|
||||
import re
|
||||
import pytz
|
||||
import time
|
||||
import json
|
||||
|
||||
from django.utils import timezone
|
||||
from django.shortcuts import HttpResponse
|
||||
from django.conf import settings
|
||||
|
@ -92,3 +95,37 @@ class RefererCheckMiddleware:
|
|||
return HttpResponseForbidden('CSRF CHECK ERROR')
|
||||
response = self.get_response(request)
|
||||
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 = [
|
||||
'jumpserver.middleware.StartMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
|
@ -105,6 +106,7 @@ MIDDLEWARE = [
|
|||
'authentication.middleware.ThirdPartyLoginMiddleware',
|
||||
'authentication.middleware.SessionCookieMiddleware',
|
||||
'simple_history.middleware.HistoryRequestMiddleware',
|
||||
'jumpserver.middleware.EndMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'jumpserver.urls'
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0b396cc9a485f6474d14ca30a1a7ba4f954b07754148b964efbb21519c55b280
|
||||
size 102849
|
||||
oid sha256:314c29cb8b10aaddbb030bf49af293be23f0153ff1f1c7562946879574ce6de8
|
||||
size 102801
|
||||
|
|
|
@ -858,7 +858,7 @@ msgstr "校验日期"
|
|||
|
||||
#: assets/models/base.py:63
|
||||
msgid "Privileged"
|
||||
msgstr "特权的"
|
||||
msgstr "特权账号"
|
||||
|
||||
#: assets/models/cmd_filter.py:32 perms/models/asset_permission.py:61
|
||||
#: users/models/group.py:31 users/models/user.py:671
|
||||
|
|
|
@ -144,7 +144,7 @@ def check_server_performance_period():
|
|||
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):
|
||||
from users.models import User
|
||||
import time
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import uuid
|
||||
import logging
|
||||
from functools import reduce
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.db import models
|
||||
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 orgs.mixins.models import OrgModelMixin
|
||||
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
|
||||
|
||||
__all__ = [
|
||||
|
|
|
@ -6,10 +6,11 @@ from rest_framework.fields import empty
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
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 users.models import User, UserGroup
|
||||
from perms.models import AssetPermission, Action
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
|
||||
__all__ = ['AssetPermissionSerializer', 'ActionsField']
|
||||
|
||||
|
@ -44,18 +45,10 @@ class ActionsDisplayField(ActionsField):
|
|||
|
||||
|
||||
class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
|
||||
users_display = serializers.ListField(
|
||||
child=serializers.CharField(), label=_('Users display'), required=False
|
||||
)
|
||||
user_groups_display = serializers.ListField(
|
||||
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
|
||||
)
|
||||
users = ObjectRelatedField(queryset=User.objects, many=True, required=False)
|
||||
user_groups = ObjectRelatedField(queryset=UserGroup.objects, many=True, required=False)
|
||||
assets = ObjectRelatedField(queryset=Asset.objects, many=True, required=False)
|
||||
nodes = ObjectRelatedField(queryset=Node.objects, many=True, required=False)
|
||||
actions = ActionsField(required=False, allow_null=True, label=_("Actions"))
|
||||
is_valid = serializers.BooleanField(read_only=True, label=_("Is valid"))
|
||||
is_expired = serializers.BooleanField(read_only=True, label=_('Is expired'))
|
||||
|
@ -64,24 +57,16 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
|
|||
model = AssetPermission
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'is_active', 'is_expired', 'is_valid', 'actions',
|
||||
'accounts',
|
||||
'created_by', 'date_created', 'date_expired',
|
||||
'accounts', 'is_active', 'is_expired', 'is_valid',
|
||||
'actions', 'created_by', 'date_created', 'date_expired',
|
||||
'date_start', 'comment', 'from_ticket'
|
||||
]
|
||||
fields_m2m = [
|
||||
'users', 'users_display', 'user_groups', 'user_groups_display', 'assets',
|
||||
'assets_display', 'nodes', 'nodes_display',
|
||||
'users_amount', 'user_groups_amount', 'assets_amount',
|
||||
'nodes_amount',
|
||||
'users', 'user_groups', 'assets', 'nodes',
|
||||
]
|
||||
fields = fields_small + fields_m2m
|
||||
read_only_fields = ['created_by', 'date_created', 'from_ticket']
|
||||
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')},
|
||||
'is_expired': {'label': _('Is expired')},
|
||||
'is_valid': {'label': _('Is valid')},
|
||||
|
|
|
@ -53,7 +53,9 @@ class PermAccountUtil(AssetPermissionUtil):
|
|||
user, asset, with_actions=True, with_perms=True
|
||||
)
|
||||
perm = perms.first()
|
||||
account = accounts.filter(username=account_username).first()
|
||||
actions = account.actions if account else []
|
||||
expire_at = perm.date_expired if perm else time.time()
|
||||
actions = []
|
||||
for account in accounts:
|
||||
if account.username == account_username:
|
||||
actions = account.actions
|
||||
expire_at = perm.date_expired.timestamp() if perm else time.time()
|
||||
return actions, expire_at
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
from .applet 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.response import Response
|
||||
|
||||
from common.permissions import IsServiceAccount
|
||||
from common.drf.api import JMSModelViewSet
|
||||
from orgs.utils import tmp_to_builtin_org
|
||||
from terminal import serializers
|
||||
from terminal.models import AppletHost, Applet, AppletHostDeployment
|
||||
from terminal.serializers import (
|
||||
AppletHostSerializer, AppletHostDeploymentSerializer,
|
||||
AppletHostStartupSerializer
|
||||
)
|
||||
from terminal.models import AppletHost, AppletHostDeployment
|
||||
from terminal.tasks import run_applet_host_deployment
|
||||
|
||||
|
||||
|
@ -13,37 +17,29 @@ __all__ = ['AppletHostViewSet', 'AppletHostDeploymentViewSet']
|
|||
|
||||
|
||||
class AppletHostViewSet(JMSModelViewSet):
|
||||
serializer_class = serializers.AppletHostSerializer
|
||||
serializer_class = AppletHostSerializer
|
||||
queryset = AppletHost.objects.all()
|
||||
rbac_perms = {
|
||||
'accounts': 'terminal.view_applethost',
|
||||
'reports': '*'
|
||||
}
|
||||
|
||||
@action(methods=['post'], detail=True, serializer_class=serializers.AppletHostReportSerializer)
|
||||
def reports(self, request, *args, **kwargs):
|
||||
# 1. Host 和 Terminal 关联
|
||||
# 2. 上报 安装的 Applets 每小时
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
with tmp_to_builtin_org(system=1):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
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()
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
data = serializer.validated_data
|
||||
instance.check_terminal_binding(request)
|
||||
instance.check_applets_state(data['applets'])
|
||||
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):
|
||||
serializer_class = serializers.AppletHostDeploymentSerializer
|
||||
serializer_class = AppletHostDeploymentSerializer
|
||||
queryset = AppletHostDeployment.objects.all()
|
||||
|
||||
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_MaxDisconnectionTime: 60000
|
||||
RDS_RemoteAppLogoffTimeLimit: 0
|
||||
TinkerInstaller: JumpServer-Remoteapp_v0.0.1.exe
|
||||
TinkerInstaller: Tinker_Installer_v0.0.1.exe
|
||||
|
||||
tasks:
|
||||
- name: Install RDS-Licensing (RDS)
|
||||
|
@ -31,12 +31,12 @@
|
|||
include_management_tools: yes
|
||||
register: rds_install
|
||||
|
||||
- name: Download JumpServer Remoteapp installer (jumpserver)
|
||||
- name: Download JumpServer Tinker installer (jumpserver)
|
||||
ansible.windows.win_get_url:
|
||||
url: "{{ DownloadHost }}/{{ TinkerInstaller }}"
|
||||
dest: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}"
|
||||
|
||||
- name: Install JumpServer Remoteapp agent (jumpserver)
|
||||
- name: Install JumpServer Tinker (jumpserver)
|
||||
ansible.windows.win_package:
|
||||
path: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}"
|
||||
arguments:
|
||||
|
@ -48,7 +48,7 @@
|
|||
- name: Set remote-server on the global system path (remote-server)
|
||||
ansible.windows.win_path:
|
||||
elements:
|
||||
- '%USERPROFILE%\AppData\Local\Programs\JumpServer-Remoteapp\'
|
||||
- '%USERPROFILE%\AppData\Local\Programs\Tinker\'
|
||||
scope: user
|
||||
|
||||
- name: Download python-3.10.8
|
||||
|
@ -153,18 +153,18 @@
|
|||
arguments:
|
||||
- /quiet
|
||||
|
||||
- name: Generate component config
|
||||
- name: Generate tinkerd component config
|
||||
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 }}"
|
||||
|
||||
- name: Install remoteapp-server service
|
||||
- name: Install tinkerd service
|
||||
ansible.windows.win_shell:
|
||||
"remoteapp-server service install"
|
||||
"tinkerd service install"
|
||||
|
||||
- name: Start remoteapp-server service
|
||||
- name: Start tinkerd service
|
||||
ansible.windows.win_shell:
|
||||
"remoteapp-server service start"
|
||||
"tinkerd service start"
|
||||
|
||||
- name: Wait Tinker api health
|
||||
ansible.windows.win_uri:
|
||||
|
|
|
@ -11,7 +11,6 @@ from common.db.models import JMSBaseModel
|
|||
from common.utils import random_string
|
||||
from assets.models import Host
|
||||
|
||||
|
||||
__all__ = ['AppletHost', 'AppletHostDeployment']
|
||||
|
||||
|
||||
|
@ -26,7 +25,7 @@ class AppletHost(Host):
|
|||
)
|
||||
applets = models.ManyToManyField(
|
||||
'Applet', verbose_name=_('Applet'),
|
||||
through='AppletPublication', through_fields=('host', 'applet'),
|
||||
through='AppletPublication', through_fields=('host', 'applet'),
|
||||
)
|
||||
LOCKING_ORG = '00000000-0000-0000-0000-000000000004'
|
||||
|
||||
|
@ -34,10 +33,10 @@ class AppletHost(Host):
|
|||
return self.name
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
if self.terminal:
|
||||
return 'online'
|
||||
return self.terminal.status
|
||||
def load(self):
|
||||
if not self.terminal:
|
||||
return 'offline'
|
||||
return self.terminal.load
|
||||
|
||||
def check_terminal_binding(self, request):
|
||||
request_terminal = getattr(request.user, 'terminal', None)
|
||||
|
@ -70,8 +69,8 @@ class AppletHost(Host):
|
|||
status_applets['published'].append(applet)
|
||||
|
||||
for status, applets in status_applets.items():
|
||||
self.publications.filter(applet__in=applets)\
|
||||
.exclude(status=status)\
|
||||
self.publications.filter(applet__in=applets) \
|
||||
.exclude(status=status) \
|
||||
.update(status=status)
|
||||
|
||||
@staticmethod
|
||||
|
@ -95,7 +94,7 @@ class AppletHost(Host):
|
|||
account = account_model(
|
||||
username=username, secret=password, name=username,
|
||||
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)
|
||||
bulk_create_with_history(accounts, account_model, batch_size=20)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import uuid
|
||||
import time
|
||||
|
||||
from django.utils import timezone
|
||||
from django.db import models
|
||||
|
@ -34,7 +35,7 @@ class TerminalStatusMixin:
|
|||
def is_alive(self):
|
||||
if not self.last_stat:
|
||||
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:
|
||||
|
|
|
@ -17,7 +17,7 @@ class AppletPublicationSerializer(serializers.ModelSerializer):
|
|||
UNPUBLISHED = 'unpublished', _('Unpublished')
|
||||
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())
|
||||
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 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.serializers import HostSerializer
|
||||
from ..models import AppletHost, AppletHostDeployment, Applet
|
||||
from .applet import AppletSerializer
|
||||
from .. import const
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AppletHostSerializer', 'AppletHostDeploymentSerializer',
|
||||
'AppletHostAccountSerializer', 'AppletHostReportSerializer'
|
||||
'AppletHostAccountSerializer', 'AppletHostAppletReportSerializer',
|
||||
'AppletHostStartupSerializer',
|
||||
]
|
||||
|
||||
|
||||
|
@ -34,14 +36,16 @@ class DeployOptionsSerializer(serializers.Serializer):
|
|||
|
||||
class AppletHostSerializer(HostSerializer):
|
||||
deploy_options = DeployOptionsSerializer(required=False, label=_("Deploy options"))
|
||||
load = LabeledChoiceField(
|
||||
read_only=True, label=_('Load status'), choices=const.ComponentLoad.choices,
|
||||
)
|
||||
|
||||
class Meta(HostSerializer.Meta):
|
||||
model = AppletHost
|
||||
fields = HostSerializer.Meta.fields + [
|
||||
'status', 'date_synced', 'deploy_options'
|
||||
'load', 'date_synced', 'deploy_options'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'status': {'read_only': True},
|
||||
'date_synced': {'read_only': True}
|
||||
}
|
||||
|
||||
|
@ -93,8 +97,14 @@ class AppletHostDeploymentSerializer(serializers.ModelSerializer):
|
|||
class AppletHostAccountSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = ['id', 'username', 'secret', 'date_updated']
|
||||
fields = ['id', 'username', 'secret', 'is_active', 'date_updated']
|
||||
|
||||
|
||||
class AppletHostReportSerializer(serializers.Serializer):
|
||||
applets = ObjectRelatedField(attrs=('id', 'name', 'version'), queryset=Applet.objects.all(), many=True)
|
||||
class AppletHostAppletReportSerializer(serializers.Serializer):
|
||||
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.dispatch import receiver
|
||||
|
||||
from orgs.utils import tmp_to_builtin_org
|
||||
from .models import Applet, AppletHost
|
||||
|
||||
|
||||
|
@ -13,7 +14,8 @@ def on_applet_host_create(sender, instance, created=False, **kwargs):
|
|||
return
|
||||
applets = Applet.objects.all()
|
||||
instance.applets.set(applets)
|
||||
instance.generate_accounts()
|
||||
with tmp_to_builtin_org(system=1):
|
||||
instance.generate_accounts()
|
||||
|
||||
|
||||
@receiver(post_save, sender=Applet)
|
||||
|
|
|
@ -59,7 +59,6 @@ class BaseTerminal(object):
|
|||
|
||||
try:
|
||||
status = status_serializer.save()
|
||||
print("Save status ok: ", status)
|
||||
time.sleep(self.interval)
|
||||
except OperationalError:
|
||||
print("Save status error, close old connections")
|
||||
|
|
|
@ -12,8 +12,8 @@ app_name = 'terminal'
|
|||
|
||||
router = BulkRouter()
|
||||
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>[a-zA-Z0-9\-]{36})?/?sessions', api.SessionViewSet, 'terminal-sessions')
|
||||
router.register(r'terminals/((?P<terminal>[^/.]{36})/)?status', api.StatusViewSet, 'terminal-status')
|
||||
router.register(r'terminals/((?P<terminal>[^/.]{36})/)?sessions', api.SessionViewSet, 'terminal-sessions')
|
||||
router.register(r'terminals', api.TerminalViewSet, 'terminal')
|
||||
router.register(r'tasks', api.TaskViewSet, 'tasks')
|
||||
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'endpoint-rules', api.EndpointRuleViewSet, 'endpoint-rule')
|
||||
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-publications', api.AppletPublicationViewSet, 'applet-publication')
|
||||
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.utils import get_current_org_id
|
||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||
from common.drf.fields import LabeledChoiceField
|
||||
from tickets.models import TicketFlow, ApprovalRule
|
||||
from tickets.const import TicketApprovalStrategy
|
||||
from tickets.const import TicketApprovalStrategy, TicketType
|
||||
|
||||
__all__ = ['TicketFlowSerializer']
|
||||
|
||||
|
||||
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_display = serializers.SerializerMethodField(label=_('Assignees display'))
|
||||
|
||||
class Meta:
|
||||
model = ApprovalRule
|
||||
fields_small = [
|
||||
'level', 'strategy', 'assignees_read_only', 'assignees_display', 'strategy_display'
|
||||
'level', 'strategy', 'assignees_read_only', 'assignees_display',
|
||||
]
|
||||
fields_m2m = ['assignees', ]
|
||||
fields = fields_small + fields_m2m
|
||||
|
@ -46,14 +49,16 @@ class TicketFlowApproveSerializer(serializers.ModelSerializer):
|
|||
|
||||
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
model = TicketFlow
|
||||
fields_mini = ['id', ]
|
||||
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'
|
||||
]
|
||||
fields = fields_small + ['rules', ]
|
||||
|
|
Loading…
Reference in New Issue