perf: change secret automation api (#9028)

Co-authored-by: feng <1304903146@qq.com>
pull/9029/head
fit2bot 2022-11-08 17:54:51 +08:00 committed by GitHub
parent e69bb9f83e
commit ce9ebd94ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 488 additions and 116 deletions

View File

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

View File

@ -0,0 +1,3 @@
from .base import *
from .change_secret import *
from .gather_accounts import *

View File

@ -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_model(tp)
task = execute_automation.delay(
pid=automation.ok, trigger=Trigger.manual, model=model
)
return Response({'task': task.id}, status=status.HTTP_201_CREATED)

View File

@ -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 = ['username', 'asset', 'reason', 'execution']
search_fields = ['username', 'reason', '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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
from .base import *
from .change_secret import *
from .gather_accounts import *

View File

@ -0,0 +1,76 @@
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, BaseAutomation, AutomationExecution
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.utils import get_logger
logger = get_logger(__file__)
__all__ = [
'BaseAutomationSerializer', 'AutomationExecutionSerializer',
'UpdateAssetSerializer', 'UpdateNodeSerializer', 'AutomationAssetsSerializer',
]
class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer):
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},
'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
fields = [
'id', 'automation', 'trigger', 'trigger_display',
'date_start', 'date_finished', 'snapshot', 'type'
]
@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)

View File

@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext as _
from rest_framework import serializers
from assets.serializers.base import AuthValidateMixin
from assets.models import ChangeSecretAutomation, ChangeSecretRecord
from assets.const import DEFAULT_PASSWORD_RULES, SecretType, SecretStrategy
from common.utils import get_logger
from .base import BaseAutomationSerializer
logger = get_logger(__file__)
__all__ = [
'ChangeSecretAutomationSerializer',
'ChangeSecretRecordSerializer',
'ChangeSecretRecordBackUpSerializer'
]
class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializer):
password_rules = serializers.DictField(default=DEFAULT_PASSWORD_RULES)
secret_strategy_display = serializers.ReadOnlyField(
source='get_secret_strategy_display', label=_('Secret strategy')
)
ssh_key_change_strategy_display = serializers.ReadOnlyField(
source='get_ssh_key_strategy_display', label=_('SSH Key strategy')
)
class Meta:
model = ChangeSecretAutomation
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields + [
'secret_strategy_display', 'ssh_key_change_strategy_display'
]
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):
asset_display = serializers.SerializerMethodField(label=_('Asset display'))
account_display = serializers.SerializerMethodField(label=_('Account display'))
is_success = serializers.SerializerMethodField(label=_('Is success'))
class Meta:
model = ChangeSecretRecord
fields = [
'id', 'asset', 'account', 'date_started', 'date_finished',
'is_success', 'error', 'execution', 'asset_display', 'account_display'
]
read_only_fields = fields
@staticmethod
def get_asset_display(instance):
return str(instance.asset)
@staticmethod
def get_account_display(instance):
return str(instance.account)
@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")

View File

@ -1,68 +1,51 @@
# -*- 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 common.drf.fields import EncryptedField from common.drf.fields import EncryptedField
from .utils import validate_password_for_ansible from assets.const import SecretType
from .utils import validate_password_for_ansible, validate_ssh_key
class AuthValidateMixin(serializers.Serializer): class AuthValidateMixin(serializers.Serializer):
password = EncryptedField( secret_type = serializers.CharField(label=_('Secret type'), max_length=16, required=True)
label=_('Password'), required=False, allow_blank=True, allow_null=True, secret = EncryptedField(
max_length=1024, validators=[validate_password_for_ansible] label=_('Secret'), required=False, max_length=16384, allow_blank=True,
) allow_null=True, write_only=True,
private_key = EncryptedField(
label=_('SSH private key'), required=False, allow_blank=True,
allow_null=True, max_length=16384
) )
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)

View File

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

View File

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

View File

@ -27,17 +27,25 @@ 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-automations')
router.register(r'automation-executions', api.AutomationExecutionViewSet, 'automation-execution')
router.register(r'change-secret-records', api.ChangeSecretRecordViewSet, 'change-secret-records')
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 +60,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