mirror of
https://github.com/dkmstr/openuds.git
synced 2025-10-06 11:33:43 +03:00
402 lines
16 KiB
Python
402 lines
16 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
#
|
|
# Copyright (c) 2014-2019 Virtual Cable S.L.
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without modification,
|
|
# are permitted provided that the following conditions are met:
|
|
#
|
|
# * Redistributions of source code must retain the above copyright notice,
|
|
# this list of conditions and the following disclaimer.
|
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
# this list of conditions and the following disclaimer in the documentation
|
|
# and/or other materials provided with the distribution.
|
|
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
|
|
# may be used to endorse or promote products derived from this software
|
|
# without specific prior written permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
"""
|
|
Author: Adolfo Gómez, dkmaster at dkmon dot com
|
|
"""
|
|
import logging
|
|
import typing
|
|
import collections.abc
|
|
|
|
from django.db import IntegrityError
|
|
from django.utils.translation import gettext as _
|
|
|
|
from uds import models
|
|
|
|
from uds.core import exceptions, types
|
|
import uds.core.types.permissions
|
|
from uds.core.util import log, permissions, ensure
|
|
from uds.core.util.model import process_uuid
|
|
from uds.core.environment import Environment
|
|
from uds.core.consts.images import DEFAULT_THUMB_BASE64
|
|
from uds.core.ui import gui
|
|
from uds.core.types.states import State
|
|
|
|
|
|
from uds.REST.model import DetailHandler
|
|
|
|
# Not imported at runtime, just for type checking
|
|
if typing.TYPE_CHECKING:
|
|
from django.db.models import Model
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ServiceItem(types.rest.ManagedObjectDictType):
|
|
id: str
|
|
name: str
|
|
tags: list[str]
|
|
comments: str
|
|
deployed_services_count: int
|
|
user_services_count: int
|
|
max_services_count_type: str
|
|
maintenance_mode: bool
|
|
permission: int
|
|
info: typing.NotRequired['ServiceInfo']
|
|
|
|
|
|
class ServiceInfo(types.rest.ItemDictType):
|
|
icon: str
|
|
needs_publication: bool
|
|
max_deployed: int
|
|
uses_cache: bool
|
|
uses_cache_l2: bool
|
|
cache_tooltip: str
|
|
cache_tooltip_l2: str
|
|
needs_osmanager: bool
|
|
allowed_protocols: list[str]
|
|
services_type_provided: str
|
|
can_reset: bool
|
|
can_list_assignables: bool
|
|
|
|
|
|
class ServicePoolResumeItem(types.rest.ItemDictType):
|
|
id: str
|
|
name: str
|
|
thumb: str
|
|
user_services_count: int
|
|
state: str
|
|
|
|
|
|
class Services(DetailHandler[ServiceItem]): # pylint: disable=too-many-public-methods
|
|
"""
|
|
Detail handler for Services, whose parent is a Provider
|
|
"""
|
|
|
|
custom_methods = ['servicepools']
|
|
|
|
@staticmethod
|
|
def service_info(item: models.Service) -> ServiceInfo:
|
|
info = item.get_type()
|
|
overrided_fields = info.overrided_pools_fields or {}
|
|
|
|
return {
|
|
'icon': info.icon64().replace('\n', ''),
|
|
'needs_publication': info.publication_type is not None,
|
|
'max_deployed': info.userservices_limit,
|
|
'uses_cache': info.uses_cache and overrided_fields.get('uses_cache', True),
|
|
'uses_cache_l2': info.uses_cache_l2,
|
|
'cache_tooltip': _(info.cache_tooltip),
|
|
'cache_tooltip_l2': _(info.cache_tooltip_l2),
|
|
'needs_osmanager': info.needs_osmanager,
|
|
'allowed_protocols': [str(i) for i in info.allowed_protocols],
|
|
'services_type_provided': info.services_type_provided,
|
|
'can_reset': info.can_reset,
|
|
'can_list_assignables': info.can_assign(),
|
|
}
|
|
|
|
@staticmethod
|
|
def service_to_dict(item: models.Service, perm: int, full: bool = False) -> ServiceItem:
|
|
"""
|
|
Convert a service db item to a dict for a rest response
|
|
:param item: Service item (db)
|
|
:param full: If full is requested, add "extra" fields to complete information
|
|
"""
|
|
ret_value: ServiceItem = {
|
|
'id': item.uuid,
|
|
'name': item.name,
|
|
'tags': [tag.tag for tag in item.tags.all()],
|
|
'comments': item.comments,
|
|
'deployed_services_count': item.deployedServices.count(),
|
|
'user_services_count': models.UserService.objects.filter(deployed_service__service=item)
|
|
.exclude(state__in=State.INFO_STATES)
|
|
.count(),
|
|
'max_services_count_type': str(item.max_services_count_type),
|
|
'maintenance_mode': item.provider.maintenance_mode,
|
|
'permission': perm,
|
|
}
|
|
Services.fill_instance_type(item, ret_value)
|
|
|
|
if full:
|
|
ret_value['info'] = Services.service_info(item)
|
|
|
|
return ret_value
|
|
|
|
def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.GetItemsResult[ServiceItem]:
|
|
parent = ensure.is_instance(parent, models.Provider)
|
|
# Check what kind of access do we have to parent provider
|
|
perm = permissions.effective_permissions(self._user, parent)
|
|
try:
|
|
if item is None:
|
|
return [Services.service_to_dict(k, perm) for k in parent.services.all()]
|
|
k = parent.services.get(uuid=process_uuid(item))
|
|
val = Services.service_to_dict(k, perm, full=True)
|
|
# On detail, ne wee to fill the instance fields by hand
|
|
self.fill_instance_fields(k, val)
|
|
return val
|
|
except Exception as e:
|
|
logger.error('Error getting services for %s: %s', parent, e)
|
|
raise self.invalid_item_response(repr(e)) from e
|
|
|
|
def get_row_style(self, parent: 'Model') -> types.ui.RowStyleInfo:
|
|
return types.ui.RowStyleInfo(prefix='row-maintenance-', field='maintenance_mode')
|
|
|
|
def _delete_incomplete_service(self, service: models.Service) -> None:
|
|
"""
|
|
Deletes a service if it is needed to (that is, if it is not None) and silently catch any exception of this operation
|
|
:param service: Service to delete (may be None, in which case it does nothing)
|
|
"""
|
|
try:
|
|
service.delete()
|
|
except Exception: # nosec: This is a delete, we don't care about exceptions
|
|
pass
|
|
|
|
def save_item(self, parent: 'Model', item: typing.Optional[str]) -> ServiceItem:
|
|
parent = ensure.is_instance(parent, models.Provider)
|
|
# Extract item db fields
|
|
# We need this fields for all
|
|
logger.debug('Saving service for %s / %s', parent, item)
|
|
|
|
# Get the sevice type as first step, to obtain "overrided_fields" and other info
|
|
service_type = parent.get_instance().get_service_by_type(self._params['data_type'])
|
|
if not service_type:
|
|
raise exceptions.rest.RequestError('Service type not found')
|
|
|
|
fields = self.fields_from_params(
|
|
['name', 'comments', 'data_type', 'tags', 'max_services_count_type'],
|
|
defaults=service_type.overrided_fields,
|
|
)
|
|
# Fix max_services_count_type to ServicesCountingType enum or ServicesCountingType.STANDARD if not found
|
|
try:
|
|
fields['max_services_count_type'] = types.services.ServicesCountingType.from_int(
|
|
int(fields['max_services_count_type'])
|
|
)
|
|
except Exception:
|
|
fields['max_services_count_type'] = types.services.ServicesCountingType.STANDARD
|
|
tags = fields['tags']
|
|
del fields['tags']
|
|
service: typing.Optional[models.Service] = None
|
|
|
|
try:
|
|
if not item: # Create new
|
|
service = parent.services.create(**fields)
|
|
else:
|
|
service = parent.services.get(uuid=process_uuid(item))
|
|
service.__dict__.update(fields)
|
|
|
|
if not service:
|
|
raise Exception('Cannot create service!')
|
|
|
|
service.tags.set([models.Tag.objects.get_or_create(tag=val)[0] for val in tags])
|
|
|
|
service_instance = service.get_instance(self._params)
|
|
|
|
# Store token if this service provides one
|
|
service.token = service_instance.get_token() or None # If '', use "None" to
|
|
|
|
# This may launch an validation exception (the get_instance(...) part)
|
|
service.data = service_instance.serialize()
|
|
|
|
service.save()
|
|
return Services.service_to_dict(
|
|
service, permissions.effective_permissions(self._user, service), full=True
|
|
)
|
|
|
|
except models.Service.DoesNotExist:
|
|
raise self.invalid_item_response() from None
|
|
except IntegrityError as e: # Duplicate key probably
|
|
if service and service.token and not item:
|
|
service.delete()
|
|
raise exceptions.rest.RequestError(
|
|
_('Service token seems to be in use by other service. Please, select a new one.')
|
|
) from e
|
|
raise exceptions.rest.RequestError(_('Element already exists (duplicate key error)')) from e
|
|
except exceptions.ui.ValidationError as e:
|
|
if (
|
|
not item and service
|
|
): # Only remove partially saved element if creating new (if editing, ignore this)
|
|
self._delete_incomplete_service(service)
|
|
raise exceptions.rest.RequestError(_('Input error: {0}'.format(e))) from e
|
|
except Exception as e:
|
|
if not item and service:
|
|
self._delete_incomplete_service(service)
|
|
logger.exception('Saving Service')
|
|
raise exceptions.rest.RequestError('incorrect invocation to PUT: {0}'.format(e)) from e
|
|
|
|
def delete_item(self, parent: 'Model', item: str) -> None:
|
|
parent = ensure.is_instance(parent, models.Provider)
|
|
try:
|
|
service = parent.services.get(uuid=process_uuid(item))
|
|
if service.deployedServices.count() == 0:
|
|
service.delete()
|
|
return
|
|
except Exception:
|
|
logger.exception('Deleting service')
|
|
raise self.invalid_item_response() from None
|
|
|
|
raise exceptions.rest.RequestError('Item has associated deployed services')
|
|
|
|
def get_title(self, parent: 'Model') -> str:
|
|
parent = ensure.is_instance(parent, models.Provider)
|
|
try:
|
|
return _('Services of {}').format(parent.name)
|
|
except Exception:
|
|
return _('Current services')
|
|
|
|
def get_fields(self, parent: 'Model') -> list[typing.Any]:
|
|
return [
|
|
{'name': {'title': _('Service name'), 'visible': True, 'type': 'iconType'}},
|
|
{'comments': {'title': _('Comments')}},
|
|
{'type_name': {'title': _('Type')}},
|
|
{
|
|
'deployed_services_count': {
|
|
'title': _('Services Pools'),
|
|
'type': 'numeric',
|
|
}
|
|
},
|
|
{'user_services_count': {'title': _('User services'), 'type': 'numeric'}},
|
|
{
|
|
'max_services_count_type': {
|
|
'title': _('Max services count type'),
|
|
'type': 'dict',
|
|
'dict': {'0': _('Standard'), '1': _('Conservative')},
|
|
},
|
|
},
|
|
{'tags': {'title': _('tags'), 'visible': False}},
|
|
]
|
|
|
|
def get_types(
|
|
self, parent: 'Model', for_type: typing.Optional[str]
|
|
) -> collections.abc.Iterable[types.rest.TypeInfoDict]:
|
|
|
|
parent = ensure.is_instance(parent, models.Provider)
|
|
logger.debug('get_types parameters: %s, %s', parent, for_type)
|
|
offers: list[types.rest.TypeInfoDict] = []
|
|
if for_type is None:
|
|
offers = [
|
|
{
|
|
'name': _(t.mod_name()),
|
|
'type': t.mod_type(),
|
|
'description': _(t.description()),
|
|
'icon': t.icon64().replace('\n', ''),
|
|
}
|
|
for t in parent.get_type().get_provided_services()
|
|
]
|
|
else:
|
|
for t in parent.get_type().get_provided_services():
|
|
if for_type == t.mod_type():
|
|
offers = [
|
|
{
|
|
'name': _(t.mod_name()),
|
|
'type': t.mod_type(),
|
|
'description': _(t.description()),
|
|
'icon': t.icon64().replace('\n', ''),
|
|
}
|
|
]
|
|
break
|
|
if not offers:
|
|
raise exceptions.rest.NotFound('type not found')
|
|
|
|
return offers # Default is that details do not have types
|
|
|
|
def get_gui(self, parent: 'Model', for_type: str) -> collections.abc.Iterable[typing.Any]:
|
|
parent = ensure.is_instance(parent, models.Provider)
|
|
try:
|
|
logger.debug('getGui parameters: %s, %s', parent, for_type)
|
|
parent_instance = parent.get_instance()
|
|
service_type = parent_instance.get_service_by_type(for_type)
|
|
if not service_type:
|
|
raise self.invalid_item_response(f'Gui for type "{for_type}" not found')
|
|
with Environment.temporary_environment() as env:
|
|
service = service_type(
|
|
env, parent_instance
|
|
) # Instantiate it so it has the opportunity to alter gui description based on parent
|
|
local_gui = self.default_fields(service.gui_description(), ['name', 'comments', 'tags'])
|
|
self.add_field(
|
|
local_gui,
|
|
{
|
|
'name': 'max_services_count_type',
|
|
'choices': [
|
|
gui.choice_item('0', _('Standard')),
|
|
gui.choice_item('1', _('Conservative')),
|
|
],
|
|
'label': _('Service counting method'),
|
|
'tooltip': _('Kind of service counting for calculating if MAX is reached'),
|
|
'type': types.ui.FieldType.CHOICE,
|
|
'readonly': False,
|
|
'order': 110,
|
|
'tab': types.ui.Tab.ADVANCED,
|
|
},
|
|
)
|
|
|
|
# Remove all overrided fields from editables
|
|
overrided_fields = service.overrided_fields or {}
|
|
local_gui = [field_gui for field_gui in local_gui if field_gui['name'] not in overrided_fields]
|
|
|
|
return local_gui
|
|
|
|
except Exception as e:
|
|
logger.exception('get_gui')
|
|
raise exceptions.rest.ResponseError(str(e)) from e
|
|
|
|
def get_logs(self, parent: 'Model', item: str) -> list[typing.Any]:
|
|
parent = ensure.is_instance(parent, models.Provider)
|
|
try:
|
|
service = parent.services.get(uuid=process_uuid(item))
|
|
logger.debug('Getting logs for %s', item)
|
|
return log.get_logs(service)
|
|
except Exception:
|
|
raise self.invalid_item_response() from None
|
|
|
|
def servicepools(self, parent: 'Model', item: str) -> list[ServicePoolResumeItem]:
|
|
parent = ensure.is_instance(parent, models.Provider)
|
|
service = parent.services.get(uuid=process_uuid(item))
|
|
logger.debug('Got parameters for servicepools: %s, %s', parent, item)
|
|
res: list[ServicePoolResumeItem] = []
|
|
for i in service.deployedServices.all():
|
|
try:
|
|
self.ensure_has_access(
|
|
i, uds.core.types.permissions.PermissionType.READ
|
|
) # Ensures access before listing...
|
|
res.append(
|
|
{
|
|
'id': i.uuid,
|
|
'name': i.name,
|
|
'thumb': i.image.thumb64 if i.image is not None else DEFAULT_THUMB_BASE64,
|
|
'user_services_count': i.userServices.exclude(
|
|
state__in=(State.REMOVED, State.ERROR)
|
|
).count(),
|
|
'state': _('With errors') if i.is_restrained() else _('Ok'),
|
|
}
|
|
)
|
|
except exceptions.rest.AccessDenied:
|
|
pass
|
|
|
|
return res
|