Dev ansible windows 2 (#2783)

* [Update] 改密支持windows

* [Update] 修改asset表结构

* [Feature] Windows支持批量改密、测试可连接性等功能

* [Update] 处理创建资产时labels的问题

* [Update] 优化测试管理系统、系统用户可连接性任务执行逻辑

* [Update] 优化ansible任务逻辑;添加自动推送rdp系统用户功能

* [Update] 添加翻译

* [Update] 优化ansible任务逻辑(测试系统用户可连接性, 通过协议过滤资产)

* [Update] 更新翻译

* [Update] 更新翻译

* [Update] 推送windows系统用户,默认添加到Users、Remote Desktop Users组中

* [Update] 优化小细节

* [Update] 更新翻译,删除多余代码

* [Update] 更新翻译信息
pull/2794/head
老广 2019-06-13 18:58:43 +08:00 committed by BaiJiangJie
parent 9f9f22548f
commit ddafd7ba26
39 changed files with 1203 additions and 557 deletions

View File

@ -16,7 +16,7 @@ from django.urls import reverse_lazy
from django.core.cache import cache from django.core.cache import cache
from django.db.models import Q 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.utils import get_logger, get_object_or_none
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser 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. API endpoint that allows Asset to be viewed or edited.
""" """
@ -47,6 +47,7 @@ class AssetViewSet(IDInCacheFilterMixin, LabelFilter, BulkModelViewSet):
serializer_class = serializers.AssetSerializer serializer_class = serializers.AssetSerializer
pagination_class = LimitOffsetPagination pagination_class = LimitOffsetPagination
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
success_message = _("%(hostname)s was %(action)s successfully")
def set_assets_node(self, assets): def set_assets_node(self, assets):
if not isinstance(assets, list): if not isinstance(assets, list):
@ -169,8 +170,8 @@ class AssetGatewayApi(generics.RetrieveAPIView):
asset = get_object_or_404(Asset, pk=asset_id) asset = get_object_or_404(Asset, pk=asset_id)
if asset.domain and \ if asset.domain and \
asset.domain.gateways.filter(protocol=asset.protocol).exists(): asset.domain.gateways.filter(protocol='ssh').exists():
gateway = random.choice(asset.domain.gateways.filter(protocol=asset.protocol)) gateway = random.choice(asset.domain.gateways.filter(protocol='ssh'))
serializer = serializers.GatewayWithAuthSerializer(instance=gateway) serializer = serializers.GatewayWithAuthSerializer(instance=gateway)
return Response(serializer.data) return Response(serializer.data)
else: else:

View File

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import ugettext_lazy as _
UPDATE_ASSETS_HARDWARE_TASKS = [ 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_{}" 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_{}_{}' ASSET_USER_CONN_CACHE_KEY = 'ASSET_USER_CONN_{}_{}'
TEST_ASSET_USER_CONN_TASKS = [ 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 = { TASK_OPTIONS = {

View File

@ -6,21 +6,39 @@ from django.utils.translation import gettext_lazy as _
from common.utils import get_logger from common.utils import get_logger
from orgs.mixins import OrgModelForm from orgs.mixins import OrgModelForm
from ..models import Asset, AdminUser from ..models import Asset, AdminUser, Protocol
logger = get_logger(__file__) 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): class AssetCreateForm(OrgModelForm):
PROTOCOL_CHOICES = Protocol.PROTOCOL_CHOICES
class Meta: class Meta:
model = Asset model = Asset
fields = [ fields = [
'hostname', 'ip', 'public_ip', 'port', 'comment', 'hostname', 'ip', 'public_ip', 'protocols', 'comment',
'nodes', 'is_active', 'admin_user', 'labels', 'platform', 'nodes', 'is_active', 'admin_user', 'labels', 'platform',
'domain', 'protocol', 'domain',
] ]
widgets = { widgets = {
'nodes': forms.SelectMultiple(attrs={ 'nodes': forms.SelectMultiple(attrs={
@ -32,7 +50,6 @@ class AssetCreateForm(OrgModelForm):
'labels': forms.SelectMultiple(attrs={ 'labels': forms.SelectMultiple(attrs={
'class': 'select2', 'data-placeholder': _('Label') 'class': 'select2', 'data-placeholder': _('Label')
}), }),
'port': forms.TextInput(),
'domain': forms.Select(attrs={ 'domain': forms.Select(attrs={
'class': 'select2', 'data-placeholder': _('Domain') 'class': 'select2', 'data-placeholder': _('Domain')
}), }),
@ -54,9 +71,9 @@ class AssetUpdateForm(OrgModelForm):
class Meta: class Meta:
model = Asset model = Asset
fields = [ fields = [
'hostname', 'ip', 'port', 'nodes', 'is_active', 'platform', 'hostname', 'ip', 'protocols', 'nodes', 'is_active', 'platform',
'public_ip', 'number', 'comment', 'admin_user', 'labels', 'public_ip', 'number', 'comment', 'admin_user', 'labels',
'domain', 'protocol', 'domain',
] ]
widgets = { widgets = {
'nodes': forms.SelectMultiple(attrs={ 'nodes': forms.SelectMultiple(attrs={
@ -68,7 +85,6 @@ class AssetUpdateForm(OrgModelForm):
'labels': forms.SelectMultiple(attrs={ 'labels': forms.SelectMultiple(attrs={
'class': 'select2', 'data-placeholder': _('Label') 'class': 'select2', 'data-placeholder': _('Label')
}), }),
'port': forms.TextInput(),
'domain': forms.Select(attrs={ 'domain': forms.Select(attrs={
'class': 'select2', 'data-placeholder': _('Domain') 'class': 'select2', 'data-placeholder': _('Domain')
}), }),
@ -101,8 +117,8 @@ class AssetBulkUpdateForm(OrgModelForm):
class Meta: class Meta:
model = Asset model = Asset
fields = [ fields = [
'assets', 'port', 'admin_user', 'labels', 'platform', 'assets', 'admin_user', 'labels', 'platform',
'protocol', 'domain', 'domain',
] ]
widgets = { widgets = {
'labels': forms.SelectMultiple( 'labels': forms.SelectMultiple(

View File

@ -100,16 +100,18 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
private_key, public_key = super().gen_keys() private_key, public_key = super().gen_keys()
if login_mode == SystemUser.LOGIN_MANUAL or \ if login_mode == SystemUser.LOGIN_MANUAL or \
protocol in [SystemUser.PROTOCOL_RDP, protocol in [SystemUser.PROTOCOL_TELNET,
SystemUser.PROTOCOL_TELNET,
SystemUser.PROTOCOL_VNC]: SystemUser.PROTOCOL_VNC]:
system_user.auto_push = 0 system_user.auto_push = 0
auto_generate_key = False
system_user.save() system_user.save()
auto_generate_key = False
if auto_generate_key: if auto_generate_key:
logger.info('Auto generate key and set system user auth') logger.info('Auto generate key and set system user auth')
if protocol == SystemUser.PROTOCOL_SSH:
system_user.auto_gen_auth() system_user.auto_gen_auth()
elif protocol == SystemUser.PROTOCOL_RDP:
system_user.auto_gen_auth_password()
else: else:
system_user.set_auth(password=password, private_key=private_key, system_user.set_auth(password=password, private_key=private_key,
public_key=public_key) public_key=public_key)

View File

@ -15,4 +15,9 @@ class Migration(migrations.Migration):
name='ip', name='ip',
field=models.CharField(db_index=True, max_length=128, verbose_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'),
),
] ]

View File

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

View File

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

View File

@ -8,4 +8,3 @@ from .asset import *
from .cmd_filter import * from .cmd_filter import *
from .utils import * from .utils import *
from .authbook import * from .authbook import *
from applications.models.remote_app import *

View File

@ -12,11 +12,12 @@ from django.db import models
from django.db.models import Q from django.db.models import Q
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.cache import cache from django.core.cache import cache
from django.core.validators import MinValueValidator, MaxValueValidator
from .user import AdminUser, SystemUser from .user import AdminUser, SystemUser
from orgs.mixins import OrgModelMixin, OrgManager from orgs.mixins import OrgModelMixin, OrgManager
__all__ = ['Asset'] __all__ = ['Asset', 'Protocol']
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -47,6 +48,35 @@ class AssetQuerySet(models.QuerySet):
return self.active() 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): class Asset(OrgModelMixin):
# Important # Important
PLATFORM_CHOICES = ( PLATFORM_CHOICES = (
@ -59,22 +89,15 @@ class Asset(OrgModelMixin):
('Other', 'Other'), ('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) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True) ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True)
hostname = models.CharField(max_length=128, verbose_name=_('Hostname')) 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')) 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')) 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) 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")) 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")) admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, null=True, verbose_name=_("Admin user"))
# Some information # 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')) number = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Asset number'))
# Collect # Collect
@ -110,7 +133,7 @@ class Asset(OrgModelMixin):
date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) 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')) 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_{}' CONNECTIVITY_CACHE_KEY = '_JMS_ASSET_CONNECTIVITY_{}'
UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3) UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3)
CONNECTIVITY_CHOICES = ( CONNECTIVITY_CHOICES = (
@ -131,19 +154,53 @@ class Asset(OrgModelMixin):
return True, '' return True, ''
return False, warning return False, warning
def support_ansible(self): @property
if self.platform in ("Windows", "Windows2016", "Other"): def protocols_name(self):
return False names = []
if self.protocol != 'ssh': for protocol in self.protocols.all():
return False names.append(protocol.name)
return True return names
def is_unixlike(self): def has_protocol(self, name):
if self.platform not in ("Windows", "Windows2016"): 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 return True
else: else:
return False 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): def get_nodes(self):
from .node import Node from .node import Node
nodes = self.nodes.all() or [Node.root()] 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) filter_arg |= Q(Q(org_id__isnull=True) | Q(org_id=''), hostname__in=hosts)
return Asset.objects.filter(filter_arg) 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 @property
def hardware_info(self): def hardware_info(self):
if self.cpu_count: if self.cpu_count:
@ -196,14 +262,16 @@ class Asset(OrgModelMixin):
cache.set(key, value, 3600*2) cache.set(key, value, 3600*2)
def get_auth_info(self): def get_auth_info(self):
if self.admin_user: if not self.admin_user:
return {}
self.admin_user.load_specific_asset_auth(self) self.admin_user.load_specific_asset_auth(self)
return { info = {
'username': self.admin_user.username, 'username': self.admin_user.username,
'password': self.admin_user.password, 'password': self.admin_user.password,
'private_key': self.admin_user.private_key_file, 'private_key': self.admin_user.private_key_file,
'become': self.admin_user.become_info,
} }
return info
def as_node(self): def as_node(self):
from .node import Node from .node import Node
@ -215,35 +283,6 @@ class Asset(OrgModelMixin):
fake_node.is_node = False fake_node.is_node = False
return fake_node 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): def as_tree_node(self, parent_node):
from common.tree import TreeNode from common.tree import TreeNode
icon_skin = 'file' icon_skin = 'file'
@ -265,9 +304,11 @@ class Asset(OrgModelMixin):
'id': self.id, 'id': self.id,
'hostname': self.hostname, 'hostname': self.hostname,
'ip': self.ip, 'ip': self.ip,
'port': self.port, 'protocols': [
{"name": p.name, "port": p.port}
for p in self.protocols.all()
],
'platform': self.platform, 'platform': self.platform,
'protocol': self.protocol,
} }
} }
} }
@ -291,10 +332,10 @@ class Asset(OrgModelMixin):
asset = cls(ip='.'.join(ip), asset = cls(ip='.'.join(ip),
hostname=forgery_py.internet.user_name(True), hostname=forgery_py.internet.user_name(True),
admin_user=choice(AdminUser.objects.all()), admin_user=choice(AdminUser.objects.all()),
port=22,
created_by='Fake') created_by='Fake')
try: try:
asset.save() asset.save()
asset.protocols.create(name="ssh", port=22)
if nodes and len(nodes) > 3: if nodes and len(nodes) > 3:
_nodes = random.sample(nodes, 3) _nodes = random.sample(nodes, 3)
else: else:

View File

@ -158,6 +158,10 @@ class AssetUser(OrgModelMixin):
private_key=private_key, private_key=private_key,
public_key=public_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): def _to_secret_json(self):
"""Push system user use it""" """Push system user use it"""
return { return {

View File

@ -201,7 +201,7 @@ class SystemUser(AssetUser):
return self.get_login_mode_display() return self.get_login_mode_display()
def is_need_push(self): 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 return True
else: else:
return False return False

View File

@ -1,13 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from rest_framework import serializers from rest_framework import serializers
from rest_framework.validators import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgResourceSerializerMixin from orgs.mixins import OrgResourceSerializerMixin
from common.mixins import BulkSerializerMixin from common.mixins import BulkSerializerMixin
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
from ..models import Asset from ..models import Asset, Protocol
from .system_user import AssetSystemUserSerializer from .system_user import AssetSystemUserSerializer
__all__ = [ __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: class Meta:
model = Asset model = Asset
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
# validators = [] # 解决批量导入时unique_together字段校验失败
fields = [ fields = [
'id', 'org_id', 'org_name', 'ip', 'hostname', 'protocol', 'port', 'id', 'org_id', 'org_name', 'ip', 'hostname', 'protocol', 'port',
'platform', 'is_active', 'public_ip', 'domain', 'admin_user', 'protocols', 'platform', 'is_active', 'public_ip', 'domain',
'nodes', 'labels', 'number', 'vendor', 'model', 'sn', 'admin_user', 'nodes', 'labels', 'number', 'vendor', 'model', 'sn',
'cpu_model', 'cpu_count', 'cpu_cores', 'cpu_vcpus', 'memory', 'cpu_model', 'cpu_count', 'cpu_cores', 'cpu_vcpus', 'memory',
'disk_total', 'disk_info', 'os', 'os_version', 'os_arch', 'disk_total', 'disk_info', 'os', 'os_version', 'os_arch',
'hostname_raw', 'comment', 'created_by', 'date_created', 'hostname_raw', 'comment', 'created_by', 'date_created',
'hardware_info', 'connectivity' 'hardware_info', 'connectivity'
] ]
read_only_fields = ( 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', 'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info',
'os', 'os_version', 'os_arch', 'hostname_raw', 'os', 'os_version', 'os_arch', 'hostname_raw',
'created_by', 'date_created', 'created_by', 'date_created',
@ -43,7 +52,6 @@ class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer, OrgResou
'hardware_info': {'label': _('Hardware info')}, 'hardware_info': {'label': _('Hardware info')},
'connectivity': {'label': _('Connectivity')}, 'connectivity': {'label': _('Connectivity')},
'org_name': {'label': _('Org name')} 'org_name': {'label': _('Org name')}
} }
@classmethod @classmethod
@ -53,18 +61,64 @@ class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer, OrgResou
.select_related('admin_user') .select_related('admin_user')
return queryset return queryset
def get_field_names(self, declared_fields, info): @staticmethod
fields = super().get_field_names(declared_fields, info) def validate_protocols(attr):
fields.extend([ protocols_name = [i.get("name", "ssh") for i in attr]
'hardware_info', 'connectivity', 'org_name' errors = [{} for i in protocols_name]
]) for i, name in enumerate(protocols_name):
return fields 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 AssetAsNodeSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Asset model = Asset
fields = ['id', 'hostname', 'ip', 'port', 'platform', 'protocol'] fields = ['id', 'hostname', 'ip', 'platform', 'protocols']
class AssetGrantedSerializer(serializers.ModelSerializer): class AssetGrantedSerializer(serializers.ModelSerializer):
@ -78,9 +132,9 @@ class AssetGrantedSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Asset model = Asset
fields = ( fields = (
"id", "hostname", "ip", "port", "system_users_granted", "id", "hostname", "ip", "system_users_granted",
"is_active", "system_users_join", "os", 'domain', "is_active", "system_users_join", "os", 'domain',
"platform", "comment", "protocol", "org_id", "org_name", "platform", "comment", "protocols", "org_id", "org_name",
) )
@staticmethod @staticmethod

View File

@ -5,6 +5,7 @@ from django.db.models.signals import post_save, m2m_changed, post_delete
from django.dispatch import receiver from django.dispatch import receiver
from common.utils import get_logger from common.utils import get_logger
from common.decorator import on_transaction_commit
from .models import Asset, SystemUser, Node, AuthBook from .models import Asset, SystemUser, Node, AuthBook
from .tasks import ( from .tasks import (
update_assets_hardware_info_util, 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") @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): def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
if created: if created:
logger.info("Asset `{}` create signal received".format(instance)) logger.info("Asset `{}` create signal received".format(instance))
# 获取资产硬件信息
update_asset_hardware_info_on_created(instance) update_asset_hardware_info_on_created(instance)
test_asset_conn_on_created(instance) test_asset_conn_on_created(instance)

View File

@ -3,6 +3,7 @@ import json
import re import re
import os import os
from collections import defaultdict
from celery import shared_task from celery import shared_task
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.core.cache import cache 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) msg = _("Asset has been disabled, skipped: {}").format(asset)
logger.info(msg) logger.info(msg)
return False return False
if not asset.support_ansible(): if not asset.is_support_ansible():
msg = _("Asset may not be support ansible, skipped: {}").format(asset) msg = _("Asset may not be support ansible, skipped: {}").format(asset)
logger.info(msg) logger.info(msg)
return False return False
@ -45,10 +46,21 @@ def clean_hosts(assets):
continue continue
clean_assets.append(asset) clean_assets.append(asset)
if not clean_assets: if not clean_assets:
print(_("No assets matched, stop task")) logger.info(_("No assets matched, stop task"))
return clean_assets 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 @shared_task
def set_assets_hardware_info(assets, result, **kwargs): 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_total = '%s %s' % sum_capacity(disk_info.values())
___disk_info = json.dumps(disk_info) ___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 = info.get('ansible_distribution', 'Unknown')
___os_version = info.get('ansible_distribution_version', 'Unknown') ___os_version = info.get('ansible_distribution_version', 'Unknown')
___os_arch = info.get('ansible_architecture', '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: if task_name is None:
task_name = _("Test assets connectivity") task_name = _("Test assets connectivity")
hosts = clean_hosts(assets) hosts = clean_hosts(assets)
if not hosts: if not hosts:
return {} return {}
tasks = const.TEST_ADMIN_USER_CONN_TASKS
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
)
created_by = assets[0].org_id 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, created = update_or_create_ansible_task(
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all', task_name=task_name, hosts=value['hosts'], tasks=value['tasks'],
options=const.TASK_OPTIONS, run_as_admin=True, created_by=created_by, pattern='all', options=const.TASK_OPTIONS, run_as_admin=True,
created_by=created_by,
) )
result = task.run() result = task.run()
summary = result[1] summary = result[1]
results_summary['success'] &= summary['success']
results_summary['contacted'].update(summary['contacted'])
results_summary['dark'].update(summary['dark'])
for asset in assets: for asset in assets:
if asset.hostname in summary.get('dark', {}): if asset.hostname in results_summary.get('dark', {}):
asset.connectivity = asset.UNREACHABLE asset.connectivity = asset.UNREACHABLE
elif asset.hostname in summary.get('contacted', []): elif asset.hostname in results_summary.get('contacted', []):
asset.connectivity = asset.REACHABLE asset.connectivity = asset.REACHABLE
else: else:
asset.connectivity = asset.UNKNOWN asset.connectivity = asset.UNKNOWN
return summary
return results_summary
@shared_task @shared_task
@ -243,8 +283,7 @@ def test_admin_user_connectivity_manual(admin_user):
## System user connective ## ## System user connective ##
@shared_task @shared_task
def set_system_user_connectivity_info(system_user, result): def set_system_user_connectivity_info(system_user, summary):
summary = result[1]
system_user.connectivity = summary system_user.connectivity = summary
@ -258,18 +297,50 @@ def test_system_user_connectivity_util(system_user, assets, task_name):
:return: :return:
""" """
from ops.utils import update_or_create_ansible_task from ops.utils import update_or_create_ansible_task
tasks = const.TEST_SYSTEM_USER_CONN_TASKS
hosts = clean_hosts(assets) hosts = clean_hosts(assets)
if not hosts: if not hosts:
return {} return {}
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
)
for _, value in hosts_category.items():
if not value['hosts']:
continue
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name, hosts=hosts, tasks=tasks, pattern='all', task_name=task_name, hosts=value['hosts'], tasks=value['tasks'],
options=const.TASK_OPTIONS, pattern='all', options=const.TASK_OPTIONS,
run_as=system_user.username, created_by=system_user.org_id, run_as=system_user.username,
created_by=system_user.org_id,
) )
result = task.run() result = task.run()
set_system_user_connectivity_info(system_user, result) summary = result[1]
return result 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 @shared_task
@ -301,11 +372,7 @@ def test_system_user_connectivity_period():
#### Push system user tasks #### #### Push system user tasks ####
def get_push_system_user_tasks(system_user): def get_push_linux_system_user_tasks(system_user):
# Set root as system user is dangerous
if system_user.username == "root":
return []
tasks = [] tasks = []
if system_user.password: if system_user.password:
tasks.append({ tasks.append({
@ -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 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 from ops.utils import update_or_create_ansible_task
if not system_user.is_need_push(): if not system_user.is_need_push():
msg = _("Push system user task skip, auto push not enable or " 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) 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) hosts = clean_hosts(assets)
if not hosts: if not hosts:
return {} return {}
hosts = clean_hosts_by_protocol(system_user, hosts)
if not hosts:
return {}
for host in hosts: for host in hosts:
system_user.load_specific_asset_auth(host) 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, created = update_or_create_ansible_task(
task_name=task_name, hosts=[host], tasks=tasks, pattern='all', task_name=task_name, hosts=[host], tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True, options=const.TASK_OPTIONS, run_as_admin=True,
@ -423,6 +543,23 @@ def test_admin_user_connectability_period():
pass 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 @shared_task
def set_asset_user_connectivity_info(asset_user, result): def set_asset_user_connectivity_info(asset_user, result):
summary = result[1] summary = result[1]
@ -437,10 +574,14 @@ def test_asset_user_connectivity_util(asset_user, task_name):
:return: :return:
""" """
from ops.utils import update_or_create_ansible_task 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): if not check_asset_can_run_ansible(asset_user.asset):
return return
tasks = get_test_asset_user_connectivity_tasks(asset_user.asset)
if not tasks:
return
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name, hosts=[asset_user.asset], tasks=tasks, pattern='all', task_name, hosts=[asset_user.asset], tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, options=const.TASK_OPTIONS,

View File

@ -95,41 +95,97 @@ var auto_push_id = '#' + '{{ form.auto_push.id_for_label }}';
var sudo_id = '#' + '{{ form.sudo.id_for_label }}'; var sudo_id = '#' + '{{ form.sudo.id_for_label }}';
var shell_id = '#' + '{{ form.shell.id_for_label }}'; var shell_id = '#' + '{{ form.shell.id_for_label }}';
var need_change_field = [ function autoLoginModeProtocol() {
auto_generate_key, private_key_id, auto_push_id, sudo_id, shell_id // 协议+自动登录模式字段控制
]; $('#auth_title_id').removeClass('hidden');
var need_change_field_login_mode = [
auto_generate_key, private_key_id, auto_push_id, password_id
];
function protocolChange() {
var protocol = $(protocol_id + " option:selected").text(); var protocol = $(protocol_id + " option:selected").text();
if (protocol === 'rdp' || protocol === 'vnc') { if (protocol === 'rdp') {
$('.auth-fields').removeClass('hidden'); 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'); $('#command-filter-block').addClass('hidden');
$.each(need_change_field, function (index, value) { $(sudo_id).closest('.form-group').addClass('hidden');
$(value).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)') { else if (protocol === 'telnet (beta)') {
$('.auth-fields').removeClass('hidden'); $('.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'); $('#command-filter-block').removeClass('hidden');
$.each(need_change_field, function (index, value) { $(sudo_id).closest('.form-group').addClass('hidden');
$(value).closest('.form-group').addClass('hidden') $(shell_id).closest('.form-group').addClass('hidden');
});
} }
else { else {
if($(login_mode_id).val() === 'manual'){ 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');
$(sudo_id).closest('.form-group').removeClass('hidden'); $(sudo_id).closest('.form-group').removeClass('hidden');
$(shell_id).closest('.form-group').removeClass('hidden'); $(shell_id).closest('.form-group').removeClass('hidden');
return
} }
authFieldsDisplay(); }
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'); $('#command-filter-block').removeClass('hidden');
$.each(need_change_field, function (index, value) { $(sudo_id).closest('.form-group').addClass('hidden');
$(value).closest('.form-group').removeClass('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() { function authFieldsDisplay() {
@ -139,34 +195,29 @@ function authFieldsDisplay() {
$('.auth-fields').removeClass('hidden'); $('.auth-fields').removeClass('hidden');
} }
} }
function loginModeChange(){ function fieldDisplay(){
if ($(login_mode_id).val() === 'manual'){ var login_mode = $(login_mode_id).val();
$('#auth_title_id').addClass('hidden'); if (login_mode === 'manual'){
$.each(need_change_field_login_mode, function(index, value){ manualLoginModeProtocol();
$(value).closest('.form-group').addClass('hidden')
})
} }
else if($(login_mode_id).val() === 'auto'){ else if(login_mode === 'auto'){
$('#auth_title_id').removeClass('hidden'); autoLoginModeProtocol();
$(password_id).closest('.form-group').removeClass('hidden')
protocolChange();
} }
} }
$(document).ready(function () { $(document).ready(function () {
$('.select2').select2(); $('.select2').select2();
authFieldsDisplay(); authFieldsDisplay();
protocolChange(); fieldDisplay();
loginModeChange();
})
.on('change', protocol_id, function(){
protocolChange();
}) })
.on('change', auto_generate_key, function(){ .on('change', auto_generate_key, function(){
authFieldsDisplay(); authFieldsDisplay();
}) })
.on('change', login_mode_id, function(){ .on('change', login_mode_id, function(){
loginModeChange(); fieldDisplay();
})
.on('change', protocol_id, function(){
fieldDisplay();
}) })
</script> </script>

View File

@ -62,7 +62,7 @@
<div class="panel-body"> <div class="panel-body">
<table class="table"> <table class="table">
<tbody> <tbody>
{% if asset.protocol == 'ssh' %} {% if asset.is_support_ansible %}
<tr class="no-borders-tr"> <tr class="no-borders-tr">
<td>{% trans 'Test connective' %}:</td> <td>{% trans 'Test connective' %}:</td>
<td> <td>
@ -118,7 +118,7 @@ function initAssetUserTable() {
var view_btn = ' <a class="btn btn-xs btn-primary btn-view-auth" data-username="DEFAULT_USERNAME">{% trans "View auth" %}</a>'.replace("DEFAULT_USERNAME", cellData); var view_btn = ' <a class="btn btn-xs btn-primary btn-view-auth" data-username="DEFAULT_USERNAME">{% trans "View auth" %}</a>'.replace("DEFAULT_USERNAME", cellData);
var test_btn = ' <a class="btn btn-xs btn-info btn-test-connective" data-username="DEFAULT_USERNAME">{% trans "Test" %}</a>'.replace("DEFAULT_USERNAME", cellData); var test_btn = ' <a class="btn btn-xs btn-info btn-test-connective" data-username="DEFAULT_USERNAME">{% trans "Test" %}</a>'.replace("DEFAULT_USERNAME", cellData);
btn += view_btn; btn += view_btn;
{% if asset.protocol == 'ssh' %} {% if asset.is_support_ansible %}
btn += test_btn; btn += test_btn;
{% endif %} {% endif %}
$(td).html(btn); $(td).html(btn);

View File

@ -16,12 +16,24 @@
<h3>{% trans 'Basic' %}</h3> <h3>{% trans 'Basic' %}</h3>
{% bootstrap_field form.hostname layout="horizontal" %} {% bootstrap_field form.hostname layout="horizontal" %}
{% bootstrap_field form.ip 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.platform layout="horizontal" %}
{% bootstrap_field form.public_ip layout="horizontal" %} {% bootstrap_field form.public_ip layout="horizontal" %}
{% bootstrap_field form.domain layout="horizontal" %} {% bootstrap_field form.domain layout="horizontal" %}
<div class="hr-line-dashed"></div>
<h3>{% trans 'Protocols' %}</h3>
<div class="protocols">
{% for fm in formset.forms %}
<div class="form-group">
<div class="col-md-2 col-md-offset-2" style="text-align: right">{{ fm.name }}</div>
<div class="col-md-6">{{ fm.port }}</div>
<div class="col-md-1" style="padding: 6px 0">
<a class="btn btn-danger btn-xs btn-protocol btn-del"><span class="fa fa-minus"></span> </a>
<a class="btn btn-primary btn-xs btn-protocol btn-add" style="display: none"><span class="fa fa-plus"></span></a>
</div>
</div>
{% endfor %}
</div>
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
<h3>{% trans 'Auth' %}</h3> <h3>{% trans 'Auth' %}</h3>
{% bootstrap_field form.admin_user layout="horizontal" %} {% bootstrap_field form.admin_user layout="horizontal" %}
@ -55,6 +67,8 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% block extra %}
{% endblock %}
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
<h3>{% trans 'Other' %}</h3> <h3>{% trans 'Other' %}</h3>
@ -73,11 +87,25 @@
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
var instanceId = "{{ object.id }}";
var protocolLen = 0;
function format(item) { function format(item) {
var group = item.element.parentElement.label; var group = item.element.parentElement.label;
return group + ':' + item.text; return group + ':' + item.text;
} }
function protocolBtnShow() {
$(".btn-protocol.btn-add").hide();
$(".btn-protocol.btn-add:last").show();
var btnDel = $(".btn-protocol.btn-del");
if (btnDel.length === 1) {
btnDel.addClass("disabled")
} else {
btnDel.removeClass("disabled")
}
}
$(document).ready(function () { $(document).ready(function () {
$('.select2').select2({ $('.select2').select2({
allowClear: true allowClear: true
@ -89,20 +117,133 @@ $(document).ready(function () {
$('#id_nodes.select2').select2({ $('#id_nodes.select2').select2({
closeOnSelect: false closeOnSelect: false
}); });
$("#id_protocol").change(function (){ protocolBtnShow()
var protocol = $("#id_protocol option:selected").text(); })
var port = 22; .on("change", "#id_platform", function () {
if(protocol === 'rdp'){ if (instanceId !== "") {
port = 3389; return
} }
else if(protocol === 'telnet (beta)'){ var platform = $(this).val();
port = 23; var protocolRef = $(".protocols").find("select").first()
var protocol = protocolRef.val();
var protocolShould = "";
if (platform.startsWith("Windows")){
protocolShould = "rdp"
} else {
protocolShould = "ssh"
} }
else if(protocol === 'vnc'){ if (protocol !== protocolShould) {
port = 5901; protocolRef.val(protocolShould);
protocolRef.trigger("change")
} }
$("#id_port").val(port);
})
.on("click", ".btn-protocol.btn-del", function () {
$(this).parent().parent().remove();
protocolBtnShow()
})
.on("click", ".btn-protocol.btn-add", function () {
var protocol = "";
var protocolsRef = $(".protocols");
var firstProtocolForm = protocolsRef.children().first();
var newProtocolForm = firstProtocolForm.clone();
var protocolChoices = $.map($(firstProtocolForm.find('select option')), function (option) {
return option.value
}); });
var protocolsSet = $.map(protocolsRef.find('select option:selected'), function(option) {
return option.value
});
for (var i=0;i<protocolChoices.length;i++) {
var p = protocolChoices[i];
if (protocolsSet.indexOf(p) === -1) {
protocol = p;
break
}
}
if (protocol === "") {
return
}
if (protocolLen === 0) {
protocolLen = protocolsRef.length;
}
var selectName = "form-" + protocolLen + "-name";
var selectId = "id_" + selectName;
var portName = "form-" + protocolLen + "-port";
var portId = "id_" + portName;
newProtocolForm.find("select").prop("name", selectName).prop("id", selectId);
newProtocolForm.find("input").prop("name", portName).prop("id", portId);
newProtocolForm.find("option[value='" + protocol + "']").attr("selected", true);
protocolsRef.append(newProtocolForm);
protocolLen += 1;
$("#" + selectId).trigger("change");
protocolBtnShow()
})
.on("change", ".protocol-name", function () {
var name = $(this).val();
var port = 22;
switch (name) {
case "ssh":
port = 22;
break;
case "rdp":
port = 3389;
break;
case "telnet":
port = 21;
break;
case "vnc":
port = 5901;
break;
default:
port = 22;
break
}
$(this).parent().parent().find(".protocol-port").val(port);
})
</script>
{% block form_submit %}
<script>
$(document).ready(function () {
})
.on("submit", "form", function (evt) {
evt.preventDefault();
var the_url = '{% url 'api-assets:asset-list' %}';
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.labels === "string") {
data["labels"] = [data["labels"]];
}
if (typeof data["nodes"] == "string") {
data["nodes"] = [data["nodes"]]
}
var props = {
url: the_url,
data: data,
method: "POST",
form: form,
redirect_to: redirect_to
};
formSubmit(props);
}) })
</script> </script>
{% endblock %} {% endblock %}
{% endblock %}

View File

@ -69,12 +69,13 @@
<td><b>{{ asset.public_ip|default:"" }}</b></td> <td><b>{{ asset.public_ip|default:"" }}</b></td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Port' %}:</td> <td>{% trans 'Protocol' %}</td>
<td><b>{{ asset.port }}</b></td> <td>
</tr> {% for protocol in asset.protocols.all %}
<tr> <b>{{ protocol.name }}:</b>
<td>{% trans 'Protocol' %}:</td> {{ protocol.port }}
<td><b>{{ asset.protocol }}</b></td> {% endfor %}
</td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Admin user' %}:</td> <td>{% trans 'Admin user' %}:</td>
@ -94,7 +95,7 @@
</tr> </tr>
<tr> <tr>
<td>{% trans 'CPU' %}:</td> <td>{% trans 'CPU' %}:</td>
<td><b>{{ asset.cpu_model|default:"" }} {{ asset.cpu_count|default:"" }}*{{ asset.cpu_cores|default:"" }}</b></td> <td><b>{{ asset.cpu_info }}</b></td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Memory' %}:</td> <td>{% trans 'Memory' %}:</td>
@ -166,7 +167,7 @@
</span> </span>
</td> </td>
</tr> </tr>
{% if asset.protocol == 'ssh' %} {% if asset.is_support_ansible %}
<tr> <tr>
<td>{% trans 'Refresh hardware' %}:</td> <td>{% trans 'Refresh hardware' %}:</td>
<td> <td>

View File

@ -1,95 +1,51 @@
{% extends '_base_create_update.html' %} {% extends 'assets/asset_create.html' %}
{% load static %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load i18n %} {% load i18n %}
{% load asset_tags %}
{% load common_tags %}
{% block custom_head_css_js_create %}
<link href="{% static "css/plugins/inputTags.css" %}" rel="stylesheet">
<script src="{% static "js/plugins/inputTags.jquery.min.js" %}"></script>
{% endblock %}
{% block form %}
<form action="" method="post" class="form-horizontal">
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}
{% csrf_token %}
<h3>{% trans 'Basic' %}</h3>
{% 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" %}
<div class="hr-line-dashed"></div>
<h3>{% trans 'Auth' %}</h3>
{% bootstrap_field form.admin_user layout="horizontal" %}
<div class="hr-line-dashed"></div>
<h3>{% trans 'Node' %}</h3>
{% bootstrap_field form.nodes layout="horizontal" %}
<div class="hr-line-dashed"></div>
<h3>{% trans 'Labels' %}</h3>
<div class="form-group">
<label for="{{ form.labels.id_for_label }}" class="col-md-2 control-label">{% trans 'Label' %}</label>
<div class="col-md-9">
<select name="labels" class="select2 labels" data-placeholder="{% trans 'Label' %}" style="width: 100%" multiple="" tabindex="4" id="{{ form.labels.id_for_label }}">
{% for name, labels in form.labels.field.queryset|group_labels %}
<optgroup label="{{ name }}">
{% for label in labels %}
{% if label in form.labels.initial %}
<option value="{{ label.id }}" selected>{{ label.value }}</option>
{% else %}
<option value="{{ label.id }}">{{ label.value }}</option>
{% endif %}
{% endfor %}
</optgroup>
{% endfor %}
</select>
</div>
</div>
{% block extra %}
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
<h3>{% trans 'Configuration' %}</h3> <h3>{% trans 'Configuration' %}</h3>
{% bootstrap_field form.number layout="horizontal" %} {% bootstrap_field form.number layout="horizontal" %}
<div class="hr-line-dashed"></div>
<h3>{% trans 'Other' %}</h3>
{% bootstrap_field form.comment layout="horizontal" %}
{% bootstrap_field form.is_active layout="horizontal" %}
<div class="hr-line-dashed"></div>
<div class="form-group">
<div class="col-sm-4 col-sm-offset-2">
<button class="btn btn-white" type="reset">{% trans 'Reset' %}</button>
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
</div>
</div>
</form>
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block form_submit %}
<script> <script>
function format(item) {
var group = item.element.parentElement.label;
return group + ':' + item.text;
}
$(document).ready(function () { $(document).ready(function () {
$('.select2').select2({
allowClear: true
});
$(".labels").select2({
allowClear: true,
templateSelection: format
});
}) })
.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);
});
</script> </script>
{% endblock %} {% endblock %}

View File

@ -118,7 +118,7 @@
<div class="panel-body"> <div class="panel-body">
<table class="table"> <table class="table">
<tbody> <tbody>
<tr class="only-ssh"> <tr class="only-ssh-rdp">
<td width="50%">{% trans 'Auto push' %}:</td> <td width="50%">{% trans 'Auto push' %}:</td>
<td> <td>
<span class="pull-right"> <span class="pull-right">
@ -135,7 +135,7 @@
</td> </td>
</tr> </tr>
{% if system_user.auto_push %} {% if system_user.auto_push %}
<tr class="only-ssh"> <tr class="only-ssh-rdp">
<td width="50%">{% trans 'Push system user now' %}:</td> <td width="50%">{% trans 'Push system user now' %}:</td>
<td> <td>
<span style="float: right"> <span style="float: right">
@ -144,7 +144,7 @@
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
<tr class="only-ssh"> <tr>
<td width="50%">{% trans 'Test assets connective' %}:</td> <td width="50%">{% trans 'Test assets connective' %}:</td>
<td> <td>
<span style="float: right"> <span style="float: right">
@ -219,8 +219,12 @@ function updateCommandFilters(command_filters) {
}); });
} }
$(document).ready(function () { $(document).ready(function () {
if($('#id_protocol_type').text() === 'rdp'){ var protocol = $('#id_protocol_type').text();
$('.only-ssh').addClass('hidden') 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'); $(".panel-body .table tr:visible:first").addClass('no-borders-tr');
$('.select2').select2() $('.select2').select2()

View File

@ -9,7 +9,7 @@
{# 目前还不支持Windows的自动推送#} {# 目前还不支持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 '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 '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.' %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -23,6 +23,7 @@ from django.utils import timezone
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect from django.shortcuts import redirect
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.forms.formsets import formset_factory
from common.mixins import JSONResponseMixin from common.mixins import JSONResponseMixin
from common.utils import get_object_or_none, get_logger from common.utils import get_object_or_none, get_logger
@ -30,8 +31,6 @@ from common.permissions import AdminUserRequiredMixin
from common.const import ( from common.const import (
create_success_msg, update_success_msg, KEY_CACHE_RESOURCES_ID 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 .. import forms
from ..models import Asset, AdminUser, SystemUser, Label, Node, Domain from ..models import Asset, AdminUser, SystemUser, Label, Node, Domain
@ -102,10 +101,30 @@ class AssetCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView):
form["nodes"].initial = node form["nodes"].initial = node
return form 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): def get_context_data(self, **kwargs):
formset = self.get_protocol_formset()
context = { context = {
'app': _('Assets'), 'app': _('Assets'),
'action': _('Create asset'), 'action': _('Create asset'),
'formset': formset,
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
@ -160,10 +179,21 @@ class AssetUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView):
template_name = 'assets/asset_update.html' template_name = 'assets/asset_update.html'
success_url = reverse_lazy('assets:asset-list') 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): def get_context_data(self, **kwargs):
formset = self.get_protocol_formset()
context = { context = {
'app': _('Assets'), 'app': _('Assets'),
'action': _('Update asset'), 'action': _('Update asset'),
'formset': formset,
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)

12
apps/common/decorator.py Normal file
View File

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

View File

@ -5,6 +5,7 @@ from django.http import JsonResponse
from django.utils import timezone from django.utils import timezone
from django.core.cache import cache from django.core.cache import cache
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.contrib import messages
from rest_framework.utils import html from rest_framework.utils import html
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
@ -203,3 +204,26 @@ class DatetimeSearchMixin:
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.get_date_range() self.get_date_range()
return super().get(request, *args, **kwargs) 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

View File

@ -3,5 +3,22 @@
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.utils.translation import ugettext_lazy as _ 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')) 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)

View File

@ -287,6 +287,9 @@ class Config(dict):
return v return v
try: try:
if tp in [list, dict]:
v = json.loads(v)
else:
v = tp(v) v = tp(v)
except Exception: except Exception:
pass pass

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,7 @@ class JMSBaseInventory(BaseInventory):
'id': asset.id, 'id': asset.id,
'hostname': asset.hostname, 'hostname': asset.hostname,
'ip': asset.ip, 'ip': asset.ip,
'port': asset.port, 'port': asset.ssh_port,
'vars': dict(), 'vars': dict(),
'groups': [], 'groups': [],
} }
@ -29,8 +29,15 @@ class JMSBaseInventory(BaseInventory):
info["vars"].update(self.make_proxy_command(asset)) info["vars"].update(self.make_proxy_command(asset))
if run_as_admin: if run_as_admin:
info.update(asset.get_auth_info()) info.update(asset.get_auth_info())
if asset.is_unixlike():
info["become"] = asset.admin_user.become_info
for node in asset.nodes.all(): for node in asset.nodes.all():
info["groups"].append(node.value) info["groups"].append(node.value)
if asset.is_windows():
info["vars"].update({
"ansible_connection": "ssh",
"ansible_shell_type": "cmd",
})
for label in asset.labels.all(): for label in asset.labels.all():
info["vars"].update({ info["vars"].update({
label.name: label.value label.name: label.value
@ -73,7 +80,7 @@ class JMSInventory(JMSBaseInventory):
""" """
def __init__(self, assets, run_as_admin=False, run_as=None, become_info=None): def __init__(self, assets, run_as_admin=False, run_as=None, become_info=None):
""" """
:param host_id_list: ["test1", ] :param assets: assets
:param run_as_admin: True 是否使用管理用户去执行, 每台服务器的管理用户可能不同 :param run_as_admin: True 是否使用管理用户去执行, 每台服务器的管理用户可能不同
:param run_as: 用户名(添加了统一的资产用户管理器之后AssetUserManager加上之后修改为username) :param run_as: 用户名(添加了统一的资产用户管理器之后AssetUserManager加上之后修改为username)
:param become_info: 是否become成某个用户去执行 :param become_info: 是否become成某个用户去执行
@ -86,17 +93,14 @@ class JMSInventory(JMSBaseInventory):
host_list = [] host_list = []
for asset in assets: for asset in assets:
info = self.convert_to_ansible(asset, run_as_admin=run_as_admin) host = self.convert_to_ansible(asset, run_as_admin=run_as_admin)
host_list.append(info)
if run_as: if run_as:
for host in host_list:
run_user_info = self.get_run_user_info(host) run_user_info = self.get_run_user_info(host)
host.update(run_user_info) host.update(run_user_info)
if become_info and asset.is_unixlike():
if become_info:
for host in host_list:
host.update(become_info) host.update(become_info)
host_list.append(host)
super().__init__(host_list=host_list) super().__init__(host_list=host_list)
def get_run_user_info(self, host): def get_run_user_info(self, host):
@ -133,12 +137,10 @@ class JMSCustomInventory(JMSBaseInventory):
host_list = [] host_list = []
for asset in assets: for asset in assets:
info = self.convert_to_ansible(asset) host = self.convert_to_ansible(asset)
host_list.append(info)
for host in host_list:
run_user_info = self.get_run_user_info() run_user_info = self.get_run_user_info()
host.update(run_user_info) host.update(run_user_info)
host_list.append(host)
super().__init__(host_list=host_list) super().__init__(host_list=host_list)

View File

@ -220,6 +220,7 @@ class AdHoc(models.Model):
time_start = time.time() time_start = time.time()
try: try:
date_start = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') date_start = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
history.date_start = date_start
print(_("{} Start task: {}").format(date_start, self.task.name)) print(_("{} Start task: {}").format(date_start, self.task.name))
raw, summary = self._run_only() raw, summary = self._run_only()
date_end = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') date_end = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')

View File

@ -134,8 +134,11 @@ function getSelectedAssetsNode() {
var assetsNodeId = []; var assetsNodeId = [];
var assetsNode = []; var assetsNode = [];
nodes.forEach(function (node) { nodes.forEach(function (node) {
if (node.meta.type === 'asset' && !node.isHidden && node.meta.asset.protocol === 'ssh') { if (node.meta.type === 'asset' && !node.isHidden) {
if (assetsNodeId.indexOf(node.id) === -1) { var protocols = $.map(node.meta.asset.protocols, function (v) {
return v.name
});
if (assetsNodeId.indexOf(node.id) === -1 && protocols.indexOf("ssh") > -1) {
assetsNodeId.push(node.id); assetsNodeId.push(node.id);
assetsNode.push(node) assetsNode.push(node)
} }

View File

@ -18,7 +18,7 @@ def update_or_create_ansible_task(
run_as_admin=False, run_as=None, become_info=None, run_as_admin=False, run_as=None, become_info=None,
): ):
if not hosts or not tasks or not task_name: if not hosts or not tasks or not task_name:
return return None, None
set_to_root_org() set_to_root_org()
defaults = { defaults = {
'name': task_name, 'name': task_name,

View File

@ -9,8 +9,10 @@ from django.forms import ModelForm
from django.http.response import HttpResponseForbidden from django.http.response import HttpResponseForbidden
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from rest_framework import serializers from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator
from common.utils import get_logger from common.utils import get_logger
from common.validators import ProjectUniqueValidator
from .utils import ( from .utils import (
current_org, set_current_org, set_to_root_org, get_current_org_id 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类型 但是coco需要资产的org_id字段所以修改为CharField类型
""" """
org_id = serializers.CharField(default=get_current_org_id) 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

View File

@ -154,7 +154,7 @@ class UserGrantedAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, ListAPIV
util = AssetPermissionUtil(user, cache_policy=self.cache_policy) util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
assets = util.get_assets() assets = util.get_assets()
for k, v in assets.items(): 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 k.system_users_granted = system_users_granted
queryset.append(k) queryset.append(k)
return queryset return queryset
@ -215,8 +215,7 @@ class UserGrantedNodesWithAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin,
for node, _assets in nodes.items(): for node, _assets in nodes.items():
assets = _assets.keys() assets = _assets.keys()
for k, v in _assets.items(): for k, v in _assets.items():
system_users_granted = [s for s in v if system_users_granted = [s for s in v if k.has_protocol(s.protocol)]
s.protocol == k.protocol]
k.system_users_granted = system_users_granted k.system_users_granted = system_users_granted
node.assets_granted = assets node.assets_granted = assets
queryset.append(node) queryset.append(node)
@ -364,7 +363,7 @@ class UserGrantedNodeChildrenApi(UserPermissionCacheMixin, ListAPIView):
for asset, system_users in nodes_granted[node].items(): for asset, system_users in nodes_granted[node].items():
fake_node = asset.as_node() fake_node = asset.as_node()
fake_node.assets_amount = 0 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.asset.system_users_granted = system_users
fake_node.key = node.key + ':0' fake_node.key = node.key + ':0'
fake_nodes.append(fake_node) fake_nodes.append(fake_node)
@ -389,7 +388,7 @@ class UserGrantedNodeChildrenApi(UserPermissionCacheMixin, ListAPIView):
fake_node = asset.as_node() fake_node = asset.as_node()
fake_node.assets_amount = 0 fake_node.assets_amount = 0
system_users = [s for s in system_users if 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.asset.system_users_granted = system_users
fake_node.key = node.key + ':0' fake_node.key = node.key + ':0'
matched_assets.append(fake_node) matched_assets.append(fake_node)

View File

@ -3,11 +3,12 @@
from common.permissions import AdminUserRequiredMixin from common.permissions import AdminUserRequiredMixin
from users.models import User, UserGroup 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 ( from assets.serializers import (
AssetGrantedSerializer, NodeSerializer AssetGrantedSerializer, NodeSerializer
) )
from applications.serializers import RemoteAppSerializer from applications.serializers import RemoteAppSerializer
from applications.models import RemoteApp

View File

@ -238,7 +238,7 @@ class AssetPermissionUtil:
for perm in permissions: for perm in permissions:
actions = perm.actions.all() actions = perm.actions.all()
for asset in perm.assets.all().valid().prefetch_related('nodes'): 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) system_users = self._structured_system_user(system_users, actions)
assets[asset].update(system_users) assets[asset].update(system_users)
return assets return assets
@ -255,7 +255,7 @@ class AssetPermissionUtil:
_assets = node.get_all_assets().valid().prefetch_related('nodes') _assets = node.get_all_assets().valid().prefetch_related('nodes')
for asset in _assets: for asset in _assets:
for system_user, attr_dict in system_users.items(): for system_user, attr_dict in system_users.items():
if system_user.protocol != asset.protocol: if not asset.has_protocol(system_user.protocol):
continue continue
if system_user in assets[asset]: if system_user in assets[asset]:
actions = assets[asset][system_user]['actions'] actions = assets[asset][system_user]['actions']
@ -279,15 +279,12 @@ class AssetPermissionUtil:
resource=resource resource=resource
) )
@property
def node_key(self): def node_key(self):
return self.get_cache_key('NODES_WITH_ASSETS') return self.get_cache_key('NODES_WITH_ASSETS')
@property
def asset_key(self): def asset_key(self):
return self.get_cache_key('ASSETS') return self.get_cache_key('ASSETS')
@property
def system_key(self): def system_key(self):
return self.get_cache_key('SYSTEM_USER') 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): 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' icon_skin = 'file'
if asset.platform.lower() == 'windows': if asset.platform.lower() == 'windows':
icon_skin = 'windows' icon_skin = 'windows'
@ -490,8 +487,8 @@ def parse_asset_to_tree_node(node, asset, system_users):
'id': asset.id, 'id': asset.id,
'hostname': asset.hostname, 'hostname': asset.hostname,
'ip': asset.ip, 'ip': asset.ip,
'port': asset.port, 'protocols': [{"name": p.name, "port": p.port}
'protocol': asset.protocol, for p in asset.protocols.all()],
'platform': asset.platform, 'platform': asset.platform,
'domain': None if not asset.domain else asset.domain.id, 'domain': None if not asset.domain else asset.domain.id,
'is_active': asset.is_active, 'is_active': asset.is_active,

View File

@ -11,9 +11,8 @@ from django.conf import settings
from common.permissions import AdminUserRequiredMixin from common.permissions import AdminUserRequiredMixin
from orgs.utils import current_org 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 ..models import RemoteAppPermission
from ..forms import RemoteAppPermissionCreateUpdateForm from ..forms import RemoteAppPermissionCreateUpdateForm

View File

@ -454,3 +454,15 @@ div.dataTables_wrapper div.dataTables_filter {
#tree-refresh .fa-refresh { #tree-refresh .fa-refresh {
font: normal normal normal 14px/1 FontAwesome !important; font: normal normal normal 14px/1 FontAwesome !important;
} }
.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;
}

View File

@ -165,11 +165,13 @@ function formSubmit(props) {
/* /*
{ {
"form": $("form"), "form": $("form"),
"data": {},
"url": "", "url": "",
"method": "POST", "method": "POST",
"redirect_to": "", "redirect_to": "",
"success": function(data, textStatue, jqXHR){}, "success": function(data, textStatue, jqXHR){},
"error": function(jqXHR, textStatus, errorThrown) {} "error": function(jqXHR, textStatus, errorThrown) {},
"message": "",
} }
*/ */
props = props || {}; props = props || {};
@ -183,6 +185,10 @@ function formSubmit(props) {
dataType: props.data_type || "json" dataType: props.data_type || "json"
}).done(function (data, textState, jqXHR) { }).done(function (data, textState, jqXHR) {
if (redirect_to) { 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; location.href = redirect_to;
} else if (typeof props.success === 'function') { } else if (typeof props.success === 'function') {
return props.success(data, textState, jqXHR); return props.success(data, textState, jqXHR);
@ -230,7 +236,15 @@ function formSubmit(props) {
var help_msg = v.join("<br/>") ; var help_msg = v.join("<br/>") ;
helpBlockRef.html(help_msg); helpBlockRef.html(help_msg);
} else { } else {
noneFieldErrorMsg += v + '<br/>'; $.each(v, function (kk, vv) {
if (typeof errors === "object") {
$.each(vv, function (kkk, vvv) {
noneFieldErrorMsg += " " + vvv + '<br/>';
})
} else{
noneFieldErrorMsg += vv + '<br/>';
}
})
} }
}); });
if (noneFieldErrorRef.length === 1 && noneFieldErrorMsg !== '') { if (noneFieldErrorRef.length === 1 && noneFieldErrorMsg !== '') {