perf: 打算重构 asset application

pull/8605/head
ibuler 2022-04-02 18:35:46 +08:00
parent 54d1996507
commit 3de881fa19
31 changed files with 667 additions and 362 deletions

View File

@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _
from common.utils import lazyproperty from common.utils import lazyproperty
from assets.models.base import BaseUser from assets.models.base import BaseUser
from assets.models import SystemUser
class Account(BaseUser): class Account(BaseUser):
@ -108,3 +109,9 @@ class Account(BaseUser):
def __str__(self): def __str__(self):
return self.smart_name return self.smart_name
class ApplicationUser(SystemUser):
class Meta:
proxy = True
verbose_name = _('Application user')

View File

@ -1,216 +1,13 @@
from collections import defaultdict
from urllib.parse import urlencode, parse_qsl
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from common.mixins import CommonModelMixin from common.mixins import CommonModelMixin
from common.tree import TreeNode
from common.utils import is_uuid from common.utils import is_uuid
from assets.models import Asset, SystemUser from assets.models import Asset
from ..utils import KubernetesTree
from .. import const from .. import const
from .tree import ApplicationTreeNodeMixin
class ApplicationTreeNodeMixin:
id: str
name: str
type: str
category: str
attrs: dict
@staticmethod
def create_tree_id(pid, type, v):
i = dict(parse_qsl(pid))
i[type] = v
tree_id = urlencode(i)
return tree_id
@classmethod
def create_choice_node(cls, c, id_, pid, tp, opened=False, counts=None,
show_empty=True, show_count=True):
count = counts.get(c.value, 0)
if count == 0 and not show_empty:
return None
label = c.label
if count is not None and show_count:
label = '{} ({})'.format(label, count)
data = {
'id': id_,
'name': label,
'title': label,
'pId': pid,
'isParent': bool(count),
'open': opened,
'iconSkin': '',
'meta': {
'type': tp,
'data': {
'name': c.name,
'value': c.value
}
}
}
return TreeNode(**data)
@classmethod
def create_root_tree_node(cls, queryset, show_count=True):
count = queryset.count() if show_count else None
root_id = 'applications'
root_name = _('Applications')
if count is not None and show_count:
root_name = '{} ({})'.format(root_name, count)
node = TreeNode(**{
'id': root_id,
'name': root_name,
'title': root_name,
'pId': '',
'isParent': True,
'open': True,
'iconSkin': '',
'meta': {
'type': 'applications_root',
}
})
return node
@classmethod
def create_category_tree_nodes(cls, pid, counts=None, show_empty=True, show_count=True):
nodes = []
categories = const.AppType.category_types_mapper().keys()
for category in categories:
if not settings.XPACK_ENABLED and const.AppCategory.is_xpack(category):
continue
i = cls.create_tree_id(pid, 'category', category.value)
node = cls.create_choice_node(
category, i, pid=pid, tp='category',
counts=counts, opened=False, show_empty=show_empty,
show_count=show_count
)
if not node:
continue
nodes.append(node)
return nodes
@classmethod
def create_types_tree_nodes(cls, pid, counts, show_empty=True, show_count=True):
nodes = []
temp_pid = pid
type_category_mapper = const.AppType.type_category_mapper()
types = const.AppType.type_category_mapper().keys()
for tp in types:
if not settings.XPACK_ENABLED and const.AppType.is_xpack(tp):
continue
category = type_category_mapper.get(tp)
pid = cls.create_tree_id(pid, 'category', category.value)
i = cls.create_tree_id(pid, 'type', tp.value)
node = cls.create_choice_node(
tp, i, pid, tp='type', counts=counts, opened=False,
show_empty=show_empty, show_count=show_count
)
pid = temp_pid
if not node:
continue
nodes.append(node)
return nodes
@staticmethod
def get_tree_node_counts(queryset):
counts = defaultdict(int)
values = queryset.values_list('type', 'category')
for i in values:
tp = i[0]
category = i[1]
counts[tp] += 1
counts[category] += 1
return counts
@classmethod
def create_category_type_tree_nodes(cls, queryset, pid, show_empty=True, show_count=True):
counts = cls.get_tree_node_counts(queryset)
tree_nodes = []
# 类别的节点
tree_nodes += cls.create_category_tree_nodes(
pid, counts, show_empty=show_empty,
show_count=show_count
)
# 类型的节点
tree_nodes += cls.create_types_tree_nodes(
pid, counts, show_empty=show_empty,
show_count=show_count
)
return tree_nodes
@classmethod
def create_tree_nodes(cls, queryset, root_node=None, show_empty=True, show_count=True):
tree_nodes = []
# 根节点有可能是组织名称
if root_node is None:
root_node = cls.create_root_tree_node(queryset, show_count=show_count)
tree_nodes.append(root_node)
tree_nodes += cls.create_category_type_tree_nodes(
queryset, root_node.id, show_empty=show_empty, show_count=show_count
)
# 应用的节点
for app in queryset:
if not settings.XPACK_ENABLED and const.AppType.is_xpack(app.type):
continue
node = app.as_tree_node(root_node.id)
tree_nodes.append(node)
return tree_nodes
def create_app_tree_pid(self, root_id):
pid = self.create_tree_id(root_id, 'category', self.category)
pid = self.create_tree_id(pid, 'type', self.type)
return pid
def as_tree_node(self, pid, k8s_as_tree=False):
if self.type == const.AppType.k8s and k8s_as_tree:
node = KubernetesTree(pid).as_tree_node(self)
else:
node = self._as_tree_node(pid)
return node
def _attrs_to_tree(self):
if self.category == const.AppCategory.db:
return self.attrs
return {}
def _as_tree_node(self, pid):
icon_skin_category_mapper = {
'remote_app': 'chrome',
'db': 'database',
'cloud': 'cloud'
}
icon_skin = icon_skin_category_mapper.get(self.category, 'file')
pid = self.create_app_tree_pid(pid)
node = TreeNode(**{
'id': str(self.id),
'name': self.name,
'title': self.name,
'pId': pid,
'isParent': False,
'open': False,
'iconSkin': icon_skin,
'meta': {
'type': 'application',
'data': {
'category': self.category,
'type': self.type,
'attrs': self._attrs_to_tree()
}
}
})
return node
class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin): class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
@ -279,8 +76,3 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
if raise_exception: if raise_exception:
raise ValueError("Remote App not has asset attr") raise ValueError("Remote App not has asset attr")
class ApplicationUser(SystemUser):
class Meta:
proxy = True
verbose_name = _('Application user')

View File

@ -0,0 +1,13 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from .application import Application
class Database(Application):
host = models.CharField(max_length=1024, verbose_name=_('Host'))
port = models.IntegerField(verbose_name=_("Port"))
database = models.CharField(max_length=1024, blank=True, null=True, verbose_name=_("Database"))
class Meta:
verbose_name = _("Database")

View File

View File

@ -0,0 +1,208 @@
from collections import defaultdict
from urllib.parse import urlencode, parse_qsl
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from common.tree import TreeNode
from ..utils import KubernetesTree
from .. import const
class ApplicationTreeNodeMixin:
id: str
name: str
type: str
category: str
attrs: dict
@staticmethod
def create_tree_id(pid, type, v):
i = dict(parse_qsl(pid))
i[type] = v
tree_id = urlencode(i)
return tree_id
@classmethod
def create_choice_node(cls, c, id_, pid, tp, opened=False, counts=None,
show_empty=True, show_count=True):
count = counts.get(c.value, 0)
if count == 0 and not show_empty:
return None
label = c.label
if count is not None and show_count:
label = '{} ({})'.format(label, count)
data = {
'id': id_,
'name': label,
'title': label,
'pId': pid,
'isParent': bool(count),
'open': opened,
'iconSkin': '',
'meta': {
'type': tp,
'data': {
'name': c.name,
'value': c.value
}
}
}
return TreeNode(**data)
@classmethod
def create_root_tree_node(cls, queryset, show_count=True):
count = queryset.count() if show_count else None
root_id = 'applications'
root_name = _('Applications')
if count is not None and show_count:
root_name = '{} ({})'.format(root_name, count)
node = TreeNode(**{
'id': root_id,
'name': root_name,
'title': root_name,
'pId': '',
'isParent': True,
'open': True,
'iconSkin': '',
'meta': {
'type': 'applications_root',
}
})
return node
@classmethod
def create_category_tree_nodes(cls, pid, counts=None, show_empty=True, show_count=True):
nodes = []
categories = const.AppType.category_types_mapper().keys()
for category in categories:
if not settings.XPACK_ENABLED and const.AppCategory.is_xpack(category):
continue
i = cls.create_tree_id(pid, 'category', category.value)
node = cls.create_choice_node(
category, i, pid=pid, tp='category',
counts=counts, opened=False, show_empty=show_empty,
show_count=show_count
)
if not node:
continue
nodes.append(node)
return nodes
@classmethod
def create_types_tree_nodes(cls, pid, counts, show_empty=True, show_count=True):
nodes = []
temp_pid = pid
type_category_mapper = const.AppType.type_category_mapper()
types = const.AppType.type_category_mapper().keys()
for tp in types:
if not settings.XPACK_ENABLED and const.AppType.is_xpack(tp):
continue
category = type_category_mapper.get(tp)
pid = cls.create_tree_id(pid, 'category', category.value)
i = cls.create_tree_id(pid, 'type', tp.value)
node = cls.create_choice_node(
tp, i, pid, tp='type', counts=counts, opened=False,
show_empty=show_empty, show_count=show_count
)
pid = temp_pid
if not node:
continue
nodes.append(node)
return nodes
@staticmethod
def get_tree_node_counts(queryset):
counts = defaultdict(int)
values = queryset.values_list('type', 'category')
for i in values:
tp = i[0]
category = i[1]
counts[tp] += 1
counts[category] += 1
return counts
@classmethod
def create_category_type_tree_nodes(cls, queryset, pid, show_empty=True, show_count=True):
counts = cls.get_tree_node_counts(queryset)
tree_nodes = []
# 类别的节点
tree_nodes += cls.create_category_tree_nodes(
pid, counts, show_empty=show_empty,
show_count=show_count
)
# 类型的节点
tree_nodes += cls.create_types_tree_nodes(
pid, counts, show_empty=show_empty,
show_count=show_count
)
return tree_nodes
@classmethod
def create_tree_nodes(cls, queryset, root_node=None, show_empty=True, show_count=True):
tree_nodes = []
# 根节点有可能是组织名称
if root_node is None:
root_node = cls.create_root_tree_node(queryset, show_count=show_count)
tree_nodes.append(root_node)
tree_nodes += cls.create_category_type_tree_nodes(
queryset, root_node.id, show_empty=show_empty, show_count=show_count
)
# 应用的节点
for app in queryset:
if not settings.XPACK_ENABLED and const.AppType.is_xpack(app.type):
continue
node = app.as_tree_node(root_node.id)
tree_nodes.append(node)
return tree_nodes
def create_app_tree_pid(self, root_id):
pid = self.create_tree_id(root_id, 'category', self.category)
pid = self.create_tree_id(pid, 'type', self.type)
return pid
def as_tree_node(self, pid, k8s_as_tree=False):
if self.type == const.AppType.k8s and k8s_as_tree:
node = KubernetesTree(pid).as_tree_node(self)
else:
node = self._as_tree_node(pid)
return node
def _attrs_to_tree(self):
if self.category == const.AppCategory.db:
return self.attrs
return {}
def _as_tree_node(self, pid):
icon_skin_category_mapper = {
'remote_app': 'chrome',
'db': 'database',
'cloud': 'cloud'
}
icon_skin = icon_skin_category_mapper.get(self.category, 'file')
pid = self.create_app_tree_pid(pid)
node = TreeNode(**{
'id': str(self.id),
'name': self.name,
'title': self.name,
'pId': pid,
'isParent': False,
'open': False,
'iconSkin': icon_skin,
'meta': {
'type': 'application',
'data': {
'category': self.category,
'type': self.type,
'attrs': self._attrs_to_tree()
}
}
})
return node

View File

@ -1,4 +1,5 @@
from .mixin import * from .mixin import *
from .platform import *
from .admin_user import * from .admin_user import *
from .asset import * from .asset import *
from .label import * from .label import *

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from rest_framework.viewsets import ModelViewSet
from rest_framework.generics import RetrieveAPIView, ListAPIView from rest_framework.generics import RetrieveAPIView, ListAPIView
from rest_framework.decorators import action
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.db.models import Q from django.db.models import Q
@ -27,8 +27,7 @@ from ..filters import FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFi
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = [
'AssetViewSet', 'AssetPlatformRetrieveApi', 'AssetViewSet', 'AssetPlatformRetrieveApi',
'AssetGatewayListApi', 'AssetPlatformViewSet', 'AssetGatewayListApi', 'AssetTaskCreateApi', 'AssetsTaskCreateApi',
'AssetTaskCreateApi', 'AssetsTaskCreateApi',
'AssetPermUserListApi', 'AssetPermUserPermissionsListApi', 'AssetPermUserListApi', 'AssetPermUserPermissionsListApi',
'AssetPermUserGroupListApi', 'AssetPermUserGroupPermissionsListApi', 'AssetPermUserGroupListApi', 'AssetPermUserGroupPermissionsListApi',
] ]
@ -52,7 +51,8 @@ class AssetViewSet(SuggestionMixin, FilterAssetByNodeMixin, OrgBulkModelViewSet)
ordering = ('hostname', ) ordering = ('hostname', )
serializer_classes = { serializer_classes = {
'default': serializers.AssetSerializer, 'default': serializers.AssetSerializer,
'suggestion': serializers.MiniAssetSerializer 'suggestion': serializers.MiniAssetSerializer,
'platform': serializers.PlatformSerializer
} }
rbac_perms = { rbac_perms = {
'match': 'assets.match_asset' 'match': 'assets.match_asset'
@ -74,6 +74,10 @@ class AssetViewSet(SuggestionMixin, FilterAssetByNodeMixin, OrgBulkModelViewSet)
assets = serializer.save() assets = serializer.save()
self.set_assets_node(assets) self.set_assets_node(assets)
@action(methods='GET', detail=True, url_path='platform')
def platform(self, request, *args, **kwargs):
pass
class AssetPlatformRetrieveApi(RetrieveAPIView): class AssetPlatformRetrieveApi(RetrieveAPIView):
queryset = Platform.objects.all() queryset = Platform.objects.all()
@ -88,20 +92,6 @@ class AssetPlatformRetrieveApi(RetrieveAPIView):
return asset.platform return asset.platform
class AssetPlatformViewSet(ModelViewSet):
queryset = Platform.objects.all()
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 AssetsTaskMixin: class AssetsTaskMixin:
def perform_assets_task(self, serializer): def perform_assets_task(self, serializer):
@ -246,7 +236,7 @@ class AssetPermUserGroupListApi(BaseAssetPermUserOrUserGroupListApi):
return user_groups return user_groups
class BaseAssetPermUserOrUserGroupPermissionsListApiMixin(generics.ListAPIView): class BasePermedAssetListApi(generics.ListAPIView):
model = AssetPermission model = AssetPermission
serializer_class = AssetPermissionSerializer serializer_class = AssetPermissionSerializer
filterset_class = AssetPermissionFilter filterset_class = AssetPermissionFilter
@ -272,7 +262,7 @@ class BaseAssetPermUserOrUserGroupPermissionsListApiMixin(generics.ListAPIView):
return queryset return queryset
class AssetPermUserPermissionsListApi(BaseAssetPermUserOrUserGroupPermissionsListApiMixin): class AssetPermUserPermissionsListApi(BasePermedAssetListApi):
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset) queryset = super().filter_queryset(queryset)
queryset = self.filter_user_related(queryset) queryset = self.filter_user_related(queryset)
@ -291,7 +281,7 @@ class AssetPermUserPermissionsListApi(BaseAssetPermUserOrUserGroupPermissionsLis
return user return user
class AssetPermUserGroupPermissionsListApi(BaseAssetPermUserOrUserGroupPermissionsListApiMixin): class AssetPermUserGroupPermissionsListApi(BasePermedAssetListApi):
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset) queryset = super().filter_queryset(queryset)
queryset = self.filter_user_group_related(queryset) queryset = self.filter_user_group_related(queryset)

View File

@ -0,0 +1,21 @@
from rest_framework.viewsets import ModelViewSet
from assets.models import Platform
from assets.serializers import PlatformSerializer
__all__ = ['AssetPlatformViewSet']
class AssetPlatformViewSet(ModelViewSet):
queryset = Platform.objects.all()
serializer_class = 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)

View File

@ -17,6 +17,6 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='asset', model_name='asset',
name='cluster', name='cluster',
field=models.ForeignKey(default=assets.models.asset.default_cluster, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='assets', to='assets.Cluster', verbose_name='Cluster'), field=models.ForeignKey(default=assets.models.default_cluster, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='assets', to='assets.Cluster', verbose_name='Cluster'),
), ),
] ]

View File

@ -50,7 +50,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='asset', model_name='asset',
name='nodes', name='nodes',
field=models.ManyToManyField(default=assets.models.asset.default_node, related_name='assets', to='assets.Node', verbose_name='Nodes'), field=models.ManyToManyField(default=assets.models.default_node, related_name='assets', to='assets.Node', verbose_name='Nodes'),
), ),
migrations.AddField( migrations.AddField(
model_name='systemuser', model_name='systemuser',

View File

@ -34,7 +34,7 @@ class Migration(migrations.Migration):
model_name='asset', model_name='asset',
name='platform', name='platform',
field=models.ForeignKey( field=models.ForeignKey(
default=assets.models.asset.Platform.default, default=assets.models.Platform.default,
on_delete=django.db.models.deletion.PROTECT, on_delete=django.db.models.deletion.PROTECT,
related_name='assets', to='assets.Platform', related_name='assets', to='assets.Platform',
verbose_name='Platform'), verbose_name='Platform'),

View File

@ -0,0 +1,20 @@
# Generated by Django 3.1.14 on 2022-03-30 10:35
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0089_auto_20220310_0616'),
]
operations = [
migrations.CreateModel(
name='Host',
fields=[
('asset_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='assets.asset')),
],
),
]

View File

@ -0,0 +1,40 @@
# Generated by Django 3.1.14 on 2022-04-01 07:58
from django.db import migrations, models
import django.db.models.deletion
def migrate_to_host(apps, schema_editor):
asset_model = apps.get_model("assets", "Asset")
host_model = apps.get_model("assets", 'Host')
db_alias = schema_editor.connection.alias
created = 0
batch_size = 1000
while True:
start = created
end = created + batch_size
assets = asset_model.objects.using(db_alias).all()[start:end]
if not assets:
break
hosts = [host_model(asset_ptr=asset) for asset in assets]
host_model.objects.using(db_alias).bulk_create(hosts, ignore_conflicts=True)
created += len(hosts)
class Migration(migrations.Migration):
dependencies = [
('assets', '0090_add_host'),
]
operations = [
migrations.AlterField(
model_name='host',
name='asset_ptr',
field=models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='assets.asset'),
),
migrations.RunPython(migrate_to_host)
]

View File

@ -0,0 +1,42 @@
# Generated by Django 3.1.14 on 2022-04-02 09:09
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0091_auto_20220401_1558'),
]
operations = [
migrations.CreateModel(
name='HostInfo',
fields=[
('vendor', models.CharField(blank=True, max_length=64, null=True, verbose_name='Vendor')),
('model', models.CharField(blank=True, max_length=54, null=True, verbose_name='Model')),
('sn', models.CharField(blank=True, max_length=128, null=True, verbose_name='Serial number')),
('cpu_model', models.CharField(blank=True, max_length=64, null=True, verbose_name='CPU model')),
('cpu_count', models.IntegerField(null=True, verbose_name='CPU count')),
('cpu_cores', models.IntegerField(null=True, verbose_name='CPU cores')),
('cpu_vcpus', models.IntegerField(null=True, verbose_name='CPU vcpus')),
('memory', models.CharField(blank=True, max_length=64, null=True, verbose_name='Memory')),
('disk_total', models.CharField(blank=True, max_length=1024, null=True, verbose_name='Disk total')),
('disk_info', models.CharField(blank=True, max_length=1024, null=True, verbose_name='Disk info')),
('os', models.CharField(blank=True, max_length=128, null=True, verbose_name='OS')),
('os_version', models.CharField(blank=True, max_length=16, null=True, verbose_name='OS version')),
('os_arch', models.CharField(blank=True, max_length=16, null=True, verbose_name='OS arch')),
('hostname_raw', models.CharField(blank=True, max_length=128, null=True, verbose_name='Hostname raw')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('host', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='assets.host', related_name='info', verbose_name='Host')),
],
options={
'verbose_name': 'HostInfo',
},
),
]

View File

@ -0,0 +1,51 @@
# Generated by Django 3.1.14 on 2022-04-02 08:27
from django.utils import timezone
from django.db import migrations
def migrate_hardware(apps, *args):
host_model = apps.get_model('assets', 'Host')
asset_model = apps.get_model('assets', 'Asset')
hardware_model = apps.get_model('assets', 'HostInfo')
created = 0
batch_size = 1000
excludes = ['id', 'host', 'date_updated']
fields = [f.name for f in hardware_model._meta.fields]
fields = [name for name in fields if name not in excludes]
while True:
start = created
end = created + batch_size
hosts = host_model.objects.all()[start:end]
asset_ids = [h.asset_ptr_id for h in hosts]
assets = asset_model.objects.filter(id__in=asset_ids)
asset_mapper = {a.id: a for a in assets}
if not hosts:
break
hardware_list = []
for host in hosts:
hardware = hardware_model()
asset = asset_mapper[host.asset_ptr_id]
hardware.host = host
hardware.date_updated = timezone.now()
for name in fields:
setattr(hardware, name, getattr(asset, name))
hardware_list.append(hardware)
hardware_model.objects.bulk_create(hardware_list, ignore_conflicts=True)
created += len(hardware_list)
class Migration(migrations.Migration):
dependencies = [
('assets', '0092_hardware'),
]
operations = [
migrations.RunPython(migrate_hardware)
]

View File

@ -0,0 +1,69 @@
# Generated by Django 3.1.14 on 2022-04-02 09:36
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0093_auto_20220403_1627'),
]
operations = [
migrations.RemoveField(
model_name='asset',
name='cpu_cores',
),
migrations.RemoveField(
model_name='asset',
name='cpu_count',
),
migrations.RemoveField(
model_name='asset',
name='cpu_model',
),
migrations.RemoveField(
model_name='asset',
name='cpu_vcpus',
),
migrations.RemoveField(
model_name='asset',
name='disk_info',
),
migrations.RemoveField(
model_name='asset',
name='disk_total',
),
migrations.RemoveField(
model_name='asset',
name='hostname_raw',
),
migrations.RemoveField(
model_name='asset',
name='memory',
),
migrations.RemoveField(
model_name='asset',
name='model',
),
migrations.RemoveField(
model_name='asset',
name='os',
),
migrations.RemoveField(
model_name='asset',
name='os_arch',
),
migrations.RemoveField(
model_name='asset',
name='os_version',
),
migrations.RemoveField(
model_name='asset',
name='sn',
),
migrations.RemoveField(
model_name='asset',
name='vendor',
),
]

View File

@ -1,4 +1,5 @@
from .base import * from .base import *
from .platform import *
from .asset import * from .asset import *
from .label import Label from .label import Label
from .user import * from .user import *

View File

@ -0,0 +1,2 @@
from .common import *
from .host import *

View File

View File

@ -11,18 +11,17 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from common.fields.model import JsonDictTextField
from common.utils import lazyproperty from common.utils import lazyproperty
from orgs.mixins.models import OrgModelMixin, OrgManager from orgs.mixins.models import OrgModelMixin, OrgManager
from ..platform import Platform
from ..base import AbsConnectivity
from .base import AbsConnectivity __all__ = ['Asset', 'ProtocolsMixin', 'AssetQuerySet', 'default_node', 'default_cluster']
__all__ = ['Asset', 'ProtocolsMixin', 'Platform', 'AssetQuerySet']
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def default_cluster(): def default_cluster():
from .cluster import Cluster from assets.models import Cluster
name = "Default" name = "Default"
defaults = {"name": name} defaults = {"name": name}
cluster, created = Cluster.objects.get_or_create( cluster, created = Cluster.objects.get_or_create(
@ -33,7 +32,7 @@ def default_cluster():
def default_node(): def default_node():
try: try:
from .node import Node from assets.models import Node
root = Node.org_root() root = Node.org_root()
return Node.objects.filter(id=root.id) return Node.objects.filter(id=root.id)
except: except:
@ -106,7 +105,7 @@ class NodesRelationMixin:
_all_nodes_keys = None _all_nodes_keys = None
def get_nodes(self): def get_nodes(self):
from .node import Node from assets.models import Node
nodes = self.nodes.all() nodes = self.nodes.all()
if not nodes: if not nodes:
nodes = Node.objects.filter(id=Node.org_root().id) nodes = Node.objects.filter(id=Node.org_root().id)
@ -122,104 +121,25 @@ class NodesRelationMixin:
return nodes return nodes
class Platform(models.Model): class Asset(AbsConnectivity, ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
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 is_windows(self):
return self.base.lower() in ('windows',)
def is_unixlike(self):
return self.base.lower() in ("linux", "unix", "macos", "bsd")
def __str__(self):
return self.name
class Meta:
verbose_name = _("Platform")
# ordering = ('name',)
class AbsHardwareInfo(models.Model):
# Collect
vendor = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Vendor'))
model = models.CharField(max_length=54, null=True, blank=True, verbose_name=_('Model'))
sn = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Serial number'))
cpu_model = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('CPU model'))
cpu_count = models.IntegerField(null=True, verbose_name=_('CPU count'))
cpu_cores = models.IntegerField(null=True, verbose_name=_('CPU cores'))
cpu_vcpus = models.IntegerField(null=True, verbose_name=_('CPU vcpus'))
memory = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Memory'))
disk_total = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk total'))
disk_info = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk info'))
os = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('OS'))
os_version = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('OS version'))
os_arch = models.CharField(max_length=16, blank=True, null=True, verbose_name=_('OS arch'))
hostname_raw = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Hostname raw'))
class Meta:
abstract = True
@property
def cpu_info(self):
info = ""
if self.cpu_model:
info += self.cpu_model
if self.cpu_count and self.cpu_cores:
info += "{}*{}".format(self.cpu_count, self.cpu_cores)
return info
@property
def hardware_info(self):
if self.cpu_count:
return '{} Core {} {}'.format(
self.cpu_vcpus or self.cpu_count * self.cpu_cores,
self.memory, self.disk_total
)
else:
return ''
class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
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)
hostname = models.CharField(max_length=128, verbose_name=_('Hostname')) hostname = models.CharField(max_length=128, verbose_name=_('Hostname'))
ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True)
protocol = models.CharField(max_length=128, default=ProtocolsMixin.Protocol.ssh, protocol = models.CharField(max_length=128, default=ProtocolsMixin.Protocol.ssh,
choices=ProtocolsMixin.Protocol.choices, verbose_name=_('Protocol')) choices=ProtocolsMixin.Protocol.choices, verbose_name=_('Protocol'))
port = models.IntegerField(default=22, verbose_name=_('Port')) port = models.IntegerField(default=22, verbose_name=_('Port'))
protocols = models.CharField(max_length=128, default='ssh/22', blank=True, verbose_name=_("Protocols")) protocols = models.CharField(max_length=128, default='ssh/22', blank=True, verbose_name=_("Protocols"))
platform = models.ForeignKey(Platform, default=Platform.default, on_delete=models.PROTECT, verbose_name=_("Platform"), related_name='assets') platform = models.ForeignKey(Platform, default=Platform.default, on_delete=models.PROTECT,
domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), on_delete=models.SET_NULL) verbose_name=_("Platform"), related_name='assets')
nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes")) 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')) is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
# Auth # Auth
admin_user = models.ForeignKey('assets.SystemUser', on_delete=models.SET_NULL, null=True, verbose_name=_("Admin user"), related_name='admin_assets') admin_user = models.ForeignKey('assets.SystemUser', on_delete=models.SET_NULL, null=True,
verbose_name=_("Admin user"), related_name='admin_assets')
# Some information # Some information
public_ip = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Public IP')) public_ip = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Public IP'))
@ -236,7 +156,7 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
return '{0.hostname}({0.ip})'.format(self) return '{0.hostname}({0.ip})'.format(self)
def set_admin_user_relation(self): def set_admin_user_relation(self):
from .authbook import AuthBook from assets.models import AuthBook
if not self.admin_user: if not self.admin_user:
return return
if self.admin_user.type != 'admin': if self.admin_user.type != 'admin':
@ -333,7 +253,7 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
return names return names
def as_node(self): def as_node(self):
from .node import Node from assets.models import Node
fake_node = Node() fake_node = Node()
fake_node.id = self.id fake_node.id = self.id
fake_node.key = self.id fake_node.key = self.id
@ -372,7 +292,7 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
return tree_node return tree_node
def get_all_system_users(self): def get_all_system_users(self):
from .user import SystemUser from assets.models import SystemUser
system_user_ids = SystemUser.assets.through.objects.filter(asset=self)\ system_user_ids = SystemUser.assets.through.objects.filter(asset=self)\
.values_list('systemuser_id', flat=True) .values_list('systemuser_id', flat=True)
system_users = SystemUser.objects.filter(id__in=system_user_ids) system_users = SystemUser.objects.filter(id__in=system_user_ids)

View File

@ -0,0 +1,9 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from .common import Asset
class Database(Asset):
database = models.CharField(max_length=1024, verbose_name=_("Database"), blank=True)

View File

@ -0,0 +1,56 @@
from django.utils.translation import gettext_lazy as _
from django.db import models
from common.mixins.models import CommonModelMixin
from .common import Asset
class Host(Asset):
pass
class HostInfo(CommonModelMixin):
host = models.OneToOneField(Host, related_name='info', on_delete=models.CASCADE,
verbose_name=_("Host"), unique=True)
# Collect
vendor = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Vendor'))
model = models.CharField(max_length=54, null=True, blank=True, verbose_name=_('Model'))
sn = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Serial number'))
cpu_model = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('CPU model'))
cpu_count = models.IntegerField(null=True, verbose_name=_('CPU count'))
cpu_cores = models.IntegerField(null=True, verbose_name=_('CPU cores'))
cpu_vcpus = models.IntegerField(null=True, verbose_name=_('CPU vcpus'))
memory = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Memory'))
disk_total = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk total'))
disk_info = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk info'))
os = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('OS'))
os_version = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('OS version'))
os_arch = models.CharField(max_length=16, blank=True, null=True, verbose_name=_('OS arch'))
hostname_raw = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Hostname raw'))
@property
def cpu_info(self):
info = ""
if self.cpu_model:
info += self.cpu_model
if self.cpu_count and self.cpu_cores:
info += "{}*{}".format(self.cpu_count, self.cpu_cores)
return info
@property
def hardware_info(self):
if self.cpu_count:
return '{} Core {} {}'.format(
self.cpu_vcpus or self.cpu_count * self.cpu_cores,
self.memory, self.disk_total
)
else:
return ''
def __str__(self):
return '{} of {}'.format(self.hardware_info, self.host.hostname)
class Meta:
verbose_name = _("HostInfo")

View File

View File

View File

@ -0,0 +1,56 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from common.fields.model import JsonDictTextField
__all__ = ['Platform']
class Category(models.TextChoices):
Host = 'host', _('Host')
Network = 'network', _('Network device')
Database = 'database', _('Database')
RemoteApp = 'remote_app', _('Microsoft remote app')
Cloud = 'cloud', _("Cloud")
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 is_windows(self):
return self.base.lower() in ('windows',)
def is_unixlike(self):
return self.base.lower() in ("linux", "unix", "macos", "bsd")
def __str__(self):
return self.name
class Meta:
verbose_name = _("Platform")
# ordering = ('name',)

View File

@ -0,0 +1 @@
from .common import *

View File

@ -5,7 +5,7 @@ from django.core.validators import RegexValidator
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import Asset, Node, Platform, SystemUser from ...models import Asset, Node, Platform, SystemUser
__all__ = [ __all__ = [
'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer', 'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
@ -82,12 +82,6 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
'protocol', 'port', 'protocols', 'is_active', 'protocol', 'port', 'protocols', 'is_active',
'public_ip', 'number', 'comment', 'public_ip', 'number', 'comment',
] ]
fields_hardware = [
'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info',
'os', 'os_version', 'os_arch', 'hostname_raw',
'cpu_info', 'hardware_info',
]
fields_fk = [ fields_fk = [
'domain', 'domain_display', 'platform', 'admin_user', 'admin_user_display' 'domain', 'domain_display', 'platform', 'admin_user', 'admin_user_display'
] ]
@ -95,16 +89,13 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
'nodes', 'nodes_display', 'labels', 'labels_display', 'nodes', 'nodes_display', 'labels', 'labels_display',
] ]
read_only_fields = [ read_only_fields = [
'connectivity', 'date_verified', 'cpu_info', 'hardware_info', 'connectivity', 'date_verified', 'created_by', 'date_created',
'created_by', 'date_created',
] ]
fields = fields_small + fields_hardware + fields_fk + fields_m2m + read_only_fields fields = fields_small + fields_fk + fields_m2m + read_only_fields
extra_kwargs = { extra_kwargs = {
'protocol': {'write_only': True}, 'protocol': {'write_only': True},
'port': {'write_only': True}, 'port': {'write_only': True},
'hardware_info': {'label': _('Hardware info'), 'read_only': True},
'admin_user_display': {'label': _('Admin user display'), 'read_only': True}, 'admin_user_display': {'label': _('Admin user display'), 'read_only': True},
'cpu_info': {'label': _('CPU info')},
} }
def get_fields(self): def get_fields(self):

View File

@ -0,0 +1,19 @@
from rest_framework import serializers
from .common import AssetSerializer
from assets.models import HostInfo
class HardwareSerializer(serializers.ModelSerializer):
class Meta:
model = HostInfo
fields = [
'id', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info',
'os', 'os_version', 'os_arch', 'hostname_raw',
'cpu_info', 'hardware_info', 'date_updated'
]
class HostSerializer(AssetSerializer):
hardware_info = HardwareSerializer(read_only=True)

View File

@ -2,31 +2,31 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelV
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from ..mixins.api import ( from ..mixins.api import (
RelationMixin, AllowBulkDestroyMixin, CommonMixin RelationMixin, AllowBulkDestroyMixin, CommonApiMixin
) )
class JMSGenericViewSet(CommonMixin, GenericViewSet): class JMSGenericViewSet(CommonApiMixin, GenericViewSet):
pass pass
class JMSViewSet(CommonMixin, ViewSet): class JMSViewSet(CommonApiMixin, ViewSet):
pass pass
class JMSModelViewSet(CommonMixin, ModelViewSet): class JMSModelViewSet(CommonApiMixin, ModelViewSet):
pass pass
class JMSReadOnlyModelViewSet(CommonMixin, ReadOnlyModelViewSet): class JMSReadOnlyModelViewSet(CommonApiMixin, ReadOnlyModelViewSet):
pass pass
class JMSBulkModelViewSet(CommonMixin, AllowBulkDestroyMixin, BulkModelViewSet): class JMSBulkModelViewSet(CommonApiMixin, AllowBulkDestroyMixin, BulkModelViewSet):
pass pass
class JMSBulkRelationModelViewSet(CommonMixin, class JMSBulkRelationModelViewSet(CommonApiMixin,
RelationMixin, RelationMixin,
AllowBulkDestroyMixin, AllowBulkDestroyMixin,
BulkModelViewSet): BulkModelViewSet):

View File

@ -13,7 +13,7 @@ from .queryset import QuerySetMixin
__all__ = [ __all__ = [
'CommonApiMixin', 'PaginatedResponseMixin', 'RelationMixin', 'CommonMixin' 'CommonApiMixin', 'PaginatedResponseMixin', 'RelationMixin',
] ]
@ -82,15 +82,10 @@ class RelationMixin:
self.send_m2m_changed_signal(instance, 'post_remove') self.send_m2m_changed_signal(instance, 'post_remove')
class CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin, RenderToJsonMixin): class CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin,
pass QuerySetMixin, RenderToJsonMixin):
class CommonMixin(SerializerMixin,
QuerySetMixin,
ExtraFilterFieldsMixin,
RenderToJsonMixin):
pass pass

1
apps/xpack.bak Submodule

@ -0,0 +1 @@
Subproject commit 244ace5a95503ffaf41b73037692e1121f7c066f