diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py
index 64fdc16dc..0b63522ea 100644
--- a/apps/assets/api/asset.py
+++ b/apps/assets/api/asset.py
@@ -4,24 +4,27 @@
import random
from rest_framework.response import Response
+from rest_framework.viewsets import ModelViewSet
+from rest_framework.generics import RetrieveAPIView
from django.shortcuts import get_object_or_404
from common.utils import get_logger, get_object_or_none
-from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
+from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
-from ..models import Asset, Node
+from ..models import Asset, Node, Platform
from .. import serializers
-from ..tasks import update_asset_hardware_info_manual, \
- test_asset_connectivity_manual
+from ..tasks import (
+ update_asset_hardware_info_manual, test_asset_connectivity_manual
+)
from ..filters import AssetByNodeFilterBackend, LabelFilterBackend
logger = get_logger(__file__)
__all__ = [
- 'AssetViewSet',
+ 'AssetViewSet', 'AssetPlatformRetrieveApi',
'AssetRefreshHardwareApi', 'AssetAdminUserTestApi',
- 'AssetGatewayApi',
+ 'AssetGatewayApi', 'AssetPlatformViewSet',
]
@@ -53,6 +56,34 @@ class AssetViewSet(OrgBulkModelViewSet):
self.set_assets_node(assets)
+class AssetPlatformRetrieveApi(RetrieveAPIView):
+ queryset = Platform.objects.all()
+ permission_classes = (IsOrgAdminOrAppUser,)
+ serializer_class = serializers.PlatformSerializer
+
+ def get_object(self):
+ asset_pk = self.kwargs.get('pk')
+ asset = get_object_or_404(Asset, pk=asset_pk)
+ return asset.platform
+
+
+class AssetPlatformViewSet(ModelViewSet):
+ queryset = Platform.objects.all()
+ permission_classes = (IsSuperUser,)
+ serializer_class = serializers.PlatformSerializer
+ filterset_fields = ['name', 'base']
+ search_fields = ['name']
+
+ def check_object_permissions(self, request, obj):
+ if request.method.lower() in ['delete', 'put', 'patch'] and \
+ obj.internal:
+ self.permission_denied(
+ request, message={"detail": "Internal platform"}
+ )
+
+ return super().check_object_permissions(request, obj)
+
+
class AssetRefreshHardwareApi(generics.RetrieveAPIView):
"""
Refresh asset hardware info
diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py
index 1451650d2..24661bad6 100644
--- a/apps/assets/api/node.py
+++ b/apps/assets/api/node.py
@@ -177,7 +177,7 @@ class NodeChildrenAsTreeApi(NodeChildrenApi):
if not include_assets:
return queryset
assets = self.instance.get_assets().only(
- "id", "hostname", "ip", 'platform', "os",
+ "id", "hostname", "ip", "os",
"org_id", "protocols",
)
for asset in assets:
diff --git a/apps/assets/forms/__init__.py b/apps/assets/forms/__init__.py
index a086cb12c..39b39a45a 100644
--- a/apps/assets/forms/__init__.py
+++ b/apps/assets/forms/__init__.py
@@ -5,3 +5,4 @@ from .label import *
from .user import *
from .domain import *
from .cmd_filter import *
+from .platform import *
diff --git a/apps/assets/forms/asset.py b/apps/assets/forms/asset.py
index 805f49f24..46c137d70 100644
--- a/apps/assets/forms/asset.py
+++ b/apps/assets/forms/asset.py
@@ -6,13 +6,13 @@ from django.utils.translation import gettext_lazy as _
from common.utils import get_logger
from orgs.mixins.forms import OrgModelForm
-from ..models import Asset, Node
+from ..models import Asset
from ..const import GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_HELP_TEXT
logger = get_logger(__file__)
__all__ = [
- 'AssetCreateForm', 'AssetUpdateForm', 'AssetBulkUpdateForm', 'ProtocolForm',
+ 'AssetCreateUpdateForm', 'AssetBulkUpdateForm', 'ProtocolForm',
]
@@ -27,17 +27,27 @@ class ProtocolForm(forms.Form):
)
-class AssetCreateForm(OrgModelForm):
+class AssetCreateUpdateForm(OrgModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- if self.data:
- return
+ self.set_platform_to_name()
+ self.set_fields_queryset()
+
+ def set_fields_queryset(self):
nodes_field = self.fields['nodes']
+ nodes_choices = []
if self.instance:
- nodes_field.choices = [(n.id, n.full_value) for n in
- self.instance.nodes.all()]
- else:
- nodes_field.choices = []
+ nodes_choices = [
+ (n.id, n.full_value) for n in
+ self.instance.nodes.all()
+ ]
+ nodes_field.choices = nodes_choices
+
+ def set_platform_to_name(self):
+ platform_field = self.fields['platform']
+ platform_field.to_field_name = 'name'
+ if self.instance:
+ self.initial['platform'] = self.instance.platform.name
def add_nodes_initial(self, node):
nodes_field = self.fields['nodes']
@@ -49,7 +59,7 @@ class AssetCreateForm(OrgModelForm):
fields = [
'hostname', 'ip', 'public_ip', 'protocols', 'comment',
'nodes', 'is_active', 'admin_user', 'labels', 'platform',
- 'domain',
+ 'domain', 'number',
]
widgets = {
'nodes': forms.SelectMultiple(attrs={
@@ -64,52 +74,8 @@ class AssetCreateForm(OrgModelForm):
'domain': forms.Select(attrs={
'class': 'select2', 'data-placeholder': _('Domain')
}),
- }
- labels = {
- 'nodes': _("Node"),
- }
- help_texts = {
- 'hostname': GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_HELP_TEXT,
- 'admin_user': _(
- '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'
- ),
- 'platform': _("Windows 2016 RDP protocol is different, If is window 2016, set it"),
- 'domain': _("If your have some network not connect with each other, you can set domain")
- }
-
-
-class AssetUpdateForm(OrgModelForm):
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- if self.data:
- return
- nodes_field = self.fields['nodes']
- if self.instance:
- nodes_field.choices = ((n.id, n.full_value) for n in
- self.instance.nodes.all())
- else:
- nodes_field.choices = []
-
- class Meta:
- model = Asset
- fields = [
- 'hostname', 'ip', 'protocols', 'nodes', 'is_active', 'platform',
- 'public_ip', 'number', 'comment', 'admin_user', 'labels',
- 'domain',
- ]
- widgets = {
- 'nodes': forms.SelectMultiple(attrs={
- 'class': 'nodes-select2', 'data-placeholder': _('Node')
- }),
- 'admin_user': forms.Select(attrs={
- 'class': 'select2', 'data-placeholder': _('Admin user')
- }),
- 'labels': forms.SelectMultiple(attrs={
- 'class': 'select2', 'data-placeholder': _('Label')
- }),
- 'domain': forms.Select(attrs={
- 'class': 'select2', 'data-placeholder': _('Domain')
+ 'platform': forms.Select(attrs={
+ 'class': 'select2', 'data-placeholder': _('Platform')
}),
}
labels = {
diff --git a/apps/assets/forms/platform.py b/apps/assets/forms/platform.py
new file mode 100644
index 000000000..88c4365d4
--- /dev/null
+++ b/apps/assets/forms/platform.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+from ..models import Platform
+
+
+__all__ = ['PlatformForm', 'PlatformMetaForm']
+
+
+class PlatformMetaForm(forms.Form):
+ SECURITY_CHOICES = (
+ ('rdp', "RDP"),
+ ('nla', "NLA"),
+ ('tls', 'TLS'),
+ ('any', "Any"),
+ )
+ CONSOLE_CHOICES = (
+ (True, _('Yes')),
+ (False, _('No')),
+ )
+ security = forms.ChoiceField(
+ choices=SECURITY_CHOICES, initial='any', label=_("RDP security"),
+ required=False,
+ )
+ console = forms.ChoiceField(
+ choices=CONSOLE_CHOICES, initial=False, label=_("RDP console"),
+ required=False,
+ )
+
+
+class PlatformForm(forms.ModelForm):
+ class Meta:
+ model = Platform
+ fields = [
+ 'name', 'base', 'comment',
+ ]
+ labels = {
+ 'base': _("Base platform")
+ }
+
diff --git a/apps/assets/migrations/0044_platform.py b/apps/assets/migrations/0044_platform.py
new file mode 100644
index 000000000..f66d00642
--- /dev/null
+++ b/apps/assets/migrations/0044_platform.py
@@ -0,0 +1,45 @@
+# Generated by Django 2.2.7 on 2019-12-06 07:26
+
+import common.fields.model
+from django.db import migrations, models
+
+
+def create_internal_platform(apps, schema_editor):
+ model = apps.get_model("assets", "Platform")
+ db_alias = schema_editor.connection.alias
+ type_platforms = (
+ ('Linux', 'Linux', None),
+ ('Unix', 'Unix', None),
+ ('MacOS', 'MacOS', None),
+ ('BSD', 'BSD', None),
+ ('Windows', 'Windows', None),
+ ('Windows2016', 'Windows', {'security': 'tls'}),
+ ('Other', 'Other', None),
+ )
+ for name, base, meta in type_platforms:
+ model.objects.using(db_alias).create(
+ name=name, base=base, internal=True, meta=meta
+ )
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('assets', '0043_auto_20191114_1111'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Platform',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.SlugField(allow_unicode=True, unique=True, verbose_name='Name')),
+ ('base', models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=16, verbose_name='Base')),
+ ('charset', models.CharField(choices=[('utf8', 'UTF-8'), ('gbk', 'GBK')], default='utf8', max_length=8, verbose_name='Charset')),
+ ('meta', common.fields.model.JsonDictTextField(blank=True, null=True, verbose_name='Meta')),
+ ('internal', models.BooleanField(default=False, verbose_name='Internal')),
+ ('comment', models.TextField(blank=True, null=True, verbose_name='Comment')),
+ ],
+ ),
+ migrations.RunPython(create_internal_platform)
+ ]
diff --git a/apps/assets/migrations/0045_auto_20191206_1607.py b/apps/assets/migrations/0045_auto_20191206_1607.py
new file mode 100644
index 000000000..f51839289
--- /dev/null
+++ b/apps/assets/migrations/0045_auto_20191206_1607.py
@@ -0,0 +1,47 @@
+# Generated by Django 2.2.7 on 2019-12-06 08:07
+
+import assets.models.asset
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+def migrate_platform_to_asset_type(apps, schema_editor):
+ asset_model = apps.get_model("assets", "Asset")
+ platform_model = apps.get_model("assets", "Platform")
+ db_alias = schema_editor.connection.alias
+
+ platforms = platform_model.objects.using(db_alias).all()
+ platforms_map = {p.name: p for p in platforms}
+ for name, p in platforms_map.items():
+ asset_model.objects.using(db_alias)\
+ .filter(_platform=name)\
+ .update(platform=p)
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('assets', '0044_platform'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='asset',
+ old_name='platform',
+ new_name='_platform',
+ ),
+ migrations.AddField(
+ model_name='asset',
+ name='platform',
+ field=models.ForeignKey(
+ default=assets.models.asset.Platform.default,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name='assets', to='assets.Platform',
+ verbose_name='Platform'),
+ ),
+ migrations.RunPython(migrate_platform_to_asset_type),
+ migrations.RemoveField(
+ model_name='asset',
+ name='_platform',
+ ),
+ ]
diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py
index d6e09786e..08a44e93c 100644
--- a/apps/assets/models/asset.py
+++ b/apps/assets/models/asset.py
@@ -11,10 +11,12 @@ from collections import OrderedDict
from django.db import models
from django.utils.translation import ugettext_lazy as _
-from .utils import Connectivity
+from common.fields.model import JsonDictTextField
+from common.utils import lazyproperty
from orgs.mixins.models import OrgModelMixin, OrgManager
+from .utils import Connectivity
-__all__ = ['Asset', 'ProtocolsMixin']
+__all__ = ['Asset', 'ProtocolsMixin', 'Platform']
logger = logging.getLogger(__name__)
@@ -37,6 +39,13 @@ def default_node():
return None
+class AssetManager(OrgManager):
+ def get_queryset(self):
+ return super().get_queryset().annotate(
+ platform_base=models.F('platform__base')
+ )
+
+
class AssetQuerySet(models.QuerySet):
def active(self):
return self.filter(is_active=True)
@@ -119,6 +128,41 @@ class NodesRelationMixin:
return nodes
+class Platform(models.Model):
+ CHARSET_CHOICES = (
+ ('utf8', 'UTF-8'),
+ ('gbk', 'GBK'),
+ )
+ BASE_CHOICES = (
+ ('Linux', 'Linux'),
+ ('Unix', 'Unix'),
+ ('MacOS', 'MacOS'),
+ ('BSD', 'BSD'),
+ ('Windows', 'Windows'),
+ ('Other', 'Other'),
+ )
+ name = models.SlugField(verbose_name=_("Name"), unique=True, allow_unicode=True)
+ base = models.CharField(choices=BASE_CHOICES, max_length=16, default='Linux', verbose_name=_("Base"))
+ charset = models.CharField(default='utf8', choices=CHARSET_CHOICES, max_length=8, verbose_name=_("Charset"))
+ meta = JsonDictTextField(blank=True, null=True, verbose_name=_("Meta"))
+ internal = models.BooleanField(default=False, verbose_name=_("Internal"))
+ comment = models.TextField(blank=True, null=True, verbose_name=_("Comment"))
+
+ @classmethod
+ def default(cls):
+ linux, created = cls.objects.get_or_create(
+ defaults={'name': 'Linux'}, name='Linux'
+ )
+ return linux.id
+
+ def __str__(self):
+ return self.name
+
+ class Meta:
+ verbose_name = _("Platform")
+ # ordering = ('name',)
+
+
class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
# Important
PLATFORM_CHOICES = (
@@ -138,9 +182,8 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
choices=ProtocolsMixin.PROTOCOL_CHOICES,
verbose_name=_('Protocol'))
port = models.IntegerField(default=22, verbose_name=_('Port'))
-
protocols = models.CharField(max_length=128, default='ssh/22', blank=True, verbose_name=_("Protocols"))
- platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform'))
+ platform = models.ForeignKey(Platform, default=Platform.default, on_delete=models.PROTECT, verbose_name=_("Platform"), related_name='assets')
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"))
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
@@ -175,7 +218,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, 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 = None
def __str__(self):
@@ -191,19 +234,20 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
return True, warning
def is_windows(self):
- if self.platform in ("Windows", "Windows2016"):
- return True
- else:
- return False
+ return self.platform_base == "Windows"
def is_unixlike(self):
- if self.platform not in ("Windows", "Windows2016", "Other"):
+ if self.platform_base 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",)
+ return self.has_protocol('ssh') and self.platform_base not in ("Other",)
+
+ @lazyproperty
+ def platform_base(self):
+ return self.platform.base
@property
def cpu_info(self):
@@ -264,9 +308,9 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
def as_tree_node(self, parent_node):
from common.tree import TreeNode
icon_skin = 'file'
- if self.platform.lower() == 'windows':
+ if self.platform_base.lower() == 'windows':
icon_skin = 'windows'
- elif self.platform.lower() == 'linux':
+ elif self.platform_base.lower() == 'linux':
icon_skin = 'linux'
data = {
'id': str(self.id),
@@ -283,7 +327,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
'hostname': self.hostname,
'ip': self.ip,
'protocols': self.protocols_as_list,
- 'platform': self.platform,
+ 'platform': self.platform_base,
}
}
}
diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py
index a24374c13..6948f20e2 100644
--- a/apps/assets/serializers/asset.py
+++ b/apps/assets/serializers/asset.py
@@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.serializers import AdaptedBulkListSerializer
-from ..models import Asset, Node, Label
+from ..models import Asset, Node, Label, Platform
from ..const import (
GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_PATTERN,
GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_ERROR_MSG
@@ -16,7 +16,8 @@ from .base import ConnectivitySerializer
__all__ = [
'AssetSerializer', 'AssetSimpleSerializer',
- 'ProtocolsField',
+ 'ProtocolsField', 'PlatformSerializer',
+ 'AssetDetailSerializer',
]
@@ -65,6 +66,9 @@ class ProtocolsField(serializers.ListField):
class AssetSerializer(BulkOrgResourceModelSerializer):
+ platform = serializers.SlugRelatedField(
+ slug_field='name', queryset=Platform.objects.all(), label=_("Platform")
+ )
protocols = ProtocolsField(label=_('Protocols'), required=False)
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
@@ -111,7 +115,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
queryset = queryset.prefetch_related(
Prefetch('nodes', queryset=Node.objects.all().only('id')),
Prefetch('labels', queryset=Label.objects.all().only('id')),
- ).select_related('admin_user', 'domain')
+ ).select_related('admin_user', 'domain', 'platform')
return queryset
def compatible_with_old_protocol(self, validated_data):
@@ -139,6 +143,21 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
return super().update(instance, validated_data)
+class PlatformSerializer(serializers.ModelSerializer):
+ meta = serializers.DictField(required=False, allow_null=True)
+
+ class Meta:
+ model = Platform
+ fields = [
+ 'id', 'name', 'base', 'charset',
+ 'internal', 'meta', 'comment'
+ ]
+
+
+class AssetDetailSerializer(AssetSerializer):
+ platform = PlatformSerializer(read_only=True)
+
+
class AssetSimpleSerializer(serializers.ModelSerializer):
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
diff --git a/apps/assets/templates/assets/_admin_user_import_modal.html b/apps/assets/templates/assets/_admin_user_import_modal.html
deleted file mode 100644
index a4afc1a14..000000000
--- a/apps/assets/templates/assets/_admin_user_import_modal.html
+++ /dev/null
@@ -1,6 +0,0 @@
-{% extends '_import_modal.html' %}
-{% load i18n %}
-
-{% block modal_title%}{% trans "Import admin user" %}{% endblock %}
-
-{% block import_modal_download_template_url %}{% url "api-assets:admin-user-list" %}{% endblock %}
diff --git a/apps/assets/templates/assets/_admin_user_update_modal.html b/apps/assets/templates/assets/_admin_user_update_modal.html
deleted file mode 100644
index 9af051dd2..000000000
--- a/apps/assets/templates/assets/_admin_user_update_modal.html
+++ /dev/null
@@ -1,4 +0,0 @@
-{% extends '_update_modal.html' %}
-{% load i18n %}
-
-{% block modal_title%}{% trans "Update admin user" %}{% endblock %}
\ No newline at end of file
diff --git a/apps/assets/templates/assets/_asset_import_modal.html b/apps/assets/templates/assets/_asset_import_modal.html
deleted file mode 100644
index 2460cb053..000000000
--- a/apps/assets/templates/assets/_asset_import_modal.html
+++ /dev/null
@@ -1,6 +0,0 @@
-{% extends '_import_modal.html' %}
-{% load i18n %}
-
-{% block modal_title%}{% trans "Import assets" %}{% endblock %}
-
-{% block import_modal_download_template_url %}{% url "api-assets:asset-list" %}{% endblock %}
diff --git a/apps/assets/templates/assets/_asset_list_modal.html b/apps/assets/templates/assets/_asset_list_modal.html
index a50934e9c..dea2c3e1e 100644
--- a/apps/assets/templates/assets/_asset_list_modal.html
+++ b/apps/assets/templates/assets/_asset_list_modal.html
@@ -25,7 +25,7 @@