diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index 39182fbf6..a89698c66 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -16,7 +16,7 @@ from django.urls import reverse_lazy from django.core.cache import cache from django.db.models import Q -from common.mixins import IDInCacheFilterMixin +from common.mixins import IDInCacheFilterMixin, ApiMessageMixin from common.utils import get_logger, get_object_or_none from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser @@ -36,7 +36,7 @@ __all__ = [ ] -class AssetViewSet(IDInCacheFilterMixin, LabelFilter, BulkModelViewSet): +class AssetViewSet(IDInCacheFilterMixin, LabelFilter, ApiMessageMixin, BulkModelViewSet): """ API endpoint that allows Asset to be viewed or edited. """ @@ -47,6 +47,7 @@ class AssetViewSet(IDInCacheFilterMixin, LabelFilter, BulkModelViewSet): serializer_class = serializers.AssetSerializer pagination_class = LimitOffsetPagination permission_classes = (IsOrgAdminOrAppUser,) + success_message = _("%(hostname)s was %(action)s successfully") def set_assets_node(self, assets): if not isinstance(assets, list): @@ -169,8 +170,8 @@ class AssetGatewayApi(generics.RetrieveAPIView): asset = get_object_or_404(Asset, pk=asset_id) if asset.domain and \ - asset.domain.gateways.filter(protocol=asset.protocol).exists(): - gateway = random.choice(asset.domain.gateways.filter(protocol=asset.protocol)) + asset.domain.gateways.filter(protocol='ssh').exists(): + gateway = random.choice(asset.domain.gateways.filter(protocol='ssh')) serializer = serializers.GatewayWithAuthSerializer(instance=gateway) return Response(serializer.data) else: diff --git a/apps/assets/const.py b/apps/assets/const.py index 0cd74ba0d..1f93482b6 100644 --- a/apps/assets/const.py +++ b/apps/assets/const.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- # -from django.utils.translation import ugettext_lazy as _ - UPDATE_ASSETS_HARDWARE_TASKS = [ { @@ -22,6 +20,14 @@ TEST_ADMIN_USER_CONN_TASKS = [ } } ] +TEST_WINDOWS_ADMIN_USER_CONN_TASKS = [ + { + "name": "ping", + "action": { + "module": "win_ping", + } + } +] ASSET_ADMIN_CONN_CACHE_KEY = "ASSET_ADMIN_USER_CONN_{}" @@ -34,7 +40,14 @@ TEST_SYSTEM_USER_CONN_TASKS = [ } } ] - +TEST_WINDOWS_SYSTEM_USER_CONN_TASKS = [ + { + "name": "ping", + "action": { + "module": "win_ping", + } + } +] ASSET_USER_CONN_CACHE_KEY = 'ASSET_USER_CONN_{}_{}' TEST_ASSET_USER_CONN_TASKS = [ @@ -45,6 +58,14 @@ TEST_ASSET_USER_CONN_TASKS = [ } } ] +TEST_WINDOWS_ASSET_USER_CONN_TASKS = [ + { + "name": "ping", + "action": { + "module": "win_ping", + } + } +] TASK_OPTIONS = { diff --git a/apps/assets/forms/asset.py b/apps/assets/forms/asset.py index 774867a6b..e172ab8ea 100644 --- a/apps/assets/forms/asset.py +++ b/apps/assets/forms/asset.py @@ -6,21 +6,39 @@ from django.utils.translation import gettext_lazy as _ from common.utils import get_logger from orgs.mixins import OrgModelForm -from ..models import Asset, AdminUser +from ..models import Asset, AdminUser, Protocol logger = get_logger(__file__) -__all__ = ['AssetCreateForm', 'AssetUpdateForm', 'AssetBulkUpdateForm'] +__all__ = [ + 'AssetCreateForm', 'AssetUpdateForm', 'AssetBulkUpdateForm', + 'ProtocolForm' +] + + +class ProtocolForm(forms.ModelForm): + class Meta: + model = Protocol + fields = ['name', 'port'] + widgets = { + 'name': forms.Select(attrs={ + 'class': 'form-control protocol-name' + }), + 'port': forms.TextInput(attrs={ + 'class': 'form-control protocol-port' + }), + } class AssetCreateForm(OrgModelForm): + PROTOCOL_CHOICES = Protocol.PROTOCOL_CHOICES + class Meta: model = Asset fields = [ - 'hostname', 'ip', 'public_ip', 'port', 'comment', + 'hostname', 'ip', 'public_ip', 'protocols', 'comment', 'nodes', 'is_active', 'admin_user', 'labels', 'platform', - 'domain', 'protocol', - + 'domain', ] widgets = { 'nodes': forms.SelectMultiple(attrs={ @@ -32,7 +50,6 @@ class AssetCreateForm(OrgModelForm): 'labels': forms.SelectMultiple(attrs={ 'class': 'select2', 'data-placeholder': _('Label') }), - 'port': forms.TextInput(), 'domain': forms.Select(attrs={ 'class': 'select2', 'data-placeholder': _('Domain') }), @@ -54,9 +71,9 @@ class AssetUpdateForm(OrgModelForm): class Meta: model = Asset fields = [ - 'hostname', 'ip', 'port', 'nodes', 'is_active', 'platform', + 'hostname', 'ip', 'protocols', 'nodes', 'is_active', 'platform', 'public_ip', 'number', 'comment', 'admin_user', 'labels', - 'domain', 'protocol', + 'domain', ] widgets = { 'nodes': forms.SelectMultiple(attrs={ @@ -68,7 +85,6 @@ class AssetUpdateForm(OrgModelForm): 'labels': forms.SelectMultiple(attrs={ 'class': 'select2', 'data-placeholder': _('Label') }), - 'port': forms.TextInput(), 'domain': forms.Select(attrs={ 'class': 'select2', 'data-placeholder': _('Domain') }), @@ -101,8 +117,8 @@ class AssetBulkUpdateForm(OrgModelForm): class Meta: model = Asset fields = [ - 'assets', 'port', 'admin_user', 'labels', 'platform', - 'protocol', 'domain', + 'assets', 'admin_user', 'labels', 'platform', + 'domain', ] widgets = { 'labels': forms.SelectMultiple( diff --git a/apps/assets/forms/user.py b/apps/assets/forms/user.py index e832ab158..5c7d3c770 100644 --- a/apps/assets/forms/user.py +++ b/apps/assets/forms/user.py @@ -100,16 +100,18 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm): private_key, public_key = super().gen_keys() if login_mode == SystemUser.LOGIN_MANUAL or \ - protocol in [SystemUser.PROTOCOL_RDP, - SystemUser.PROTOCOL_TELNET, + protocol in [SystemUser.PROTOCOL_TELNET, SystemUser.PROTOCOL_VNC]: system_user.auto_push = 0 - auto_generate_key = False system_user.save() + auto_generate_key = False if auto_generate_key: logger.info('Auto generate key and set system user auth') - system_user.auto_gen_auth() + if protocol == SystemUser.PROTOCOL_SSH: + system_user.auto_gen_auth() + elif protocol == SystemUser.PROTOCOL_RDP: + system_user.auto_gen_auth_password() else: system_user.set_auth(password=password, private_key=private_key, public_key=public_key) diff --git a/apps/assets/migrations/0027_auto_20190521_1703.py b/apps/assets/migrations/0027_auto_20190521_1703.py index da38b1791..91446019f 100644 --- a/apps/assets/migrations/0027_auto_20190521_1703.py +++ b/apps/assets/migrations/0027_auto_20190521_1703.py @@ -15,4 +15,9 @@ class Migration(migrations.Migration): name='ip', field=models.CharField(db_index=True, max_length=128, verbose_name='IP'), ), + migrations.AlterField( + model_name='asset', + name='public_ip', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Public IP'), + ), ] diff --git a/apps/assets/migrations/0028_protocol.py b/apps/assets/migrations/0028_protocol.py new file mode 100644 index 000000000..230c43773 --- /dev/null +++ b/apps/assets/migrations/0028_protocol.py @@ -0,0 +1,29 @@ +# Generated by Django 2.1.7 on 2019-05-22 02:58 + +import django.core.validators +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0027_auto_20190521_1703'), + ] + + operations = [ + migrations.CreateModel( + name='Protocol', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)'), ('vnc', 'vnc')], default='ssh', max_length=16, verbose_name='Name')), + ('port', models.IntegerField(default=22, validators=[django.core.validators.MaxValueValidator(65535), django.core.validators.MinValueValidator(1)], verbose_name='Port')), + ], + ), + migrations.AddField( + model_name='asset', + name='protocols', + field=models.ManyToManyField(to='assets.Protocol', + verbose_name='Protocol'), + ), + ] diff --git a/apps/assets/migrations/0029_auto_20190522_1114.py b/apps/assets/migrations/0029_auto_20190522_1114.py new file mode 100644 index 000000000..e1d20bb5f --- /dev/null +++ b/apps/assets/migrations/0029_auto_20190522_1114.py @@ -0,0 +1,22 @@ +# Generated by Django 2.1.7 on 2019-05-22 03:14 + +from django.db import migrations + + +def migrate_assets_protocol(apps, schema_editor): + asset_model = apps.get_model("assets", "Asset") + db_alias = schema_editor.connection.alias + assets = asset_model.objects.using(db_alias).all() + for asset in assets: + asset.protocols.create(name=asset.protocol, port=asset.port) + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0028_protocol'), + ] + + operations = [ + migrations.RunPython(migrate_assets_protocol), + ] diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index c82c66a69..b87a18796 100644 --- a/apps/assets/models/__init__.py +++ b/apps/assets/models/__init__.py @@ -8,4 +8,3 @@ from .asset import * from .cmd_filter import * from .utils import * from .authbook import * -from applications.models.remote_app import * diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index 47a34243d..9512d26ba 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -12,11 +12,12 @@ from django.db import models from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from django.core.cache import cache +from django.core.validators import MinValueValidator, MaxValueValidator from .user import AdminUser, SystemUser from orgs.mixins import OrgModelMixin, OrgManager -__all__ = ['Asset'] +__all__ = ['Asset', 'Protocol'] logger = logging.getLogger(__name__) @@ -47,6 +48,35 @@ class AssetQuerySet(models.QuerySet): return self.active() +class AssetManager(OrgManager): + def get_queryset(self): + queryset = super().get_queryset().prefetch_related("nodes", "protocols") + return queryset + + +class Protocol(models.Model): + PROTOCOL_SSH = 'ssh' + PROTOCOL_RDP = 'rdp' + PROTOCOL_TELNET = 'telnet' + PROTOCOL_VNC = 'vnc' + PROTOCOL_CHOICES = ( + (PROTOCOL_SSH, 'ssh'), + (PROTOCOL_RDP, 'rdp'), + (PROTOCOL_TELNET, 'telnet (beta)'), + (PROTOCOL_VNC, 'vnc'), + ) + PORT_VALIDATORS = [MaxValueValidator(65535), MinValueValidator(1)] + + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + name = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, + default=PROTOCOL_SSH, verbose_name=_("Name")) + port = models.IntegerField(default=22, verbose_name=_("Port"), + validators=PORT_VALIDATORS) + + def __str__(self): + return "{}:{}".format(self.name, self.port) + + class Asset(OrgModelMixin): # Important PLATFORM_CHOICES = ( @@ -59,22 +89,15 @@ class Asset(OrgModelMixin): ('Other', 'Other'), ) - PROTOCOL_SSH = 'ssh' - PROTOCOL_RDP = 'rdp' - PROTOCOL_TELNET = 'telnet' - PROTOCOL_VNC = 'vnc' - PROTOCOL_CHOICES = ( - (PROTOCOL_SSH, 'ssh'), - (PROTOCOL_RDP, 'rdp'), - (PROTOCOL_TELNET, 'telnet (beta)'), - (PROTOCOL_VNC, 'vnc'), - ) - id = models.UUIDField(default=uuid.uuid4, primary_key=True) ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True) hostname = models.CharField(max_length=128, verbose_name=_('Hostname')) - protocol = models.CharField(max_length=128, default=PROTOCOL_SSH, choices=PROTOCOL_CHOICES, verbose_name=_('Protocol')) + protocol = models.CharField(max_length=128, default=Protocol.PROTOCOL_SSH, + choices=Protocol.PROTOCOL_CHOICES, + verbose_name=_('Protocol')) port = models.IntegerField(default=22, verbose_name=_('Port')) + + protocols = models.ManyToManyField('Protocol', verbose_name=_("Protocol")) platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform')) domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), on_delete=models.SET_NULL) nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes")) @@ -84,7 +107,7 @@ class Asset(OrgModelMixin): admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, null=True, verbose_name=_("Admin user")) # Some information - public_ip = models.GenericIPAddressField(max_length=32, blank=True, null=True, verbose_name=_('Public IP')) + public_ip = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Public IP')) number = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Asset number')) # Collect @@ -110,7 +133,7 @@ class Asset(OrgModelMixin): date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) - objects = OrgManager.from_queryset(AssetQuerySet)() + objects = AssetManager.from_queryset(AssetQuerySet)() CONNECTIVITY_CACHE_KEY = '_JMS_ASSET_CONNECTIVITY_{}' UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3) CONNECTIVITY_CHOICES = ( @@ -131,19 +154,53 @@ class Asset(OrgModelMixin): return True, '' return False, warning - def support_ansible(self): - if self.platform in ("Windows", "Windows2016", "Other"): - return False - if self.protocol != 'ssh': - return False - return True + @property + def protocols_name(self): + names = [] + for protocol in self.protocols.all(): + names.append(protocol.name) + return names - def is_unixlike(self): - if self.platform not in ("Windows", "Windows2016"): + def has_protocol(self, name): + return name in self.protocols_name + + def get_protocol_by_name(self, name): + for i in self.protocols.all(): + if i.name.lower() == name.lower(): + return i + return None + + @property + def protocol_ssh(self): + return self.get_protocol_by_name("ssh") + + @property + def protocol_rdp(self): + return self.get_protocol_by_name("rdp") + + @property + def ssh_port(self): + if self.protocol_ssh: + port = self.protocol_ssh.port + else: + port = 22 + return port + + def is_windows(self): + if self.platform in ("Windows", "Windows2016"): return True else: return False + def is_unixlike(self): + if self.platform not in ("Windows", "Windows2016", "Other"): + return True + else: + return False + + def is_support_ansible(self): + return self.has_protocol('ssh') and self.platform not in ("Other",) + def get_nodes(self): from .node import Node nodes = self.nodes.all() or [Node.root()] @@ -172,6 +229,15 @@ class Asset(OrgModelMixin): filter_arg |= Q(Q(org_id__isnull=True) | Q(org_id=''), hostname__in=hosts) return Asset.objects.filter(filter_arg) + @property + def cpu_info(self): + info = "" + if self.cpu_model: + info += self.cpu_model + if self.cpu_count and self.cpu_cores: + info += "{}*{}".format(self.cpu_count, self.cpu_cores) + return info + @property def hardware_info(self): if self.cpu_count: @@ -196,14 +262,16 @@ class Asset(OrgModelMixin): cache.set(key, value, 3600*2) def get_auth_info(self): - if self.admin_user: - self.admin_user.load_specific_asset_auth(self) - return { - 'username': self.admin_user.username, - 'password': self.admin_user.password, - 'private_key': self.admin_user.private_key_file, - 'become': self.admin_user.become_info, - } + if not self.admin_user: + return {} + + self.admin_user.load_specific_asset_auth(self) + info = { + 'username': self.admin_user.username, + 'password': self.admin_user.password, + 'private_key': self.admin_user.private_key_file, + } + return info def as_node(self): from .node import Node @@ -215,35 +283,6 @@ class Asset(OrgModelMixin): fake_node.is_node = False return fake_node - def to_json(self): - info = { - 'id': self.id, - 'hostname': self.hostname, - 'ip': self.ip, - 'port': self.port, - } - if self.domain and self.domain.gateway_set.all(): - info["gateways"] = [d.id for d in self.domain.gateway_set.all()] - return info - - def _to_secret_json(self): - """ - Ansible use it create inventory - Todo: May be move to ops implements it - """ - data = self.to_json() - if self.admin_user: - self.admin_user.load_specific_asset_auth(self) - admin_user = self.admin_user - data.update({ - 'username': admin_user.username, - 'password': admin_user.password, - 'private_key': admin_user.private_key_file, - 'become': admin_user.become_info, - 'groups': [node.value for node in self.nodes.all()], - }) - return data - def as_tree_node(self, parent_node): from common.tree import TreeNode icon_skin = 'file' @@ -265,9 +304,11 @@ class Asset(OrgModelMixin): 'id': self.id, 'hostname': self.hostname, 'ip': self.ip, - 'port': self.port, + 'protocols': [ + {"name": p.name, "port": p.port} + for p in self.protocols.all() + ], 'platform': self.platform, - 'protocol': self.protocol, } } } @@ -291,10 +332,10 @@ class Asset(OrgModelMixin): asset = cls(ip='.'.join(ip), hostname=forgery_py.internet.user_name(True), admin_user=choice(AdminUser.objects.all()), - port=22, created_by='Fake') try: asset.save() + asset.protocols.create(name="ssh", port=22) if nodes and len(nodes) > 3: _nodes = random.sample(nodes, 3) else: diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 30d8ae0f5..40336fd00 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -158,6 +158,10 @@ class AssetUser(OrgModelMixin): private_key=private_key, public_key=public_key) + def auto_gen_auth_password(self): + password = str(uuid.uuid4()) + self.set_auth(password=password) + def _to_secret_json(self): """Push system user use it""" return { diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 1e661d631..ecd9ae429 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -201,7 +201,7 @@ class SystemUser(AssetUser): return self.get_login_mode_display() def is_need_push(self): - if self.auto_push and self.protocol == self.PROTOCOL_SSH: + if self.auto_push and self.protocol in [self.PROTOCOL_SSH, self.PROTOCOL_RDP]: return True else: return False diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index 3e1cf39bb..9ac3ba725 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- # from rest_framework import serializers +from rest_framework.validators import ValidationError from django.utils.translation import ugettext_lazy as _ from orgs.mixins import OrgResourceSerializerMixin from common.mixins import BulkSerializerMixin from common.serializers import AdaptedBulkListSerializer -from ..models import Asset +from ..models import Asset, Protocol from .system_user import AssetSystemUserSerializer __all__ = [ @@ -16,25 +17,33 @@ __all__ = [ ] -class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer, OrgResourceSerializerMixin): +class ProtocolSerializer(serializers.ModelSerializer): + class Meta: + model = Protocol + fields = ["name", "port"] + + +class AssetSerializer(BulkSerializerMixin, OrgResourceSerializerMixin, + serializers.ModelSerializer): + protocols = ProtocolSerializer(many=True) + """ 资产的数据结构 """ class Meta: model = Asset list_serializer_class = AdaptedBulkListSerializer - # validators = [] # 解决批量导入时unique_together字段校验失败 fields = [ 'id', 'org_id', 'org_name', 'ip', 'hostname', 'protocol', 'port', - 'platform', 'is_active', 'public_ip', 'domain', 'admin_user', - 'nodes', 'labels', 'number', 'vendor', 'model', 'sn', + 'protocols', 'platform', 'is_active', 'public_ip', 'domain', + 'admin_user', 'nodes', 'labels', 'number', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count', 'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info', 'os', 'os_version', 'os_arch', 'hostname_raw', 'comment', 'created_by', 'date_created', 'hardware_info', 'connectivity' ] read_only_fields = ( - 'number', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count', + 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count', 'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info', 'os', 'os_version', 'os_arch', 'hostname_raw', 'created_by', 'date_created', @@ -43,7 +52,6 @@ class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer, OrgResou 'hardware_info': {'label': _('Hardware info')}, 'connectivity': {'label': _('Connectivity')}, 'org_name': {'label': _('Org name')} - } @classmethod @@ -53,18 +61,64 @@ class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer, OrgResou .select_related('admin_user') return queryset - def get_field_names(self, declared_fields, info): - fields = super().get_field_names(declared_fields, info) - fields.extend([ - 'hardware_info', 'connectivity', 'org_name' - ]) - return fields + @staticmethod + def validate_protocols(attr): + protocols_name = [i.get("name", "ssh") for i in attr] + errors = [{} for i in protocols_name] + for i, name in enumerate(protocols_name): + if name in protocols_name[:i]: + errors[i] = {"name": _("Protocol duplicate: {}").format(name)} + if any(errors): + raise ValidationError(errors) + return attr + + def create(self, validated_data): + protocols_data = validated_data.pop("protocols") + + # 兼容老的api + protocol = validated_data.get("protocol") + port = validated_data.get("port") + if not protocols_data and protocol and port: + protocols_data = [{"name": protocol, "port": port}] + + if not protocol and not port and protocols_data: + validated_data["protocol"] = protocols_data[0]["name"] + validated_data["port"] = protocols_data[0]["port"] + + protocols_serializer = ProtocolSerializer(data=protocols_data, many=True) + protocols_serializer.is_valid(raise_exception=True) + protocols = protocols_serializer.save() + instance = super().create(validated_data) + instance.protocols.set(protocols) + return instance + + def update(self, instance, validated_data): + protocols_data = validated_data.pop("protocols") + + # 兼容老的api + protocol = validated_data.get("protocol") + port = validated_data.get("port") + if not protocols_data and protocol and port: + protocols_data = [{"name": protocol, "port": port}] + + if not protocol and not port and protocols_data: + validated_data["protocol"] = protocols_data[0]["name"] + validated_data["port"] = protocols_data[0]["port"] + + protocols_serializer = ProtocolSerializer(data=protocols_data, many=True) + protocols_serializer.is_valid(raise_exception=True) + protocols = protocols_serializer.save() + + instance = super().update(instance, validated_data) + instance.protocols.all().delete() + instance.protocols.set(protocols) + return instance class AssetAsNodeSerializer(serializers.ModelSerializer): class Meta: model = Asset - fields = ['id', 'hostname', 'ip', 'port', 'platform', 'protocol'] + fields = ['id', 'hostname', 'ip', 'platform', 'protocols'] class AssetGrantedSerializer(serializers.ModelSerializer): @@ -78,9 +132,9 @@ class AssetGrantedSerializer(serializers.ModelSerializer): class Meta: model = Asset fields = ( - "id", "hostname", "ip", "port", "system_users_granted", + "id", "hostname", "ip", "system_users_granted", "is_active", "system_users_join", "os", 'domain', - "platform", "comment", "protocol", "org_id", "org_name", + "platform", "comment", "protocols", "org_id", "org_name", ) @staticmethod diff --git a/apps/assets/signals_handler.py b/apps/assets/signals_handler.py index 4afb97e75..23e923a3e 100644 --- a/apps/assets/signals_handler.py +++ b/apps/assets/signals_handler.py @@ -5,6 +5,7 @@ from django.db.models.signals import post_save, m2m_changed, post_delete from django.dispatch import receiver from common.utils import get_logger +from common.decorator import on_transaction_commit from .models import Asset, SystemUser, Node, AuthBook from .tasks import ( update_assets_hardware_info_util, @@ -32,9 +33,12 @@ def set_asset_root_node(asset): @receiver(post_save, sender=Asset, dispatch_uid="my_unique_identifier") +@on_transaction_commit def on_asset_created_or_update(sender, instance=None, created=False, **kwargs): if created: logger.info("Asset `{}` create signal received".format(instance)) + + # 获取资产硬件信息 update_asset_hardware_info_on_created(instance) test_asset_conn_on_created(instance) diff --git a/apps/assets/tasks.py b/apps/assets/tasks.py index 172175f38..61f0d29ec 100644 --- a/apps/assets/tasks.py +++ b/apps/assets/tasks.py @@ -3,6 +3,7 @@ import json import re import os +from collections import defaultdict from celery import shared_task from django.utils.translation import ugettext as _ from django.core.cache import cache @@ -31,7 +32,7 @@ def check_asset_can_run_ansible(asset): msg = _("Asset has been disabled, skipped: {}").format(asset) logger.info(msg) return False - if not asset.support_ansible(): + if not asset.is_support_ansible(): msg = _("Asset may not be support ansible, skipped: {}").format(asset) logger.info(msg) return False @@ -45,10 +46,21 @@ def clean_hosts(assets): continue clean_assets.append(asset) if not clean_assets: - print(_("No assets matched, stop task")) + logger.info(_("No assets matched, stop task")) return clean_assets +def clean_hosts_by_protocol(system_user, assets): + hosts = [ + asset for asset in assets + if asset.has_protocol(system_user.protocol) + ] + if not hosts: + msg = _("No assets matched related system user protocol, stop task") + logger.info(msg) + return hosts + + @shared_task def set_assets_hardware_info(assets, result, **kwargs): """ @@ -96,7 +108,7 @@ def set_assets_hardware_info(assets, result, **kwargs): ___disk_total = '%s %s' % sum_capacity(disk_info.values()) ___disk_info = json.dumps(disk_info) - ___platform = info.get('ansible_system', 'Unknown') + # ___platform = info.get('ansible_system', 'Unknown') ___os = info.get('ansible_distribution', 'Unknown') ___os_version = info.get('ansible_distribution_version', 'Unknown') ___os_arch = info.get('ansible_architecture', 'Unknown') @@ -163,25 +175,53 @@ def test_asset_connectivity_util(assets, task_name=None): if task_name is None: task_name = _("Test assets connectivity") + hosts = clean_hosts(assets) if not hosts: return {} - tasks = const.TEST_ADMIN_USER_CONN_TASKS - created_by = assets[0].org_id - task, created = update_or_create_ansible_task( - task_name=task_name, hosts=hosts, tasks=tasks, pattern='all', - options=const.TASK_OPTIONS, run_as_admin=True, created_by=created_by, + + hosts_category = { + 'linux': { + 'hosts': [], + 'tasks': const.TEST_ADMIN_USER_CONN_TASKS + }, + 'windows': { + 'hosts': [], + 'tasks': const.TEST_WINDOWS_ADMIN_USER_CONN_TASKS + } + } + for host in hosts: + hosts_list = hosts_category['windows']['hosts'] if host.is_windows() \ + else hosts_category['linux']['hosts'] + hosts_list.append(host) + + results_summary = dict( + contacted=defaultdict(dict), dark=defaultdict(dict), success=True ) - result = task.run() - summary = result[1] + created_by = assets[0].org_id + for _, value in hosts_category.items(): + if not value['hosts']: + continue + task, created = update_or_create_ansible_task( + task_name=task_name, hosts=value['hosts'], tasks=value['tasks'], + pattern='all', options=const.TASK_OPTIONS, run_as_admin=True, + created_by=created_by, + ) + result = task.run() + summary = result[1] + results_summary['success'] &= summary['success'] + results_summary['contacted'].update(summary['contacted']) + results_summary['dark'].update(summary['dark']) + for asset in assets: - if asset.hostname in summary.get('dark', {}): + if asset.hostname in results_summary.get('dark', {}): asset.connectivity = asset.UNREACHABLE - elif asset.hostname in summary.get('contacted', []): + elif asset.hostname in results_summary.get('contacted', []): asset.connectivity = asset.REACHABLE else: asset.connectivity = asset.UNKNOWN - return summary + + return results_summary @shared_task @@ -243,8 +283,7 @@ def test_admin_user_connectivity_manual(admin_user): ## System user connective ## @shared_task -def set_system_user_connectivity_info(system_user, result): - summary = result[1] +def set_system_user_connectivity_info(system_user, summary): system_user.connectivity = summary @@ -258,18 +297,50 @@ def test_system_user_connectivity_util(system_user, assets, task_name): :return: """ from ops.utils import update_or_create_ansible_task - tasks = const.TEST_SYSTEM_USER_CONN_TASKS + hosts = clean_hosts(assets) if not hosts: return {} - task, created = update_or_create_ansible_task( - task_name, hosts=hosts, tasks=tasks, pattern='all', - options=const.TASK_OPTIONS, - run_as=system_user.username, created_by=system_user.org_id, + + hosts = clean_hosts_by_protocol(system_user, hosts) + if not hosts: + return {} + + hosts_category = { + 'linux': { + 'hosts': [], + 'tasks': const.TEST_SYSTEM_USER_CONN_TASKS + }, + 'windows': { + 'hosts': [], + 'tasks': const.TEST_WINDOWS_SYSTEM_USER_CONN_TASKS + } + } + for host in hosts: + hosts_list = hosts_category['windows']['hosts'] if host.is_windows() \ + else hosts_category['linux']['hosts'] + hosts_list.append(host) + + results_summary = dict( + contacted=defaultdict(dict), dark=defaultdict(dict), success=True ) - result = task.run() - set_system_user_connectivity_info(system_user, result) - return result + for _, value in hosts_category.items(): + if not value['hosts']: + continue + task, created = update_or_create_ansible_task( + task_name=task_name, hosts=value['hosts'], tasks=value['tasks'], + pattern='all', options=const.TASK_OPTIONS, + run_as=system_user.username, + created_by=system_user.org_id, + ) + result = task.run() + summary = result[1] + results_summary['success'] &= summary['success'] + results_summary['contacted'].update(summary['contacted']) + results_summary['dark'].update(summary['dark']) + + set_system_user_connectivity_info(system_user, results_summary) + return results_summary @shared_task @@ -301,11 +372,7 @@ def test_system_user_connectivity_period(): #### Push system user tasks #### -def get_push_system_user_tasks(system_user): - # Set root as system user is dangerous - if system_user.username == "root": - return [] - +def get_push_linux_system_user_tasks(system_user): tasks = [] if system_user.password: tasks.append({ @@ -320,12 +387,12 @@ def get_push_system_user_tasks(system_user): }) tasks.extend([ { - 'name': 'Check home dir exists', - 'action': { - 'module': 'stat', - 'args': 'path=/home/{}'.format(system_user.username) - }, - 'register': 'home_existed' + 'name': 'Check home dir exists', + 'action': { + 'module': 'stat', + 'args': 'path=/home/{}'.format(system_user.username) + }, + 'register': 'home_existed' }, { 'name': "Set home dir permission", @@ -364,6 +431,46 @@ def get_push_system_user_tasks(system_user): ) } }) + + return tasks + + +def get_push_windows_system_user_tasks(system_user): + tasks = [] + if system_user.password: + tasks.append({ + 'name': 'Add user {}'.format(system_user.username), + 'action': { + 'module': 'win_user', + 'args': 'fullname={} ' + 'name={} ' + 'password={} ' + 'state=present ' + 'update_password=always' + 'password_expired=no ' + 'password_never_expires=yes ' + 'groups="Users,Remote Desktop Users" ' + 'groups_action=add ' + ''.format(system_user.name, + system_user.username, + system_user.password), + } + }) + return tasks + + +def get_push_system_user_tasks(host, system_user): + if host.is_unixlike(): + tasks = get_push_linux_system_user_tasks(system_user) + elif host.is_windows(): + tasks = get_push_windows_system_user_tasks(system_user) + else: + msg = _( + "The asset {} system platform {} does not " + "support run Ansible tasks".format(host.hostname, host.platform) + ) + logger.info(msg) + tasks = [] return tasks @@ -372,16 +479,29 @@ def push_system_user_util(system_user, assets, task_name): from ops.utils import update_or_create_ansible_task if not system_user.is_need_push(): msg = _("Push system user task skip, auto push not enable or " - "protocol is not ssh: {}").format(system_user.name) + "protocol is not ssh or rdp: {}").format(system_user.name) logger.info(msg) - return + return {} + + # Set root as system user is dangerous + if system_user.username.lower() in ["root", "administrator"]: + msg = _("For security, do not push user {}".format(system_user.username)) + logger.info(msg) + return {} hosts = clean_hosts(assets) if not hosts: return {} + + hosts = clean_hosts_by_protocol(system_user, hosts) + if not hosts: + return {} + for host in hosts: system_user.load_specific_asset_auth(host) - tasks = get_push_system_user_tasks(system_user) + tasks = get_push_system_user_tasks(host, system_user) + if not tasks: + continue task, created = update_or_create_ansible_task( task_name=task_name, hosts=[host], tasks=tasks, pattern='all', options=const.TASK_OPTIONS, run_as_admin=True, @@ -423,6 +543,23 @@ def test_admin_user_connectability_period(): pass +#### Test Asset user connectivity task #### + +def get_test_asset_user_connectivity_tasks(asset): + if asset.is_unixlike(): + tasks = const.TEST_ASSET_USER_CONN_TASKS + elif asset.is_windows(): + tasks = const.TEST_WINDOWS_ASSET_USER_CONN_TASKS + else: + msg = _( + "The asset {} system platform {} does not " + "support run Ansible tasks".format(asset.hostname, asset.platform) + ) + logger.info(msg) + tasks = [] + return tasks + + @shared_task def set_asset_user_connectivity_info(asset_user, result): summary = result[1] @@ -437,10 +574,14 @@ def test_asset_user_connectivity_util(asset_user, task_name): :return: """ from ops.utils import update_or_create_ansible_task - tasks = const.TEST_ASSET_USER_CONN_TASKS + if not check_asset_can_run_ansible(asset_user.asset): return + tasks = get_test_asset_user_connectivity_tasks(asset_user.asset) + if not tasks: + return + task, created = update_or_create_ansible_task( task_name, hosts=[asset_user.asset], tasks=tasks, pattern='all', options=const.TASK_OPTIONS, diff --git a/apps/assets/templates/assets/_system_user.html b/apps/assets/templates/assets/_system_user.html index 3c9cf1221..71c3420d0 100644 --- a/apps/assets/templates/assets/_system_user.html +++ b/apps/assets/templates/assets/_system_user.html @@ -95,43 +95,99 @@ var auto_push_id = '#' + '{{ form.auto_push.id_for_label }}'; var sudo_id = '#' + '{{ form.sudo.id_for_label }}'; var shell_id = '#' + '{{ form.shell.id_for_label }}'; -var need_change_field = [ - auto_generate_key, private_key_id, auto_push_id, sudo_id, shell_id -]; -var need_change_field_login_mode = [ - auto_generate_key, private_key_id, auto_push_id, password_id -]; - -function protocolChange() { +function autoLoginModeProtocol() { + // 协议+自动登录模式字段控制 + $('#auth_title_id').removeClass('hidden'); var protocol = $(protocol_id + " option:selected").text(); - if (protocol === 'rdp' || protocol === 'vnc') { - $('.auth-fields').removeClass('hidden'); + if (protocol === 'rdp') { + authFieldsDisplay(); + $(auto_generate_key).closest('.form-group').removeClass('hidden'); + $(private_key_id).closest('.form-group').addClass('hidden'); + $(password_id).closest('.form-group').removeClass('hidden'); + $(auto_push_id).closest('.form-group').removeClass('hidden'); $('#command-filter-block').addClass('hidden'); - $.each(need_change_field, function (index, value) { - $(value).closest('.form-group').addClass('hidden') - }); + $(sudo_id).closest('.form-group').addClass('hidden'); + $(shell_id).closest('.form-group').addClass('hidden'); + } + else if (protocol === 'vnc') { + $('.auth-fields').removeClass('hidden'); + $(auto_generate_key).closest('.form-group').addClass('hidden'); + $(private_key_id).closest('.form-group').addClass('hidden'); + $(password_id).closest('.form-group').removeClass('hidden'); + $(auto_push_id).closest('.form-group').addClass('hidden'); + $('#command-filter-block').addClass('hidden'); + $(sudo_id).closest('.form-group').addClass('hidden'); + $(shell_id).closest('.form-group').addClass('hidden'); } else if (protocol === 'telnet (beta)') { $('.auth-fields').removeClass('hidden'); + $(auto_generate_key).closest('.form-group').addClass('hidden'); + $(private_key_id).closest('.form-group').addClass('hidden'); + $(password_id).closest('.form-group').removeClass('hidden'); + $(auto_push_id).closest('.form-group').addClass('hidden'); $('#command-filter-block').removeClass('hidden'); - $.each(need_change_field, function (index, value) { - $(value).closest('.form-group').addClass('hidden') - }); + $(sudo_id).closest('.form-group').addClass('hidden'); + $(shell_id).closest('.form-group').addClass('hidden'); } else { - if($(login_mode_id).val() === 'manual'){ - $(sudo_id).closest('.form-group').removeClass('hidden'); - $(shell_id).closest('.form-group').removeClass('hidden'); - return - } authFieldsDisplay(); + $(auto_generate_key).closest('.form-group').removeClass('hidden'); + $(private_key_id).closest('.form-group').removeClass('hidden'); + $(password_id).closest('.form-group').removeClass('hidden'); + $(auto_push_id).closest('.form-group').removeClass('hidden'); $('#command-filter-block').removeClass('hidden'); - $.each(need_change_field, function (index, value) { - $(value).closest('.form-group').removeClass('hidden') - }); + $(sudo_id).closest('.form-group').removeClass('hidden'); + $(shell_id).closest('.form-group').removeClass('hidden'); } } +function manualLoginModeProtocol() { + // 协议+手动登录模式字段控制 + $('#auth_title_id').addClass('hidden'); + var protocol = $(protocol_id + " option:selected").text(); + if (protocol === 'rdp') { + $('.auth-fields').addClass('hidden'); + $(auto_generate_key).closest('.form-group').addClass('hidden'); + $(password_id).closest('.form-group').addClass('hidden'); + $(private_key_id).closest('.form-group').addClass('hidden'); + $(auto_push_id).closest('.form-group').addClass('hidden'); + $('#command-filter-block').addClass('hidden'); + $(sudo_id).closest('.form-group').addClass('hidden'); + $(shell_id).closest('.form-group').addClass('hidden'); + } + else if (protocol === 'vnc') { + $('.auth-fields').addClass('hidden'); + $(auto_generate_key).closest('.form-group').addClass('hidden'); + $(password_id).closest('.form-group').addClass('hidden'); + $(private_key_id).closest('.form-group').addClass('hidden'); + $(auto_push_id).closest('.form-group').addClass('hidden'); + $('#command-filter-block').addClass('hidden'); + $(sudo_id).closest('.form-group').addClass('hidden'); + $(shell_id).closest('.form-group').addClass('hidden'); + } + else if (protocol === 'telnet (beta)') { + $('.auth-fields').addClass('hidden'); + $(auto_generate_key).closest('.form-group').addClass('hidden'); + $(password_id).closest('.form-group').addClass('hidden'); + $(private_key_id).closest('.form-group').addClass('hidden'); + $(auto_push_id).closest('.form-group').addClass('hidden'); + $('#command-filter-block').removeClass('hidden'); + $(sudo_id).closest('.form-group').addClass('hidden'); + $(shell_id).closest('.form-group').addClass('hidden'); + } + else { + $('.auth-fields').addClass('hidden'); + $(auto_generate_key).closest('.form-group').addClass('hidden'); + $(password_id).closest('.form-group').addClass('hidden'); + $(private_key_id).closest('.form-group').addClass('hidden'); + $(auto_push_id).closest('.form-group').addClass('hidden'); + $('#command-filter-block').removeClass('hidden'); + $(sudo_id).closest('.form-group').removeClass('hidden'); + $(shell_id).closest('.form-group').removeClass('hidden'); + } + +} + function authFieldsDisplay() { if ($(auto_generate_key).prop('checked')) { $('.auth-fields').addClass('hidden'); @@ -139,34 +195,29 @@ function authFieldsDisplay() { $('.auth-fields').removeClass('hidden'); } } -function loginModeChange(){ - if ($(login_mode_id).val() === 'manual'){ - $('#auth_title_id').addClass('hidden'); - $.each(need_change_field_login_mode, function(index, value){ - $(value).closest('.form-group').addClass('hidden') - }) +function fieldDisplay(){ + var login_mode = $(login_mode_id).val(); + if (login_mode === 'manual'){ + manualLoginModeProtocol(); } - else if($(login_mode_id).val() === 'auto'){ - $('#auth_title_id').removeClass('hidden'); - $(password_id).closest('.form-group').removeClass('hidden') - protocolChange(); + else if(login_mode === 'auto'){ + autoLoginModeProtocol(); } } $(document).ready(function () { $('.select2').select2(); authFieldsDisplay(); - protocolChange(); - loginModeChange(); -}) -.on('change', protocol_id, function(){ - protocolChange(); + fieldDisplay(); }) .on('change', auto_generate_key, function(){ authFieldsDisplay(); }) .on('change', login_mode_id, function(){ - loginModeChange(); + fieldDisplay(); +}) +.on('change', protocol_id, function(){ + fieldDisplay(); }) diff --git a/apps/assets/templates/assets/asset_asset_user_list.html b/apps/assets/templates/assets/asset_asset_user_list.html index ef192d790..b5a99efea 100644 --- a/apps/assets/templates/assets/asset_asset_user_list.html +++ b/apps/assets/templates/assets/asset_asset_user_list.html @@ -62,7 +62,7 @@
- {% if asset.protocol == 'ssh' %} + {% if asset.is_support_ansible %} - - - - - - + + @@ -94,7 +95,7 @@ - + @@ -166,7 +167,7 @@ - {% if asset.protocol == 'ssh' %} + {% if asset.is_support_ansible %}
{% trans 'Test connective' %}: @@ -118,7 +118,7 @@ function initAssetUserTable() { var view_btn = ' {% trans "View auth" %}'.replace("DEFAULT_USERNAME", cellData); var test_btn = ' {% trans "Test" %}'.replace("DEFAULT_USERNAME", cellData); btn += view_btn; - {% if asset.protocol == 'ssh' %} + {% if asset.is_support_ansible %} btn += test_btn; {% endif %} $(td).html(btn); diff --git a/apps/assets/templates/assets/asset_create.html b/apps/assets/templates/assets/asset_create.html index 7a32264cc..1d04f2bec 100644 --- a/apps/assets/templates/assets/asset_create.html +++ b/apps/assets/templates/assets/asset_create.html @@ -16,12 +16,24 @@

{% trans 'Basic' %}

{% bootstrap_field form.hostname layout="horizontal" %} {% bootstrap_field form.ip layout="horizontal" %} - {% bootstrap_field form.protocol layout="horizontal" %} - {% bootstrap_field form.port layout="horizontal" %} {% bootstrap_field form.platform layout="horizontal" %} {% bootstrap_field form.public_ip layout="horizontal" %} {% bootstrap_field form.domain layout="horizontal" %} +
+

{% trans 'Protocols' %}

+
+ {% for fm in formset.forms %} +
+
{{ fm.name }}
+
{{ fm.port }}
+
+ + +
+
+ {% endfor %} +

{% trans 'Auth' %}

{% bootstrap_field form.admin_user layout="horizontal" %} @@ -55,6 +67,8 @@ {% endif %} + {% block extra %} + {% endblock %}

{% trans 'Other' %}

@@ -73,11 +87,25 @@ {% block custom_foot_js %} + {% block form_submit %} + + {% endblock %} {% endblock %} \ No newline at end of file diff --git a/apps/assets/templates/assets/asset_detail.html b/apps/assets/templates/assets/asset_detail.html index 01c048af8..e89b56283 100644 --- a/apps/assets/templates/assets/asset_detail.html +++ b/apps/assets/templates/assets/asset_detail.html @@ -69,12 +69,13 @@
{{ asset.public_ip|default:"" }}
{% trans 'Port' %}:{{ asset.port }}
{% trans 'Protocol' %}:{{ asset.protocol }}{% trans 'Protocol' %} + {% for protocol in asset.protocols.all %} + {{ protocol.name }}: + {{ protocol.port }} + {% endfor %} +
{% trans 'Admin user' %}:
{% trans 'CPU' %}:{{ asset.cpu_model|default:"" }} {{ asset.cpu_count|default:"" }}*{{ asset.cpu_cores|default:"" }}{{ asset.cpu_info }}
{% trans 'Memory' %}:
{% trans 'Refresh hardware' %}: diff --git a/apps/assets/templates/assets/asset_update.html b/apps/assets/templates/assets/asset_update.html index caff4286d..8e59c097b 100644 --- a/apps/assets/templates/assets/asset_update.html +++ b/apps/assets/templates/assets/asset_update.html @@ -1,95 +1,51 @@ -{% extends '_base_create_update.html' %} -{% load static %} +{% extends 'assets/asset_create.html' %} {% load bootstrap3 %} {% load i18n %} -{% load asset_tags %} -{% load common_tags %} - -{% block custom_head_css_js_create %} - - -{% endblock %} - -{% block form %} -
- {% if form.non_field_errors %} -
- {{ form.non_field_errors }} -
- {% endif %} - {% csrf_token %} -

{% trans 'Basic' %}

- {% bootstrap_field form.hostname layout="horizontal" %} - {% bootstrap_field form.ip layout="horizontal" %} - {% bootstrap_field form.protocol layout="horizontal" %} - {% bootstrap_field form.port layout="horizontal" %} - {% bootstrap_field form.platform layout="horizontal" %} - {% bootstrap_field form.public_ip layout="horizontal" %} - {% bootstrap_field form.domain layout="horizontal" %} - -
-

{% trans 'Auth' %}

- {% bootstrap_field form.admin_user layout="horizontal" %} - -
-

{% trans 'Node' %}

- {% bootstrap_field form.nodes layout="horizontal" %} - -
-

{% trans 'Labels' %}

-
- -
- -
-
+{% block extra %}

{% trans 'Configuration' %}

{% bootstrap_field form.number layout="horizontal" %} - -
-

{% trans 'Other' %}

- {% bootstrap_field form.comment layout="horizontal" %} - {% bootstrap_field form.is_active layout="horizontal" %} - -
-
-
- - -
-
- -
{% endblock %} -{% block custom_foot_js %} - + .on("submit", "form", function (evt) { + evt.preventDefault(); + var the_url = '{% url 'api-assets:asset-detail' pk=object.id %}'; + var redirect_to = '{% url "assets:asset-list" %}'; + var form = $("form"); + var protocols = {}; + var data = form.serializeObject(); + $.each(data, function (k, v) { + if (k.startsWith("form")){ + delete data[k]; + var _k = k.split("-"); + var formName = _k.slice(0, 2).join("-"); + var key = _k[_k.length-1]; + if (!protocols[formName]) { + protocols[formName] = {} + } + protocols[formName][key] = v + } + }); + protocols = $.map(protocols, function ( v) { + return v + }); + data["protocols"] = protocols; + if (typeof data["nodes"] == "string") { + data["nodes"] = [data["nodes"]] + } + var props = { + url: the_url, + data: data, + method: "PUT", + form: form, + redirect_to: redirect_to + }; + formSubmit(props); + }); + {% endblock %} \ No newline at end of file diff --git a/apps/assets/templates/assets/system_user_detail.html b/apps/assets/templates/assets/system_user_detail.html index 26e9bbef0..55f625d81 100644 --- a/apps/assets/templates/assets/system_user_detail.html +++ b/apps/assets/templates/assets/system_user_detail.html @@ -118,7 +118,7 @@
- + {% if system_user.auto_push %} - + {% endif %} - +
{% trans 'Auto push' %}: @@ -135,7 +135,7 @@
{% trans 'Push system user now' %}: @@ -144,7 +144,7 @@
{% trans 'Test assets connective' %}: @@ -219,8 +219,12 @@ function updateCommandFilters(command_filters) { }); } $(document).ready(function () { - if($('#id_protocol_type').text() === 'rdp'){ - $('.only-ssh').addClass('hidden') + var protocol = $('#id_protocol_type').text(); + if(protocol !== 'ssh'){ + $('.only-ssh').addClass("hidden") + } + if(["ssh", "rdp"].indexOf(protocol) === -1){ + $('.only-ssh-rdp').addClass('hidden'); } $(".panel-body .table tr:visible:first").addClass('no-borders-tr'); $('.select2').select2() diff --git a/apps/assets/templates/assets/system_user_list.html b/apps/assets/templates/assets/system_user_list.html index 2b1a63115..0744b793b 100644 --- a/apps/assets/templates/assets/system_user_list.html +++ b/apps/assets/templates/assets/system_user_list.html @@ -9,7 +9,7 @@ {# 目前还不支持Windows的自动推送#} {% trans 'System user is Jumpserver jump login assets used by the users, can be understood as the user login assets, such as web, sa, the dba (` ssh web@some-host `), rather than using a user the username login server jump (` ssh xiaoming@some-host `); '%} {% trans 'In simple terms, users log into Jumpserver using their own username, and Jumpserver uses system users to log into assets. '%} - {% trans 'When system users are created, if you choose auto push Jumpserver to use ansible push system users into the asset, if the asset (Switch, Windows) does not support ansible, please manually fill in the account password. Automatic push for Windows is not currently supported.' %} + {% trans 'When system users are created, if you choose auto push Jumpserver to use ansible push system users into the asset, if the asset (Switch) does not support ansible, please manually fill in the account password.' %} {% endblock %} diff --git a/apps/assets/views/asset.py b/apps/assets/views/asset.py index 4a3ce2273..c884b4902 100644 --- a/apps/assets/views/asset.py +++ b/apps/assets/views/asset.py @@ -23,6 +23,7 @@ from django.utils import timezone from django.contrib.auth.mixins import LoginRequiredMixin from django.shortcuts import redirect from django.contrib.messages.views import SuccessMessageMixin +from django.forms.formsets import formset_factory from common.mixins import JSONResponseMixin from common.utils import get_object_or_none, get_logger @@ -30,8 +31,6 @@ from common.permissions import AdminUserRequiredMixin from common.const import ( create_success_msg, update_success_msg, KEY_CACHE_RESOURCES_ID ) -from ..const import CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX -from orgs.utils import current_org from .. import forms from ..models import Asset, AdminUser, SystemUser, Label, Node, Domain @@ -102,10 +101,30 @@ class AssetCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): form["nodes"].initial = node return form + def get_protocol_formset(self): + ProtocolFormset = formset_factory(forms.ProtocolForm, extra=0, min_num=1, max_num=5) + if self.request.method == "POST": + formset = ProtocolFormset(self.request.POST) + else: + formset = ProtocolFormset() + return formset + + def form_valid(self, form): + formset = self.get_protocol_formset() + valid = formset.is_valid() + if not valid: + return self.form_invalid(form) + protocols = formset.save() + instance = super().form_valid(form) + instance.protocols.set(protocols) + return instance + def get_context_data(self, **kwargs): + formset = self.get_protocol_formset() context = { 'app': _('Assets'), 'action': _('Create asset'), + 'formset': formset, } kwargs.update(context) return super().get_context_data(**kwargs) @@ -160,10 +179,21 @@ class AssetUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): template_name = 'assets/asset_update.html' success_url = reverse_lazy('assets:asset-list') + def get_protocol_formset(self): + ProtocolFormset = formset_factory(forms.ProtocolForm, extra=0, min_num=1, max_num=5) + if self.request.method == "POST": + formset = ProtocolFormset(self.request.POST) + else: + initial_data = [{"name": p.name, "port": p.port} for p in self.object.protocols.all()] + formset = ProtocolFormset(initial=initial_data) + return formset + def get_context_data(self, **kwargs): + formset = self.get_protocol_formset() context = { 'app': _('Assets'), 'action': _('Update asset'), + 'formset': formset, } kwargs.update(context) return super().get_context_data(**kwargs) diff --git a/apps/common/decorator.py b/apps/common/decorator.py new file mode 100644 index 000000000..6edc4f3c3 --- /dev/null +++ b/apps/common/decorator.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# +from django.db import transaction + + +def on_transaction_commit(func): + """ + 如果不调用on_commit, 对象创建时添加多对多字段值失败 + """ + def inner(*args, **kwargs): + transaction.on_commit(lambda: func(*args, **kwargs)) + return inner diff --git a/apps/common/mixins.py b/apps/common/mixins.py index 8e4af26dd..12f147689 100644 --- a/apps/common/mixins.py +++ b/apps/common/mixins.py @@ -5,6 +5,7 @@ from django.http import JsonResponse from django.utils import timezone from django.core.cache import cache from django.utils.translation import ugettext_lazy as _ +from django.contrib import messages from rest_framework.utils import html from rest_framework.settings import api_settings from rest_framework.exceptions import ValidationError @@ -203,3 +204,26 @@ class DatetimeSearchMixin: def get(self, request, *args, **kwargs): self.get_date_range() return super().get(request, *args, **kwargs) + + +class ApiMessageMixin: + success_message = _("%(name)s was %(action)s successfully") + _action_map = {"create": _("create"), "update": _("update")} + + def get_success_message(self, cleaned_data): + data = {k: v for k, v in cleaned_data.items()} + action = getattr(self, "action", "create") + data["action"] = self._action_map.get(action) + message = self.success_message % data + return message + + def dispatch(self, request, *args, **kwargs): + resp = super().dispatch(request, *args, **kwargs) + if request.method.lower() in ("get", "delete"): + return resp + if resp.status_code >= 400: + return resp + message = self.get_success_message(resp.data) + if message: + messages.success(request, message) + return resp diff --git a/apps/common/validators.py b/apps/common/validators.py index b273bd1de..8106f27bd 100644 --- a/apps/common/validators.py +++ b/apps/common/validators.py @@ -3,5 +3,22 @@ from django.core.validators import RegexValidator from django.utils.translation import ugettext_lazy as _ +from rest_framework.validators import ( + UniqueTogetherValidator, ValidationError +) -alphanumeric = RegexValidator(r'^[0-9a-zA-Z_@\-\.]*$', _('Special char not allowed')) \ No newline at end of file + +alphanumeric = RegexValidator(r'^[0-9a-zA-Z_@\-\.]*$', _('Special char not allowed')) + + +class ProjectUniqueValidator(UniqueTogetherValidator): + def __call__(self, attrs): + try: + super().__call__(attrs) + except ValidationError as e: + errors = {} + for field in self.fields: + if field == "org_id": + continue + errors[field] = _('This field must be unique.') + raise ValidationError(errors) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 28ac12456..0362a68e3 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -287,7 +287,10 @@ class Config(dict): return v try: - v = tp(v) + if tp in [list, dict]: + v = json.loads(v) + else: + v = tp(v) except Exception: pass return v diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 917c82326..1c782fa73 100644 Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index de1d9f770..899cd30b7 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Jumpserver 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-06-12 17:56+0800\n" +"POT-Creation-Date: 2019-06-13 18:56+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: Jumpserver team\n" @@ -76,7 +76,7 @@ msgstr "运行参数" #: applications/templates/applications/remote_app_list.html:22 #: applications/templates/applications/user_remote_app_list.html:18 #: assets/forms/domain.py:15 assets/forms/label.py:13 -#: assets/models/asset.py:279 assets/models/authbook.py:27 +#: assets/models/asset.py:320 assets/models/authbook.py:27 #: assets/serializers/admin_user.py:23 assets/serializers/system_user.py:28 #: assets/templates/assets/admin_user_list.html:49 #: assets/templates/assets/domain_detail.html:60 @@ -95,7 +95,7 @@ msgstr "运行参数" #: terminal/templates/terminal/session_list.html:41 #: terminal/templates/terminal/session_list.html:72 #: xpack/plugins/change_auth_plan/forms.py:114 -#: xpack/plugins/change_auth_plan/models.py:409 +#: xpack/plugins/change_auth_plan/models.py:413 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:46 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:54 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:13 @@ -132,11 +132,11 @@ msgstr "系统用户" #: applications/templates/applications/remote_app_detail.html:53 #: applications/templates/applications/remote_app_list.html:20 #: applications/templates/applications/user_remote_app_list.html:16 -#: assets/forms/domain.py:73 assets/forms/user.py:84 assets/forms/user.py:146 -#: assets/models/base.py:26 assets/models/cluster.py:18 -#: assets/models/cmd_filter.py:20 assets/models/domain.py:20 -#: assets/models/group.py:20 assets/models/label.py:18 -#: assets/templates/assets/admin_user_detail.html:56 +#: assets/forms/domain.py:73 assets/forms/user.py:84 assets/forms/user.py:148 +#: assets/models/asset.py:72 assets/models/base.py:26 +#: assets/models/cluster.py:18 assets/models/cmd_filter.py:20 +#: assets/models/domain.py:20 assets/models/group.py:20 +#: assets/models/label.py:18 assets/templates/assets/admin_user_detail.html:56 #: assets/templates/assets/admin_user_list.html:47 #: assets/templates/assets/cmd_filter_detail.html:61 #: assets/templates/assets/cmd_filter_list.html:24 @@ -173,7 +173,7 @@ msgstr "系统用户" #: users/templates/users/user_profile.html:51 #: users/templates/users/user_pubkey_update.html:53 #: xpack/plugins/change_auth_plan/forms.py:97 -#: xpack/plugins/change_auth_plan/models.py:58 +#: xpack/plugins/change_auth_plan/models.py:61 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:61 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:12 #: xpack/plugins/cloud/models.py:49 xpack/plugins/cloud/models.py:119 @@ -204,11 +204,11 @@ msgstr "参数" #: applications/models/remote_app.py:43 #: applications/templates/applications/remote_app_detail.html:77 -#: assets/models/asset.py:109 assets/models/base.py:34 +#: assets/models/asset.py:132 assets/models/base.py:34 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:25 #: assets/models/cmd_filter.py:58 assets/models/group.py:21 #: assets/templates/assets/admin_user_detail.html:68 -#: assets/templates/assets/asset_detail.html:128 +#: assets/templates/assets/asset_detail.html:129 #: assets/templates/assets/cmd_filter_detail.html:77 #: assets/templates/assets/domain_detail.html:72 #: assets/templates/assets/system_user_detail.html:100 @@ -218,7 +218,7 @@ msgstr "参数" #: perms/templates/perms/remote_app_permission_detail.html:90 #: users/models/user.py:102 users/serializers/v1.py:72 #: users/templates/users/user_detail.html:111 -#: xpack/plugins/change_auth_plan/models.py:103 +#: xpack/plugins/change_auth_plan/models.py:106 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:113 #: xpack/plugins/cloud/models.py:55 xpack/plugins/cloud/models.py:127 msgid "Created by" @@ -228,7 +228,7 @@ msgstr "创建者" # msgstr "创建者" #: applications/models/remote_app.py:46 #: applications/templates/applications/remote_app_detail.html:73 -#: assets/models/asset.py:110 assets/models/cluster.py:26 +#: assets/models/asset.py:133 assets/models/cluster.py:26 #: assets/models/domain.py:23 assets/models/group.py:22 #: assets/models/label.py:25 assets/serializers/admin_user.py:37 #: assets/templates/assets/admin_user_detail.html:64 @@ -256,13 +256,13 @@ msgstr "创建日期" #: applications/templates/applications/remote_app_detail.html:81 #: applications/templates/applications/remote_app_list.html:24 #: applications/templates/applications/user_remote_app_list.html:20 -#: assets/models/asset.py:111 assets/models/base.py:31 +#: assets/models/asset.py:134 assets/models/base.py:31 #: assets/models/cluster.py:29 assets/models/cmd_filter.py:22 #: assets/models/cmd_filter.py:55 assets/models/domain.py:21 #: assets/models/domain.py:53 assets/models/group.py:23 #: assets/models/label.py:23 assets/templates/assets/admin_user_detail.html:72 #: assets/templates/assets/admin_user_list.html:53 -#: assets/templates/assets/asset_detail.html:136 +#: assets/templates/assets/asset_detail.html:137 #: assets/templates/assets/cmd_filter_detail.html:65 #: assets/templates/assets/cmd_filter_list.html:27 #: assets/templates/assets/cmd_filter_rule_list.html:62 @@ -282,7 +282,7 @@ msgstr "创建日期" #: users/templates/users/user_group_detail.html:67 #: users/templates/users/user_group_list.html:37 #: users/templates/users/user_profile.html:134 -#: xpack/plugins/change_auth_plan/models.py:99 +#: xpack/plugins/change_auth_plan/models.py:102 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:117 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:19 #: xpack/plugins/cloud/models.py:54 xpack/plugins/cloud/models.py:125 @@ -310,8 +310,7 @@ msgstr "远程应用" #: assets/templates/assets/_system_user.html:75 #: assets/templates/assets/admin_user_create_update.html:45 #: assets/templates/assets/asset_bulk_update.html:23 -#: assets/templates/assets/asset_create.html:67 -#: assets/templates/assets/asset_update.html:71 +#: assets/templates/assets/asset_create.html:81 #: assets/templates/assets/cmd_filter_create_update.html:15 #: assets/templates/assets/cmd_filter_rule_create_update.html:40 #: assets/templates/assets/domain_create_update.html:16 @@ -347,9 +346,8 @@ msgstr "重置" #: assets/templates/assets/_system_user.html:76 #: assets/templates/assets/admin_user_create_update.html:46 #: assets/templates/assets/asset_bulk_update.html:24 -#: assets/templates/assets/asset_create.html:68 +#: assets/templates/assets/asset_create.html:82 #: assets/templates/assets/asset_list.html:125 -#: assets/templates/assets/asset_update.html:72 #: assets/templates/assets/cmd_filter_create_update.html:16 #: assets/templates/assets/cmd_filter_rule_create_update.html:41 #: assets/templates/assets/domain_create_update.html:17 @@ -562,9 +560,9 @@ msgstr "连接" #: assets/templates/assets/system_user_detail.html:22 #: assets/views/admin_user.py:29 assets/views/admin_user.py:47 #: assets/views/admin_user.py:63 assets/views/admin_user.py:78 -#: assets/views/admin_user.py:102 assets/views/asset.py:53 -#: assets/views/asset.py:69 assets/views/asset.py:107 assets/views/asset.py:148 -#: assets/views/asset.py:165 assets/views/asset.py:189 +#: assets/views/admin_user.py:102 assets/views/asset.py:52 +#: assets/views/asset.py:68 assets/views/asset.py:125 assets/views/asset.py:167 +#: assets/views/asset.py:194 assets/views/asset.py:219 #: assets/views/cmd_filter.py:30 assets/views/cmd_filter.py:46 #: assets/views/cmd_filter.py:62 assets/views/cmd_filter.py:78 #: assets/views/cmd_filter.py:97 assets/views/cmd_filter.py:130 @@ -575,7 +573,7 @@ msgstr "连接" #: assets/views/label.py:26 assets/views/label.py:43 assets/views/label.py:69 #: assets/views/system_user.py:28 assets/views/system_user.py:44 #: assets/views/system_user.py:60 assets/views/system_user.py:74 -#: templates/_nav.html:19 xpack/plugins/change_auth_plan/models.py:65 +#: templates/_nav.html:19 xpack/plugins/change_auth_plan/models.py:68 msgid "Assets" msgstr "资产管理" @@ -595,7 +593,12 @@ msgstr "远程应用详情" msgid "My RemoteApp" msgstr "我的远程应用" -#: assets/api/asset.py:128 +#: assets/api/asset.py:50 +#, python-format +msgid "%(hostname)s was %(action)s successfully" +msgstr "%(hostname)s %(action)s成功" + +#: assets/api/asset.py:129 msgid "Please select assets that need to be updated" msgstr "请选择需要更新的资产" @@ -611,47 +614,44 @@ msgstr "更新节点资产硬件信息: {}" msgid "Test if the assets under the node are connectable: {}" msgstr "测试节点下资产是否可连接: {}" -#: assets/forms/asset.py:27 assets/models/asset.py:80 assets/models/user.py:133 -#: assets/templates/assets/asset_detail.html:194 -#: assets/templates/assets/asset_detail.html:202 +#: assets/forms/asset.py:45 assets/models/asset.py:103 +#: assets/models/user.py:133 assets/templates/assets/asset_detail.html:195 +#: assets/templates/assets/asset_detail.html:203 #: assets/templates/assets/system_user_asset.html:95 #: perms/models/asset_permission.py:38 -#: xpack/plugins/change_auth_plan/models.py:69 +#: xpack/plugins/change_auth_plan/models.py:72 msgid "Nodes" msgstr "节点管理" -#: assets/forms/asset.py:30 assets/forms/asset.py:66 assets/models/asset.py:84 +#: assets/forms/asset.py:48 assets/forms/asset.py:83 assets/models/asset.py:107 #: assets/models/cluster.py:19 assets/models/user.py:91 -#: assets/templates/assets/asset_detail.html:80 templates/_nav.html:24 +#: assets/templates/assets/asset_detail.html:81 templates/_nav.html:24 #: xpack/plugins/cloud/models.py:124 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:65 #: xpack/plugins/orgs/templates/orgs/org_list.html:18 msgid "Admin user" msgstr "管理用户" -#: assets/forms/asset.py:33 assets/forms/asset.py:69 assets/forms/asset.py:109 -#: assets/templates/assets/asset_create.html:36 -#: assets/templates/assets/asset_create.html:38 +#: assets/forms/asset.py:51 assets/forms/asset.py:86 assets/forms/asset.py:125 +#: assets/templates/assets/asset_create.html:48 +#: assets/templates/assets/asset_create.html:50 #: assets/templates/assets/asset_list.html:93 -#: assets/templates/assets/asset_update.html:41 -#: assets/templates/assets/asset_update.html:43 #: assets/templates/assets/user_asset_list.html:33 #: xpack/plugins/orgs/templates/orgs/org_list.html:20 msgid "Label" msgstr "标签" -#: assets/forms/asset.py:37 assets/forms/asset.py:73 assets/models/asset.py:79 +#: assets/forms/asset.py:54 assets/forms/asset.py:89 assets/models/asset.py:102 #: assets/models/domain.py:26 assets/models/domain.py:52 -#: assets/templates/assets/asset_detail.html:84 +#: assets/templates/assets/asset_detail.html:85 #: assets/templates/assets/user_asset_list.html:173 #: xpack/plugins/orgs/templates/orgs/org_list.html:17 msgid "Domain" msgstr "网域" -#: assets/forms/asset.py:41 assets/forms/asset.py:63 assets/forms/asset.py:77 -#: assets/forms/asset.py:112 assets/models/node.py:31 -#: assets/templates/assets/asset_create.html:30 -#: assets/templates/assets/asset_update.html:35 +#: assets/forms/asset.py:58 assets/forms/asset.py:80 assets/forms/asset.py:93 +#: assets/forms/asset.py:128 assets/models/node.py:31 +#: assets/templates/assets/asset_create.html:42 #: perms/forms/asset_permission.py:49 perms/forms/asset_permission.py:59 #: perms/models/asset_permission.py:57 #: perms/templates/perms/asset_permission_list.html:57 @@ -666,7 +666,7 @@ msgstr "网域" msgid "Node" msgstr "节点" -#: assets/forms/asset.py:45 assets/forms/asset.py:81 +#: assets/forms/asset.py:62 assets/forms/asset.py:97 msgid "" "root or other NOPASSWD sudo privilege user existed in asset,If asset is " "windows or other set any one, more see admin user left menu" @@ -674,17 +674,17 @@ msgstr "" "root或其他拥有NOPASSWD: ALL权限的用户, 如果是windows或其它硬件可以随意设置一" "个, 更多信息查看左侧 `管理用户` 菜单" -#: assets/forms/asset.py:48 assets/forms/asset.py:84 +#: assets/forms/asset.py:65 assets/forms/asset.py:100 msgid "Windows 2016 RDP protocol is different, If is window 2016, set it" msgstr "Windows 2016的RDP协议与之前不同,如果是请设置" -#: assets/forms/asset.py:49 assets/forms/asset.py:85 +#: assets/forms/asset.py:66 assets/forms/asset.py:101 msgid "" "If your have some network not connect with each other, you can set domain" msgstr "如果有多个的互相隔离的网络,设置资产属于的网域,使用网域网关跳转登录" -#: assets/forms/asset.py:92 assets/forms/asset.py:96 assets/forms/domain.py:17 -#: assets/forms/label.py:15 +#: assets/forms/asset.py:108 assets/forms/asset.py:112 +#: assets/forms/domain.py:17 assets/forms/label.py:15 #: perms/templates/perms/asset_permission_asset.html:88 #: xpack/plugins/change_auth_plan/forms.py:105 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:84 @@ -699,7 +699,7 @@ msgstr "不能包含特殊字符" msgid "SSH gateway support proxy SSH,RDP,VNC" msgstr "SSH网关,支持代理SSH,RDP和VNC" -#: assets/forms/domain.py:74 assets/forms/user.py:85 assets/forms/user.py:147 +#: assets/forms/domain.py:74 assets/forms/user.py:85 assets/forms/user.py:149 #: assets/models/base.py:27 #: assets/templates/assets/_asset_user_auth_modal.html:15 #: assets/templates/assets/_asset_user_view_auth_modal.html:31 @@ -721,8 +721,8 @@ msgstr "SSH网关,支持代理SSH,RDP和VNC" #: users/templates/users/user_list.html:36 #: users/templates/users/user_profile.html:47 #: xpack/plugins/change_auth_plan/forms.py:99 -#: xpack/plugins/change_auth_plan/models.py:60 -#: xpack/plugins/change_auth_plan/models.py:405 +#: xpack/plugins/change_auth_plan/models.py:63 +#: xpack/plugins/change_auth_plan/models.py:409 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:65 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:53 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:12 @@ -749,8 +749,8 @@ msgstr "密码或密钥密码" #: users/templates/users/user_profile_update.html:40 #: users/templates/users/user_pubkey_update.html:40 #: users/templates/users/user_update.html:20 -#: xpack/plugins/change_auth_plan/models.py:90 -#: xpack/plugins/change_auth_plan/models.py:260 +#: xpack/plugins/change_auth_plan/models.py:93 +#: xpack/plugins/change_auth_plan/models.py:264 msgid "Password" msgstr "密码" @@ -767,21 +767,21 @@ msgstr "不合法的密钥,仅支持RSA/DSA格式的密钥" msgid "Password and private key file must be input one" msgstr "密码和私钥, 必须输入一个" -#: assets/forms/user.py:134 +#: assets/forms/user.py:136 msgid "* Automatic login mode must fill in the username." msgstr "自动登录模式,必须填写用户名" -#: assets/forms/user.py:149 assets/models/cmd_filter.py:31 +#: assets/forms/user.py:151 assets/models/cmd_filter.py:31 #: assets/models/user.py:141 assets/templates/assets/_system_user.html:66 #: assets/templates/assets/system_user_detail.html:165 msgid "Command filter" msgstr "命令过滤器" -#: assets/forms/user.py:153 +#: assets/forms/user.py:155 msgid "Auto push system user to asset" msgstr "自动推送系统用户到资产" -#: assets/forms/user.py:154 +#: assets/forms/user.py:156 msgid "" "1-100, High level will be using login asset as default, if user was granted " "more than 2 system user" @@ -789,17 +789,26 @@ msgstr "" "1-100, 1最低优先级,100最高优先级。授权多个用户时,高优先级的系统用户将会作为" "默认登录用户" -#: assets/forms/user.py:156 +#: assets/forms/user.py:158 msgid "" "If you choose manual login mode, you do not need to fill in the username and " "password." msgstr "如果选择手动登录模式,用户名和密码可以不填写" -#: assets/forms/user.py:158 +#: assets/forms/user.py:160 msgid "Use comma split multi command, ex: /bin/whoami,/bin/ifconfig" msgstr "使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig" -#: assets/models/asset.py:74 assets/models/domain.py:49 +#: assets/models/asset.py:73 assets/models/asset.py:98 +#: assets/models/domain.py:50 assets/templates/assets/admin_user_assets.html:50 +#: assets/templates/assets/domain_gateway_list.html:69 +#: assets/templates/assets/system_user_asset.html:52 +#: assets/templates/assets/user_asset_list.html:168 +#: settings/templates/settings/replay_storage_create.html:59 +msgid "Port" +msgstr "端口" + +#: assets/models/asset.py:93 assets/models/domain.py:49 #: assets/templates/assets/_asset_list_modal.html:46 #: assets/templates/assets/admin_user_assets.html:49 #: assets/templates/assets/asset_detail.html:64 @@ -816,7 +825,7 @@ msgstr "使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig" msgid "IP" msgstr "IP" -#: assets/models/asset.py:75 assets/templates/assets/_asset_list_modal.html:45 +#: assets/models/asset.py:94 assets/templates/assets/_asset_list_modal.html:45 #: assets/templates/assets/_asset_user_auth_modal.html:9 #: assets/templates/assets/_asset_user_view_auth_modal.html:25 #: assets/templates/assets/admin_user_assets.html:48 @@ -833,8 +842,9 @@ msgstr "IP" msgid "Hostname" msgstr "主机名" -#: assets/models/asset.py:76 assets/models/domain.py:51 -#: assets/models/user.py:136 assets/templates/assets/asset_detail.html:76 +#: assets/models/asset.py:97 assets/models/asset.py:100 +#: assets/models/domain.py:51 assets/models/user.py:136 +#: assets/templates/assets/asset_detail.html:72 #: assets/templates/assets/domain_gateway_list.html:70 #: assets/templates/assets/system_user_detail.html:70 #: assets/templates/assets/system_user_list.html:53 @@ -843,108 +853,97 @@ msgstr "主机名" msgid "Protocol" msgstr "协议" -#: assets/models/asset.py:77 assets/models/domain.py:50 -#: assets/templates/assets/admin_user_assets.html:50 -#: assets/templates/assets/asset_detail.html:72 -#: assets/templates/assets/domain_gateway_list.html:69 -#: assets/templates/assets/system_user_asset.html:52 -#: assets/templates/assets/user_asset_list.html:168 -#: settings/templates/settings/replay_storage_create.html:59 -msgid "Port" -msgstr "端口" - -#: assets/models/asset.py:78 assets/templates/assets/asset_detail.html:108 +#: assets/models/asset.py:101 assets/templates/assets/asset_detail.html:109 #: assets/templates/assets/user_asset_list.html:170 msgid "Platform" msgstr "系统平台" -#: assets/models/asset.py:81 assets/models/cmd_filter.py:21 +#: assets/models/asset.py:104 assets/models/cmd_filter.py:21 #: assets/models/domain.py:54 assets/models/label.py:22 -#: assets/templates/assets/asset_detail.html:116 +#: assets/templates/assets/asset_detail.html:117 #: assets/templates/assets/user_asset_list.html:174 msgid "Is active" msgstr "激活" -#: assets/models/asset.py:87 assets/templates/assets/asset_detail.html:68 +#: assets/models/asset.py:110 assets/templates/assets/asset_detail.html:68 msgid "Public IP" msgstr "公网IP" -#: assets/models/asset.py:88 assets/templates/assets/asset_detail.html:124 +#: assets/models/asset.py:111 assets/templates/assets/asset_detail.html:125 msgid "Asset number" msgstr "资产编号" -#: assets/models/asset.py:91 assets/templates/assets/asset_detail.html:88 +#: assets/models/asset.py:114 assets/templates/assets/asset_detail.html:89 msgid "Vendor" msgstr "制造商" -#: assets/models/asset.py:92 assets/templates/assets/asset_detail.html:92 +#: assets/models/asset.py:115 assets/templates/assets/asset_detail.html:93 msgid "Model" msgstr "型号" -#: assets/models/asset.py:93 assets/templates/assets/asset_detail.html:120 +#: assets/models/asset.py:116 assets/templates/assets/asset_detail.html:121 msgid "Serial number" msgstr "序列号" -#: assets/models/asset.py:95 +#: assets/models/asset.py:118 msgid "CPU model" msgstr "CPU型号" -#: assets/models/asset.py:96 +#: assets/models/asset.py:119 #: xpack/plugins/license/templates/license/license_detail.html:80 msgid "CPU count" msgstr "CPU数量" -#: assets/models/asset.py:97 +#: assets/models/asset.py:120 msgid "CPU cores" msgstr "CPU核数" -#: assets/models/asset.py:98 +#: assets/models/asset.py:121 msgid "CPU vcpus" msgstr "CPU总数" -#: assets/models/asset.py:99 assets/templates/assets/asset_detail.html:100 +#: assets/models/asset.py:122 assets/templates/assets/asset_detail.html:101 msgid "Memory" msgstr "内存" -#: assets/models/asset.py:100 +#: assets/models/asset.py:123 msgid "Disk total" msgstr "硬盘大小" -#: assets/models/asset.py:101 +#: assets/models/asset.py:124 msgid "Disk info" msgstr "硬盘信息" -#: assets/models/asset.py:103 assets/templates/assets/asset_detail.html:112 +#: assets/models/asset.py:126 assets/templates/assets/asset_detail.html:113 #: assets/templates/assets/user_asset_list.html:171 msgid "OS" msgstr "操作系统" -#: assets/models/asset.py:104 +#: assets/models/asset.py:127 msgid "OS version" msgstr "系统版本" -#: assets/models/asset.py:105 +#: assets/models/asset.py:128 msgid "OS arch" msgstr "系统架构" -#: assets/models/asset.py:106 +#: assets/models/asset.py:129 msgid "Hostname raw" msgstr "主机名原始" -#: assets/models/asset.py:108 assets/templates/assets/asset_create.html:34 -#: assets/templates/assets/asset_detail.html:231 -#: assets/templates/assets/asset_update.html:39 templates/_nav.html:26 +#: assets/models/asset.py:131 assets/templates/assets/asset_create.html:46 +#: assets/templates/assets/asset_detail.html:232 templates/_nav.html:26 msgid "Labels" msgstr "标签管理" -#: assets/models/asset.py:117 assets/models/base.py:38 +#: assets/models/asset.py:140 assets/models/base.py:38 #: assets/serializers/admin_user.py:22 assets/serializers/system_user.py:19 #: assets/templates/assets/admin_user_list.html:51 #: assets/templates/assets/system_user_list.html:57 msgid "Unreachable" msgstr "不可达" -#: assets/models/asset.py:118 assets/models/base.py:39 +#: assets/models/asset.py:141 assets/models/base.py:39 #: assets/serializers/admin_user.py:24 assets/serializers/system_user.py:27 #: assets/templates/assets/admin_user_assets.html:51 #: assets/templates/assets/admin_user_list.html:50 @@ -956,7 +955,7 @@ msgstr "不可达" msgid "Reachable" msgstr "可连接" -#: assets/models/asset.py:119 assets/models/base.py:40 +#: assets/models/asset.py:142 assets/models/base.py:40 #: authentication/utils.py:9 xpack/plugins/license/models.py:78 msgid "Unknown" msgstr "未知" @@ -975,13 +974,13 @@ msgstr "版本" msgid "AuthBook" msgstr "" -#: assets/models/base.py:29 xpack/plugins/change_auth_plan/models.py:94 -#: xpack/plugins/change_auth_plan/models.py:267 +#: assets/models/base.py:29 xpack/plugins/change_auth_plan/models.py:97 +#: xpack/plugins/change_auth_plan/models.py:271 msgid "SSH private key" msgstr "ssh密钥" -#: assets/models/base.py:30 xpack/plugins/change_auth_plan/models.py:97 -#: xpack/plugins/change_auth_plan/models.py:263 +#: assets/models/base.py:30 xpack/plugins/change_auth_plan/models.py:100 +#: xpack/plugins/change_auth_plan/models.py:267 msgid "SSH public key" msgstr "ssh公钥" @@ -1204,18 +1203,22 @@ msgstr "%(value)s is not an even number" msgid "Date updated" msgstr "更新日期" -#: assets/serializers/asset.py:43 +#: assets/serializers/asset.py:52 msgid "Hardware info" msgstr "硬件信息" -#: assets/serializers/asset.py:44 +#: assets/serializers/asset.py:53 msgid "Connectivity" msgstr "连接" -#: assets/serializers/asset.py:45 +#: assets/serializers/asset.py:54 msgid "Org name" msgstr "组织名" +#: assets/serializers/asset.py:70 +msgid "Protocol duplicate: {}" +msgstr "协议重复: {}" + #: assets/serializers/asset_user.py:23 users/forms.py:247 #: users/models/user.py:91 users/templates/users/first_login.html:42 #: users/templates/users/user_password_update.html:46 @@ -1237,72 +1240,86 @@ msgstr "可连接资产" msgid "Login mode display" msgstr "登录模式显示" -#: assets/tasks.py:31 +#: assets/tasks.py:32 msgid "Asset has been disabled, skipped: {}" msgstr "资产或许不支持ansible, 跳过: {}" -#: assets/tasks.py:35 +#: assets/tasks.py:36 msgid "Asset may not be support ansible, skipped: {}" msgstr "资产或许不支持ansible, 跳过: {}" -#: assets/tasks.py:48 +#: assets/tasks.py:49 msgid "No assets matched, stop task" msgstr "没有匹配到资产,结束任务" -#: assets/tasks.py:73 +#: assets/tasks.py:59 +msgid "No assets matched related system user protocol, stop task" +msgstr "没有匹配到与系统用户协议相关的资产,结束任务" + +#: assets/tasks.py:85 msgid "Get asset info failed: {}" msgstr "获取资产信息失败:{}" -#: assets/tasks.py:123 +#: assets/tasks.py:135 msgid "Update some assets hardware info" msgstr "更新资产硬件信息" -#: assets/tasks.py:140 +#: assets/tasks.py:152 msgid "Update asset hardware info: {}" msgstr "更新资产硬件信息: {}" -#: assets/tasks.py:165 +#: assets/tasks.py:177 msgid "Test assets connectivity" msgstr "测试资产可连接性" -#: assets/tasks.py:189 +#: assets/tasks.py:229 msgid "Test assets connectivity: {}" msgstr "测试资产可连接性: {}" -#: assets/tasks.py:231 +#: assets/tasks.py:271 msgid "Test admin user connectivity period: {}" msgstr "定期测试管理账号可连接性: {}" -#: assets/tasks.py:238 +#: assets/tasks.py:278 msgid "Test admin user connectivity: {}" msgstr "测试管理行号可连接性: {}" -#: assets/tasks.py:277 +#: assets/tasks.py:348 msgid "Test system user connectivity: {}" msgstr "测试系统用户可连接性: {}" -#: assets/tasks.py:284 +#: assets/tasks.py:355 msgid "Test system user connectivity: {} => {}" msgstr "测试系统用户可连接性: {} => {}" -#: assets/tasks.py:297 +#: assets/tasks.py:368 msgid "Test system user connectivity period: {}" msgstr "定期测试系统用户可连接性: {}" -#: assets/tasks.py:374 -msgid "" -"Push system user task skip, auto push not enable or protocol is not ssh: {}" -msgstr "推送系统用户任务跳过,自动推送没有打开,或协议不是ssh: {}" +#: assets/tasks.py:469 assets/tasks.py:555 +#: xpack/plugins/change_auth_plan/models.py:522 +msgid "The asset {} system platform {} does not support run Ansible tasks" +msgstr "资产 {} 系统平台 {} 不支持运行 Ansible 任务" -#: assets/tasks.py:396 assets/tasks.py:410 +#: assets/tasks.py:481 +msgid "" +"Push system user task skip, auto push not enable or protocol is not ssh or " +"rdp: {}" +msgstr "推送系统用户任务跳过,自动推送没有打开,或协议不是ssh或rdp: {}" + +#: assets/tasks.py:488 +msgid "For security, do not push user {}" +msgstr "为了安全,禁止推送用户 {}" + +#: assets/tasks.py:516 assets/tasks.py:530 msgid "Push system users to assets: {}" msgstr "推送系统用户到入资产: {}" -#: assets/tasks.py:402 +#: assets/tasks.py:522 msgid "Push system users to asset: {} => {}" msgstr "推送系统用户到入资产: {} => {}" -#: assets/tasks.py:459 +#: assets/tasks.py:600 msgid "Test asset user connectivity: {}" msgstr "测试资产用户可连接性: {}" @@ -1347,7 +1364,7 @@ msgstr "启用MFA" msgid "Import assets" msgstr "导入资产" -#: assets/templates/assets/_asset_list_modal.html:7 assets/views/asset.py:54 +#: assets/templates/assets/_asset_list_modal.html:7 assets/views/asset.py:53 #: templates/_nav.html:22 xpack/plugins/change_auth_plan/views.py:110 msgid "Asset list" msgstr "资产列表" @@ -1382,7 +1399,7 @@ msgstr "需要二次认证来查看账号信息" #: assets/templates/assets/_asset_user_view_auth_modal.html:20 #: assets/templates/assets/admin_user_detail.html:100 -#: assets/templates/assets/asset_detail.html:211 +#: assets/templates/assets/asset_detail.html:212 #: assets/templates/assets/asset_list.html:682 #: assets/templates/assets/cmd_filter_detail.html:106 #: assets/templates/assets/system_user_asset.html:112 @@ -1434,7 +1451,6 @@ msgstr "如果使用了nat端口映射,请设置为ssh真实监听的端口" #: assets/templates/assets/_system_user.html:37 #: assets/templates/assets/asset_create.html:16 -#: assets/templates/assets/asset_update.html:21 #: assets/templates/assets/gateway_create_update.html:37 #: perms/templates/perms/asset_permission_create_update.html:38 #: perms/templates/perms/remote_app_permission_create_update.html:39 @@ -1443,8 +1459,7 @@ msgid "Basic" msgstr "基本" #: assets/templates/assets/_system_user.html:44 -#: assets/templates/assets/asset_create.html:26 -#: assets/templates/assets/asset_update.html:31 +#: assets/templates/assets/asset_create.html:38 #: assets/templates/assets/gateway_create_update.html:45 #: users/templates/users/_user.html:21 msgid "Auth" @@ -1455,8 +1470,7 @@ msgid "Auto generate key" msgstr "自动生成密钥" #: assets/templates/assets/_system_user.html:69 -#: assets/templates/assets/asset_create.html:60 -#: assets/templates/assets/asset_update.html:64 +#: assets/templates/assets/asset_create.html:74 #: assets/templates/assets/gateway_create_update.html:53 #: perms/templates/perms/asset_permission_create_update.html:53 #: perms/templates/perms/remote_app_permission_create_update.html:52 @@ -1476,7 +1490,7 @@ msgstr "更新系统用户" #: assets/templates/assets/_user_asset_detail_modal.html:11 #: assets/templates/assets/asset_asset_user_list.html:13 -#: assets/templates/assets/asset_detail.html:20 assets/views/asset.py:190 +#: assets/templates/assets/asset_detail.html:20 assets/views/asset.py:220 msgid "Asset detail" msgstr "资产详情" @@ -1501,7 +1515,7 @@ msgstr "快速更新" #: assets/templates/assets/admin_user_assets.html:70 #: assets/templates/assets/asset_asset_user_list.html:67 -#: assets/templates/assets/asset_detail.html:179 +#: assets/templates/assets/asset_detail.html:180 msgid "Test connective" msgstr "测试可连接性" @@ -1509,7 +1523,7 @@ msgstr "测试可连接性" #: assets/templates/assets/admin_user_assets.html:118 #: assets/templates/assets/asset_asset_user_list.html:70 #: assets/templates/assets/asset_asset_user_list.html:119 -#: assets/templates/assets/asset_detail.html:182 +#: assets/templates/assets/asset_detail.html:183 #: assets/templates/assets/system_user_asset.html:75 #: assets/templates/assets/system_user_asset.html:166 #: assets/templates/assets/system_user_detail.html:151 @@ -1530,7 +1544,7 @@ msgstr "查看认证" #: assets/templates/assets/admin_user_assets.html:196 #: assets/templates/assets/asset_asset_user_list.html:162 -#: assets/templates/assets/asset_detail.html:311 +#: assets/templates/assets/asset_detail.html:312 #: assets/templates/assets/system_user_asset.html:353 #: users/templates/users/user_detail.html:307 #: users/templates/users/user_detail.html:334 @@ -1617,7 +1631,7 @@ msgid "Please select file" msgstr "选择文件" #: assets/templates/assets/asset_asset_user_list.html:16 -#: assets/templates/assets/asset_detail.html:23 assets/views/asset.py:70 +#: assets/templates/assets/asset_detail.html:23 assets/views/asset.py:69 msgid "Asset user list" msgstr "资产用户列表" @@ -1630,7 +1644,7 @@ msgid "Password version" msgstr "密码版本" #: assets/templates/assets/asset_asset_user_list.html:60 -#: assets/templates/assets/asset_detail.html:148 +#: assets/templates/assets/asset_detail.html:149 #: terminal/templates/terminal/session_detail.html:81 #: users/templates/users/user_detail.html:138 #: users/templates/users/user_profile.html:146 @@ -1649,21 +1663,25 @@ msgstr "选择需要修改属性" msgid "Select all" msgstr "全选" -#: assets/templates/assets/asset_detail.html:96 +#: assets/templates/assets/asset_create.html:24 +msgid "Protocols" +msgstr "协议" + +#: assets/templates/assets/asset_detail.html:97 msgid "CPU" msgstr "CPU" -#: assets/templates/assets/asset_detail.html:104 +#: assets/templates/assets/asset_detail.html:105 msgid "Disk" msgstr "硬盘" -#: assets/templates/assets/asset_detail.html:132 +#: assets/templates/assets/asset_detail.html:133 #: users/templates/users/user_detail.html:115 #: users/templates/users/user_profile.html:104 msgid "Date joined" msgstr "创建日期" -#: assets/templates/assets/asset_detail.html:154 +#: assets/templates/assets/asset_detail.html:155 #: assets/templates/assets/user_asset_list.html:46 #: perms/models/asset_permission.py:60 perms/models/base.py:38 #: perms/templates/perms/asset_permission_create_update.html:55 @@ -1679,11 +1697,11 @@ msgstr "创建日期" msgid "Active" msgstr "激活中" -#: assets/templates/assets/asset_detail.html:171 +#: assets/templates/assets/asset_detail.html:172 msgid "Refresh hardware" msgstr "更新硬件信息" -#: assets/templates/assets/asset_detail.html:174 +#: assets/templates/assets/asset_detail.html:175 msgid "Refresh" msgstr "刷新" @@ -1696,7 +1714,7 @@ msgstr "" "左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的," "右侧是属于该节点下的资产" -#: assets/templates/assets/asset_list.html:69 assets/views/asset.py:108 +#: assets/templates/assets/asset_list.html:69 assets/views/asset.py:126 msgid "Create asset" msgstr "创建资产" @@ -1830,7 +1848,7 @@ msgstr "删除" msgid "Asset Deleting failed." msgstr "删除失败" -#: assets/templates/assets/asset_update.html:60 +#: assets/templates/assets/asset_update.html:7 msgid "Configuration" msgstr "配置" @@ -2001,13 +2019,11 @@ msgstr "" #: assets/templates/assets/system_user_list.html:12 msgid "" "When system users are created, if you choose auto push Jumpserver to use " -"ansible push system users into the asset, if the asset (Switch, Windows) " -"does not support ansible, please manually fill in the account password. " -"Automatic push for Windows is not currently supported." +"ansible push system users into the asset, if the asset (Switch) does not " +"support ansible, please manually fill in the account password." msgstr "" "系统用户创建时,如果选择了自动推送 Jumpserver会使用ansible自动推送系统用户到" -"资产中,如果资产(交换机、windows)不支持ansible, 请手动填写账号密码。目前还不" -"支持Windows的自动推送" +"资产中,如果资产(交换机)不支持ansible, 请手动填写账号密码。" #: assets/templates/assets/system_user_list.html:43 #: assets/views/system_user.py:45 @@ -2039,23 +2055,23 @@ msgstr "管理用户列表" msgid "Admin user detail" msgstr "管理用户详情" -#: assets/views/asset.py:81 templates/_nav_user.html:4 +#: assets/views/asset.py:80 templates/_nav_user.html:4 msgid "My assets" msgstr "我的资产" -#: assets/views/asset.py:122 +#: assets/views/asset.py:141 msgid "Bulk update asset success" msgstr "批量更新资产成功" -#: assets/views/asset.py:149 +#: assets/views/asset.py:168 msgid "Bulk update asset" msgstr "批量更新资产" -#: assets/views/asset.py:166 +#: assets/views/asset.py:195 msgid "Update asset" msgstr "更新资产" -#: assets/views/asset.py:307 +#: assets/views/asset.py:337 msgid "already exists" msgstr "已经存在" @@ -2223,7 +2239,7 @@ msgid "User agent" msgstr "Agent" #: audits/models.py:100 audits/templates/audits/login_log_list.html:57 -#: xpack/plugins/change_auth_plan/models.py:413 +#: xpack/plugins/change_auth_plan/models.py:417 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:15 #: xpack/plugins/cloud/models.py:172 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:69 @@ -2249,8 +2265,8 @@ msgstr "登录日期" #: perms/templates/perms/asset_permission_detail.html:86 #: perms/templates/perms/remote_app_permission_detail.html:78 #: terminal/models.py:165 terminal/templates/terminal/session_list.html:78 -#: xpack/plugins/change_auth_plan/models.py:246 -#: xpack/plugins/change_auth_plan/models.py:416 +#: xpack/plugins/change_auth_plan/models.py:250 +#: xpack/plugins/change_auth_plan/models.py:420 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:59 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:17 msgid "Date start" @@ -2603,18 +2619,35 @@ msgstr "" msgid "Encrypt field using Secret Key" msgstr "" -#: common/mixins.py:35 +#: common/mixins.py:36 msgid "is discard" msgstr "" -#: common/mixins.py:36 +#: common/mixins.py:37 msgid "discard time" msgstr "" -#: common/validators.py:7 +#: common/mixins.py:210 +#, python-format +msgid "%(name)s was %(action)s successfully" +msgstr "%(name)s %(action)s成功" + +#: common/mixins.py:211 +msgid "create" +msgstr "创建" + +#: common/mixins.py:211 +msgid "update" +msgstr "更新" + +#: common/validators.py:11 msgid "Special char not allowed" msgstr "不能包含特殊字符" +#: common/validators.py:23 +msgid "This field must be unique." +msgstr "字段必须唯一" + #: jumpserver/views.py:185 msgid "" "
Luna is a separately deployed program, you need to deploy Luna, coco, " @@ -2690,48 +2723,48 @@ msgstr "Become" msgid "Create by" msgstr "创建者" -#: ops/models/adhoc.py:223 +#: ops/models/adhoc.py:224 msgid "{} Start task: {}" msgstr "{} 任务开始: {}" -#: ops/models/adhoc.py:226 +#: ops/models/adhoc.py:227 msgid "{} Task finish" msgstr "{} 任务结束" -#: ops/models/adhoc.py:324 +#: ops/models/adhoc.py:325 msgid "Start time" msgstr "开始时间" -#: ops/models/adhoc.py:325 +#: ops/models/adhoc.py:326 msgid "End time" msgstr "完成时间" -#: ops/models/adhoc.py:326 ops/templates/ops/adhoc_history.html:57 +#: ops/models/adhoc.py:327 ops/templates/ops/adhoc_history.html:57 #: ops/templates/ops/task_history.html:63 ops/templates/ops/task_list.html:33 -#: xpack/plugins/change_auth_plan/models.py:249 -#: xpack/plugins/change_auth_plan/models.py:419 +#: xpack/plugins/change_auth_plan/models.py:253 +#: xpack/plugins/change_auth_plan/models.py:423 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:58 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:16 msgid "Time" msgstr "时间" -#: ops/models/adhoc.py:327 ops/templates/ops/adhoc_detail.html:106 +#: ops/models/adhoc.py:328 ops/templates/ops/adhoc_detail.html:106 #: ops/templates/ops/adhoc_history.html:55 #: ops/templates/ops/adhoc_history_detail.html:69 #: ops/templates/ops/task_detail.html:84 ops/templates/ops/task_history.html:61 msgid "Is finished" msgstr "是否完成" -#: ops/models/adhoc.py:328 ops/templates/ops/adhoc_history.html:56 +#: ops/models/adhoc.py:329 ops/templates/ops/adhoc_history.html:56 #: ops/templates/ops/task_history.html:62 msgid "Is success" msgstr "是否成功" -#: ops/models/adhoc.py:329 +#: ops/models/adhoc.py:330 msgid "Adhoc raw result" msgstr "结果" -#: ops/models/adhoc.py:330 +#: ops/models/adhoc.py:331 msgid "Adhoc result summary" msgstr "汇总" @@ -2859,33 +2892,33 @@ msgstr "任务列表" msgid "Go" msgstr "" -#: ops/templates/ops/command_execution_create.html:152 +#: ops/templates/ops/command_execution_create.html:155 msgid "Selected assets" msgstr "已选择资产" -#: ops/templates/ops/command_execution_create.html:155 +#: ops/templates/ops/command_execution_create.html:158 msgid "In total" msgstr "总共" -#: ops/templates/ops/command_execution_create.html:190 +#: ops/templates/ops/command_execution_create.html:193 msgid "" "Select the left asset, select the running system user, execute command in " "batch" msgstr "选择左侧资产, 选择运行的系统用户,批量执行命令" -#: ops/templates/ops/command_execution_create.html:208 +#: ops/templates/ops/command_execution_create.html:211 msgid "Unselected assets" msgstr "没有选中资产" -#: ops/templates/ops/command_execution_create.html:212 +#: ops/templates/ops/command_execution_create.html:215 msgid "No input command" msgstr "没有输入命令" -#: ops/templates/ops/command_execution_create.html:216 +#: ops/templates/ops/command_execution_create.html:219 msgid "No system user was selected" msgstr "没有选择系统用户" -#: ops/templates/ops/command_execution_create.html:261 +#: ops/templates/ops/command_execution_create.html:264 msgid "Pending" msgstr "等待" @@ -2966,7 +2999,7 @@ msgstr "命令执行列表" msgid "Command execution" msgstr "命令执行" -#: orgs/mixins.py:81 orgs/models.py:24 +#: orgs/mixins.py:83 orgs/models.py:24 msgid "Organization" msgstr "组织管理" @@ -3167,12 +3200,12 @@ msgstr "添加用户组" #: perms/views/asset_permission.py:33 perms/views/asset_permission.py:65 #: perms/views/asset_permission.py:80 perms/views/asset_permission.py:95 #: perms/views/asset_permission.py:130 perms/views/asset_permission.py:162 -#: perms/views/remote_app_permission.py:33 -#: perms/views/remote_app_permission.py:48 -#: perms/views/remote_app_permission.py:63 -#: perms/views/remote_app_permission.py:76 -#: perms/views/remote_app_permission.py:102 -#: perms/views/remote_app_permission.py:138 templates/_nav.html:41 +#: perms/views/remote_app_permission.py:32 +#: perms/views/remote_app_permission.py:47 +#: perms/views/remote_app_permission.py:62 +#: perms/views/remote_app_permission.py:75 +#: perms/views/remote_app_permission.py:101 +#: perms/views/remote_app_permission.py:137 templates/_nav.html:41 #: xpack/plugins/orgs/templates/orgs/org_list.html:21 msgid "Perms" msgstr "权限管理" @@ -3201,27 +3234,27 @@ msgstr "资产授权用户列表" msgid "Asset permission asset list" msgstr "资产授权资产列表" -#: perms/views/remote_app_permission.py:34 +#: perms/views/remote_app_permission.py:33 msgid "RemoteApp permission list" msgstr "远程应用授权列表" -#: perms/views/remote_app_permission.py:49 +#: perms/views/remote_app_permission.py:48 msgid "Create RemoteApp permission" msgstr "创建远程应用授权规则" -#: perms/views/remote_app_permission.py:64 +#: perms/views/remote_app_permission.py:63 msgid "Update RemoteApp permission" msgstr "更新远程应用授权规则" -#: perms/views/remote_app_permission.py:77 +#: perms/views/remote_app_permission.py:76 msgid "RemoteApp permission detail" msgstr "远程应用授权详情" -#: perms/views/remote_app_permission.py:103 +#: perms/views/remote_app_permission.py:102 msgid "RemoteApp permission user list" msgstr "远程应用授权用户列表" -#: perms/views/remote_app_permission.py:139 +#: perms/views/remote_app_permission.py:138 msgid "RemoteApp permission RemoteApp list" msgstr "远程应用授权远程应用列表" @@ -4380,7 +4413,7 @@ msgstr "生成重置密码链接,通过邮件发送给用户" msgid "Set password" msgstr "设置密码" -#: users/forms.py:117 xpack/plugins/change_auth_plan/models.py:83 +#: users/forms.py:117 xpack/plugins/change_auth_plan/models.py:86 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:51 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:69 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:57 @@ -5284,23 +5317,17 @@ msgstr "定时执行" #: xpack/plugins/change_auth_plan/forms.py:120 msgid "" -"Tips: Currently only unix-like assets are supported, while Windows assets " -"are not" -msgstr "提示:目前仅支持类Unix资产,暂不支持Windows资产" - -#: xpack/plugins/change_auth_plan/forms.py:122 -msgid "" "Tips: The username of the user on the asset to be modified. if the user " "exists, change the password; If the user does not exist, create the user." msgstr "" "提示:用户名为将要修改的资产上的用户的用户名。如果用户存在,则修改密码;如果" "用户不存在,则创建用户。" -#: xpack/plugins/change_auth_plan/forms.py:126 +#: xpack/plugins/change_auth_plan/forms.py:124 msgid "Tips: (Units: hour)" msgstr "提示:(单位: 时)" -#: xpack/plugins/change_auth_plan/forms.py:127 +#: xpack/plugins/change_auth_plan/forms.py:125 msgid "" "eg: Every Sunday 03:05 run <5 3 * * 0>
Tips: Using 5 digits linux " "crontab expressions ( -1) { assetsNodeId.push(node.id); assetsNode.push(node) } diff --git a/apps/ops/utils.py b/apps/ops/utils.py index 4a7def4fa..70cba27ba 100644 --- a/apps/ops/utils.py +++ b/apps/ops/utils.py @@ -18,7 +18,7 @@ def update_or_create_ansible_task( run_as_admin=False, run_as=None, become_info=None, ): if not hosts or not tasks or not task_name: - return + return None, None set_to_root_org() defaults = { 'name': task_name, diff --git a/apps/orgs/mixins.py b/apps/orgs/mixins.py index f41ffccbe..f6edd545e 100644 --- a/apps/orgs/mixins.py +++ b/apps/orgs/mixins.py @@ -9,8 +9,10 @@ from django.forms import ModelForm from django.http.response import HttpResponseForbidden from django.core.exceptions import ValidationError from rest_framework import serializers +from rest_framework.validators import UniqueTogetherValidator from common.utils import get_logger +from common.validators import ProjectUniqueValidator from .utils import ( current_org, set_current_org, set_to_root_org, get_current_org_id ) @@ -216,3 +218,14 @@ class OrgResourceSerializerMixin(serializers.Serializer): 但是coco需要资产的org_id字段,所以修改为CharField类型 """ org_id = serializers.CharField(default=get_current_org_id) + + def get_validators(self): + _validators = super().get_validators() + validators = [] + + for v in _validators: + if isinstance(v, UniqueTogetherValidator) \ + and "org_id" in v.fields: + v = ProjectUniqueValidator(v.queryset, v.fields) + validators.append(v) + return validators diff --git a/apps/perms/api/user_permission.py b/apps/perms/api/user_permission.py index b4f8fc07e..1445ccac6 100644 --- a/apps/perms/api/user_permission.py +++ b/apps/perms/api/user_permission.py @@ -154,7 +154,7 @@ class UserGrantedAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, ListAPIV util = AssetPermissionUtil(user, cache_policy=self.cache_policy) assets = util.get_assets() for k, v in assets.items(): - system_users_granted = [s for s in v if s.protocol == k.protocol] + system_users_granted = [s for s in v if k.has_protocol(s.protocol)] k.system_users_granted = system_users_granted queryset.append(k) return queryset @@ -215,8 +215,7 @@ class UserGrantedNodesWithAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, for node, _assets in nodes.items(): assets = _assets.keys() for k, v in _assets.items(): - system_users_granted = [s for s in v if - s.protocol == k.protocol] + system_users_granted = [s for s in v if k.has_protocol(s.protocol)] k.system_users_granted = system_users_granted node.assets_granted = assets queryset.append(node) @@ -364,7 +363,7 @@ class UserGrantedNodeChildrenApi(UserPermissionCacheMixin, ListAPIView): for asset, system_users in nodes_granted[node].items(): fake_node = asset.as_node() fake_node.assets_amount = 0 - system_users = [s for s in system_users if s.protocol == asset.protocol] + system_users = [s for s in system_users if asset.has_protocol(s.protocol)] fake_node.asset.system_users_granted = system_users fake_node.key = node.key + ':0' fake_nodes.append(fake_node) @@ -389,7 +388,7 @@ class UserGrantedNodeChildrenApi(UserPermissionCacheMixin, ListAPIView): fake_node = asset.as_node() fake_node.assets_amount = 0 system_users = [s for s in system_users if - s.protocol == asset.protocol] + asset.has_protocol(s.protocol)] fake_node.asset.system_users_granted = system_users fake_node.key = node.key + ':0' matched_assets.append(fake_node) diff --git a/apps/perms/hands.py b/apps/perms/hands.py index 2a208fefa..5f3aafd84 100644 --- a/apps/perms/hands.py +++ b/apps/perms/hands.py @@ -3,11 +3,12 @@ from common.permissions import AdminUserRequiredMixin from users.models import User, UserGroup -from assets.models import Asset, SystemUser, Node, RemoteApp +from assets.models import Asset, SystemUser, Node from assets.serializers import ( AssetGrantedSerializer, NodeSerializer ) from applications.serializers import RemoteAppSerializer +from applications.models import RemoteApp diff --git a/apps/perms/utils/asset_permission.py b/apps/perms/utils/asset_permission.py index ed9f8f1a3..924f36675 100644 --- a/apps/perms/utils/asset_permission.py +++ b/apps/perms/utils/asset_permission.py @@ -238,7 +238,7 @@ class AssetPermissionUtil: for perm in permissions: actions = perm.actions.all() for asset in perm.assets.all().valid().prefetch_related('nodes'): - system_users = perm.system_users.filter(protocol=asset.protocol) + system_users = perm.system_users.filter(protocol__in=asset.protocols_name) system_users = self._structured_system_user(system_users, actions) assets[asset].update(system_users) return assets @@ -255,7 +255,7 @@ class AssetPermissionUtil: _assets = node.get_all_assets().valid().prefetch_related('nodes') for asset in _assets: for system_user, attr_dict in system_users.items(): - if system_user.protocol != asset.protocol: + if not asset.has_protocol(system_user.protocol): continue if system_user in assets[asset]: actions = assets[asset][system_user]['actions'] @@ -279,15 +279,12 @@ class AssetPermissionUtil: resource=resource ) - @property def node_key(self): return self.get_cache_key('NODES_WITH_ASSETS') - @property def asset_key(self): return self.get_cache_key('ASSETS') - @property def system_key(self): return self.get_cache_key('SYSTEM_USER') @@ -457,7 +454,7 @@ def parse_node_to_tree_node(node): def parse_asset_to_tree_node(node, asset, system_users): - system_users_protocol_matched = [s for s in system_users if s.protocol == asset.protocol] + system_users_protocol_matched = [s for s in system_users if asset.has_protocol(s.protocol)] icon_skin = 'file' if asset.platform.lower() == 'windows': icon_skin = 'windows' @@ -490,8 +487,8 @@ def parse_asset_to_tree_node(node, asset, system_users): 'id': asset.id, 'hostname': asset.hostname, 'ip': asset.ip, - 'port': asset.port, - 'protocol': asset.protocol, + 'protocols': [{"name": p.name, "port": p.port} + for p in asset.protocols.all()], 'platform': asset.platform, 'domain': None if not asset.domain else asset.domain.id, 'is_active': asset.is_active, diff --git a/apps/perms/views/remote_app_permission.py b/apps/perms/views/remote_app_permission.py index 660d43ccd..2ab95091f 100644 --- a/apps/perms/views/remote_app_permission.py +++ b/apps/perms/views/remote_app_permission.py @@ -11,9 +11,8 @@ from django.conf import settings from common.permissions import AdminUserRequiredMixin from orgs.utils import current_org -from users.models import UserGroup -from assets.models import RemoteApp +from ..hands import RemoteApp, UserGroup from ..models import RemoteAppPermission from ..forms import RemoteAppPermissionCreateUpdateForm diff --git a/apps/static/css/jumpserver.css b/apps/static/css/jumpserver.css index c3dcfb386..b1f2633d8 100644 --- a/apps/static/css/jumpserver.css +++ b/apps/static/css/jumpserver.css @@ -453,4 +453,16 @@ div.dataTables_wrapper div.dataTables_filter { #tree-refresh .fa-refresh { font: normal normal normal 14px/1 FontAwesome !important; -} \ No newline at end of file +} + +.select2-selection__rendered span.select2-selection, .select2-container .select2-selection--single, .select2-selection__arrow { + height: 34px !important; +} + +.select2-selection { + border-radius: 0 !important; +} + +span.select2-selection__placeholder { + line-height: 34px !important; +} diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js index 0197312fd..c3805920b 100644 --- a/apps/static/js/jumpserver.js +++ b/apps/static/js/jumpserver.js @@ -165,11 +165,13 @@ function formSubmit(props) { /* { "form": $("form"), + "data": {}, "url": "", "method": "POST", "redirect_to": "", "success": function(data, textStatue, jqXHR){}, - "error": function(jqXHR, textStatus, errorThrown) {} + "error": function(jqXHR, textStatus, errorThrown) {}, + "message": "", } */ props = props || {}; @@ -183,6 +185,10 @@ function formSubmit(props) { dataType: props.data_type || "json" }).done(function (data, textState, jqXHR) { if (redirect_to) { + if (props.message) { + var messages="ed65330a45559c87345a0eb6ac7812d18d0d8976$[[\"__json_message\"\0540\05425\054\"asdfasdf \\u521b\\u5efa\\u6210\\u529f\"]]" + setCookie("messages", messages) + } location.href = redirect_to; } else if (typeof props.success === 'function') { return props.success(data, textState, jqXHR); @@ -230,7 +236,15 @@ function formSubmit(props) { var help_msg = v.join("
") ; helpBlockRef.html(help_msg); } else { - noneFieldErrorMsg += v + '
'; + $.each(v, function (kk, vv) { + if (typeof errors === "object") { + $.each(vv, function (kkk, vvv) { + noneFieldErrorMsg += " " + vvv + '
'; + }) + } else{ + noneFieldErrorMsg += vv + '
'; + } + }) } }); if (noneFieldErrorRef.length === 1 && noneFieldErrorMsg !== '') {