mirror of https://github.com/jumpserver/jumpserver
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
parent
a43bb25b5a
commit
d2429f7883
|
@ -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')
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -5,6 +5,7 @@ path_perms_map = {
|
|||
'settings': '*',
|
||||
'replay': 'default',
|
||||
'applets': 'terminal.view_applet',
|
||||
'virtual_apps': 'terminal.view_virtualapp',
|
||||
'playbooks': 'ops.view_playbook'
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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', ]
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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')
|
|
@ -9,3 +9,5 @@ from .sharing import *
|
|||
from .storage import *
|
||||
from .task import *
|
||||
from .terminal import *
|
||||
from .virtualapp import *
|
||||
from .virtualapp_provider import *
|
||||
|
|
|
@ -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'))
|
||||
|
|
@ -3,3 +3,4 @@ from .db_port import *
|
|||
from .session import *
|
||||
from .session_sharing import *
|
||||
from .terminal import *
|
||||
from .virtualapp import *
|
||||
|
|
|
@ -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)
|
|
@ -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…
Reference in New Issue