mirror of https://github.com/jumpserver/jumpserver
Browse Source
* feat: 支持 virtual app * perf: 增加 virtual host * perf: 新增 virtual app 上传接口 * perf: 更名为 app provider * perf: 优化代码 --------- Co-authored-by: Eric <xplzv@126.com>pull/12257/head
fit2bot
12 months ago
committed by
GitHub
25 changed files with 605 additions and 5 deletions
@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*- |
||||
# |
||||
from .session import * |
||||
from .component import * |
||||
from .applet import * |
||||
from .component import * |
||||
from .db_listen_port import * |
||||
from .session import * |
||||
from .virtualapp import * |
||||
|
@ -0,0 +1,3 @@
|
||||
from .provider import * |
||||
from .relation import * |
||||
from .virtualapp import * |
@ -0,0 +1,63 @@
|
||||
from django.core.cache import cache |
||||
from rest_framework.decorators import action |
||||
from rest_framework.exceptions import ValidationError |
||||
from rest_framework.response import Response |
||||
|
||||
from common.api import JMSBulkModelViewSet |
||||
from common.permissions import IsServiceAccount |
||||
from orgs.utils import tmp_to_builtin_org |
||||
from terminal.models import AppProvider |
||||
from terminal.serializers import ( |
||||
AppProviderSerializer, AppProviderContainerSerializer |
||||
) |
||||
|
||||
__all__ = ['AppProviderViewSet', ] |
||||
|
||||
|
||||
class AppProviderViewSet(JMSBulkModelViewSet): |
||||
serializer_class = AppProviderSerializer |
||||
queryset = AppProvider.objects.all() |
||||
search_fields = ['name', 'hostname', ] |
||||
rbac_perms = { |
||||
'containers': 'terminal.view_appprovider', |
||||
'status': 'terminal.view_appprovider', |
||||
} |
||||
|
||||
cache_status_key_prefix = 'virtual_host_{}_status' |
||||
|
||||
def dispatch(self, request, *args, **kwargs): |
||||
with tmp_to_builtin_org(system=1): |
||||
return super().dispatch(request, *args, **kwargs) |
||||
|
||||
def get_permissions(self): |
||||
if self.action == 'create': |
||||
return [IsServiceAccount()] |
||||
return super().get_permissions() |
||||
|
||||
def perform_create(self, serializer): |
||||
request_terminal = getattr(self.request.user, 'terminal', None) |
||||
if not request_terminal: |
||||
raise ValidationError('Request user has no terminal') |
||||
data = dict() |
||||
data['terminal'] = request_terminal |
||||
data['id'] = self.request.user.id |
||||
serializer.save(**data) |
||||
|
||||
@action(detail=True, methods=['get'], serializer_class=AppProviderContainerSerializer) |
||||
def containers(self, request, *args, **kwargs): |
||||
instance = self.get_object() |
||||
key = self.cache_status_key_prefix.format(instance.id) |
||||
data = cache.get(key) |
||||
if not data: |
||||
data = [] |
||||
return self.get_paginated_response_from_queryset(data) |
||||
|
||||
@action(detail=True, methods=['post'], serializer_class=AppProviderContainerSerializer) |
||||
def status(self, request, *args, **kwargs): |
||||
instance = self.get_object() |
||||
serializer = self.get_serializer(data=request.data, many=True) |
||||
serializer.is_valid(raise_exception=True) |
||||
validated_data = serializer.validated_data |
||||
key = self.cache_status_key_prefix.format(instance.id) |
||||
cache.set(key, validated_data, 60 * 3) |
||||
return Response({'msg': 'ok'}) |
@ -0,0 +1,64 @@
|
||||
from typing import Callable |
||||
|
||||
from django.conf import settings |
||||
from django.shortcuts import get_object_or_404 |
||||
from rest_framework.request import Request |
||||
|
||||
from common.api import JMSModelViewSet |
||||
from common.permissions import IsServiceAccount |
||||
from common.utils import is_uuid |
||||
from rbac.permissions import RBACPermission |
||||
from terminal.models import AppProvider |
||||
from terminal.serializers import ( |
||||
VirtualAppPublicationSerializer |
||||
) |
||||
|
||||
|
||||
class ProviderMixin: |
||||
request: Request |
||||
permission_denied: Callable |
||||
kwargs: dict |
||||
rbac_perms = ( |
||||
('list', 'terminal.view_appprovider'), |
||||
('retrieve', 'terminal.view_appprovider'), |
||||
) |
||||
|
||||
def get_permissions(self): |
||||
if self.kwargs.get('host') and settings.DEBUG: |
||||
return [RBACPermission()] |
||||
else: |
||||
return [IsServiceAccount()] |
||||
|
||||
def self_provider(self): |
||||
try: |
||||
return self.request.user.terminal.app_provider |
||||
except AttributeError: |
||||
raise self.permission_denied(self.request, 'User has no app provider') |
||||
|
||||
def pk_provider(self): |
||||
return get_object_or_404(AppProvider, id=self.kwargs.get('provider')) |
||||
|
||||
@property |
||||
def provider(self): |
||||
if self.kwargs.get('provider'): |
||||
host = self.pk_provider() |
||||
else: |
||||
host = self.self_provider() |
||||
return host |
||||
|
||||
|
||||
class AppProviderAppViewSet(ProviderMixin, JMSModelViewSet): |
||||
provider: AppProvider |
||||
serializer_class = VirtualAppPublicationSerializer |
||||
filterset_fields = ['provider__name', 'app__name', 'status'] |
||||
|
||||
def get_object(self): |
||||
pk = self.kwargs.get('pk') |
||||
if not is_uuid(pk): |
||||
return self.provider.publications.get(app__name=pk) |
||||
else: |
||||
return self.provider.publications.get(id=pk) |
||||
|
||||
def get_queryset(self): |
||||
queryset = self.provider.publications.all() |
||||
return queryset |
@ -0,0 +1,77 @@
|
||||
import os.path |
||||
import shutil |
||||
import zipfile |
||||
from typing import Callable |
||||
|
||||
from django.core.files.storage import default_storage |
||||
from django.utils._os import safe_join |
||||
from django.utils.translation import gettext as _ |
||||
from rest_framework import viewsets |
||||
from rest_framework.decorators import action |
||||
from rest_framework.request import Request |
||||
from rest_framework.response import Response |
||||
from rest_framework.serializers import ValidationError |
||||
|
||||
from common.api import JMSBulkModelViewSet |
||||
from common.serializers import FileSerializer |
||||
from terminal import serializers |
||||
from terminal.models import VirtualAppPublication, VirtualApp |
||||
|
||||
__all__ = ['VirtualAppViewSet', 'VirtualAppPublicationViewSet'] |
||||
|
||||
|
||||
class UploadMixin: |
||||
get_serializer: Callable |
||||
request: Request |
||||
get_object: Callable |
||||
|
||||
def extract_zip_pkg(self): |
||||
serializer = self.get_serializer(data=self.request.data) |
||||
serializer.is_valid(raise_exception=True) |
||||
file = serializer.validated_data['file'] |
||||
save_to = 'virtual_apps/{}'.format(file.name + '.tmp.zip') |
||||
if default_storage.exists(save_to): |
||||
default_storage.delete(save_to) |
||||
rel_path = default_storage.save(save_to, file) |
||||
path = default_storage.path(rel_path) |
||||
extract_to = default_storage.path('virtual_apps/{}.tmp'.format(file.name)) |
||||
if os.path.exists(extract_to): |
||||
shutil.rmtree(extract_to) |
||||
try: |
||||
with zipfile.ZipFile(path) as zp: |
||||
if zp.testzip() is not None: |
||||
raise ValidationError({'error': _('Invalid zip file')}) |
||||
zp.extractall(extract_to) |
||||
except RuntimeError as e: |
||||
raise ValidationError({'error': _('Invalid zip file') + ': {}'.format(e)}) |
||||
tmp_dir = safe_join(extract_to, file.name.replace('.zip', '')) |
||||
return tmp_dir |
||||
|
||||
@action(detail=False, methods=['post'], serializer_class=FileSerializer) |
||||
def upload(self, request, *args, **kwargs): |
||||
tmp_dir = self.extract_zip_pkg() |
||||
manifest = VirtualApp.validate_pkg(tmp_dir) |
||||
name = manifest['name'] |
||||
instance = VirtualApp.objects.filter(name=name).first() |
||||
if instance: |
||||
return Response({'error': 'virtual app already exists: {}'.format(name)}, status=400) |
||||
|
||||
app, serializer = VirtualApp.install_from_dir(tmp_dir) |
||||
return Response(serializer.data, status=201) |
||||
|
||||
|
||||
class VirtualAppViewSet(UploadMixin, JMSBulkModelViewSet): |
||||
queryset = VirtualApp.objects.all() |
||||
serializer_class = serializers.VirtualAppSerializer |
||||
filterset_fields = ['name', 'image_name', 'is_active'] |
||||
search_fields = ['name', ] |
||||
rbac_perms = { |
||||
'upload': 'terminal.add_virtualapp', |
||||
} |
||||
|
||||
|
||||
class VirtualAppPublicationViewSet(viewsets.ModelViewSet): |
||||
queryset = VirtualAppPublication.objects.all() |
||||
serializer_class = serializers.VirtualAppPublicationSerializer |
||||
filterset_fields = ['app__name', 'provider__name', 'status'] |
||||
search_fields = ['app__name', 'provider__name', ] |
@ -0,0 +1,93 @@
|
||||
# Generated by Django 4.1.10 on 2023-12-05 07:02 |
||||
|
||||
from django.db import migrations, models |
||||
import django.db.models.deletion |
||||
import uuid |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('terminal', '0067_alter_replaystorage_type'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.CreateModel( |
||||
name='AppProvider', |
||||
fields=[ |
||||
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), |
||||
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated 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')), |
||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')), |
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), |
||||
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')), |
||||
('hostname', models.CharField(max_length=128, verbose_name='Hostname')), |
||||
], |
||||
options={ |
||||
'ordering': ('-date_created',), |
||||
}, |
||||
), |
||||
migrations.CreateModel( |
||||
name='VirtualApp', |
||||
fields=[ |
||||
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), |
||||
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated 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')), |
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), |
||||
('name', models.SlugField(max_length=128, unique=True, verbose_name='Name')), |
||||
('display_name', models.CharField(max_length=128, verbose_name='Display name')), |
||||
('version', models.CharField(max_length=16, verbose_name='Version')), |
||||
('author', models.CharField(max_length=128, verbose_name='Author')), |
||||
('is_active', models.BooleanField(default=True, verbose_name='Is active')), |
||||
('protocols', models.JSONField(default=list, verbose_name='Protocol')), |
||||
('image_name', models.CharField(max_length=128, verbose_name='Image name')), |
||||
('image_protocol', models.CharField(default='vnc', max_length=16, verbose_name='Image protocol')), |
||||
('image_port', models.IntegerField(default=5900, verbose_name='Image port')), |
||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')), |
||||
('tags', models.JSONField(default=list, verbose_name='Tags')), |
||||
], |
||||
options={ |
||||
'verbose_name': 'Virtual app', |
||||
}, |
||||
), |
||||
migrations.AlterField( |
||||
model_name='terminal', |
||||
name='type', |
||||
field=models.CharField(choices=[('koko', 'KoKo'), ('guacamole', 'Guacamole'), ('omnidb', 'OmniDB'), ('xrdp', 'Xrdp'), ('lion', 'Lion'), ('core', 'Core'), ('celery', 'Celery'), ('magnus', 'Magnus'), ('razor', 'Razor'), ('tinker', 'Tinker'), ('video_worker', 'Video Worker'), ('chen', 'Chen'), ('kael', 'Kael'), ('panda', 'Panda')], default='koko', max_length=64, verbose_name='type'), |
||||
), |
||||
migrations.CreateModel( |
||||
name='VirtualAppPublication', |
||||
fields=[ |
||||
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), |
||||
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated 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')), |
||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')), |
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), |
||||
('status', models.CharField(default='pending', max_length=16, verbose_name='Status')), |
||||
('app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='publications', to='terminal.virtualapp', verbose_name='Virtual App')), |
||||
('provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='publications', to='terminal.appprovider', verbose_name='App Provider')), |
||||
], |
||||
options={ |
||||
'verbose_name': 'Virtual app publication', |
||||
'unique_together': {('provider', 'app')}, |
||||
}, |
||||
), |
||||
migrations.AddField( |
||||
model_name='virtualapp', |
||||
name='providers', |
||||
field=models.ManyToManyField(through='terminal.VirtualAppPublication', to='terminal.appprovider', verbose_name='Providers'), |
||||
), |
||||
migrations.AddField( |
||||
model_name='appprovider', |
||||
name='apps', |
||||
field=models.ManyToManyField(through='terminal.VirtualAppPublication', to='terminal.virtualapp', verbose_name='VirtualApp'), |
||||
), |
||||
migrations.AddField( |
||||
model_name='appprovider', |
||||
name='terminal', |
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='app_provider', to='terminal.terminal', verbose_name='Terminal'), |
||||
), |
||||
] |
@ -1,3 +1,4 @@
|
||||
from .session import * |
||||
from .component import * |
||||
from .applet import * |
||||
from .virtualapp import * |
||||
|
@ -0,0 +1,2 @@
|
||||
from .provider import * |
||||
from .virtualapp import * |
@ -0,0 +1,28 @@
|
||||
from django.db import models |
||||
from django.utils.translation import gettext_lazy as _ |
||||
|
||||
from common.db.models import JMSBaseModel |
||||
|
||||
__all__ = ['AppProvider', ] |
||||
|
||||
|
||||
class AppProvider(JMSBaseModel): |
||||
name = models.CharField(max_length=128, verbose_name=_('Name'), unique=True) |
||||
hostname = models.CharField(max_length=128, verbose_name=_('Hostname')) |
||||
terminal = models.OneToOneField( |
||||
'terminal.Terminal', on_delete=models.CASCADE, null=True, blank=True, |
||||
related_name='app_provider', verbose_name=_('Terminal') |
||||
) |
||||
apps = models.ManyToManyField( |
||||
'VirtualApp', verbose_name=_('VirtualApp'), |
||||
through='VirtualAppPublication', through_fields=('provider', 'app'), |
||||
) |
||||
|
||||
class Meta: |
||||
ordering = ('-date_created',) |
||||
|
||||
@property |
||||
def load(self): |
||||
if not self.terminal: |
||||
return 'offline' |
||||
return self.terminal.load |
@ -0,0 +1,103 @@
|
||||
import os |
||||
import shutil |
||||
|
||||
from django.conf import settings |
||||
from django.core.files.storage import default_storage |
||||
from django.db import models |
||||
from django.utils._os import safe_join |
||||
from django.utils.translation import gettext_lazy as _ |
||||
from rest_framework.serializers import ValidationError |
||||
|
||||
from common.db.models import JMSBaseModel |
||||
from common.utils import lazyproperty |
||||
from common.utils.yml import yaml_load_with_i18n |
||||
|
||||
__all__ = ['VirtualApp', 'VirtualAppPublication'] |
||||
|
||||
|
||||
class VirtualApp(JMSBaseModel): |
||||
name = models.SlugField(max_length=128, verbose_name=_('Name'), unique=True) |
||||
display_name = models.CharField(max_length=128, verbose_name=_('Display name')) |
||||
version = models.CharField(max_length=16, verbose_name=_('Version')) |
||||
author = models.CharField(max_length=128, verbose_name=_('Author')) |
||||
is_active = models.BooleanField(default=True, verbose_name=_('Is active')) |
||||
protocols = models.JSONField(default=list, verbose_name=_('Protocol')) |
||||
image_name = models.CharField(max_length=128, verbose_name=_('Image name')) |
||||
image_protocol = models.CharField(max_length=16, default='vnc', verbose_name=_('Image protocol')) |
||||
image_port = models.IntegerField(default=5900, verbose_name=_('Image port')) |
||||
comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) |
||||
tags = models.JSONField(default=list, verbose_name=_('Tags')) |
||||
providers = models.ManyToManyField( |
||||
through_fields=('app', 'provider',), through='VirtualAppPublication', |
||||
to='AppProvider', verbose_name=_('Providers') |
||||
) |
||||
|
||||
class Meta: |
||||
verbose_name = _('Virtual app') |
||||
|
||||
def __str__(self): |
||||
return self.name |
||||
|
||||
@property |
||||
def path(self): |
||||
return default_storage.path('virtual_apps/{}'.format(self.name)) |
||||
|
||||
@lazyproperty |
||||
def readme(self): |
||||
readme_file = os.path.join(self.path, 'README.md') |
||||
if os.path.isfile(readme_file): |
||||
with open(readme_file, 'r') as f: |
||||
return f.read() |
||||
return '' |
||||
|
||||
@property |
||||
def icon(self): |
||||
path = os.path.join(self.path, 'icon.png') |
||||
if not os.path.exists(path): |
||||
return None |
||||
return os.path.join(settings.MEDIA_URL, 'virtual_apps', self.name, 'icon.png') |
||||
|
||||
@staticmethod |
||||
def validate_pkg(d): |
||||
files = ['manifest.yml', 'icon.png', ] |
||||
for name in files: |
||||
path = safe_join(d, name) |
||||
if not os.path.exists(path): |
||||
raise ValidationError({'error': _('Applet pkg not valid, Missing file {}').format(name)}) |
||||
|
||||
with open(safe_join(d, 'manifest.yml'), encoding='utf8') as f: |
||||
manifest = yaml_load_with_i18n(f) |
||||
|
||||
if not manifest.get('name', ''): |
||||
raise ValidationError({'error': 'Missing name in manifest.yml'}) |
||||
return manifest |
||||
|
||||
@classmethod |
||||
def install_from_dir(cls, path): |
||||
from terminal.serializers import VirtualAppSerializer |
||||
manifest = cls.validate_pkg(path) |
||||
name = manifest['name'] |
||||
instance = cls.objects.filter(name=name).first() |
||||
serializer = VirtualAppSerializer(instance=instance, data=manifest) |
||||
serializer.is_valid(raise_exception=True) |
||||
instance = serializer.save() |
||||
|
||||
pkg_path = default_storage.path('virtual_apps/{}'.format(name)) |
||||
if os.path.exists(pkg_path): |
||||
shutil.rmtree(pkg_path) |
||||
shutil.copytree(path, pkg_path) |
||||
return instance, serializer |
||||
|
||||
|
||||
class VirtualAppPublication(JMSBaseModel): |
||||
provider = models.ForeignKey( |
||||
'AppProvider', on_delete=models.CASCADE, related_name='publications', verbose_name=_('App Provider') |
||||
) |
||||
app = models.ForeignKey( |
||||
'VirtualApp', on_delete=models.CASCADE, related_name='publications', verbose_name=_('Virtual App') |
||||
) |
||||
status = models.CharField(max_length=16, default='pending', verbose_name=_('Status')) |
||||
|
||||
class Meta: |
||||
verbose_name = _('Virtual app publication') |
||||
unique_together = ('provider', 'app') |
@ -0,0 +1,41 @@
|
||||
from django.utils.translation import gettext_lazy as _ |
||||
from rest_framework import serializers |
||||
|
||||
from common.const.choices import Status |
||||
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField |
||||
from terminal.const import PublishStatus |
||||
from ..models import VirtualApp, VirtualAppPublication, AppProvider |
||||
|
||||
__all__ = [ |
||||
'VirtualAppSerializer', 'VirtualAppPublicationSerializer' |
||||
] |
||||
|
||||
|
||||
class VirtualAppSerializer(serializers.ModelSerializer): |
||||
icon = serializers.ReadOnlyField(label=_("Icon")) |
||||
image_protocol = serializers.CharField(max_length=16, default='vnc') |
||||
image_port = serializers.IntegerField(default=5900) |
||||
|
||||
class Meta: |
||||
model = VirtualApp |
||||
fields_mini = ['id', 'display_name', 'name', 'image_name', 'is_active'] |
||||
read_only_fields = [ |
||||
'icon', 'readme', 'date_created', 'date_updated', |
||||
] |
||||
fields = fields_mini + [ |
||||
'version', 'author', 'image_protocol', 'image_port', |
||||
'protocols', 'tags', 'comment', |
||||
] + read_only_fields |
||||
|
||||
|
||||
class VirtualAppPublicationSerializer(serializers.ModelSerializer): |
||||
app = ObjectRelatedField(attrs=('id', 'name', 'image_name',), label=_("Virtual App"), |
||||
queryset=VirtualApp.objects.all()) |
||||
provider = ObjectRelatedField(queryset=AppProvider.objects.all(), label=_("App Provider")) |
||||
status = LabeledChoiceField(choices=PublishStatus.choices, label=_("Status"), default=Status.pending) |
||||
|
||||
class Meta: |
||||
model = VirtualAppPublication |
||||
fields_mini = ['id', 'provider', 'app'] |
||||
read_only_fields = ['date_created', 'date_updated'] |
||||
fields = fields_mini + ['status', 'comment'] + read_only_fields |
@ -0,0 +1,31 @@
|
||||
from django.utils.translation import gettext_lazy as _ |
||||
from rest_framework import serializers |
||||
|
||||
from common.serializers.fields import LabeledChoiceField |
||||
from terminal import const |
||||
from ..models import AppProvider |
||||
|
||||
__all__ = ['AppProviderSerializer', 'AppProviderContainerSerializer', ] |
||||
|
||||
|
||||
class AppProviderSerializer(serializers.ModelSerializer): |
||||
load = LabeledChoiceField( |
||||
read_only=True, label=_('Load status'), choices=const.ComponentLoad.choices, |
||||
) |
||||
|
||||
class Meta: |
||||
model = AppProvider |
||||
field_mini = ['id', 'name', 'hostname'] |
||||
read_only_fields = [ |
||||
'date_created', 'date_updated', |
||||
] |
||||
fields = field_mini + ['load', 'terminal'] + read_only_fields |
||||
|
||||
|
||||
class AppProviderContainerSerializer(serializers.Serializer): |
||||
container_id = serializers.CharField(label=_('Container ID')) |
||||
container_image = serializers.CharField(label=_('Container Image')) |
||||
container_name = serializers.CharField(label=_('Container Name')) |
||||
container_status = serializers.CharField(label=_('Container Status')) |
||||
container_ports = serializers.ListField(child=serializers.CharField(), label=_('Container Ports')) |
||||
|
@ -0,0 +1,24 @@
|
||||
from django.db.models.signals import post_save |
||||
from django.dispatch import receiver |
||||
|
||||
from common.decorators import on_transaction_commit |
||||
from ..models import AppProvider, VirtualApp |
||||
|
||||
|
||||
@receiver(post_save, sender=AppProvider) |
||||
@on_transaction_commit |
||||
def on_virtual_host_create(sender, instance, created=False, **kwargs): |
||||
if not created: |
||||
return |
||||
apps = VirtualApp.objects.all() |
||||
instance.apps.set(apps) |
||||
|
||||
|
||||
@receiver(post_save, sender=VirtualApp) |
||||
def on_virtual_app_create(sender, instance, created=False, **kwargs): |
||||
if not created: |
||||
return |
||||
providers = AppProvider.objects.all() |
||||
if len(providers) == 0: |
||||
return |
||||
instance.providers.set(providers) |
Loading…
Reference in new issue