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

View File

@ -224,7 +224,10 @@ class AllTypes(ChoicesMixin):
if _protocols:
protocols_data = [p for p in protocols_data if p['name'] in _protocols]
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 = {
**default_platform_data, **d,

View File

@ -14,7 +14,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='asset',
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(
model_name='asset',

View File

@ -55,7 +55,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='changesecretautomation',
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(
model_name='changesecretautomation',

View File

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

View File

@ -107,6 +107,8 @@ class AssetSerializer(OrgResourceSerializerMixin, WritableNestedModelSerializer)
return
nodes_to_set = []
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()
if 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):
return func(*args, **kwds)
return inner
@classmethod
@ -105,22 +104,21 @@ class DistributedLock(RedisLock):
if self._reentrant:
if self.locked_by_current_thread():
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
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)
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)
else:
logger.debug(
f'Acquired reentrant-lock failed: lock_id={self.id} lock={self.name} thread={self._thread_id}')
logger.debug(f'Acquired reentrant-lock failed: lock_id={self.id} lock={self.name}')
return acquired
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)
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
@property
@ -139,17 +137,17 @@ class DistributedLock(RedisLock):
def _release_on_reentrant_locked_by_brother(self):
if self._acquired_reentrant_lock:
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
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):
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)
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:
# 这里要保证先删除 thread_local 的标记,
delattr(thread_local, self.name)
@ -171,9 +169,9 @@ class DistributedLock(RedisLock):
def _release(self):
try:
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:
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)
def release(self):
@ -188,12 +186,12 @@ class DistributedLock(RedisLock):
_release = self._release_on_reentrant_locked_by_brother
else:
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:
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)
else:
_release()

View File

@ -88,6 +88,21 @@ def tmp_to_org(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():
kwargs = {}

View File

@ -1,10 +1,5 @@
# -*- coding: utf-8 -*-
#
from .terminal import *
from .session import *
from .command import *
from .task import *
from .storage import *
from .status import *
from .sharing import *
from .endpoint import *
from .component import *
from .applet 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 import status
from rest_framework.request import Request
from common.drf.api import JMSBulkModelViewSet
from common.permissions import IsValidUserOrConnectionToken
from django.utils.translation import ugettext_lazy as _
from django.shortcuts import get_object_or_404
from assets.models import Asset
from orgs.utils import tmp_to_root_org
from terminal.models import Session
from ..models import Endpoint, EndpointRule
from .. import serializers
from common.permissions import IsValidUserOrConnectionToken
from terminal.models import Session, Endpoint, EndpointRule
from terminal import serializers
__all__ = ['EndpointViewSet', 'EndpointRuleViewSet']

View File

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

View File

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

View File

@ -14,9 +14,9 @@ from common.exceptions import JMSException
from common.drf.api import JMSBulkModelViewSet
from common.utils import get_object_or_none, get_request_ip
from common.permissions import WithBootstrapToken
from ..models import Terminal
from .. import serializers
from .. import exceptions
from terminal.models import Terminal
from terminal import serializers
from terminal import exceptions
__all__ = [
'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 terminal.backends.command.serializers import InsecureCommandAlertSerializer
from terminal.exceptions import StorageInvalid
from ..backends import (
from terminal.backends import (
get_command_storage, get_multi_command_storage,
SessionCommandSerializer,
)
from ..notifications import CommandAlertMessage
from terminal.notifications import CommandAlertMessage
logger = get_logger(__name__)
__all__ = ['CommandViewSet', 'InsecureCommandAlertAPI']

View File

@ -24,15 +24,16 @@ from common.drf.renders import PassthroughRenderer
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import tmp_to_root_org, tmp_to_org
from users.models import User
from .. import utils
from ..utils import find_session_replay_local, download_session_replay
from ..models import Session
from .. import serializers
from terminal.utils import is_session_approver
from terminal.utils import (
find_session_replay_local, download_session_replay,
is_session_approver, get_session_replay_url
)
from terminal.models import Session
from terminal import serializers
__all__ = [
'SessionViewSet', 'SessionReplayViewSet', 'SessionJoinValidateAPI',
'MySessionAPIView',
'SessionViewSet', 'SessionReplayViewSet',
'SessionJoinValidateAPI', 'MySessionAPIView',
]
logger = get_logger(__name__)
@ -93,7 +94,7 @@ class SessionViewSet(OrgBulkModelViewSet):
url_name='replay-download')
def download(self, request, *args, **kwargs):
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:
return Response({"error": url}, status=404)
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 common.const.http import PATCH
from common.permissions import IsValidUser
from orgs.mixins.api import OrgModelViewSet
from .. import serializers, models
from terminal import serializers, models
__all__ = ['SessionSharingViewSet', 'SessionJoinRecordsViewSet']

View File

@ -7,10 +7,10 @@ from rest_framework import status
from rest_framework.permissions import IsAuthenticated
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 terminal.models import Session, Task
from terminal import serializers
from terminal.utils import is_session_approver
__all__ = ['TaskViewSet', 'KillSessionAPI', 'KillSessionForTicketAPI']
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
import django.db.models.deletion
@ -8,6 +8,7 @@ import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0110_auto_20221021_1506'),
('terminal', '0053_auto_20220830_1244'),
]
@ -21,11 +22,14 @@ class Migration(migrations.Migration):
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('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')),
('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')),
('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')),
('tags', models.JSONField(default=list, verbose_name='Tags')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
],
options={
@ -33,14 +37,13 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
name='AppletProvider',
name='AppletHost',
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)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('account_automation', models.BooleanField(default=False, verbose_name='Account automation')),
('date_synced', models.DateTimeField(blank=True, null=True, verbose_name='Date synced')),
@ -50,22 +53,6 @@ class Migration(migrations.Migration):
'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(
name='AppletPublication',
fields=[
@ -77,20 +64,36 @@ class Migration(migrations.Migration):
('status', models.CharField(max_length=16, verbose_name='Status')),
('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')),
('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={
'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(
model_name='appletprovider',
model_name='applethost',
name='applets',
field=models.ManyToManyField(through='terminal.AppletPublication', to='terminal.Applet', verbose_name='Applet'),
),
migrations.AddField(
model_name='appletprovider',
name='asset',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='assets.asset', verbose_name='Asset'),
model_name='applethost',
name='host',
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 .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.utils.translation import gettext_lazy as _
@ -9,25 +14,53 @@ __all__ = ['Applet', 'AppletPublication']
class Applet(JMSBaseModel):
class Type(models.TextChoices):
app = 'app', _('App')
general = 'general', _('General')
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)
display_name = models.CharField(max_length=128, verbose_name=_('Display name'))
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'))
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'))
tags = models.JSONField(default=list, verbose_name=_('Tags'))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
def __str__(self):
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):
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'))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
class Meta:
unique_together = ('applet', 'provider')
unique_together = ('applet', 'host')

View File

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

View File

@ -5,3 +5,4 @@ from .session import *
from .storage import *
from .sharing 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'endpoints', api.EndpointViewSet, 'endpoint')
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 = [
path('my-sessions/', api.MySessionAPIView.as_view(), name='my-session'),