Fixed cache bug intruduced on encoders module removal

Improbed type checking for REST (ongoing)
Optimized queries (using prefetch) for service pools listing
This commit is contained in:
Adolfo Gómez García 2020-11-13 11:23:22 +01:00
parent 45ca92b77e
commit 6c54f8e75a
12 changed files with 647 additions and 283 deletions

View File

@ -56,6 +56,10 @@ from .handlers import (
from . import processors
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds.core.util.request import ExtendedHttpRequest
logger = logging.getLogger(__name__)
__all__ = ['Handler', 'Dispatcher']
@ -72,7 +76,7 @@ class Dispatcher(View):
# pylint: disable=too-many-locals, too-many-return-statements, too-many-branches, too-many-statements
@method_decorator(csrf_exempt)
def dispatch(self, request: http.HttpRequest, *args, **kwargs):
def dispatch(self, request: 'ExtendedHttpRequest', *args, **kwargs):
"""
Processes the REST request and routes it wherever it needs to be routed
"""

View File

@ -36,7 +36,16 @@ import typing
from django.db.models import Q, Count
from django.utils.translation import ugettext, ugettext_lazy as _
from uds.models import ServicePool, OSManager, Service, Image, ServicePoolGroup, Account, User, getSqlDatetime
from uds.models import (
ServicePool,
OSManager,
Service,
Image,
ServicePoolGroup,
Account,
User,
getSqlDatetime,
)
from uds.models.calendar_action import (
CALENDAR_ACTION_INITIAL,
CALENDAR_ACTION_MAX,
@ -63,7 +72,14 @@ from uds.core.util import permissions
from uds.REST.model import ModelHandler
from uds.REST import RequestError, ResponseError
from .user_services import AssignedService, CachedService, Groups, Transports, Publications, Changelog
from .user_services import (
AssignedService,
CachedService,
Groups,
Transports,
Publications,
Changelog,
)
from .op_calendars import AccessCalendars, ActionsCalendars
from .services import Services
@ -75,6 +91,7 @@ class ServicesPools(ModelHandler):
"""
Handles Services Pools REST requests
"""
model = ServicePool
detail = {
'services': AssignedService,
@ -84,13 +101,30 @@ class ServicesPools(ModelHandler):
'publications': Publications,
'changelog': Changelog,
'access': AccessCalendars,
'actions': ActionsCalendars
'actions': ActionsCalendars,
}
save_fields = ['name', 'short_name', 'comments', 'tags', 'service_id',
'osmanager_id', 'image_id', 'pool_group_id', 'initial_srvs',
'cache_l1_srvs', 'cache_l2_srvs', 'max_srvs', 'show_transports', 'visible',
'allow_users_remove', 'allow_users_reset', 'ignores_unused', 'account_id', 'calendar_message']
save_fields = [
'name',
'short_name',
'comments',
'tags',
'service_id',
'osmanager_id',
'image_id',
'pool_group_id',
'initial_srvs',
'cache_l1_srvs',
'cache_l2_srvs',
'max_srvs',
'show_transports',
'visible',
'allow_users_remove',
'allow_users_reset',
'ignores_unused',
'account_id',
'calendar_message',
]
remove_fields = ['osmanager_id', 'service_id']
@ -120,14 +154,53 @@ class ServicesPools(ModelHandler):
def getItems(self, *args, **kwargs):
# Optimized query, due that there is a lot of info needed for theee
d = getSqlDatetime() - datetime.timedelta(seconds=GlobalConfig.RESTRAINT_TIME.getInt())
return super().getItems(overview=kwargs.get('overview', True),
query=(ServicePool.objects.prefetch_related('service', 'service__provider', 'servicesPoolGroup', 'image', 'tags', 'memberOfMeta__meta_pool', 'account')
.annotate(valid_count=Count('userServices', filter=~Q(userServices__state__in=State.INFO_STATES)))
.annotate(preparing_count=Count('userServices', filter=Q(userServices__state=State.PREPARING)))
.annotate(error_count=Count('userServices', filter=Q(userServices__state=State.ERROR, userServices__state_date__gt=d)))
.annotate(usage_count=Count('userServices', filter=Q(userServices__state__in=State.VALID_STATES, userServices__cache_level=0)))
)
d = getSqlDatetime() - datetime.timedelta(
seconds=GlobalConfig.RESTRAINT_TIME.getInt()
)
return super().getItems(
overview=kwargs.get('overview', True),
query=(
ServicePool.objects.prefetch_related(
'service',
'service__provider',
'servicesPoolGroup',
'servicesPoolGroup__image',
'osmanager',
'image',
'tags',
'memberOfMeta__meta_pool',
'account',
)
.annotate(
valid_count=Count(
'userServices',
filter=~Q(userServices__state__in=State.INFO_STATES),
)
)
.annotate(
preparing_count=Count(
'userServices', filter=Q(userServices__state=State.PREPARING)
)
)
.annotate(
error_count=Count(
'userServices',
filter=Q(
userServices__state=State.ERROR,
userServices__state_date__gt=d,
),
)
)
.annotate(
usage_count=Count(
'userServices',
filter=Q(
userServices__state__in=State.VALID_STATES,
userServices__cache_level=0,
),
)
)
),
)
# return super().getItems(overview=kwargs.get('overview', True), prefetch=['service', 'service__provider', 'servicesPoolGroup', 'image', 'tags'])
# return super(ServicesPools, self).getItems(*args, **kwargs)
@ -161,7 +234,9 @@ class ServicesPools(ModelHandler):
'parent_type': item.service.data_type,
'comments': item.comments,
'state': state,
'thumb': item.image.thumb64 if item.image is not None else DEFAULT_THUMB_BASE64,
'thumb': item.image.thumb64
if item.image is not None
else DEFAULT_THUMB_BASE64,
'account': item.account.name if item.account is not None else '',
'account_id': item.account.uuid if item.account is not None else None,
'service_id': item.service.uuid,
@ -177,7 +252,10 @@ class ServicesPools(ModelHandler):
'allow_users_reset': item.allow_users_reset,
'ignores_unused': item.ignores_unused,
'fallbackAccess': item.fallbackAccess,
'meta_member': [{'id': i.meta_pool.uuid, 'name': i.meta_pool.name} for i in item.memberOfMeta.all()],
'meta_member': [
{'id': i.meta_pool.uuid, 'name': i.meta_pool.name}
for i in item.memberOfMeta.all()
],
'calendar_message': item.calendar_message,
}
@ -189,8 +267,12 @@ class ServicesPools(ModelHandler):
restrained = item.error_count >= GlobalConfig.RESTRAINT_COUNT.getInt() # type: ignore
usage_count = item.usage_count # type: ignore
else:
valid_count = item.userServices.exclude(state__in=State.INFO_STATES).count()
preparing_count = item.userServices.filter(state=State.PREPARING).count()
valid_count = item.userServices.exclude(
state__in=State.INFO_STATES
).count()
preparing_count = item.userServices.filter(
state=State.PREPARING
).count()
restrained = item.isRestrained()
usage_count = -1
@ -203,9 +285,10 @@ class ServicesPools(ModelHandler):
if item.servicesPoolGroup.image is not None:
poolGroupThumb = item.servicesPoolGroup.image.thumb64
val['state'] = state
val['thumb'] = item.image.thumb64 if item.image is not None else DEFAULT_THUMB_BASE64
val['thumb'] = (
item.image.thumb64 if item.image is not None else DEFAULT_THUMB_BASE64
)
val['user_services_count'] = valid_count
val['user_services_in_preparation'] = preparing_count
val['tags'] = [tag.tag for tag in item.tags.all()]
@ -227,51 +310,74 @@ class ServicesPools(ModelHandler):
# if OSManager.objects.count() < 1: # No os managers, can't create db
# raise ResponseError(ugettext('Create at least one OS Manager before creating a new service pool'))
if Service.objects.count() < 1:
raise ResponseError(ugettext('Create at least a service before creating a new service pool'))
raise ResponseError(
ugettext('Create at least a service before creating a new service pool')
)
g = self.addDefaultFields([], ['name', 'short_name', 'comments', 'tags'])
for f in [{
for f in [
{
'name': 'service_id',
'values': [gui.choiceItem('', '')] + gui.sortedChoices([gui.choiceItem(v.uuid, v.provider.name + '\\' + v.name) for v in Service.objects.all()]),
'values': [gui.choiceItem('', '')]
+ gui.sortedChoices(
[
gui.choiceItem(v.uuid, v.provider.name + '\\' + v.name)
for v in Service.objects.all()
]
),
'label': ugettext('Base service'),
'tooltip': ugettext('Service used as base of this service pool'),
'type': gui.InputField.CHOICE_TYPE,
'rdonly': True,
'order': 100, # Ensures is At end
}, {
},
{
'name': 'osmanager_id',
'values': [gui.choiceItem(-1, '')] + gui.sortedChoices([gui.choiceItem(v.uuid, v.name) for v in OSManager.objects.all()]),
'values': [gui.choiceItem(-1, '')]
+ gui.sortedChoices(
[gui.choiceItem(v.uuid, v.name) for v in OSManager.objects.all()]
),
'label': ugettext('OS Manager'),
'tooltip': ugettext('OS Manager used as base of this service pool'),
'type': gui.InputField.CHOICE_TYPE,
'rdonly': True,
'order': 101,
}, {
},
{
'name': 'allow_users_remove',
'value': False,
'label': ugettext('Allow removal by users'),
'tooltip': ugettext('If active, the user will be allowed to remove the service "manually". Be careful with this, because the user will have the "power" to delete it\'s own service'),
'tooltip': ugettext(
'If active, the user will be allowed to remove the service "manually". Be careful with this, because the user will have the "power" to delete it\'s own service'
),
'type': gui.InputField.CHECKBOX_TYPE,
'order': 111,
'tab': ugettext('Advanced'),
}, {
},
{
'name': 'allow_users_reset',
'value': False,
'label': ugettext('Allow reset by users'),
'tooltip': ugettext('If active, the user will be allowed to reset the service'),
'tooltip': ugettext(
'If active, the user will be allowed to reset the service'
),
'type': gui.InputField.CHECKBOX_TYPE,
'order': 112,
'tab': ugettext('Advanced'),
}, {
},
{
'name': 'ignores_unused',
'value': False,
'label': ugettext('Ignores unused'),
'tooltip': ugettext('If the option is enabled, UDS will not attempt to detect and remove the user services assigned but not in use.'),
'tooltip': ugettext(
'If the option is enabled, UDS will not attempt to detect and remove the user services assigned but not in use.'
),
'type': gui.InputField.CHECKBOX_TYPE,
'order': 113,
'tab': ugettext('Advanced'),
}, {
},
{
'name': 'visible',
'value': True,
'label': ugettext('Visible'),
@ -279,31 +385,51 @@ class ServicesPools(ModelHandler):
'type': gui.InputField.CHECKBOX_TYPE,
'order': 107,
'tab': ugettext('Display'),
}, {
},
{
'name': 'image_id',
'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)] + gui.sortedChoices([gui.choiceImage(v.uuid, v.name, v.thumb64) for v in Image.objects.all()]),
'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)]
+ gui.sortedChoices(
[
gui.choiceImage(v.uuid, v.name, v.thumb64)
for v in Image.objects.all()
]
),
'label': ugettext('Associated Image'),
'tooltip': ugettext('Image assocciated with this service'),
'type': gui.InputField.IMAGECHOICE_TYPE,
'order': 120,
'tab': ugettext('Display'),
}, {
},
{
'name': 'pool_group_id',
'values': [gui.choiceImage(-1, _('Default'), DEFAULT_THUMB_BASE64)] + gui.sortedChoices([gui.choiceImage(v.uuid, v.name, v.thumb64) for v in ServicePoolGroup.objects.all()]),
'values': [gui.choiceImage(-1, _('Default'), DEFAULT_THUMB_BASE64)]
+ gui.sortedChoices(
[
gui.choiceImage(v.uuid, v.name, v.thumb64)
for v in ServicePoolGroup.objects.all()
]
),
'label': ugettext('Pool group'),
'tooltip': ugettext('Pool group for this pool (for pool classify on display)'),
'tooltip': ugettext(
'Pool group for this pool (for pool classify on display)'
),
'type': gui.InputField.IMAGECHOICE_TYPE,
'order': 121,
'tab': ugettext('Display'),
}, {
},
{
'name': 'calendar_message',
'value': '',
'label': ugettext('Calendar access denied text'),
'tooltip': ugettext('Custom message to be shown to users if access is limited by calendar rules.'),
'tooltip': ugettext(
'Custom message to be shown to users if access is limited by calendar rules.'
),
'type': gui.InputField.TEXT_TYPE,
'order': 122,
'tab': ugettext('Display'),
}, {
},
{
'name': 'initial_srvs',
'value': '0',
'minValue': '0',
@ -312,55 +438,74 @@ class ServicesPools(ModelHandler):
'type': gui.InputField.NUMERIC_TYPE,
'order': 130,
'tab': ugettext('Availability'),
}, {
},
{
'name': 'cache_l1_srvs',
'value': '0',
'minValue': '0',
'label': ugettext('Services to keep in cache'),
'tooltip': ugettext('Services kept in cache for improved user service assignation'),
'tooltip': ugettext(
'Services kept in cache for improved user service assignation'
),
'type': gui.InputField.NUMERIC_TYPE,
'order': 131,
'tab': ugettext('Availability'),
}, {
},
{
'name': 'cache_l2_srvs',
'value': '0',
'minValue': '0',
'label': ugettext('Services to keep in L2 cache'),
'tooltip': ugettext('Services kept in cache of level2 for improved service generation'),
'tooltip': ugettext(
'Services kept in cache of level2 for improved service generation'
),
'type': gui.InputField.NUMERIC_TYPE,
'order': 132,
'tab': ugettext('Availability'),
}, {
},
{
'name': 'max_srvs',
'value': '0',
'minValue': '1',
'label': ugettext('Maximum number of services to provide'),
'tooltip': ugettext('Maximum number of service (assigned and L1 cache) that can be created for this service'),
'tooltip': ugettext(
'Maximum number of service (assigned and L1 cache) that can be created for this service'
),
'type': gui.InputField.NUMERIC_TYPE,
'order': 133,
'tab': ugettext('Availability'),
}, {
},
{
'name': 'show_transports',
'value': True,
'label': ugettext('Show transports'),
'tooltip': ugettext('If active, alternative transports for user will be shown'),
'tooltip': ugettext(
'If active, alternative transports for user will be shown'
),
'type': gui.InputField.CHECKBOX_TYPE,
'tab': ugettext('Advanced'),
'order': 130,
}, {
},
{
'name': 'account_id',
'values': [gui.choiceItem(-1, '')] + gui.sortedChoices([gui.choiceItem(v.uuid, v.name) for v in Account.objects.all()]),
'values': [gui.choiceItem(-1, '')]
+ gui.sortedChoices(
[gui.choiceItem(v.uuid, v.name) for v in Account.objects.all()]
),
'label': ugettext('Accounting'),
'tooltip': ugettext('Account associated to this service pool'),
'type': gui.InputField.CHOICE_TYPE,
'tab': ugettext('Advanced'),
'order': 131,
}]:
},
]:
self.addField(g, f)
return g
def beforeSave(self, fields: typing.Dict[str, typing.Any]) -> None: # pylint: disable=too-many-branches,too-many-statements
def beforeSave(
self, fields: typing.Dict[str, typing.Any]
) -> None: # pylint: disable=too-many-branches,too-many-statements
# logger.debug(self._params)
try:
try:
@ -379,7 +524,9 @@ class ServicesPools(ModelHandler):
self._params['allow_users_reset'] = False
if serviceType.needsManager is True:
osmanager = OSManager.objects.get(uuid=processUuid(fields['osmanager_id']))
osmanager = OSManager.objects.get(
uuid=processUuid(fields['osmanager_id'])
)
fields['osmanager_id'] = osmanager.id
else:
del fields['osmanager_id']
@ -390,12 +537,23 @@ class ServicesPools(ModelHandler):
fields[k] = v
if serviceType.maxDeployed != -1:
fields['max_srvs'] = min((int(fields['max_srvs']), serviceType.maxDeployed))
fields['initial_srvs'] = min(int(fields['initial_srvs']), serviceType.maxDeployed)
fields['cache_l1_srvs'] = min(int(fields['cache_l1_srvs']), serviceType.maxDeployed)
fields['max_srvs'] = min(
(int(fields['max_srvs']), serviceType.maxDeployed)
)
fields['initial_srvs'] = min(
int(fields['initial_srvs']), serviceType.maxDeployed
)
fields['cache_l1_srvs'] = min(
int(fields['cache_l1_srvs']), serviceType.maxDeployed
)
if serviceType.usesCache is False:
for k in ('initial_srvs', 'cache_l1_srvs', 'cache_l2_srvs', 'max_srvs'):
for k in (
'initial_srvs',
'cache_l1_srvs',
'cache_l2_srvs',
'max_srvs',
):
fields[k] = 0
if serviceType.usesCache_L2 is False:
@ -405,7 +563,13 @@ class ServicesPools(ModelHandler):
raise RequestError(ugettext('This service requires an OS Manager'))
# If max < initial or cache_1 or cache_l2
fields['max_srvs'] = max((int(fields['initial_srvs']), int(fields['cache_l1_srvs']), int(fields['max_srvs'])))
fields['max_srvs'] = max(
(
int(fields['initial_srvs']),
int(fields['cache_l1_srvs']),
int(fields['max_srvs']),
)
)
# *** ACCOUNT ***
accountId = fields['account_id']
@ -414,7 +578,9 @@ class ServicesPools(ModelHandler):
if accountId != '-1':
try:
fields['account_id'] = Account.objects.get(uuid=processUuid(accountId)).id
fields['account_id'] = Account.objects.get(
uuid=processUuid(accountId)
).id
except Exception:
logger.exception('Getting account ID')
@ -487,7 +653,11 @@ class ServicesPools(ModelHandler):
validActions: typing.Tuple[typing.Dict, ...] = ()
itemInfo = item.service.getType()
if itemInfo.usesCache is True:
validActions += (CALENDAR_ACTION_INITIAL, CALENDAR_ACTION_CACHE_L1, CALENDAR_ACTION_MAX)
validActions += (
CALENDAR_ACTION_INITIAL,
CALENDAR_ACTION_CACHE_L1,
CALENDAR_ACTION_MAX,
)
if itemInfo.usesCache_L2 is True:
validActions += (CALENDAR_ACTION_CACHE_L2,)
@ -495,21 +665,33 @@ class ServicesPools(ModelHandler):
validActions += (CALENDAR_ACTION_PUBLISH,)
# Transport & groups actions
validActions += (CALENDAR_ACTION_ADD_TRANSPORT, CALENDAR_ACTION_DEL_TRANSPORT, CALENDAR_ACTION_ADD_GROUP, CALENDAR_ACTION_DEL_GROUP)
validActions += (
CALENDAR_ACTION_ADD_TRANSPORT,
CALENDAR_ACTION_DEL_TRANSPORT,
CALENDAR_ACTION_ADD_GROUP,
CALENDAR_ACTION_DEL_GROUP,
)
# Advanced actions
validActions += (CALENDAR_ACTION_IGNORE_UNUSED, CALENDAR_ACTION_REMOVE_USERSERVICES)
validActions += (
CALENDAR_ACTION_IGNORE_UNUSED,
CALENDAR_ACTION_REMOVE_USERSERVICES,
)
return validActions
def listAssignables(self, item: ServicePool) -> typing.Any:
service = item.service.getInstance()
return [gui.choiceItem(i[0], i[1]) for i in service.listAssignables()]
def createFromAssignable(self, item: ServicePool) ->typing.Any:
def createFromAssignable(self, item: ServicePool) -> typing.Any:
if 'user_id' not in self._params or 'assignable_id' not in self._params:
return self.invalidRequestException('Invalid parameters')
logger.debug('Creating from assignable: %s', self._params)
userServiceManager().createFromAssignable(item, User.objects.get(uuid=processUuid(self._params['user_id'])), self._params['assignable_id'])
userServiceManager().createFromAssignable(
item,
User.objects.get(uuid=processUuid(self._params['user_id'])),
self._params['assignable_id'],
)
return True

View File

@ -45,7 +45,8 @@ from uds.core.ui import gui as uiGui
from uds.core.util import log
from uds.core.util import permissions
from uds.core.util.model import processUuid
from uds.models import Tag
from uds.models import Tag, TaggingMixin, ManagedObjectModel
from .handlers import (
Handler,
@ -54,12 +55,11 @@ from .handlers import (
RequestError,
ResponseError,
AccessDenied,
NotSupportedError
NotSupportedError,
)
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds.models.managed_object_model import ManagedObjectModel
from uds.models import User
from uds.core import Module
@ -80,12 +80,15 @@ class SaveException(HandlerError):
Exception thrown if couldn't save
"""
class BaseModelHandler(Handler):
"""
Base Handler for Master & Detail Handlers
"""
def addField(self, gui: typing.List[typing.Any], field: typing.Dict[str, typing.Any]) -> typing.List[typing.Any]:
def addField(
self, gui: typing.List[typing.Any], field: typing.Dict[str, typing.Any]
) -> typing.List[typing.Any]:
"""
Add a field to a "gui" description.
This method checks that every required field element is in there.
@ -108,15 +111,17 @@ class BaseModelHandler(Handler):
'rdonly': field.get('rdonly', False),
'type': field.get('type', uiGui.InputField.TEXT_TYPE),
'order': field.get('order', 0),
'values': field.get('values', [])
}
'values': field.get('values', []),
},
}
if 'tab' in field:
v['gui']['tab'] = field['tab']
gui.append(v)
return gui
def addDefaultFields(self, gui: typing.List[typing.Any], flds: typing.List[str]) -> typing.List[typing.Any]:
def addDefaultFields(
self, gui: typing.List[typing.Any], flds: typing.List[str]
) -> typing.List[typing.Any]:
"""
Adds default fields (based in a list) to a "gui" description
:param gui: Gui list where the "default" fielsds will be added
@ -124,66 +129,88 @@ class BaseModelHandler(Handler):
'priority' and 'small_name', 'short_name', 'tags'
"""
if 'tags' in flds:
self.addField(gui, {
'name': 'tags',
'label': _('Tags'),
'type': 'taglist',
'tooltip': _('Tags for this element'),
'order': 0 - 105,
})
self.addField(
gui,
{
'name': 'tags',
'label': _('Tags'),
'type': 'taglist',
'tooltip': _('Tags for this element'),
'order': 0 - 105,
},
)
if 'name' in flds:
self.addField(gui, {
'name': 'name',
'type': 'text',
'required': True,
'label': _('Name'),
'length': 128,
'tooltip': _('Name of this element'),
'order': 0 - 100,
})
self.addField(
gui,
{
'name': 'name',
'type': 'text',
'required': True,
'label': _('Name'),
'length': 128,
'tooltip': _('Name of this element'),
'order': 0 - 100,
},
)
if 'short_name' in flds:
self.addField(gui, {
'name': 'short_name',
'type': 'text',
'label': _('Short name'),
'tooltip': _('Short name for user service visualization'),
'required': False,
'length': 32,
'order': 0 - 95,
})
self.addField(
gui,
{
'name': 'short_name',
'type': 'text',
'label': _('Short name'),
'tooltip': _('Short name for user service visualization'),
'required': False,
'length': 32,
'order': 0 - 95,
},
)
if 'comments' in flds:
self.addField(gui, {
'name': 'comments',
'label': _('Comments'),
'tooltip': _('Comments for this element'),
'length': 256,
'order': 0 - 90,
})
self.addField(
gui,
{
'name': 'comments',
'label': _('Comments'),
'tooltip': _('Comments for this element'),
'length': 256,
'order': 0 - 90,
},
)
if 'priority' in flds:
self.addField(gui, {
'name': 'priority',
'type': 'numeric',
'label': _('Priority'),
'tooltip': _('Selects the priority of this element (lower number means higher priority)'),
'required': True,
'value': 1,
'length': 4,
'order': 0 - 85,
})
self.addField(
gui,
{
'name': 'priority',
'type': 'numeric',
'label': _('Priority'),
'tooltip': _(
'Selects the priority of this element (lower number means higher priority)'
),
'required': True,
'value': 1,
'length': 4,
'order': 0 - 85,
},
)
if 'small_name' in flds:
self.addField(gui, {
'name': 'small_name',
'type': 'text',
'label': _('Label'),
'tooltip': _('Label for this element'),
'required': True,
'length': 128,
'order': 0 - 80,
})
self.addField(
gui,
{
'name': 'small_name',
'type': 'text',
'label': _('Label'),
'tooltip': _('Label for this element'),
'required': True,
'length': 128,
'order': 0 - 80,
},
)
return gui
def ensureAccess(self, obj: models.Model, permission: int, root: bool = False) -> int:
def ensureAccess(
self, obj: models.Model, permission: int, root: bool = False
) -> int:
perm = permissions.getEffectivePermission(self._user, obj, root)
if perm < permission:
raise self.accessDenied()
@ -201,23 +228,25 @@ class BaseModelHandler(Handler):
Returns a dictionary describing the type (the name, the icon, description, etc...)
"""
res = self.typeInfo(type_)
res.update({
'name': _(type_.name()),
'type': type_.type(),
'description': _(type_.description()),
'icon': type_.icon64().replace('\n', '')
})
res.update(
{
'name': _(type_.name()),
'type': type_.type(),
'description': _(type_.description()),
'icon': type_.icon64().replace('\n', ''),
}
)
if hasattr(type_, 'group'):
res['group'] = _(type_.group) # Add group info is it is contained
return res
def processTableFields(
self,
title: str,
fields: typing.List[typing.Any],
row_style: typing.Dict[str, typing.Any],
subtitle: typing.Optional[str] = None
) -> typing.Dict[str, typing.Any]:
self,
title: str,
fields: typing.List[typing.Any],
row_style: typing.MutableMapping[str, typing.Any],
subtitle: typing.Optional[str] = None,
) -> typing.MutableMapping[str, typing.Any]:
"""
Returns a dict containing the table fields description
"""
@ -225,10 +254,12 @@ class BaseModelHandler(Handler):
'title': title,
'fields': fields,
'row-style': row_style,
'subtitle': subtitle or ''
'subtitle': subtitle or '',
}
def readFieldsFromParams(self, fldList: typing.List[str]) -> typing.Dict[str, typing.Any]:
def readFieldsFromParams(
self, fldList: typing.List[str]
) -> typing.Dict[str, typing.Any]:
"""
Reads the indicated fields from the parameters received, and if
:param fldList: List of required fields
@ -244,26 +275,32 @@ class BaseModelHandler(Handler):
return args
def fillIntanceFields(self, item: 'ManagedObjectModel', res: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:
def fillIntanceFields(
self, item: 'models.Model', res: typing.Dict[str, typing.Any]
) -> typing.Dict[str, typing.Any]:
"""
For Managed Objects (db element that contains a serialized object), fills a dictionary with the "field" parameters values.
For non managed objects, it does nothing
:param item: Item to extract fields
:param res: Dictionary to "extend" with instance key-values pairs
"""
if hasattr(item, 'getInstance'):
if isinstance(item, ManagedObjectModel):
i = item.getInstance()
i.initGui() # Defaults & stuff
value: typing.Any
for key, value in i.valuesDict().items():
if isinstance(value, str):
value = {"true": True, "false": False}.get(value, value) # Translate "true" & "false" to True & False (booleans)
value = {"true": True, "false": False}.get(
value, value
) # Translate "true" & "false" to True & False (booleans)
logger.debug('%s = %s', key, value)
res[key] = value
return res
# Exceptions
def invalidRequestException(self, message: typing.Optional[str] = None) -> HandlerError:
def invalidRequestException(
self, message: typing.Optional[str] = None
) -> HandlerError:
"""
Raises an invalid request error with a default translated string
:param message: Custom message to add to exception. If it is None, "Invalid Request" is used
@ -271,7 +308,9 @@ class BaseModelHandler(Handler):
message = message or _('Invalid Request')
return RequestError('{} {}: {}'.format(message, self.__class__, self._args))
def invalidResponseException(self, message: typing.Optional[str] = None) -> HandlerError:
def invalidResponseException(
self, message: typing.Optional[str] = None
) -> HandlerError:
message = 'Invalid response' if message is None else message
return ResponseError(message)
@ -279,9 +318,13 @@ class BaseModelHandler(Handler):
"""
Raises a NotFound exception with translated "Method not found" string to current locale
"""
return RequestError(_('Method not found in {}: {}').format(self.__class__, self._args))
return RequestError(
_('Method not found in {}: {}').format(self.__class__, self._args)
)
def invalidItemException(self, message: typing.Optional[str] = None) -> HandlerError:
def invalidItemException(
self, message: typing.Optional[str] = None
) -> HandlerError:
"""
Raises a NotFound exception, with location info
"""
@ -307,7 +350,9 @@ class BaseModelHandler(Handler):
"""
Invokes a test for an item
"""
logger.debug('Called base test for %s --> %s', self.__class__.__name__, self._params)
logger.debug(
'Called base test for %s --> %s', self.__class__.__name__, self._params
)
raise self.invalidMethodException()
@ -335,6 +380,7 @@ class DetailHandler(BaseModelHandler):
Also accepts GET methods for "custom" methods
"""
custom_methods: typing.ClassVar[typing.List[str]] = []
_parent: typing.Optional['ModelHandler']
_path: str
@ -344,13 +390,13 @@ class DetailHandler(BaseModelHandler):
_user: 'User'
def __init__(
self,
parentHandler: 'ModelHandler',
path: str,
params: typing.Any,
*args: str,
**kwargs: typing.Any
): # pylint: disable=super-init-not-called
self,
parentHandler: 'ModelHandler',
path: str,
params: typing.Any,
*args: str,
**kwargs: typing.Any
): # pylint: disable=super-init-not-called
"""
Detail Handlers in fact "disabled" handler most initialization, that is no needed because
parent modelhandler has already done it (so we must access through parent handler)
@ -363,7 +409,9 @@ class DetailHandler(BaseModelHandler):
self._kwargs = kwargs
self._user = kwargs.get('user', None)
def __checkCustom(self, check: str, parent: models.Model, arg: typing.Any = None) -> typing.Any:
def __checkCustom(
self, check: str, parent: models.Model, arg: typing.Any = None
) -> typing.Any:
"""
checks curron methods
:param check: Method to check
@ -380,7 +428,9 @@ class DetailHandler(BaseModelHandler):
return None
def get(self) -> typing.Any: # pylint: disable=too-many-branches,too-many-return-statements
def get(
self,
) -> typing.Any: # pylint: disable=too-many-branches,too-many-return-statements
"""
Processes GET method for a detail Handler
"""
@ -409,7 +459,11 @@ class DetailHandler(BaseModelHandler):
logger.debug('Types: %s', types_)
return types_
if self._args[0] == TABLEINFO:
return self.processTableFields(self.getTitle(parent), self.getFields(parent), self.getRowStyle(parent))
return self.processTableFields(
self.getTitle(parent),
self.getFields(parent),
self.getRowStyle(parent),
)
# try to get id
return self.getItems(parent, processUuid(self._args[0]))
@ -449,7 +503,9 @@ class DetailHandler(BaseModelHandler):
raise self.invalidRequestException()
logger.debug('Invoking proper saving detail item %s', item)
return self.saveItem(parent, item)
self.saveItem(parent, item)
# Empty response
return ''
def post(self) -> typing.Any:
"""
@ -494,7 +550,9 @@ class DetailHandler(BaseModelHandler):
# if item is None: # Returns ALL detail items
# return []
# return {} # Returns one item
raise NotImplementedError('Must provide an getItems method for {} class'.format(self.__class__))
raise NotImplementedError(
'Must provide an getItems method for {} class'.format(self.__class__)
)
# Default save
def saveItem(self, parent: models.Model, item: typing.Optional[str]) -> None:
@ -559,7 +617,9 @@ class DetailHandler(BaseModelHandler):
# raise RequestError('Gui not provided for this type of object')
return []
def getTypes(self, parent: models.Model, forType: typing.Optional[str]) -> typing.Iterable[typing.Dict[str, typing.Any]]:
def getTypes(
self, parent: models.Model, forType: typing.Optional[str]
) -> typing.Iterable[typing.Dict[str, typing.Any]]:
"""
The default is that detail element will not have any types (they are "homogeneous")
but we provided this method, that can be overridden, in case one detail needs it
@ -594,12 +654,13 @@ class ModelHandler(BaseModelHandler):
Note: Instance variables are the variables declared and serialized by modules.
The only detail that has types within is "Service", child of "Provider"
"""
# Authentication related
authenticated = True
needs_staff = True
# Which model does this manage, must be a django model ofc
model: typing.ClassVar[models.Model]
model: typing.ClassVar[typing.Type[models.Model]]
# By default, filter is empty
fltr: typing.Optional[str] = None
@ -607,9 +668,13 @@ class ModelHandler(BaseModelHandler):
# This is an array of tuples of two items, where first is method and second inticates if method needs parent id (normal behavior is it needs it)
# For example ('services', True) -- > .../id_parent/services
# ('services', False) --> ..../services
custom_methods: typing.ClassVar[typing.List[typing.Tuple[str, bool]]] = [] # If this model respond to "custom" methods, we will declare them here
custom_methods: typing.ClassVar[
typing.List[typing.Tuple[str, bool]]
] = [] # If this model respond to "custom" methods, we will declare them here
# If this model has details, which ones
detail: typing.ClassVar[typing.Optional[typing.Dict[str, typing.Type[DetailHandler]]]] = None # Dictionary containing detail routing
detail: typing.ClassVar[
typing.Optional[typing.Dict[str, typing.Type[DetailHandler]]]
] = None # Dictionary containing detail routing
# Put needed fields
save_fields: typing.ClassVar[typing.List[str]] = []
# Put removable fields before updating
@ -704,7 +769,7 @@ class ModelHandler(BaseModelHandler):
del self._params['filter'] # Remove parameter
logger.debug('Found a filter expression (%s)', self.fltr)
def doFilter(self, data: typing.List[typing.Dict[str, typing.Any]]) -> typing.List[typing.Dict[str, typing.Any]]:
def doFilter(self, data: typing.Any) -> typing.Any:
# Right now, filtering only supports a single filter, in a future
# we may improve it
if self.fltr is None:
@ -727,7 +792,7 @@ class ModelHandler(BaseModelHandler):
r = re.compile(s + fnmatch.translate(pattern) + e, re.RegexFlag.IGNORECASE)
def fltr_function(item: typing.Dict[str, typing.Any]):
def fltr_function(item: typing.MutableMapping[str, typing.Any]):
try:
if fld not in item or r.match(item[fld]) is None:
return False
@ -746,8 +811,10 @@ class ModelHandler(BaseModelHandler):
# Helper to process detail
# Details can be managed (writen) by any user that has MANAGEMENT permission over parent
def processDetail(self):
logger.debug('Processing detail %s for with params %s', self._path, self._params)
def processDetail(self) -> typing.Any:
logger.debug(
'Processing detail %s for with params %s', self._path, self._params
)
try:
item: models.Model = self.model.objects.filter(uuid=self._args[0])[0]
# If we do not have access to parent to, at least, read...
@ -757,14 +824,28 @@ class ModelHandler(BaseModelHandler):
else:
requiredPermission = permissions.PERMISSION_READ
if permissions.checkPermissions(self._user, item, requiredPermission) is False:
logger.debug('Permission for user %s does not comply with %s', self._user, requiredPermission)
if (
permissions.checkPermissions(self._user, item, requiredPermission)
is False
):
logger.debug(
'Permission for user %s does not comply with %s',
self._user,
requiredPermission,
)
raise self.accessDenied()
detailCls = self.detail[self._args[1]] # pylint: disable=unsubscriptable-object
if not self.detail:
raise self.invalidRequestException()
detailCls = self.detail[
self._args[1]
] # pylint: disable=unsubscriptable-object
args = list(self._args[2:])
path = self._path + '/'.join(args[:2])
detail = detailCls(self, path, self._params, *args, parent=item, user=self._user)
detail = detailCls(
self, path, self._params, *args, parent=item, user=self._user
)
method = getattr(detail, self._operation)
return method()
@ -775,7 +856,9 @@ class ModelHandler(BaseModelHandler):
raise Exception('Invalid code executed on processDetail')
def getItems(self, *args, **kwargs) -> typing.Generator[typing.Dict[str, typing.Any], None, None]:
def getItems(
self, *args, **kwargs
) -> typing.Generator[typing.MutableMapping[str, typing.Any], None, None]:
if 'overview' in kwargs:
overview = kwargs['overview']
del kwargs['overview']
@ -795,11 +878,20 @@ class ModelHandler(BaseModelHandler):
del kwargs['query']
else:
logger.debug('Args: %s, kwargs: %s', args, kwargs)
query = self.model.objects.filter(*args, **kwargs).prefetch_related(*prefetch)
query = self.model.objects.filter(*args, **kwargs).prefetch_related(
*prefetch
)
for item in query:
try:
if permissions.checkPermissions(typing.cast('User', self._user), item, permissions.PERMISSION_READ) is False:
if (
permissions.checkPermissions(
typing.cast('User', self._user),
item,
permissions.PERMISSION_READ,
)
is False
):
continue
if kwargs.get('overview', True):
yield self.item_as_dict_overview(item)
@ -819,7 +911,7 @@ class ModelHandler(BaseModelHandler):
self.extractFilter()
return self.doFilter(self.doGet())
def doGet(self): # pylint: disable=too-many-statements,too-many-branches,too-many-return-statements
def doGet(self) -> typing.Any:
logger.debug('method GET for %s, %s', self.__class__.__name__, self._args)
nArgs = len(self._args)
@ -835,7 +927,13 @@ class ModelHandler(BaseModelHandler):
operation = getattr(self, self._args[1])
item = self.model.objects.get(uuid=self._args[0].lower())
except Exception as e:
logger.error('Invalid custom method exception %s/%s/%s: %s', self.__class__.__name__, self._args, self._params, e)
logger.error(
'Invalid custom method exception %s/%s/%s: %s',
self.__class__.__name__,
self._args,
self._params,
e,
)
raise self.invalidMethodException()
return operation(item)
@ -855,9 +953,14 @@ class ModelHandler(BaseModelHandler):
if self._args[0] == TYPES:
return list(self.getTypes())
if self._args[0] == TABLEINFO:
return self.processTableFields(self.table_title, self.table_fields, self.table_row_style, self.table_subtitle)
return self.processTableFields(
self.table_title,
self.table_fields,
self.table_row_style,
self.table_subtitle,
)
if self._args[0] == GUI:
return self.getGui(None)
return self.getGui('')
# get item ID
try:
@ -890,7 +993,9 @@ class ModelHandler(BaseModelHandler):
if nArgs != 2:
raise self.invalidRequestException()
try:
item = self.model.objects.get(uuid=self._args[0].lower()) # DB maybe case sensitive??, anyway, uuids are stored in lowercase
item = self.model.objects.get(
uuid=self._args[0].lower()
) # DB maybe case sensitive??, anyway, uuids are stored in lowercase
return self.getLogs(item)
except Exception:
raise self.invalidItemException()
@ -901,8 +1006,7 @@ class ModelHandler(BaseModelHandler):
raise self.invalidRequestException() # Will not return
def post(self):
def post(self) -> typing.Any:
"""
Processes a POST request
"""
@ -914,7 +1018,7 @@ class ModelHandler(BaseModelHandler):
raise self.invalidMethodException() # Will not return
def put(self): # pylint: disable=too-many-branches, too-many-statements
def put(self) -> typing.Any:
"""
Processes a PUT request
"""
@ -926,7 +1030,10 @@ class ModelHandler(BaseModelHandler):
if len(self._args) > 1: # Detail?
return self.processDetail()
self.ensureAccess(self.model(), permissions.PERMISSION_ALL, root=True) # Must have write permissions to create, modify, etc..
# Here, self.model() indicates an "django model object with default params"
self.ensureAccess(
self.model(), permissions.PERMISSION_ALL, root=True
) # Must have write permissions to create, modify, etc..
try:
# Extract fields
@ -954,11 +1061,16 @@ class ModelHandler(BaseModelHandler):
item.__dict__.update(args) # Update fields from args
# Now if tags, update them
if tags:
logger.debug('Updating tags: %s', tags)
item.tags.set([Tag.objects.get_or_create(tag=val)[0] for val in tags if val != ''])
elif isinstance(tags, list): # Present, but list is empty (will be proccesed on "if" else)
item.tags.clear()
if isinstance(item, TaggingMixin):
if tags:
logger.debug('Updating tags: %s', tags)
item.tags.set(
[Tag.objects.get_or_create(tag=val)[0] for val in tags if val != '']
)
elif isinstance(
tags, list
): # Present, but list is empty (will be proccesed on "if" else)
item.tags.clear()
except self.model.DoesNotExist:
raise NotFound('Item not found')
@ -973,14 +1085,19 @@ class ModelHandler(BaseModelHandler):
raise RequestError('incorrect invocation to PUT')
if not deleteOnError:
self.checkSave(item) # Will raise an exception if item can't be saved (only for modify operations..)
self.checkSave(
item
) # Will raise an exception if item can't be saved (only for modify operations..)
# Store associated object if requested (data_type)
try:
data_type: typing.Optional[str] = self._params.get('data_type', self._params.get('type'))
if data_type:
item.data_type = data_type
item.data = item.getInstance(self._params).serialize()
if isinstance(item, ManagedObjectModel):
data_type: typing.Optional[str] = self._params.get(
'data_type', self._params.get('type')
)
if data_type:
item.data_type = data_type
item.data = item.getInstance(self._params).serialize()
item.save()
@ -995,7 +1112,7 @@ class ModelHandler(BaseModelHandler):
return res
def delete(self) -> str:
def delete(self) -> typing.Any:
"""
Processes a DELETE request
"""
@ -1006,7 +1123,9 @@ class ModelHandler(BaseModelHandler):
if len(self._args) != 1:
raise RequestError('Delete need one and only one argument')
self.ensureAccess(self.model(), permissions.PERMISSION_ALL, root=True) # Must have write permissions to delete
self.ensureAccess(
self.model(), permissions.PERMISSION_ALL, root=True
) # Must have write permissions to delete
try:
item = self.model.objects.get(uuid=self._args[0].lower())

View File

@ -55,6 +55,10 @@ from uds.core.auths import Authenticator as AuthenticatorInstance
from uds.models import User, Authenticator
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds.core.util.request import ExtendedHttpRequest
logger = logging.getLogger(__name__)
authLogger = logging.getLogger('authLog')
@ -110,11 +114,11 @@ def webLoginRequired(admin: typing.Union[bool, str] = False) -> typing.Callable[
if admin == 'admin', needs admin
"""
def decorator(view_func: typing.Callable[..., RT]) -> typing.Callable[..., RT]:
def _wrapped_view(request: HttpRequest, *args, **kwargs) -> RT:
def _wrapped_view(request: 'ExtendedHttpRequest', *args, **kwargs) -> RT:
"""
Wrapped function for decorator
"""
if request.user is None:
if not request.user:
# url = request.build_absolute_uri(GlobalConfig.LOGIN_URL.get())
# if GlobalConfig.REDIRECT_TO_HTTPS.getBool() is True:
# url = url.replace('http://', 'https://')

View File

@ -129,7 +129,7 @@ class Cache:
now = getSqlDatetime()
try:
DBCache.objects.create(
owner=self._owner, key=key, value=value, created=now, validity=validity
owner=self._owner, key=key, value=strValue, created=now, validity=validity
) # @UndefinedVariable
except Exception:
try:

View File

@ -141,7 +141,7 @@ class CalendarChecker:
memCache = caches['memory']
# First, try to get data from cache if it is valid
cacheKey = str(hash(self.calendar.modified)) + str(dtime.date().toordinal()) + self.calendar.uuid + 'checker'
cacheKey = str(self.calendar.modified.toordinal()) + str(dtime.date().toordinal()) + self.calendar.uuid + 'checker'
# First, check "local memory cache", and if not found, from DB cache
cached = memCache.get(cacheKey) or CalendarChecker.cache.get(cacheKey, None)

View File

@ -52,6 +52,9 @@ class ExtendedHttpRequest(HttpRequest):
os: DictAsObj
user: typing.Optional[User] # type: ignore # HttpRequests users "user" for it own, but we redefine it because base is not used...
class ExtendedHttpRequestWithUser(ExtendedHttpRequest):
user: User
logger = logging.getLogger(__name__)
_requests: typing.Dict[int, typing.Tuple[weakref.ref, datetime.datetime]] = {}

View File

@ -35,7 +35,8 @@ import logging
# Utility imports
from .util import getSqlDatetime, getSqlDatetimeAsUnix, NEVER, NEVER_UNIX
# Imports all models so they are available for migrations
# Imports all models so they are available for migrations, etc..
from .managed_object_model import ManagedObjectModel
# Permissions
from .permissions import Permissions
@ -104,7 +105,7 @@ from .account_usage import AccountUsage
from .proxy import Proxy
# Tagging
from .tag import Tag
from .tag import Tag, TaggingMixin
# Utility
from .dbfile import DBFile

View File

@ -138,7 +138,7 @@ class MetaPool(UUIDModel, TaggingMixin): # type: ignore
Checks if the access for a service pool is allowed or not (based esclusively on associated calendars)
"""
if chkDateTime is None:
chkDateTime = typing.cast('datetime.datetime', getSqlDatetime())
chkDateTime = getSqlDatetime()
access = self.fallbackAccess
# Let's see if we can access by current datetime

View File

@ -151,7 +151,7 @@ class User(UUIDModel):
"""
if self.parent:
try:
usr = User.objects.prefetch_related('groups').get(uuid=self.parent)
usr = User.objects.prefetch_related('authenticator', 'groups').get(uuid=self.parent)
except Exception: # If parent do not exists
usr = self
else:

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2019 Virtual Cable S.L.
# Copyright (c) 2012-2020 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -35,7 +35,14 @@ from django.utils.translation import ugettext
from django.utils import formats
from django.urls import reverse
from uds.models import ServicePool, Transport, Network, ServicePoolGroup, MetaPool, getSqlDatetime
from uds.models import (
ServicePool,
Transport,
Network,
ServicePoolGroup,
MetaPool,
getSqlDatetime,
)
from uds.core.util.config import GlobalConfig
from uds.core.util import html
@ -43,14 +50,18 @@ from uds.core.managers import userServiceManager
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds.core.util.request import ExtendedHttpRequest
from uds.core.util.request import ExtendedHttpRequestWithUser
from uds.core.util.tools import DictAsObj
logger = logging.getLogger(__name__)
def getServicesData(request: 'ExtendedHttpRequest') -> typing.Dict[str, typing.Any]: # pylint: disable=too-many-locals, too-many-branches, too-many-statements
def getServicesData(
request: 'ExtendedHttpRequestWithUser',
) -> typing.Dict[
str, typing.Any
]: # pylint: disable=too-many-locals, too-many-branches, too-many-statements
"""Obtains the service data dictionary will all available services for this request
Arguments:
@ -65,22 +76,22 @@ def getServicesData(request: 'ExtendedHttpRequest') -> typing.Dict[str, typing.A
'autorun': autorun
"""
# Session data
os: 'DictAsObj' = request.os
if not request.user:
return {}
# We look for services for this authenticator groups. User is logged in in just 1 authenticator, so his groups must coincide with those assigned to ds
groups = list(request.user.getGroups())
availServicePools = list(ServicePool.getDeployedServicesForGroups(groups, request.user)) # Pass in user to get "number_assigned" to optimize
availMetaPools = list(MetaPool.getForGroups(groups, request.user)) # Pass in user to get "number_assigned" to optimize
availServicePools = list(
ServicePool.getDeployedServicesForGroups(groups, request.user)
) # Pass in user to get "number_assigned" to optimize
availMetaPools = list(
MetaPool.getForGroups(groups, request.user)
) # Pass in user to get "number_assigned" to optimize
now = getSqlDatetime()
# Information for administrators
nets = ''
validTrans = ''
logger.debug('OS: %s', os['OS'])
osName = request.os['OS']
logger.debug('OS: %s', osName)
if request.user.isStaff():
nets = ','.join([n.name for n in Network.networksFor(request.ip)])
@ -99,13 +110,20 @@ def getServicesData(request: 'ExtendedHttpRequest') -> typing.Dict[str, typing.A
for meta in availMetaPools:
# Check that we have access to at least one transport on some of its children
hasUsablePools = False
in_use = typing.cast(typing.Any, meta).number_in_use > 0 # Override, because we used hear annotations
in_use = (
typing.cast(typing.Any, meta).number_in_use > 0
) # Override, because we used hear annotations
for member in meta.members.all():
# if pool.isInMaintenance():
# continue
for t in member.pool.transports.all():
typeTrans = t.getType()
if t.getType() and t.validForIp(request.ip) and typeTrans.supportsOs(os['OS']) and t.validForOs(os['OS']):
if (
t.getType()
and t.validForIp(request.ip)
and typeTrans.supportsOs(osName)
and t.validForOs(osName)
):
hasUsablePools = True
break
@ -120,31 +138,39 @@ def getServicesData(request: 'ExtendedHttpRequest') -> typing.Dict[str, typing.A
# If no usable pools, this is not visible
if hasUsablePools:
group = meta.servicesPoolGroup.as_dict if meta.servicesPoolGroup else ServicePoolGroup.default().as_dict
group = (
meta.servicesPoolGroup.as_dict
if meta.servicesPoolGroup
else ServicePoolGroup.default().as_dict
)
services.append({
'id': 'M' + meta.uuid,
'name': meta.name,
'visual_name': meta.visual_name,
'description': meta.comments,
'group': group,
'transports': [{
'id': 'meta',
'name': 'meta',
'link': html.udsMetaLink(request, 'M' + meta.uuid),
'priority': 0
}],
'imageId': meta.image and meta.image.uuid or 'x',
'show_transports': False,
'allow_users_remove': False,
'allow_users_reset': False,
'maintenance': meta.isInMaintenance(),
'not_accesible': not meta.isAccessAllowed(now),
'in_use': in_use,
'to_be_replaced': None,
'to_be_replaced_text': '',
'custom_calendar_text': meta.calendar_message,
})
services.append(
{
'id': 'M' + meta.uuid,
'name': meta.name,
'visual_name': meta.visual_name,
'description': meta.comments,
'group': group,
'transports': [
{
'id': 'meta',
'name': 'meta',
'link': html.udsMetaLink(request, 'M' + meta.uuid),
'priority': 0,
}
],
'imageId': meta.image and meta.image.uuid or 'x',
'show_transports': False,
'allow_users_remove': False,
'allow_users_reset': False,
'maintenance': meta.isInMaintenance(),
'not_accesible': not meta.isAccessAllowed(now),
'in_use': in_use,
'to_be_replaced': None,
'to_be_replaced_text': '',
'custom_calendar_text': meta.calendar_message,
}
)
# Now generic user service
for svr in availServicePools:
@ -155,23 +181,24 @@ def getServicesData(request: 'ExtendedHttpRequest') -> typing.Dict[str, typing.A
use = str(svr.usage(typing.cast(typing.Any, svr).usage_count)) + '%'
trans = []
for t in sorted(svr.transports.all(), key=lambda x: x.priority): # In memory sort, allows reuse prefetched and not too big array
for t in sorted(
svr.transports.all(), key=lambda x: x.priority
): # In memory sort, allows reuse prefetched and not too big array
try:
typeTrans = t.getType()
except Exception:
continue
if t.validForIp(request.ip) and typeTrans.supportsOs(os['OS']) and t.validForOs(os['OS']):
if (
t.validForIp(request.ip)
and typeTrans.supportsOs(osName)
and t.validForOs(osName)
):
if typeTrans.ownLink:
link = reverse('TransportOwnLink', args=('F' + svr.uuid, t.uuid))
else:
link = html.udsAccessLink(request, 'F' + svr.uuid, t.uuid)
trans.append(
{
'id': t.uuid,
'name': t.name,
'link': link,
'priority': t.priority
}
{'id': t.uuid, 'name': t.name, 'link': link, 'priority': t.priority}
)
# If empty transports, do not include it on list
@ -190,47 +217,69 @@ def getServicesData(request: 'ExtendedHttpRequest') -> typing.Dict[str, typing.A
# if ads:
# in_use = ads.in_use
group = svr.servicesPoolGroup.as_dict if svr.servicesPoolGroup else ServicePoolGroup.default().as_dict
group = (
svr.servicesPoolGroup.as_dict
if svr.servicesPoolGroup
else ServicePoolGroup.default().as_dict
)
# Only add toBeReplaced info in case we allow it. This will generate some "overload" on the services
toBeReplaced = svr.toBeReplaced(request.user) if typing.cast(
typing.Any, svr).pubs_active > 0 and GlobalConfig.NOTIFY_REMOVAL_BY_PUB.getBool(False) else None
toBeReplaced = (
svr.toBeReplaced(request.user)
if typing.cast(typing.Any, svr).pubs_active > 0
and GlobalConfig.NOTIFY_REMOVAL_BY_PUB.getBool(False)
else None
)
# tbr = False
if toBeReplaced:
toBeReplaced = formats.date_format(toBeReplaced, "SHORT_DATETIME_FORMAT")
toBeReplacedTxt = ugettext(
'This service is about to be replaced by a new version. Please, close the session before {} and save all your work to avoid loosing it.').format(toBeReplaced)
'This service is about to be replaced by a new version. Please, close the session before {} and save all your work to avoid loosing it.'
).format(toBeReplaced)
else:
toBeReplacedTxt = ''
def datator(x): return x.replace('{use}', use).replace('{total}', str(svr.max_srvs))
def datator(x):
return x.replace('{use}', use).replace('{total}', str(svr.max_srvs))
services.append({
'id': 'F' + svr.uuid,
'name': datator(svr.name),
'visual_name': datator(svr.visual_name.replace('{use}', use).replace('{total}', str(svr.max_srvs))),
'description': svr.comments,
'group': group,
'transports': trans,
'imageId': imageId,
'show_transports': svr.show_transports,
'allow_users_remove': svr.allow_users_remove,
'allow_users_reset': svr.allow_users_reset,
'maintenance': svr.isInMaintenance(),
'not_accesible': not svr.isAccessAllowed(now),
'in_use': in_use,
'to_be_replaced': toBeReplaced,
'to_be_replaced_text': toBeReplacedTxt,
'custom_calendar_text': svr.calendar_message,
})
services.append(
{
'id': 'F' + svr.uuid,
'name': datator(svr.name),
'visual_name': datator(
svr.visual_name.replace('{use}', use).replace(
'{total}', str(svr.max_srvs)
)
),
'description': svr.comments,
'group': group,
'transports': trans,
'imageId': imageId,
'show_transports': svr.show_transports,
'allow_users_remove': svr.allow_users_remove,
'allow_users_reset': svr.allow_users_reset,
'maintenance': svr.isInMaintenance(),
'not_accesible': not svr.isAccessAllowed(now),
'in_use': in_use,
'to_be_replaced': toBeReplaced,
'to_be_replaced_text': toBeReplacedTxt,
'custom_calendar_text': svr.calendar_message,
}
)
# logger.debug('Services: %s', services)
# Sort services and remove services with no transports...
services = [s for s in sorted(services, key=lambda s: s['name'].upper()) if s['transports']]
services = [
s for s in sorted(services, key=lambda s: s['name'].upper()) if s['transports']
]
autorun = False
if len(services) == 1 and GlobalConfig.AUTORUN_SERVICE.getBool(False) and services[0]['transports']:
if (
len(services) == 1
and GlobalConfig.AUTORUN_SERVICE.getBool(False)
and services[0]['transports']
):
if request.session.get('autorunDone', '0') == '0':
request.session['autorunDone'] = '1'
autorun = True
@ -241,5 +290,5 @@ def getServicesData(request: 'ExtendedHttpRequest') -> typing.Dict[str, typing.A
'ip': request.ip,
'nets': nets,
'transports': validTrans,
'autorun': autorun
'autorun': autorun,
}

View File

@ -51,14 +51,16 @@ from uds.web.util import services
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from django.http import HttpRequest # pylint: disable=ungrouped-imports
from uds.core.util.request import ExtendedHttpRequest, ExtendedHttpRequestWithUser
logger = logging.getLogger(__name__)
@webLoginRequired(admin=False)
def transportOwnLink(request: 'HttpRequest', idService: str, idTransport: str):
def transportOwnLink(request: 'ExtendedHttpRequestWithUser', idService: str, idTransport: str):
response: typing.MutableMapping[str, typing.Any] = {}
# For type checkers to "be happy"
try:
res = userServiceManager().getService(request.user, request.os, request.ip, idService, idTransport)
ip, userService, iads, trans, itrans = res # pylint: disable=unused-variable
@ -87,7 +89,7 @@ def transportOwnLink(request: 'HttpRequest', idService: str, idTransport: str):
@cache_page(3600, key_prefix='img', cache='memory')
def transportIcon(request: 'HttpRequest', idTrans: str) -> HttpResponse:
def transportIcon(request: 'ExtendedHttpRequest', idTrans: str) -> HttpResponse:
try:
transport: Transport = Transport.objects.get(uuid=processUuid(idTrans))
return HttpResponse(transport.getInstance().icon(), content_type='image/png')
@ -96,7 +98,7 @@ def transportIcon(request: 'HttpRequest', idTrans: str) -> HttpResponse:
@cache_page(3600, key_prefix='img', cache='memory')
def serviceImage(request: 'HttpRequest', idImage: str) -> HttpResponse:
def serviceImage(request: 'ExtendedHttpRequest', idImage: str) -> HttpResponse:
try:
icon = Image.objects.get(uuid=processUuid(idImage))
return icon.imageResponse()
@ -112,7 +114,7 @@ def serviceImage(request: 'HttpRequest', idImage: str) -> HttpResponse:
@webLoginRequired(admin=False)
@never_cache
def userServiceEnabler(request: 'HttpRequest', idService: str, idTransport: str) -> HttpResponse:
def userServiceEnabler(request: 'ExtendedHttpRequestWithUser', idService: str, idTransport: str) -> HttpResponse:
# Maybe we could even protect this even more by limiting referer to own server /? (just a meditation..)
logger.debug('idService: %s, idTransport: %s', idService, idTransport)
url = ''
@ -166,12 +168,12 @@ def userServiceEnabler(request: 'HttpRequest', idService: str, idTransport: str)
content_type='application/json'
)
def closer(request: 'HttpRequest') -> HttpResponse:
def closer(request: 'ExtendedHttpRequest') -> HttpResponse:
return HttpResponse('<html><body onload="window.close()"></body></html>')
@webLoginRequired(admin=False)
@never_cache
def action(request: 'HttpRequest', idService: str, actionString: str) -> HttpResponse:
def action(request: 'ExtendedHttpRequestWithUser', idService: str, actionString: str) -> HttpResponse:
userService = userServiceManager().locateUserService(request.user, idService, create=False)
response: typing.Any = None
rebuild: bool = False