Browse Source

feat: 支持 virtual app (#12199)

* 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
parent
commit
d2429f7883
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 13
      apps/authentication/api/connection_token.py
  2. 11
      apps/authentication/models/connection_token.py
  3. 10
      apps/authentication/serializers/connect_token_secret.py
  4. 1
      apps/jumpserver/conf.py
  5. 1
      apps/jumpserver/rewriting/storage/permissions.py
  6. 2
      apps/jumpserver/settings/custom.py
  7. 1
      apps/settings/serializers/public.py
  8. 5
      apps/terminal/api/__init__.py
  9. 3
      apps/terminal/api/virtualapp/__init__.py
  10. 63
      apps/terminal/api/virtualapp/provider.py
  11. 64
      apps/terminal/api/virtualapp/relation.py
  12. 77
      apps/terminal/api/virtualapp/virtualapp.py
  13. 28
      apps/terminal/connect_methods.py
  14. 1
      apps/terminal/const.py
  15. 93
      apps/terminal/migrations/0068_virtualapp.py
  16. 1
      apps/terminal/models/__init__.py
  17. 2
      apps/terminal/models/virtualapp/__init__.py
  18. 28
      apps/terminal/models/virtualapp/provider.py
  19. 103
      apps/terminal/models/virtualapp/virtualapp.py
  20. 2
      apps/terminal/serializers/__init__.py
  21. 41
      apps/terminal/serializers/virtualapp.py
  22. 31
      apps/terminal/serializers/virtualapp_provider.py
  23. 1
      apps/terminal/signal_handlers/__init__.py
  24. 24
      apps/terminal/signal_handlers/virtualapp.py
  25. 4
      apps/terminal/urls/api_urls.py

13
apps/authentication/api/connection_token.py

@ -33,7 +33,7 @@ from ..models import ConnectionToken, date_expired_default
from ..serializers import (
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
SuperConnectionTokenSerializer, ConnectTokenAppletOptionSerializer,
ConnectionTokenReusableSerializer,
ConnectionTokenReusableSerializer, ConnectTokenVirtualAppOptionSerializer
)
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
@ -464,6 +464,7 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
'get_secret_detail': 'authentication.view_superconnectiontokensecret',
'get_applet_info': 'authentication.view_superconnectiontoken',
'release_applet_account': 'authentication.view_superconnectiontoken',
'get_virtual_app_info': 'authentication.view_superconnectiontoken',
}
def get_queryset(self):
@ -529,6 +530,16 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
serializer = ConnectTokenAppletOptionSerializer(data)
return Response(serializer.data)
@action(methods=['POST'], detail=False, url_path='virtual-app-option')
def get_virtual_app_info(self, *args, **kwargs):
token_id = self.request.data.get('id')
token = get_object_or_404(ConnectionToken, pk=token_id)
if token.is_expired:
return Response({'error': 'Token expired'}, status=status.HTTP_400_BAD_REQUEST)
data = token.get_virtual_app_option()
serializer = ConnectTokenVirtualAppOptionSerializer(data)
return Response(serializer.data)
@action(methods=['DELETE', 'POST'], detail=False, url_path='applet-account/release')
def release_applet_account(self, *args, **kwargs):
account_id = self.request.data.get('id')

11
apps/authentication/models/connection_token.py

@ -18,7 +18,7 @@ from common.utils import lazyproperty, pretty_string, bulk_get
from common.utils.timezone import as_current_tz
from orgs.mixins.models import JMSOrgBaseModel
from orgs.utils import tmp_to_org
from terminal.models import Applet
from terminal.models import Applet, VirtualApp
def date_expired_default():
@ -177,6 +177,15 @@ class ConnectionToken(JMSOrgBaseModel):
}
return options
def get_virtual_app_option(self):
method = self.connect_method_object
if not method or method.get('type') != 'virtual_app' or method.get('disabled', False):
return None
virtual_app = VirtualApp.objects.filter(name=method.get('value')).first()
if not virtual_app:
return None
return virtual_app
def get_applet_option(self):
method = self.connect_method_object
if not method or method.get('type') != 'applet' or method.get('disabled', False):

10
apps/authentication/serializers/connect_token_secret.py

@ -15,7 +15,8 @@ from users.models import User
from ..models import ConnectionToken
__all__ = [
'ConnectionTokenSecretSerializer', 'ConnectTokenAppletOptionSerializer'
'ConnectionTokenSecretSerializer', 'ConnectTokenAppletOptionSerializer',
'ConnectTokenVirtualAppOptionSerializer',
]
@ -161,3 +162,10 @@ class ConnectTokenAppletOptionSerializer(serializers.Serializer):
account = _ConnectionTokenAccountSerializer(read_only=True)
gateway = _ConnectionTokenGatewaySerializer(read_only=True)
remote_app_option = serializers.JSONField(read_only=True)
class ConnectTokenVirtualAppOptionSerializer(serializers.Serializer):
name = serializers.CharField(label=_('Name'))
image_name = serializers.CharField(label=_('Image name'))
image_port = serializers.IntegerField(label=_('Image port'))
image_protocol = serializers.CharField(label=_('Image protocol'))

1
apps/jumpserver/conf.py

@ -598,6 +598,7 @@ class Config(dict):
'GPT_BASE_URL': '',
'GPT_PROXY': '',
'GPT_MODEL': 'gpt-3.5-turbo',
'VIRTUAL_APP_ENABLED': False,
}
old_config_map = {

1
apps/jumpserver/rewriting/storage/permissions.py

@ -5,6 +5,7 @@ path_perms_map = {
'settings': '*',
'replay': 'default',
'applets': 'terminal.view_applet',
'virtual_apps': 'terminal.view_virtualapp',
'playbooks': 'ops.view_playbook'
}

2
apps/jumpserver/settings/custom.py

@ -219,3 +219,5 @@ GPT_API_KEY = CONFIG.GPT_API_KEY
GPT_BASE_URL = CONFIG.GPT_BASE_URL
GPT_PROXY = CONFIG.GPT_PROXY
GPT_MODEL = CONFIG.GPT_MODEL
VIRTUAL_APP_ENABLED = CONFIG.VIRTUAL_APP_ENABLED

1
apps/settings/serializers/public.py

@ -53,6 +53,7 @@ class PrivateSettingSerializer(PublicSettingSerializer):
CONNECTION_TOKEN_REUSABLE = serializers.BooleanField()
CACHE_LOGIN_PASSWORD_ENABLED = serializers.BooleanField()
VAULT_ENABLED = serializers.BooleanField()
VIRTUAL_APP_ENABLED = serializers.BooleanField()
class ServerInfoSerializer(serializers.Serializer):

5
apps/terminal/api/__init__.py

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

3
apps/terminal/api/virtualapp/__init__.py

@ -0,0 +1,3 @@
from .provider import *
from .relation import *
from .virtualapp import *

63
apps/terminal/api/virtualapp/provider.py

@ -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'})

64
apps/terminal/api/virtualapp/relation.py

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

77
apps/terminal/api/virtualapp/virtualapp.py

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

28
apps/terminal/connect_methods.py

@ -113,6 +113,26 @@ class AppletMethod:
return methods
class VirtualAppMethod:
@classmethod
def get_methods(cls):
from .models import VirtualApp
methods = defaultdict(list)
if not getattr(settings, 'VIRTUAL_APP_ENABLED'):
return methods
virtual_apps = VirtualApp.objects.filter(is_active=True)
for virtual_app in virtual_apps:
for protocol in virtual_app.protocols:
methods[protocol].append({
'value': virtual_app.name,
'label': virtual_app.name,
'type': 'virtual_app',
'disabled': not virtual_app.is_active,
})
return methods
class ConnectMethodUtil:
_all_methods = {}
@ -243,6 +263,7 @@ class ConnectMethodUtil:
methods = defaultdict(list)
spec_web_methods = WebMethod.get_spec_methods()
applet_methods = AppletMethod.get_methods()
virtual_app_methods = VirtualAppMethod.get_methods()
native_methods = NativeClient.get_methods(os=os)
for component, component_protocol in cls.components().items():
@ -295,5 +316,12 @@ class ConnectMethodUtil:
method['component'] = TerminalType.tinker.value
methods[asset_protocol].extend(applet_methods)
# 虚拟应用方式,这个只有 panda 提供,并且协议可能是自定义的
for protocol, virtual_app_methods in virtual_app_methods.items():
for method in virtual_app_methods:
method['listen'] = Protocol.http
method['component'] = TerminalType.panda.value
methods[protocol].extend(virtual_app_methods)
cls._all_methods[os] = methods
return methods

1
apps/terminal/const.py

@ -66,6 +66,7 @@ class TerminalType(TextChoices):
video_worker = 'video_worker', 'Video Worker'
chen = 'chen', 'Chen'
kael = 'kael', 'Kael'
panda = 'panda', 'Panda'
@classmethod
def types(cls):

93
apps/terminal/migrations/0068_virtualapp.py

@ -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
apps/terminal/models/__init__.py

@ -1,3 +1,4 @@
from .session import *
from .component import *
from .applet import *
from .virtualapp import *

2
apps/terminal/models/virtualapp/__init__.py

@ -0,0 +1,2 @@
from .provider import *
from .virtualapp import *

28
apps/terminal/models/virtualapp/provider.py

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

103
apps/terminal/models/virtualapp/virtualapp.py

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

2
apps/terminal/serializers/__init__.py

@ -9,3 +9,5 @@ from .sharing import *
from .storage import *
from .task import *
from .terminal import *
from .virtualapp import *
from .virtualapp_provider import *

41
apps/terminal/serializers/virtualapp.py

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

31
apps/terminal/serializers/virtualapp_provider.py

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

1
apps/terminal/signal_handlers/__init__.py

@ -3,3 +3,4 @@ from .db_port import *
from .session import *
from .session_sharing import *
from .terminal import *
from .virtualapp import *

24
apps/terminal/signal_handlers/virtualapp.py

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

4
apps/terminal/urls/api_urls.py

@ -30,6 +30,10 @@ router.register(r'applet-hosts', api.AppletHostViewSet, 'applet-host')
router.register(r'applet-publications', api.AppletPublicationViewSet, 'applet-publication')
router.register(r'applet-host-deployments', api.AppletHostDeploymentViewSet, 'applet-host-deployment')
router.register(r'db-listen-ports', api.DBListenPortViewSet, 'db-listen-ports')
router.register(r'virtual-apps', api.VirtualAppViewSet, 'virtual-app')
router.register(r'app-providers', api.AppProviderViewSet, 'app-provider')
router.register(r'app-providers/((?P<provider>[^/.]+)/)?apps', api.AppProviderAppViewSet, 'app-provider-app')
router.register(r'virtual-app-publications', api.VirtualAppPublicationViewSet, 'virtual-app-publication')
urlpatterns = [
path('my-sessions/', api.MySessionAPIView.as_view(), name='my-session'),

Loading…
Cancel
Save