1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-10-06 11:33:43 +03:00
Files
openuds/server/src/uds/REST/methods/services.py

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