merge: dev

pull/8991/head
Jiangjie.Bai 2022-10-26 16:15:22 +08:00
commit fb6f51b9cb
30 changed files with 429 additions and 105 deletions

View File

@ -67,6 +67,7 @@ class HostTypes(BaseType):
return { return {
cls.LINUX: [ cls.LINUX: [
{'name': 'Linux'}, {'name': 'Linux'},
{'name': 'Gateway'}
], ],
cls.UNIX: [ cls.UNIX: [
{'name': 'Unix'}, {'name': 'Unix'},
@ -75,16 +76,31 @@ class HostTypes(BaseType):
{'name': 'AIX', 'automation': { {'name': 'AIX', 'automation': {
'push_account_method': 'push_account_aix', 'push_account_method': 'push_account_aix',
'change_secret_method': 'push_secret_aix' 'change_secret_method': 'push_secret_aix'
}}, }}
], ],
cls.WINDOWS: [ cls.WINDOWS: [
{'name': 'Windows'}, {'name': 'Windows'},
{'name': 'Windows-TLS', 'protocols_setting': { {
'rdp': {'security': 'tls'}, 'name': 'Windows-TLS',
}}, 'protocols_setting': {
{'name': 'Windows-RDP', 'protocols_setting': { 'rdp': {'security': 'tls'},
'rdp': {'security': 'rdp'}, }
}} },
{
'name': 'Windows-RDP',
'protocols_setting': {
'rdp': {'security': 'rdp'},
}
},
{
'name': 'RemoteAppHost',
'_protocols': ['rdp', 'ssh'],
'protocols_setting': {
'ssh': {
'required': True
}
}
}
], ],
cls.OTHER_HOST: [] cls.OTHER_HOST: []
} }

View File

@ -224,7 +224,10 @@ class AllTypes(ChoicesMixin):
if _protocols: if _protocols:
protocols_data = [p for p in protocols_data if p['name'] in _protocols] protocols_data = [p for p in protocols_data if p['name'] in _protocols]
for p in protocols_data: for p in protocols_data:
p['setting'] = {**_protocols_setting.get(p['name'], {}), **p.get('setting', {})} setting = _protocols_setting.get(p['name'], {})
p['required'] = setting.pop('required', False)
p['default'] = setting.pop('default', False)
p['setting'] = {**setting, **p.get('setting', {})}
platform_data = { platform_data = {
**default_platform_data, **d, **default_platform_data, **d,

View File

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

View File

@ -55,7 +55,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='changesecretautomation', model_name='changesecretautomation',
name='secret_strategy', name='secret_strategy',
field=models.CharField(choices=[('specific', 'Specific'), ('random_one', 'All assets use the same random password'), ('random_all', 'All assets use different random password')], default='random_one', max_length=16, verbose_name='Secret strategy'), field=models.CharField(choices=[('specific', 'Specific'), ('random_one', 'All assets use the same random password'), ('random_all', 'All assets use different random password')], default='specific', max_length=16, verbose_name='Secret strategy'),
), ),
migrations.AddField( migrations.AddField(
model_name='changesecretautomation', model_name='changesecretautomation',

View File

@ -90,8 +90,7 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name')) name = models.CharField(max_length=128, verbose_name=_('Name'))
address = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True) address = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True)
platform = models.ForeignKey(Platform, default=Platform.default, on_delete=models.PROTECT, platform = models.ForeignKey(Platform, on_delete=models.PROTECT, verbose_name=_("Platform"), related_name='assets')
verbose_name=_("Platform"), related_name='assets')
domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets',
verbose_name=_("Domain"), on_delete=models.SET_NULL) verbose_name=_("Domain"), on_delete=models.SET_NULL)
nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets',

View File

@ -107,6 +107,8 @@ class AssetSerializer(OrgResourceSerializerMixin, WritableNestedModelSerializer)
return return
nodes_to_set = [] nodes_to_set = []
for full_value in nodes_display: for full_value in nodes_display:
if not full_value.startswith('/'):
full_value = '/' + instance.org.name + '/' + full_value
node = Node.objects.filter(full_value=full_value).first() node = Node.objects.filter(full_value=full_value).first()
if node: if node:
nodes_to_set.append(node) nodes_to_set.append(node)

View File

@ -0,0 +1,29 @@
# Generated by Django 3.2.14 on 2022-10-25 11:08
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0110_auto_20221021_1506'),
('authentication', '0012_auto_20220816_1629'),
]
operations = [
migrations.RemoveField(
model_name='connectiontoken',
name='type',
),
migrations.AddField(
model_name='connectiontoken',
name='account_display',
field=models.CharField(default='', max_length=128, verbose_name='Account display'),
),
migrations.AlterField(
model_name='connectiontoken',
name='account',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connection_tokens', to='assets.account', verbose_name='Account'),
),
]

View File

@ -76,7 +76,6 @@ class DistributedLock(RedisLock):
# 要创建一个新的锁对象 # 要创建一个新的锁对象
with self.__class__(**self.kwargs_copy): with self.__class__(**self.kwargs_copy):
return func(*args, **kwds) return func(*args, **kwds)
return inner return inner
@classmethod @classmethod
@ -105,22 +104,21 @@ class DistributedLock(RedisLock):
if self._reentrant: if self._reentrant:
if self.locked_by_current_thread(): if self.locked_by_current_thread():
self._acquired_reentrant_lock = True self._acquired_reentrant_lock = True
logger.debug(f'Reentry lock ok: lock_id={self.id} owner_id={self.get_owner_id()} lock={self.name} thread={self._thread_id}') logger.debug(f'Reentry lock ok: lock_id={self.id} owner_id={self.get_owner_id()} lock={self.name}')
return True return True
logger.debug(f'Attempt acquire reentrant-lock: lock_id={self.id} lock={self.name} thread={self._thread_id}') logger.debug(f'Attempt acquire reentrant-lock: lock_id={self.id} lock={self.name}')
acquired = super().acquire(blocking=blocking, timeout=timeout) acquired = super().acquire(blocking=blocking, timeout=timeout)
if acquired: if acquired:
logger.debug(f'Acquired reentrant-lock ok: lock_id={self.id} lock={self.name} thread={self._thread_id}') logger.debug(f'Acquired reentrant-lock ok: lock_id={self.id} lock={self.name}')
setattr(thread_local, self.name, self.id) setattr(thread_local, self.name, self.id)
else: else:
logger.debug( logger.debug(f'Acquired reentrant-lock failed: lock_id={self.id} lock={self.name}')
f'Acquired reentrant-lock failed: lock_id={self.id} lock={self.name} thread={self._thread_id}')
return acquired return acquired
else: else:
logger.debug(f'Attempt acquire lock: lock_id={self.id} lock={self.name} thread={self._thread_id}') logger.debug(f'Attempt acquire lock: lock_id={self.id} lock={self.name}')
acquired = super().acquire(blocking=blocking, timeout=timeout) acquired = super().acquire(blocking=blocking, timeout=timeout)
logger.debug(f'Acquired lock: ok={acquired} lock_id={self.id} lock={self.name} thread={self._thread_id}') logger.debug(f'Acquired lock: ok={acquired} lock_id={self.id} lock={self.name}')
return acquired return acquired
@property @property
@ -139,17 +137,17 @@ class DistributedLock(RedisLock):
def _release_on_reentrant_locked_by_brother(self): def _release_on_reentrant_locked_by_brother(self):
if self._acquired_reentrant_lock: if self._acquired_reentrant_lock:
self._acquired_reentrant_lock = False self._acquired_reentrant_lock = False
logger.debug(f'Released reentrant-lock: lock_id={self.id} owner_id={self.get_owner_id()} lock={self.name} thread={self._thread_id}') logger.debug(f'Released reentrant-lock: lock_id={self.id} owner_id={self.get_owner_id()} lock={self.name}')
return return
else: else:
self._raise_exc_with_log(f'Reentrant-lock is not acquired: lock_id={self.id} owner_id={self.get_owner_id()} lock={self.name} thread={self._thread_id}') self._raise_exc_with_log(f'Reentrant-lock is not acquired: lock_id={self.id} owner_id={self.get_owner_id()} lock={self.name}')
def _release_on_reentrant_locked_by_me(self): def _release_on_reentrant_locked_by_me(self):
logger.debug(f'Release reentrant-lock locked by me: lock_id={self.id} lock={self.name} thread={self._thread_id}') logger.debug(f'Release reentrant-lock locked by me: lock_id={self.id} lock={self.name}')
id = getattr(thread_local, self.name, None) id = getattr(thread_local, self.name, None)
if id != self.id: if id != self.id:
raise PermissionError(f'Reentrant-lock is not locked by me: lock_id={self.id} owner_id={self.get_owner_id()} lock={self.name} thread={self._thread_id}') raise PermissionError(f'Reentrant-lock is not locked by me: lock_id={self.id} owner_id={self.get_owner_id()} lock={self.name}')
try: try:
# 这里要保证先删除 thread_local 的标记, # 这里要保证先删除 thread_local 的标记,
delattr(thread_local, self.name) delattr(thread_local, self.name)
@ -171,9 +169,9 @@ class DistributedLock(RedisLock):
def _release(self): def _release(self):
try: try:
self._release_redis_lock() self._release_redis_lock()
logger.debug(f'Released lock: lock_id={self.id} lock={self.name} thread={self._thread_id}') logger.debug(f'Released lock: lock_id={self.id} lock={self.name}')
except NotAcquired as e: except NotAcquired as e:
logger.error(f'Release lock failed: lock_id={self.id} lock={self.name} thread={self._thread_id} error: {e}') logger.error(f'Release lock failed: lock_id={self.id} lock={self.name} error: {e}')
self._raise_exc(e) self._raise_exc(e)
def release(self): def release(self):
@ -188,12 +186,12 @@ class DistributedLock(RedisLock):
_release = self._release_on_reentrant_locked_by_brother _release = self._release_on_reentrant_locked_by_brother
else: else:
self._raise_exc_with_log( self._raise_exc_with_log(
f'Reentrant-lock is not acquired: lock_id={self.id} lock={self.name} thread={self._thread_id}') f'Reentrant-lock is not acquired: lock_id={self.id} lock={self.name}')
# 处理是否在事务提交时才释放锁 # 处理是否在事务提交时才释放锁
if self._release_on_transaction_commit: if self._release_on_transaction_commit:
logger.debug( logger.debug(
f'Release lock on transaction commit ... :lock_id={self.id} lock={self.name} thread={self._thread_id}') f'Release lock on transaction commit ... :lock_id={self.id} lock={self.name}')
transaction.on_commit(_release) transaction.on_commit(_release)
else: else:
_release() _release()

View File

@ -88,6 +88,21 @@ def tmp_to_org(org):
set_current_org(ori_org) set_current_org(ori_org)
@contextmanager
def tmp_to_builtin_org(system=0, default=0):
if system:
org_id = Organization.SYSTEM_ID
elif default:
org_id = Organization.DEFAULT_ID
else:
raise ValueError("Must set system or default")
ori_org = get_current_org()
set_current_org(org_id)
yield
if ori_org is not None:
set_current_org(ori_org)
def get_org_filters(): def get_org_filters():
kwargs = {} kwargs = {}

View File

@ -1,10 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .terminal import *
from .session import * from .session import *
from .command import * from .component import *
from .task import * from .applet import *
from .storage import *
from .status import *
from .sharing import *
from .endpoint import *

View File

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

View File

@ -0,0 +1,86 @@
import os.path
import shutil
import zipfile
import yaml
from django.core.files.storage import default_storage
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from terminal import serializers, models
from terminal.serializers import AppletUploadSerializer
class AppletViewSet(viewsets.ModelViewSet):
queryset = models.Applet.objects.all()
serializer_class = serializers.AppletSerializer
rbac_perms = {
'upload': 'terminal.add_applet',
}
def perform_destroy(self, instance):
if not instance.name:
raise ValidationError('Applet is not null')
path = default_storage.path('applets/{}'.format(instance.name))
if os.path.exists(path):
shutil.rmtree(path)
instance.delete()
def extract_and_check_file(self, request):
serializer = self.get_serializer(data=self.request.data)
serializer.is_valid(raise_exception=True)
file = serializer.validated_data['file']
save_to = 'applets/{}'.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('applets/{}.tmp'.format(file.name))
if os.path.exists(extract_to):
shutil.rmtree(extract_to)
with zipfile.ZipFile(path) as zp:
if zp.testzip() is not None:
return Response({'msg': 'Invalid Zip file'}, status=400)
zp.extractall(extract_to)
tmp_dir = os.path.join(extract_to, file.name.replace('.zip', ''))
files = ['manifest.yml', 'icon.png', 'i18n.yml']
for name in files:
path = os.path.join(tmp_dir, name)
if not os.path.exists(path):
raise ValidationError({'error': 'Missing file {}'.format(name)})
with open(os.path.join(tmp_dir, 'manifest.yml')) as f:
manifest = yaml.safe_load(f)
if not manifest.get('name', ''):
raise ValidationError({'error': 'Missing name in manifest.yml'})
return manifest, tmp_dir
@action(detail=False, methods=['post'], serializer_class=AppletUploadSerializer)
def upload(self, request, *args, **kwargs):
manifest, tmp_dir = self.extract_and_check_file(request)
name = manifest['name']
update = request.query_params.get('update')
instance = models.Applet.objects.filter(name=name).first()
if instance and not update:
return Response({'error': 'Applet already exists: {}'.format(name)}, status=400)
serializer = serializers.AppletSerializer(data=manifest, instance=instance)
serializer.is_valid(raise_exception=True)
save_to = default_storage.path('applets/{}'.format(name))
if os.path.exists(save_to):
shutil.rmtree(save_to)
shutil.move(tmp_dir, save_to)
serializer.save()
return Response(serializer.data, status=201)
class AppletPublicationViewSet(viewsets.ModelViewSet):
queryset = models.AppletPublication.objects.all()
serializer_class = serializers.AppletPublicationSerializer

View File

@ -0,0 +1,29 @@
from rest_framework import viewsets
from rest_framework.decorators import action
from orgs.utils import tmp_to_root_org
from orgs.models import Organization
from assets.models import Host
from terminal import serializers, models
__all__ = ['AppletHostViewSet', 'AppletHostDeploymentViewSet']
class AppletHostViewSet(viewsets.ModelViewSet):
queryset = models.AppletHost.objects.all()
serializer_class = serializers.AppletHostSerializer
@action(methods=['get'], detail=False)
def hosts(self, request):
with tmp_to_root_org():
kwargs = {
'platform__name': 'RemoteAppHost',
'org_id': Organization.SYSTEM_ID
}
return Host.objects.filter(**kwargs)
class AppletHostDeploymentViewSet(viewsets.ModelViewSet):
queryset = models.AppletHostDeployment.objects.all()
serializer_class = serializers.AppletHostDeploymentSerializer

View File

@ -0,0 +1,4 @@
from .terminal import *
from .storage import *
from .status import *
from .endpoint import *

View File

@ -2,15 +2,15 @@ from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
from rest_framework.request import Request from rest_framework.request import Request
from common.drf.api import JMSBulkModelViewSet from common.drf.api import JMSBulkModelViewSet
from common.permissions import IsValidUserOrConnectionToken
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from assets.models import Asset from assets.models import Asset
from orgs.utils import tmp_to_root_org from orgs.utils import tmp_to_root_org
from terminal.models import Session from terminal.models import Session, Endpoint, EndpointRule
from ..models import Endpoint, EndpointRule from terminal import serializers
from .. import serializers
from common.permissions import IsValidUserOrConnectionToken
__all__ = ['EndpointViewSet', 'EndpointRuleViewSet'] __all__ = ['EndpointViewSet', 'EndpointRuleViewSet']

View File

@ -9,9 +9,9 @@ from rest_framework import viewsets, generics
from rest_framework.views import Response from rest_framework.views import Response
from rest_framework import status from rest_framework import status
from ..models import Terminal, Status, Session from terminal.models import Terminal, Status, Session
from .. import serializers from terminal import serializers
from ..utils import TypedComponentsStatusMetricsUtil from terminal.utils import TypedComponentsStatusMetricsUtil
logger = logging.getLogger(__file__) logger = logging.getLogger(__file__)

View File

@ -11,8 +11,8 @@ from django_filters import utils
from terminal import const from terminal import const
from common.const.http import GET from common.const.http import GET
from terminal.filters import CommandStorageFilter, CommandFilter, CommandFilterForStorageTree from terminal.filters import CommandStorageFilter, CommandFilter, CommandFilterForStorageTree
from ..models import CommandStorage, ReplayStorage from terminal.models import CommandStorage, ReplayStorage
from ..serializers import CommandStorageSerializer, ReplayStorageSerializer from terminal.serializers import CommandStorageSerializer, ReplayStorageSerializer
__all__ = [ __all__ = [
'CommandStorageViewSet', 'CommandStorageTestConnectiveApi', 'CommandStorageViewSet', 'CommandStorageTestConnectiveApi',

View File

@ -14,9 +14,9 @@ from common.exceptions import JMSException
from common.drf.api import JMSBulkModelViewSet from common.drf.api import JMSBulkModelViewSet
from common.utils import get_object_or_none, get_request_ip from common.utils import get_object_or_none, get_request_ip
from common.permissions import WithBootstrapToken from common.permissions import WithBootstrapToken
from ..models import Terminal from terminal.models import Terminal
from .. import serializers from terminal import serializers
from .. import exceptions from terminal import exceptions
__all__ = [ __all__ = [
'TerminalViewSet', 'TerminalConfig', 'TerminalViewSet', 'TerminalConfig',

View File

@ -0,0 +1,4 @@
from .session import *
from .sharing import *
from .command import *
from .task import *

View File

@ -13,11 +13,11 @@ from common.drf.api import JMSBulkModelViewSet
from common.utils import get_logger from common.utils import get_logger
from terminal.backends.command.serializers import InsecureCommandAlertSerializer from terminal.backends.command.serializers import InsecureCommandAlertSerializer
from terminal.exceptions import StorageInvalid from terminal.exceptions import StorageInvalid
from ..backends import ( from terminal.backends import (
get_command_storage, get_multi_command_storage, get_command_storage, get_multi_command_storage,
SessionCommandSerializer, SessionCommandSerializer,
) )
from ..notifications import CommandAlertMessage from terminal.notifications import CommandAlertMessage
logger = get_logger(__name__) logger = get_logger(__name__)
__all__ = ['CommandViewSet', 'InsecureCommandAlertAPI'] __all__ = ['CommandViewSet', 'InsecureCommandAlertAPI']

View File

@ -24,15 +24,16 @@ from common.drf.renders import PassthroughRenderer
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import tmp_to_root_org, tmp_to_org from orgs.utils import tmp_to_root_org, tmp_to_org
from users.models import User from users.models import User
from .. import utils from terminal.utils import (
from ..utils import find_session_replay_local, download_session_replay find_session_replay_local, download_session_replay,
from ..models import Session is_session_approver, get_session_replay_url
from .. import serializers )
from terminal.utils import is_session_approver from terminal.models import Session
from terminal import serializers
__all__ = [ __all__ = [
'SessionViewSet', 'SessionReplayViewSet', 'SessionJoinValidateAPI', 'SessionViewSet', 'SessionReplayViewSet',
'MySessionAPIView', 'SessionJoinValidateAPI', 'MySessionAPIView',
] ]
logger = get_logger(__name__) logger = get_logger(__name__)
@ -93,7 +94,7 @@ class SessionViewSet(OrgBulkModelViewSet):
url_name='replay-download') url_name='replay-download')
def download(self, request, *args, **kwargs): def download(self, request, *args, **kwargs):
session = self.get_object() session = self.get_object()
local_path, url = utils.get_session_replay_url(session) local_path, url = get_session_replay_url(session)
if local_path is None: if local_path is None:
return Response({"error": url}, status=404) return Response({"error": url}, status=404)
file = self.prepare_offline_file(session, local_path) file = self.prepare_offline_file(session, local_path)

View File

@ -5,9 +5,8 @@ from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.const.http import PATCH from common.const.http import PATCH
from common.permissions import IsValidUser
from orgs.mixins.api import OrgModelViewSet from orgs.mixins.api import OrgModelViewSet
from .. import serializers, models from terminal import serializers, models
__all__ = ['SessionSharingViewSet', 'SessionJoinRecordsViewSet'] __all__ = ['SessionSharingViewSet', 'SessionJoinRecordsViewSet']

View File

@ -7,10 +7,10 @@ from rest_framework import status
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from common.utils import get_object_or_none from common.utils import get_object_or_none
from ..models import Session, Task
from .. import serializers
from terminal.utils import is_session_approver
from orgs.utils import tmp_to_root_org from orgs.utils import tmp_to_root_org
from terminal.models import Session, Task
from terminal import serializers
from terminal.utils import is_session_approver
__all__ = ['TaskViewSet', 'KillSessionAPI', 'KillSessionForTicketAPI'] __all__ = ['TaskViewSet', 'KillSessionAPI', 'KillSessionForTicketAPI']
logger = logging.getLogger(__file__) logger = logging.getLogger(__file__)

View File

@ -1,4 +1,4 @@
# Generated by Django 3.2.14 on 2022-10-21 06:33 # Generated by Django 3.2.14 on 2022-10-24 06:52
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -8,6 +8,7 @@ import uuid
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('assets', '0110_auto_20221021_1506'),
('terminal', '0053_auto_20220830_1244'), ('terminal', '0053_auto_20220830_1244'),
] ]
@ -21,11 +22,14 @@ class Migration(migrations.Migration):
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')), ('name', models.CharField(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')), ('version', models.CharField(max_length=16, verbose_name='Version')),
('type', models.CharField(choices=[('app', 'App'), ('web', 'Web')], max_length=16, verbose_name='Type')),
('icon', models.ImageField(upload_to='applet/icon', verbose_name='Icon')),
('author', models.CharField(max_length=128, verbose_name='Author')), ('author', models.CharField(max_length=128, verbose_name='Author')),
('type', models.CharField(choices=[('general', 'General'), ('web', 'Web')], default='general', max_length=16, verbose_name='Type')),
('vcs_type', models.CharField(max_length=16, null=True, verbose_name='VCS type')),
('vcs_url', models.CharField(max_length=256, null=True, verbose_name='URL')),
('protocols', models.JSONField(default=list, verbose_name='Protocol')), ('protocols', models.JSONField(default=list, verbose_name='Protocol')),
('tags', models.JSONField(default=list, verbose_name='Tags')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
], ],
options={ options={
@ -33,14 +37,13 @@ class Migration(migrations.Migration):
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='AppletProvider', name='AppletHost',
fields=[ fields=[
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), ('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')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('account_automation', models.BooleanField(default=False, verbose_name='Account automation')), ('account_automation', models.BooleanField(default=False, verbose_name='Account automation')),
('date_synced', models.DateTimeField(blank=True, null=True, verbose_name='Date synced')), ('date_synced', models.DateTimeField(blank=True, null=True, verbose_name='Date synced')),
@ -50,22 +53,6 @@ class Migration(migrations.Migration):
'abstract': False, 'abstract': False,
}, },
), ),
migrations.CreateModel(
name='ProviderDeployment',
fields=[
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=32, 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)),
('status', models.CharField(max_length=16, verbose_name='Status')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='terminal.appletprovider', verbose_name='Provider')),
],
options={
'abstract': False,
},
),
migrations.CreateModel( migrations.CreateModel(
name='AppletPublication', name='AppletPublication',
fields=[ fields=[
@ -77,20 +64,36 @@ class Migration(migrations.Migration):
('status', models.CharField(max_length=16, verbose_name='Status')), ('status', models.CharField(max_length=16, verbose_name='Status')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('applet', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='terminal.applet', verbose_name='Applet')), ('applet', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='terminal.applet', verbose_name='Applet')),
('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='terminal.appletprovider', verbose_name='Provider')), ('host', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='terminal.applethost', verbose_name='Host')),
], ],
options={ options={
'unique_together': {('applet', 'provider')}, 'unique_together': {('applet', 'host')},
},
),
migrations.CreateModel(
name='AppletHostDeployment',
fields=[
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=32, 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)),
('status', models.CharField(max_length=16, verbose_name='Status')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='terminal.applethost', verbose_name='Hosting')),
],
options={
'abstract': False,
}, },
), ),
migrations.AddField( migrations.AddField(
model_name='appletprovider', model_name='applethost',
name='applets', name='applets',
field=models.ManyToManyField(through='terminal.AppletPublication', to='terminal.Applet', verbose_name='Applet'), field=models.ManyToManyField(through='terminal.AppletPublication', to='terminal.Applet', verbose_name='Applet'),
), ),
migrations.AddField( migrations.AddField(
model_name='appletprovider', model_name='applethost',
name='asset', name='host',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='assets.asset', verbose_name='Asset'), field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='assets.host', verbose_name='Host'),
), ),
] ]

View File

@ -1,2 +1,2 @@
from .applet import * from .applet import *
from .provider import * from .host import *

View File

@ -1,3 +1,8 @@
import yaml
import os.path
from django.conf import settings
from django.core.files.storage import default_storage
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -9,25 +14,53 @@ __all__ = ['Applet', 'AppletPublication']
class Applet(JMSBaseModel): class Applet(JMSBaseModel):
class Type(models.TextChoices): class Type(models.TextChoices):
app = 'app', _('App') general = 'general', _('General')
web = 'web', _('Web') web = 'web', _('Web')
class VCSType(models.TextChoices):
manual = 'manual', _('Manual')
git = 'git', _('Git')
archive = 'archive', _('Remote gzip')
name = models.CharField(max_length=128, verbose_name=_('Name'), unique=True) name = models.CharField(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')) version = models.CharField(max_length=16, verbose_name=_('Version'))
type = models.CharField(max_length=16, choices=Type.choices, verbose_name=_('Type'))
icon = models.ImageField(upload_to='applet/icon', verbose_name=_('Icon'))
author = models.CharField(max_length=128, verbose_name=_('Author')) author = models.CharField(max_length=128, verbose_name=_('Author'))
type = models.CharField(max_length=16, verbose_name=_('Type'), default='general', choices=Type.choices)
vcs_type = models.CharField(max_length=16, verbose_name=_('VCS type'), null=True)
vcs_url = models.CharField(max_length=256, verbose_name=_('URL'), null=True)
protocols = models.JSONField(default=list, verbose_name=_('Protocol')) protocols = models.JSONField(default=list, verbose_name=_('Protocol'))
tags = models.JSONField(default=list, verbose_name=_('Tags'))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
def __str__(self): def __str__(self):
return self.name return self.name
@property
def path(self):
return default_storage.path('applets/{}'.format(self.name))
@property
def manifest(self):
path = os.path.join(self.path, 'manifest.yml')
if not os.path.exists(path):
return None
with open(path, 'r') as f:
return yaml.safe_load(f)
@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, 'applets', self.name, 'icon.png')
class AppletPublication(JMSBaseModel): class AppletPublication(JMSBaseModel):
applet = models.ForeignKey('Applet', on_delete=models.PROTECT, verbose_name=_('Applet')) applet = models.ForeignKey('Applet', on_delete=models.PROTECT, verbose_name=_('Applet'))
provider = models.ForeignKey('AppletProvider', on_delete=models.PROTECT, verbose_name=_('Provider')) host = models.ForeignKey('AppletHost', on_delete=models.PROTECT, verbose_name=_('Host'))
status = models.CharField(max_length=16, verbose_name=_('Status')) status = models.CharField(max_length=16, verbose_name=_('Status'))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
class Meta: class Meta:
unique_together = ('applet', 'provider') unique_together = ('applet', 'host')

View File

@ -1,31 +1,34 @@
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from celery import current_app
from common.db.models import JMSBaseModel from common.db.models import JMSBaseModel
__all__ = ['AppletProvider', 'ProviderDeployment'] __all__ = ['AppletHost', 'AppletHostDeployment']
class AppletProvider(JMSBaseModel): class AppletHost(JMSBaseModel):
name = models.CharField(max_length=128, verbose_name=_('Name'), unique=True) host = models.ForeignKey('assets.Host', on_delete=models.PROTECT, verbose_name=_('Host'))
asset = models.ForeignKey('assets.Asset', on_delete=models.PROTECT, verbose_name=_('Asset'))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
account_automation = models.BooleanField(default=False, verbose_name=_('Account automation')) account_automation = models.BooleanField(default=False, verbose_name=_('Account automation'))
date_synced = models.DateTimeField(null=True, blank=True, verbose_name=_('Date synced')) date_synced = models.DateTimeField(null=True, blank=True, verbose_name=_('Date synced'))
status = models.CharField(max_length=16, verbose_name=_('Status')) status = models.CharField(max_length=16, verbose_name=_('Status'))
applets = models.ManyToManyField( applets = models.ManyToManyField(
'Applet', verbose_name=_('Applet'), 'Applet', verbose_name=_('Applet'),
through='AppletPublication', through_fields=('provider', 'applet'), through='AppletPublication', through_fields=('host', 'applet'),
) )
def __str__(self):
return self.host.name
class ProviderDeployment(JMSBaseModel):
provider = models.ForeignKey('AppletProvider', on_delete=models.CASCADE, verbose_name=_('Provider')) class AppletHostDeployment(JMSBaseModel):
host = models.ForeignKey('AppletHost', on_delete=models.CASCADE, verbose_name=_('Hosting'))
status = models.CharField(max_length=16, verbose_name=_('Status')) status = models.CharField(max_length=16, verbose_name=_('Status'))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
def install(self): def __str__(self):
return self.host
def start(self):
pass pass

View File

@ -5,3 +5,4 @@ from .session import *
from .storage import * from .storage import *
from .sharing import * from .sharing import *
from .endpoint import * from .endpoint import *
from .applet import *

View File

@ -0,0 +1,97 @@
from rest_framework import serializers
from django.utils.translation import gettext_lazy as _
from common.drf.fields import ObjectRelatedField, LabeledChoiceField
from assets.models import Host, Platform
from assets.serializers import HostSerializer
from orgs.utils import tmp_to_builtin_org
from ..models import Applet, AppletPublication, AppletHost, AppletHostDeployment
__all__ = [
'AppletSerializer', 'AppletPublicationSerializer',
'AppletHostSerializer', 'AppletHostDeploymentSerializer',
'AppletUploadSerializer'
]
class AppletSerializer(serializers.ModelSerializer):
icon = serializers.ReadOnlyField(label=_("Icon"))
type = LabeledChoiceField(choices=Applet.Type.choices, label=_("Type"))
class Meta:
model = Applet
fields_mini = ['id', 'name', 'display_name']
read_only_fields = [
'icon', 'date_created', 'date_updated'
]
fields = fields_mini + [
'version', 'author', 'type', 'protocols',
'tags', 'comment'
] + read_only_fields
class AppletUploadSerializer(serializers.Serializer):
file = serializers.FileField()
class AppletPublicationSerializer(serializers.ModelSerializer):
applet = ObjectRelatedField(queryset=Applet.objects.all())
host = ObjectRelatedField(queryset=AppletHost.objects.all())
class Meta:
model = AppletPublication
fields_mini = ['id', 'applet', 'host']
read_only_fields = ['date_created', 'date_updated']
fields = fields_mini + [
'status', 'comment',
] + read_only_fields
class AppletHostSerializer(serializers.ModelSerializer):
host = HostSerializer(allow_null=True, required=False)
class Meta:
model = AppletHost
fields_mini = ['id', 'host']
read_only_fields = ['date_synced', 'status', 'date_created', 'date_updated']
fields = fields_mini + ['comment', 'account_automation'] + read_only_fields
def __init__(self, *args, **kwargs):
self.host_data = kwargs.get('data', {}).pop('host', {})
super().__init__(*args, **kwargs)
def _create_host(self):
platform = Platform.objects.get(name='RemoteAppHost')
data = {
**self.host_data,
'platform': platform.id,
'nodes_display': [
'RemoteAppHosts'
]
}
serializer = HostSerializer(data=data)
try:
serializer.is_valid(raise_exception=True)
except serializers.ValidationError:
raise serializers.ValidationError({'host': serializer.errors})
host = serializer.save()
return host
def create(self, validated_data):
with tmp_to_builtin_org(system=1):
host = self._create_host()
instance = super().create({**validated_data, 'host': host})
return instance
class AppletHostDeploymentSerializer(serializers.ModelSerializer):
host = ObjectRelatedField(queryset=AppletHost.objects.all())
class Meta:
model = AppletHostDeployment
fields_mini = ['id', 'host']
read_only_fields = ['date_created', 'date_updated']
fields = fields_mini + [
'status', 'comment',
] + read_only_fields

View File

@ -24,6 +24,11 @@ router.register(r'session-sharings', api.SessionSharingViewSet, 'session-sharing
router.register(r'session-join-records', api.SessionJoinRecordsViewSet, 'session-sharing-record') router.register(r'session-join-records', api.SessionJoinRecordsViewSet, 'session-sharing-record')
router.register(r'endpoints', api.EndpointViewSet, 'endpoint') router.register(r'endpoints', api.EndpointViewSet, 'endpoint')
router.register(r'endpoint-rules', api.EndpointRuleViewSet, 'endpoint-rule') router.register(r'endpoint-rules', api.EndpointRuleViewSet, 'endpoint-rule')
router.register(r'applets', api.AppletViewSet, 'applet')
router.register(r'applet-hosts', api.AppletHostViewSet, 'applet-host')
router.register(r'applet-publication', api.AppletPublicationViewSet, 'applet-publication')
router.register(r'applet-host-deployment', api.AppletHostDeploymentViewSet, 'applet-host-deployment')
urlpatterns = [ urlpatterns = [
path('my-sessions/', api.MySessionAPIView.as_view(), name='my-session'), path('my-sessions/', api.MySessionAPIView.as_view(), name='my-session'),