forked from shaba/openuds
More formating & minor typing fixes
This commit is contained in:
parent
03bfb3efbb
commit
8285e2daad
@ -50,14 +50,14 @@ from .handlers import (
|
||||
NotFound,
|
||||
RequestError,
|
||||
ResponseError,
|
||||
NotSupportedError
|
||||
NotSupportedError,
|
||||
)
|
||||
|
||||
from . import processors
|
||||
|
||||
# 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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -70,12 +70,15 @@ class Dispatcher(View):
|
||||
"""
|
||||
This class is responsible of dispatching REST requests
|
||||
"""
|
||||
|
||||
# This attribute will contain all paths--> handler relations, filled at Initialized method
|
||||
services: typing.ClassVar[typing.Dict[str, typing.Any]] = {'': None} # Will include a default /rest handler, but rigth now this will be fine
|
||||
services: typing.ClassVar[typing.Dict[str, typing.Any]] = {
|
||||
'': None
|
||||
} # Will include a default /rest handler, but rigth now this will be fine
|
||||
|
||||
# pylint: disable=too-many-locals, too-many-return-statements, too-many-branches, too-many-statements
|
||||
@method_decorator(csrf_exempt)
|
||||
def dispatch(self, request: 'ExtendedHttpRequest', *args, **kwargs):
|
||||
def dispatch(self, request: 'ExtendedHttpRequestWithUser', *args, **kwargs):
|
||||
"""
|
||||
Processes the REST request and routes it wherever it needs to be routed
|
||||
"""
|
||||
@ -98,7 +101,9 @@ class Dispatcher(View):
|
||||
content_type = path[0].split('.')[1]
|
||||
|
||||
clean_path = path[0].split('.')[0]
|
||||
if not clean_path: # Skip empty path elements, so /x/y == /x////y for example (due to some bugs detected on some clients)
|
||||
if (
|
||||
not clean_path
|
||||
): # Skip empty path elements, so /x/y == /x////y for example (due to some bugs detected on some clients)
|
||||
path = path[1:]
|
||||
continue
|
||||
|
||||
@ -115,9 +120,13 @@ class Dispatcher(View):
|
||||
# Here, service points to the path
|
||||
cls: typing.Optional[typing.Type[Handler]] = service['']
|
||||
if cls is None:
|
||||
return http.HttpResponseNotFound('Method not found', content_type="text/plain")
|
||||
return http.HttpResponseNotFound(
|
||||
'Method not found', content_type="text/plain"
|
||||
)
|
||||
|
||||
processor = processors.available_processors_ext_dict.get(content_type, processors.default_processor)(request)
|
||||
processor = processors.available_processors_ext_dict.get(
|
||||
content_type, processors.default_processor
|
||||
)(request)
|
||||
|
||||
# Obtain method to be invoked
|
||||
http_method: str = request.method.lower() if request.method else ''
|
||||
@ -128,24 +137,40 @@ class Dispatcher(View):
|
||||
handler = None
|
||||
|
||||
try:
|
||||
handler = cls(request, full_path, http_method, processor.processParameters(), *args, **kwargs)
|
||||
handler = cls(
|
||||
request,
|
||||
full_path,
|
||||
http_method,
|
||||
processor.processParameters(),
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
operation: typing.Callable[[], typing.Any] = getattr(handler, http_method)
|
||||
except processors.ParametersException as e:
|
||||
logger.debug('Path: %s', full_path)
|
||||
logger.debug('Error: %s', e)
|
||||
return http.HttpResponseServerError('Invalid parameters invoking {0}: {1}'.format(full_path, e), content_type="text/plain")
|
||||
return http.HttpResponseServerError(
|
||||
'Invalid parameters invoking {0}: {1}'.format(full_path, e),
|
||||
content_type="text/plain",
|
||||
)
|
||||
except AttributeError:
|
||||
allowedMethods = []
|
||||
for n in ['get', 'post', 'put', 'delete']:
|
||||
if hasattr(handler, n):
|
||||
allowedMethods.append(n)
|
||||
return http.HttpResponseNotAllowed(allowedMethods, content_type="text/plain")
|
||||
return http.HttpResponseNotAllowed(
|
||||
allowedMethods, content_type="text/plain"
|
||||
)
|
||||
except AccessDenied:
|
||||
return http.HttpResponseForbidden('access denied', content_type="text/plain")
|
||||
return http.HttpResponseForbidden(
|
||||
'access denied', content_type="text/plain"
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('error accessing attribute')
|
||||
logger.debug('Getting attribute %s for %s', http_method, full_path)
|
||||
return http.HttpResponseServerError('Unexcepected error', content_type="text/plain")
|
||||
return http.HttpResponseServerError(
|
||||
'Unexcepected error', content_type="text/plain"
|
||||
)
|
||||
|
||||
# Invokes the handler's operation, add headers to response and returns
|
||||
try:
|
||||
@ -180,12 +205,16 @@ class Dispatcher(View):
|
||||
Try to register Handler subclasses that have not been inherited
|
||||
"""
|
||||
for cls in classes:
|
||||
if not cls.__subclasses__(): # Only classes that has not been inherited will be registered as Handlers
|
||||
if (
|
||||
not cls.__subclasses__()
|
||||
): # Only classes that has not been inherited will be registered as Handlers
|
||||
if not cls.name:
|
||||
name = cls.__name__.lower()
|
||||
else:
|
||||
name = cls.name
|
||||
logger.debug('Adding handler %s for method %s in path %s', cls, name, cls.path)
|
||||
logger.debug(
|
||||
'Adding handler %s for method %s in path %s', cls, name, cls.path
|
||||
)
|
||||
service_node = Dispatcher.services # Root path
|
||||
if cls.path:
|
||||
for k in cls.path.split('/'):
|
||||
@ -214,7 +243,9 @@ class Dispatcher(View):
|
||||
pkgpath = os.path.join(os.path.dirname(sys.modules[__name__].__file__), package)
|
||||
for _, name, _ in pkgutil.iter_modules([pkgpath]):
|
||||
# __import__(__name__ + '.' + package + '.' + name, globals(), locals(), [], 0)
|
||||
importlib.import_module( __name__ + '.' + package + '.' + name) # import module
|
||||
importlib.import_module(
|
||||
__name__ + '.' + package + '.' + name
|
||||
) # import module
|
||||
|
||||
importlib.invalidate_caches()
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright (c) 2012-2019 Virtual Cable S.L.
|
||||
# Copyright (c) 2012-2021 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
@ -12,7 +12,7 @@
|
||||
# * 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. nor the names of its contributors
|
||||
# * 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.
|
||||
#
|
||||
@ -30,11 +30,9 @@
|
||||
"""
|
||||
@author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
"""
|
||||
import datetime
|
||||
import typing
|
||||
import logging
|
||||
|
||||
from django.utils import timezone
|
||||
from django.contrib.sessions.backends.base import SessionBase
|
||||
from django.contrib.sessions.backends.db import SessionStore
|
||||
|
||||
@ -47,7 +45,7 @@ from uds.core.managers import cryptoManager
|
||||
# Not imported at runtime, just for type checking
|
||||
if typing.TYPE_CHECKING:
|
||||
from uds.core.util.request import ExtendedHttpRequestWithUser
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
AUTH_TOKEN_HEADER = 'HTTP_X_AUTH_TOKEN'
|
||||
@ -93,31 +91,59 @@ class Handler:
|
||||
"""
|
||||
REST requests handler base class
|
||||
"""
|
||||
raw: typing.ClassVar[bool] = False # If true, Handler will return directly an HttpResponse Object
|
||||
name: typing.ClassVar[typing.Optional[str]] = None # If name is not used, name will be the class name in lower case
|
||||
path: typing.ClassVar[typing.Optional[str]] = None # Path for this method, so we can do /auth/login, /auth/logout, /auth/auths in a simple way
|
||||
authenticated: typing.ClassVar[bool] = True # By default, all handlers needs authentication. Will be overwriten if needs_admin or needs_staff,
|
||||
needs_admin: typing.ClassVar[bool] = False # By default, the methods will be accessible by anyone if nothing else indicated
|
||||
|
||||
raw: typing.ClassVar[
|
||||
bool
|
||||
] = False # If true, Handler will return directly an HttpResponse Object
|
||||
name: typing.ClassVar[
|
||||
typing.Optional[str]
|
||||
] = None # If name is not used, name will be the class name in lower case
|
||||
path: typing.ClassVar[
|
||||
typing.Optional[str]
|
||||
] = None # Path for this method, so we can do /auth/login, /auth/logout, /auth/auths in a simple way
|
||||
authenticated: typing.ClassVar[
|
||||
bool
|
||||
] = True # By default, all handlers needs authentication. Will be overwriten if needs_admin or needs_staff,
|
||||
needs_admin: typing.ClassVar[
|
||||
bool
|
||||
] = False # By default, the methods will be accessible by anyone if nothing else indicated
|
||||
needs_staff: typing.ClassVar[bool] = False # By default, staff
|
||||
|
||||
_request: 'ExtendedHttpRequestWithUser' # It's a modified HttpRequest
|
||||
_path: str
|
||||
_operation: str
|
||||
_params: typing.Any # This is a deserliazied object from request. Can be anything as 'a' or {'a': 1} or ....
|
||||
_args: typing.Tuple[str, ...] # This are the "path" split by /, that is, the REST invocation arguments
|
||||
_args: typing.Tuple[
|
||||
str, ...
|
||||
] # This are the "path" split by /, that is, the REST invocation arguments
|
||||
_kwargs: typing.Dict
|
||||
_headers: typing.Dict[str, str]
|
||||
_session: typing.Optional[SessionStore]
|
||||
_authToken: typing.Optional[str]
|
||||
_user: 'User'
|
||||
|
||||
|
||||
# method names: 'get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'
|
||||
def __init__(self, request: 'ExtendedHttpRequestWithUser', path: str, operation: str, params: typing.Any, *args: str, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
request: 'ExtendedHttpRequestWithUser',
|
||||
path: str,
|
||||
operation: str,
|
||||
params: typing.Any,
|
||||
*args: str,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
logger.debug('Data: %s %s %s', self.__class__, self.needs_admin, self.authenticated)
|
||||
if (self.needs_admin or self.needs_staff) and not self.authenticated: # If needs_admin, must also be authenticated
|
||||
raise Exception('class {} is not authenticated but has needs_admin or needs_staff set!!'.format(self.__class__))
|
||||
logger.debug(
|
||||
'Data: %s %s %s', self.__class__, self.needs_admin, self.authenticated
|
||||
)
|
||||
if (
|
||||
self.needs_admin or self.needs_staff
|
||||
) and not self.authenticated: # If needs_admin, must also be authenticated
|
||||
raise Exception(
|
||||
'class {} is not authenticated but has needs_admin or needs_staff set!!'.format(
|
||||
self.__class__
|
||||
)
|
||||
)
|
||||
|
||||
self._request = request
|
||||
self._path = path
|
||||
@ -127,7 +153,9 @@ class Handler:
|
||||
self._kwargs = kwargs
|
||||
self._headers = {}
|
||||
self._authToken = None
|
||||
if self.authenticated: # Only retrieve auth related data on authenticated handlers
|
||||
if (
|
||||
self.authenticated
|
||||
): # Only retrieve auth related data on authenticated handlers
|
||||
try:
|
||||
self._authToken = self._request.META.get(AUTH_TOKEN_HEADER, '')
|
||||
self._session = SessionStore(session_key=self._authToken)
|
||||
@ -150,7 +178,6 @@ class Handler:
|
||||
else:
|
||||
self._user = User() # Empty user for non authenticated handlers
|
||||
|
||||
|
||||
def headers(self) -> typing.Dict[str, str]:
|
||||
"""
|
||||
Returns the headers of the REST request (all)
|
||||
@ -191,16 +218,16 @@ class Handler:
|
||||
|
||||
@staticmethod
|
||||
def storeSessionAuthdata(
|
||||
session: SessionBase,
|
||||
id_auth: int,
|
||||
username: str,
|
||||
password: str,
|
||||
locale: str,
|
||||
platform: str,
|
||||
is_admin: bool,
|
||||
staff_member: bool,
|
||||
scrambler: str
|
||||
):
|
||||
session: SessionBase,
|
||||
id_auth: int,
|
||||
username: str,
|
||||
password: str,
|
||||
locale: str,
|
||||
platform: str,
|
||||
is_admin: bool,
|
||||
staff_member: bool,
|
||||
scrambler: str,
|
||||
):
|
||||
"""
|
||||
Stores the authentication data inside current session
|
||||
:param session: session handler (Djano user session object)
|
||||
@ -220,20 +247,20 @@ class Handler:
|
||||
'locale': locale,
|
||||
'platform': platform,
|
||||
'is_admin': is_admin,
|
||||
'staff_member': staff_member
|
||||
'staff_member': staff_member,
|
||||
}
|
||||
|
||||
def genAuthToken(
|
||||
self,
|
||||
id_auth: int,
|
||||
username: str,
|
||||
password: str,
|
||||
locale: str,
|
||||
platform: str,
|
||||
is_admin: bool,
|
||||
staf_member: bool,
|
||||
scrambler: str
|
||||
):
|
||||
self,
|
||||
id_auth: int,
|
||||
username: str,
|
||||
password: str,
|
||||
locale: str,
|
||||
platform: str,
|
||||
is_admin: bool,
|
||||
staf_member: bool,
|
||||
scrambler: str,
|
||||
):
|
||||
"""
|
||||
Generates the authentication token from a session, that is basically
|
||||
the session key itself
|
||||
@ -244,11 +271,21 @@ class Handler:
|
||||
:param staf_member: If user is considered staff member or not
|
||||
"""
|
||||
session = SessionStore()
|
||||
Handler.storeSessionAuthdata(session, id_auth, username, password, locale, platform, is_admin, staf_member, scrambler)
|
||||
Handler.storeSessionAuthdata(
|
||||
session,
|
||||
id_auth,
|
||||
username,
|
||||
password,
|
||||
locale,
|
||||
platform,
|
||||
is_admin,
|
||||
staf_member,
|
||||
scrambler,
|
||||
)
|
||||
session.save()
|
||||
self._authToken = session.session_key
|
||||
self._session = session
|
||||
|
||||
|
||||
return self._authToken
|
||||
|
||||
def cleanAuthToken(self) -> None:
|
||||
@ -282,13 +319,20 @@ class Handler:
|
||||
self._session.accessed = True
|
||||
self._session.save()
|
||||
except Exception:
|
||||
logger.exception('Got an exception setting session value %s to %s', key, value)
|
||||
logger.exception(
|
||||
'Got an exception setting session value %s to %s', key, value
|
||||
)
|
||||
|
||||
def validSource(self) -> bool:
|
||||
try:
|
||||
return net.ipInNetwork(self._request.ip, GlobalConfig.ADMIN_TRUSTED_SOURCES.get(True))
|
||||
return net.ipInNetwork(
|
||||
self._request.ip, GlobalConfig.ADMIN_TRUSTED_SOURCES.get(True)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning('Error checking truted ADMIN source: "%s" does not seems to be a valid network string. Using Unrestricted access.', GlobalConfig.ADMIN_TRUSTED_SOURCES.get())
|
||||
logger.warning(
|
||||
'Error checking truted ADMIN source: "%s" does not seems to be a valid network string. Using Unrestricted access.',
|
||||
GlobalConfig.ADMIN_TRUSTED_SOURCES.get(),
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@ -312,8 +356,10 @@ class Handler:
|
||||
authId = self.getValue('auth')
|
||||
username = self.getValue('username')
|
||||
# Maybe it's root user??
|
||||
if (GlobalConfig.SUPER_USER_ALLOW_WEBACCESS.getBool(True) and
|
||||
username == GlobalConfig.SUPER_USER_LOGIN.get(True) and
|
||||
authId == -1):
|
||||
if (
|
||||
GlobalConfig.SUPER_USER_ALLOW_WEBACCESS.getBool(True)
|
||||
and username == GlobalConfig.SUPER_USER_LOGIN.get(True)
|
||||
and authId == -1
|
||||
):
|
||||
return getRootUser()
|
||||
return Authenticator.objects.get(pk=authId).users.get(name=username)
|
||||
|
@ -50,6 +50,7 @@ class Accounts(ModelHandler):
|
||||
"""
|
||||
Processes REST requests about accounts
|
||||
"""
|
||||
|
||||
model = Account
|
||||
detail = {'usage': AccountsUsage}
|
||||
|
||||
@ -72,7 +73,7 @@ class Accounts(ModelHandler):
|
||||
'tags': [tag.tag for tag in item.tags.all()],
|
||||
'comments': item.comments,
|
||||
'time_mark': item.time_mark,
|
||||
'permission': permissions.getEffectivePermission(self._user, item)
|
||||
'permission': permissions.getEffectivePermission(self._user, item),
|
||||
}
|
||||
|
||||
def getGui(self, type_: str) -> typing.List[typing.Any]:
|
||||
|
@ -70,7 +70,7 @@ class AccountsUsage(DetailHandler): # pylint: disable=too-many-public-methods
|
||||
'running': item.user_service is not None,
|
||||
'elapsed': item.elapsed,
|
||||
'elapsed_timemark': item.elapsed_timemark,
|
||||
'permission': perm
|
||||
'permission': perm,
|
||||
}
|
||||
|
||||
return retVal
|
||||
|
@ -64,7 +64,9 @@ class ActorTokens(ModelHandler):
|
||||
def item_as_dict(self, item: ActorToken) -> typing.Dict[str, typing.Any]:
|
||||
return {
|
||||
'id': item.token,
|
||||
'name': _('Token isued by {} from {}').format(item.username, item.hostname or item.ip),
|
||||
'name': _('Token isued by {} from {}').format(
|
||||
item.username, item.hostname or item.ip
|
||||
),
|
||||
'stamp': item.stamp,
|
||||
'username': item.username,
|
||||
'ip': item.ip,
|
||||
@ -73,7 +75,7 @@ class ActorTokens(ModelHandler):
|
||||
'pre_command': item.pre_command,
|
||||
'post_command': item.post_command,
|
||||
'runonce_command': item.runonce_command,
|
||||
'log_level': ['DEBUG', 'INFO', 'ERROR', 'FATAL'][item.log_level%4]
|
||||
'log_level': ['DEBUG', 'INFO', 'ERROR', 'FATAL'][item.log_level % 4],
|
||||
}
|
||||
|
||||
def delete(self) -> str:
|
||||
@ -83,7 +85,9 @@ class ActorTokens(ModelHandler):
|
||||
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:
|
||||
self.model.objects.get(token=self._args[0]).delete()
|
||||
|
@ -75,7 +75,7 @@ class CalendarRules(DetailHandler): # pylint: disable=too-many-public-methods
|
||||
'interval': item.interval,
|
||||
'duration': item.duration,
|
||||
'duration_unit': item.duration_unit,
|
||||
'permission': perm
|
||||
'permission': perm,
|
||||
}
|
||||
|
||||
return retVal
|
||||
@ -98,7 +98,13 @@ class CalendarRules(DetailHandler): # pylint: disable=too-many-public-methods
|
||||
{'name': {'title': _('Rule name')}},
|
||||
{'start': {'title': _('Starts'), 'type': 'datetime'}},
|
||||
{'end': {'title': _('Ends'), 'type': 'date'}},
|
||||
{'frequency': {'title': _('Repeats'), 'type': 'dict', 'dict': dict((v[0], str(v[1])) for v in freqs)}},
|
||||
{
|
||||
'frequency': {
|
||||
'title': _('Repeats'),
|
||||
'type': 'dict',
|
||||
'dict': dict((v[0], str(v[1])) for v in freqs),
|
||||
}
|
||||
},
|
||||
{'interval': {'title': _('Every'), 'type': 'callback'}},
|
||||
{'duration': {'title': _('Duration'), 'type': 'callback'}},
|
||||
{'comments': {'title': _('Comments')}},
|
||||
@ -108,7 +114,18 @@ class CalendarRules(DetailHandler): # pylint: disable=too-many-public-methods
|
||||
# Extract item db fields
|
||||
# We need this fields for all
|
||||
logger.debug('Saving rule %s / %s', parent, item)
|
||||
fields = self.readFieldsFromParams(['name', 'comments', 'frequency', 'start', 'end', 'interval', 'duration', 'duration_unit'])
|
||||
fields = self.readFieldsFromParams(
|
||||
[
|
||||
'name',
|
||||
'comments',
|
||||
'frequency',
|
||||
'start',
|
||||
'end',
|
||||
'interval',
|
||||
'duration',
|
||||
'duration_unit',
|
||||
]
|
||||
)
|
||||
|
||||
if int(fields['interval']) < 1:
|
||||
raise self.invalidItemException('Repeat must be greater than zero')
|
||||
|
@ -50,6 +50,7 @@ class Calendars(ModelHandler):
|
||||
"""
|
||||
Processes REST requests about calendars
|
||||
"""
|
||||
|
||||
model = Calendar
|
||||
detail = {'rules': CalendarRules}
|
||||
|
||||
@ -57,7 +58,14 @@ class Calendars(ModelHandler):
|
||||
|
||||
table_title = _('Calendars')
|
||||
table_fields = [
|
||||
{'name': {'title': _('Name'), 'visible': True, 'type': 'icon', 'icon': 'fa fa-calendar text-success'}},
|
||||
{
|
||||
'name': {
|
||||
'title': _('Name'),
|
||||
'visible': True,
|
||||
'type': 'icon',
|
||||
'icon': 'fa fa-calendar text-success',
|
||||
}
|
||||
},
|
||||
{'comments': {'title': _('Comments')}},
|
||||
{'modified': {'title': _('Modified'), 'type': 'datetime'}},
|
||||
{'tags': {'title': _('tags'), 'visible': False}},
|
||||
@ -70,7 +78,7 @@ class Calendars(ModelHandler):
|
||||
'tags': [tag.tag for tag in item.tags.all()],
|
||||
'comments': item.comments,
|
||||
'modified': item.modified,
|
||||
'permission': permissions.getEffectivePermission(self._user, item)
|
||||
'permission': permissions.getEffectivePermission(self._user, item),
|
||||
}
|
||||
|
||||
def getGui(self, type_: str) -> typing.List[typing.Any]:
|
||||
|
@ -58,15 +58,16 @@ class Client(Handler):
|
||||
"""
|
||||
Processes Client requests
|
||||
"""
|
||||
|
||||
authenticated = False # Client requests are not authenticated
|
||||
|
||||
@staticmethod
|
||||
def result(
|
||||
result: typing.Any = None,
|
||||
error: typing.Optional[typing.Union[str, int]] = None,
|
||||
errorCode: int = 0,
|
||||
retryable: bool = False
|
||||
) -> typing.Dict[str, typing.Any]:
|
||||
result: typing.Any = None,
|
||||
error: typing.Optional[typing.Union[str, int]] = None,
|
||||
errorCode: int = 0,
|
||||
retryable: bool = False,
|
||||
) -> typing.Dict[str, typing.Any]:
|
||||
"""
|
||||
Helper method to create a "result" set for actor response
|
||||
:param result: Result value to return (can be None, in which case it is converted to empty string '')
|
||||
@ -84,7 +85,9 @@ class Client(Handler):
|
||||
if errorCode != 0:
|
||||
# Reformat error so it is better understood by users
|
||||
# error += ' (code {0:04X})'.format(errorCode)
|
||||
error = _('Your service is being created. Please, wait while we complete it') + ' ({}%)'.format(int(errorCode * 25))
|
||||
error = _(
|
||||
'Your service is being created. Please, wait while we complete it'
|
||||
) + ' ({}%)'.format(int(errorCode * 25))
|
||||
|
||||
res['error'] = error
|
||||
res['retryable'] = '1' if retryable else '0'
|
||||
@ -106,17 +109,26 @@ class Client(Handler):
|
||||
logger.debug('Client args for GET: %s', self._args)
|
||||
|
||||
if not self._args: # Gets version
|
||||
return Client.result({
|
||||
'availableVersion': CLIENT_VERSION,
|
||||
'requiredVersion': REQUIRED_CLIENT_VERSION,
|
||||
'downloadUrl': self._request.build_absolute_uri(reverse('page.client-download'))
|
||||
})
|
||||
return Client.result(
|
||||
{
|
||||
'availableVersion': CLIENT_VERSION,
|
||||
'requiredVersion': REQUIRED_CLIENT_VERSION,
|
||||
'downloadUrl': self._request.build_absolute_uri(
|
||||
reverse('page.client-download')
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
if len(self._args) == 1: # Simple test
|
||||
return Client.result(_('Correct'))
|
||||
|
||||
try:
|
||||
ticket, scrambler = self._args # If more than 2 args, got an error. pylint: disable=unbalanced-tuple-unpacking
|
||||
(
|
||||
ticket,
|
||||
scrambler,
|
||||
) = (
|
||||
self._args
|
||||
) # If more than 2 args, got an error. pylint: disable=unbalanced-tuple-unpacking
|
||||
hostname = self._params['hostname'] # Or if hostname is not included...
|
||||
srcIp = self._request.ip
|
||||
|
||||
@ -127,7 +139,13 @@ class Client(Handler):
|
||||
except Exception:
|
||||
raise RequestError('Invalid request')
|
||||
|
||||
logger.debug('Got Ticket: %s, scrambled: %s, Hostname: %s, Ip: %s', ticket, scrambler, hostname, srcIp)
|
||||
logger.debug(
|
||||
'Got Ticket: %s, scrambled: %s, Hostname: %s, Ip: %s',
|
||||
ticket,
|
||||
scrambler,
|
||||
hostname,
|
||||
srcIp,
|
||||
)
|
||||
|
||||
try:
|
||||
data = TicketStore.get(ticket)
|
||||
@ -138,10 +156,28 @@ class Client(Handler):
|
||||
|
||||
try:
|
||||
logger.debug(data)
|
||||
ip, userService, userServiceInstance, transport, transportInstance = userServiceManager().getService(
|
||||
self._request.user, self._request.os, self._request.ip, data['service'], data['transport'], clientHostname=hostname
|
||||
(
|
||||
ip,
|
||||
userService,
|
||||
userServiceInstance,
|
||||
transport,
|
||||
transportInstance,
|
||||
) = userServiceManager().getService(
|
||||
self._request.user,
|
||||
self._request.os,
|
||||
self._request.ip,
|
||||
data['service'],
|
||||
data['transport'],
|
||||
clientHostname=hostname,
|
||||
)
|
||||
logger.debug(
|
||||
'Res: %s %s %s %s %s',
|
||||
ip,
|
||||
userService,
|
||||
userServiceInstance,
|
||||
transport,
|
||||
transportInstance,
|
||||
)
|
||||
logger.debug('Res: %s %s %s %s %s', ip, userService, userServiceInstance, transport, transportInstance)
|
||||
password = cryptoManager().symDecrpyt(data['password'], scrambler)
|
||||
|
||||
# Set "accesedByClient"
|
||||
@ -155,25 +191,44 @@ class Client(Handler):
|
||||
if not transportInstance:
|
||||
raise Exception('No transport instance!!!')
|
||||
|
||||
transportScript, signature, params = transportInstance.getEncodedTransportScript(userService, transport, ip, self._request.os, self._request.user, password, self._request)
|
||||
(
|
||||
transportScript,
|
||||
signature,
|
||||
params,
|
||||
) = transportInstance.getEncodedTransportScript(
|
||||
userService,
|
||||
transport,
|
||||
ip,
|
||||
self._request.os,
|
||||
self._request.user,
|
||||
password,
|
||||
self._request,
|
||||
)
|
||||
|
||||
logger.debug('Signature: %s', signature)
|
||||
logger.debug('Data:#######\n%s\n###########', params)
|
||||
|
||||
return Client.result(result={
|
||||
'script': transportScript,
|
||||
'signature': signature, # It is already on base64
|
||||
'params': codecs.encode(codecs.encode(json.dumps(params).encode(), 'bz2'), 'base64').decode(),
|
||||
})
|
||||
return Client.result(
|
||||
result={
|
||||
'script': transportScript,
|
||||
'signature': signature, # It is already on base64
|
||||
'params': codecs.encode(
|
||||
codecs.encode(json.dumps(params).encode(), 'bz2'), 'base64'
|
||||
).decode(),
|
||||
}
|
||||
)
|
||||
except ServiceNotReadyError as e:
|
||||
# Set that client has accesed userService
|
||||
if e.userService:
|
||||
e.userService.setProperty('accessedByClient', '1')
|
||||
|
||||
# Refresh ticket and make this retrayable
|
||||
TicketStore.revalidate(ticket, 20) # Retry will be in at most 5 seconds, so 20 is fine :)
|
||||
return Client.result(error=errors.SERVICE_IN_PREPARATION, errorCode=e.code, retryable=True)
|
||||
TicketStore.revalidate(
|
||||
ticket, 20
|
||||
) # Retry will be in at most 5 seconds, so 20 is fine :)
|
||||
return Client.result(
|
||||
error=errors.SERVICE_IN_PREPARATION, errorCode=e.code, retryable=True
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("Exception")
|
||||
return Client.result(error=str(e))
|
||||
|
||||
|
@ -42,9 +42,15 @@ logger = logging.getLogger(__name__)
|
||||
# Pair of section/value removed from current UDS version
|
||||
REMOVED = {
|
||||
'UDS': (
|
||||
'allowPreferencesAccess', 'customHtmlLogin', 'UDS Theme',
|
||||
'UDS Theme Enhaced', 'css', 'allowPreferencesAccess',
|
||||
'loginUrl', 'maxLoginTries', 'loginBlockTime'
|
||||
'allowPreferencesAccess',
|
||||
'customHtmlLogin',
|
||||
'UDS Theme',
|
||||
'UDS Theme Enhaced',
|
||||
'css',
|
||||
'allowPreferencesAccess',
|
||||
'loginUrl',
|
||||
'maxLoginTries',
|
||||
'loginBlockTime',
|
||||
),
|
||||
'Cluster': ('Destination CPU Load', 'Migration CPU Load', 'Migration Free Memory'),
|
||||
'IPAUTH': ('autoLogin',),
|
||||
@ -81,7 +87,7 @@ class Config(Handler):
|
||||
'crypt': cfg.isCrypted(),
|
||||
'longText': cfg.isLongText(),
|
||||
'type': cfg.getType(),
|
||||
'params': cfg.getParams()
|
||||
'params': cfg.getParams(),
|
||||
}
|
||||
logger.debug('Configuration: %s', res)
|
||||
return res
|
||||
|
@ -88,7 +88,11 @@ class Connection(Handler):
|
||||
# Ensure user is present on request, used by web views methods
|
||||
self._request.user = self._user
|
||||
|
||||
return Connection.result(result=services.getServicesData(typing.cast(ExtendedHttpRequestWithUser, self._request)))
|
||||
return Connection.result(
|
||||
result=services.getServicesData(
|
||||
typing.cast(ExtendedHttpRequestWithUser, self._request)
|
||||
)
|
||||
)
|
||||
|
||||
def connection(self, doNotCheck: bool = False):
|
||||
idService = self._args[0]
|
||||
@ -183,7 +187,9 @@ class Connection(Handler):
|
||||
self._request.user = self._user # type: ignore
|
||||
self._request._cryptedpass = self._session['REST']['password'] # type: ignore
|
||||
self._request._scrambler = self._request.META['HTTP_SCRAMBLER'] # type: ignore
|
||||
linkInfo = services.enableService(self._request, idService=self._args[0], idTransport=self._args[1])
|
||||
linkInfo = services.enableService(
|
||||
self._request, idService=self._args[0], idTransport=self._args[1]
|
||||
)
|
||||
if linkInfo['error']:
|
||||
return Connection.result(error=linkInfo['error'])
|
||||
return Connection.result(result=linkInfo['url'])
|
||||
|
@ -49,13 +49,21 @@ class Images(ModelHandler):
|
||||
"""
|
||||
Handles the gallery REST interface
|
||||
"""
|
||||
|
||||
path = 'gallery'
|
||||
model = Image
|
||||
save_fields = ['name', 'data']
|
||||
|
||||
table_title = _('Image Gallery')
|
||||
table_fields = [
|
||||
{'thumb': {'title': _('Image'), 'visible': True, 'type': 'image', 'width': '96px'}},
|
||||
{
|
||||
'thumb': {
|
||||
'title': _('Image'),
|
||||
'visible': True,
|
||||
'type': 'image',
|
||||
'width': '96px',
|
||||
}
|
||||
},
|
||||
{'name': {'title': _('Name')}},
|
||||
{'size': {'title': _('Size')}},
|
||||
]
|
||||
@ -69,17 +77,17 @@ class Images(ModelHandler):
|
||||
item.updateThumbnail()
|
||||
item.save()
|
||||
|
||||
|
||||
def getGui(self, type_: str) -> typing.List[typing.Any]:
|
||||
return self.addField(
|
||||
self.addDefaultFields([], ['name']), {
|
||||
self.addDefaultFields([], ['name']),
|
||||
{
|
||||
'name': 'data',
|
||||
'value': '',
|
||||
'label': ugettext('Image'),
|
||||
'tooltip': ugettext('Image object'),
|
||||
'type': gui.InputField.IMAGECHOICE_TYPE,
|
||||
'order': 100, # At end
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
def item_as_dict(self, item: Image) -> typing.Dict[str, typing.Any]:
|
||||
@ -92,7 +100,9 @@ class Images(ModelHandler):
|
||||
def item_as_dict_overview(self, item: Image) -> typing.Dict[str, typing.Any]:
|
||||
return {
|
||||
'id': item.uuid,
|
||||
'size': '{}x{}, {} bytes (thumb {} bytes)'.format(item.width, item.height, len(item.data), len(item.thumb)),
|
||||
'size': '{}x{}, {} bytes (thumb {} bytes)'.format(
|
||||
item.width, item.height, len(item.data), len(item.thumb)
|
||||
),
|
||||
'name': item.name,
|
||||
'thumb': item.thumb64,
|
||||
}
|
||||
|
@ -53,15 +53,22 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Enclosed methods under /auth path
|
||||
|
||||
|
||||
class Login(Handler):
|
||||
"""
|
||||
Responsible of user authentication
|
||||
"""
|
||||
|
||||
path = 'auth'
|
||||
authenticated = False # Public method
|
||||
|
||||
@staticmethod
|
||||
def result(result: str = 'error', token: str = None, scrambler: str = None, error: str = None) -> typing.MutableMapping[str, typing.Any]:
|
||||
def result(
|
||||
result: str = 'error',
|
||||
token: str = None,
|
||||
scrambler: str = None,
|
||||
error: str = None,
|
||||
) -> typing.MutableMapping[str, typing.Any]:
|
||||
res = {
|
||||
'result': result,
|
||||
'token': token,
|
||||
@ -109,15 +116,31 @@ class Login(Handler):
|
||||
cache = Cache('RESTapi')
|
||||
fails = cache.get(self._request.ip) or 0
|
||||
if fails > ALLOWED_FAILS:
|
||||
logger.info('Access to REST API %s is blocked for %s seconds since last fail', self._request.ip, GlobalConfig.LOGIN_BLOCK.getInt())
|
||||
|
||||
logger.info(
|
||||
'Access to REST API %s is blocked for %s seconds since last fail',
|
||||
self._request.ip,
|
||||
GlobalConfig.LOGIN_BLOCK.getInt(),
|
||||
)
|
||||
|
||||
try:
|
||||
if 'auth_id' not in self._params and 'authId' not in self._params and 'authSmallName' not in self._params and 'auth' not in self._params:
|
||||
if (
|
||||
'auth_id' not in self._params
|
||||
and 'authId' not in self._params
|
||||
and 'authSmallName' not in self._params
|
||||
and 'auth' not in self._params
|
||||
):
|
||||
raise RequestError('Invalid parameters (no auth)')
|
||||
|
||||
scrambler: str = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(32)) # @UndefinedVariable
|
||||
authId: typing.Optional[str] = self._params.get('authId', self._params.get('auth_id', None))
|
||||
authSmallName: typing.Optional[str] = self._params.get('authSmallName', None)
|
||||
scrambler: str = ''.join(
|
||||
random.SystemRandom().choice(string.ascii_letters + string.digits)
|
||||
for _ in range(32)
|
||||
) # @UndefinedVariable
|
||||
authId: typing.Optional[str] = self._params.get(
|
||||
'authId', self._params.get('auth_id', None)
|
||||
)
|
||||
authSmallName: typing.Optional[str] = self._params.get(
|
||||
'authSmallName', None
|
||||
)
|
||||
authName: typing.Optional[str] = self._params.get('auth', None)
|
||||
platform: str = self._params.get('platform', self._request.os)
|
||||
|
||||
@ -126,9 +149,18 @@ class Login(Handler):
|
||||
|
||||
username, password = self._params['username'], self._params['password']
|
||||
locale: str = self._params.get('locale', 'en')
|
||||
if authName == 'admin' or authSmallName == 'admin' or authId == '00000000-0000-0000-0000-000000000000':
|
||||
if GlobalConfig.SUPER_USER_LOGIN.get(True) == username and GlobalConfig.SUPER_USER_PASS.get(True) == password:
|
||||
self.genAuthToken(-1, username, password, locale, platform, True, True, scrambler)
|
||||
if (
|
||||
authName == 'admin'
|
||||
or authSmallName == 'admin'
|
||||
or authId == '00000000-0000-0000-0000-000000000000'
|
||||
):
|
||||
if (
|
||||
GlobalConfig.SUPER_USER_LOGIN.get(True) == username
|
||||
and GlobalConfig.SUPER_USER_PASS.get(True) == password
|
||||
):
|
||||
self.genAuthToken(
|
||||
-1, username, password, locale, platform, True, True, scrambler
|
||||
)
|
||||
return Login.result(result='ok', token=self.getAuthToken())
|
||||
return Login.result(error='Invalid credentials')
|
||||
|
||||
@ -149,13 +181,24 @@ class Login(Handler):
|
||||
# Sleep a while here to "prottect"
|
||||
time.sleep(3) # Wait 3 seconds if credentials fails for "protection"
|
||||
# And store in cache for blocking for a while if fails
|
||||
cache.put(self._request.ip, fails+1, GlobalConfig.LOGIN_BLOCK.getInt())
|
||||
|
||||
cache.put(
|
||||
self._request.ip, fails + 1, GlobalConfig.LOGIN_BLOCK.getInt()
|
||||
)
|
||||
|
||||
return Login.result(error='Invalid credentials')
|
||||
return Login.result(
|
||||
result='ok',
|
||||
token=self.genAuthToken(auth.id, user.name, password, locale, platform, user.is_admin, user.staff_member, scrambler),
|
||||
scrambler=scrambler
|
||||
token=self.genAuthToken(
|
||||
auth.id,
|
||||
user.name,
|
||||
password,
|
||||
locale,
|
||||
platform,
|
||||
user.is_admin,
|
||||
user.staff_member,
|
||||
scrambler,
|
||||
),
|
||||
scrambler=scrambler,
|
||||
)
|
||||
|
||||
except Exception:
|
||||
@ -169,6 +212,7 @@ class Logout(Handler):
|
||||
"""
|
||||
Responsible of user de-authentication
|
||||
"""
|
||||
|
||||
path = 'auth'
|
||||
authenticated = True # By default, all handlers needs authentication
|
||||
|
||||
@ -190,14 +234,16 @@ class Auths(Handler):
|
||||
auth: Authenticator
|
||||
for auth in Authenticator.objects.all():
|
||||
theType = auth.getType()
|
||||
if paramAll or (theType.isCustom() is False and theType.typeType not in ('IP',)):
|
||||
if paramAll or (
|
||||
theType.isCustom() is False and theType.typeType not in ('IP',)
|
||||
):
|
||||
yield {
|
||||
'authId': auth.uuid,
|
||||
'authSmallName': str(auth.small_name),
|
||||
'auth': auth.name,
|
||||
'type': theType.typeType,
|
||||
'priority': auth.priority,
|
||||
'isCustom': theType.isCustom()
|
||||
'isCustom': theType.isCustom(),
|
||||
}
|
||||
|
||||
def get(self):
|
||||
|
@ -54,6 +54,7 @@ class MetaPools(ModelHandler):
|
||||
"""
|
||||
Handles Services Pools REST requests
|
||||
"""
|
||||
|
||||
model = MetaPool
|
||||
detail = {
|
||||
'pools': MetaServicesPool,
|
||||
@ -62,8 +63,18 @@ class MetaPools(ModelHandler):
|
||||
'access': AccessCalendars,
|
||||
}
|
||||
|
||||
save_fields = ['name', 'short_name', 'comments', 'tags',
|
||||
'image_id', 'servicesPoolGroup_id', 'visible', 'policy', 'calendar_message', 'transport_grouping']
|
||||
save_fields = [
|
||||
'name',
|
||||
'short_name',
|
||||
'comments',
|
||||
'tags',
|
||||
'image_id',
|
||||
'servicesPoolGroup_id',
|
||||
'visible',
|
||||
'policy',
|
||||
'calendar_message',
|
||||
'transport_grouping',
|
||||
]
|
||||
|
||||
table_title = _('Meta Pools')
|
||||
table_fields = [
|
||||
@ -93,8 +104,16 @@ class MetaPools(ModelHandler):
|
||||
poolGroupThumb = item.servicesPoolGroup.image.thumb64
|
||||
|
||||
allPools = item.members.all()
|
||||
userServicesCount = sum((i.pool.userServices.exclude(state__in=State.INFO_STATES).count() for i in allPools))
|
||||
userServicesInPreparation = sum((i.pool.userServices.filter(state=State.PREPARING).count()) for i in allPools)
|
||||
userServicesCount = sum(
|
||||
(
|
||||
i.pool.userServices.exclude(state__in=State.INFO_STATES).count()
|
||||
for i in allPools
|
||||
)
|
||||
)
|
||||
userServicesInPreparation = sum(
|
||||
(i.pool.userServices.filter(state=State.PREPARING).count())
|
||||
for i in allPools
|
||||
)
|
||||
|
||||
val = {
|
||||
'id': item.uuid,
|
||||
@ -102,7 +121,9 @@ class MetaPools(ModelHandler):
|
||||
'short_name': item.short_name,
|
||||
'tags': [tag.tag for tag in item.tags.all()],
|
||||
'comments': item.comments,
|
||||
'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,
|
||||
'image_id': item.image.uuid if item.image is not None else None,
|
||||
'servicesPoolGroup_id': poolGroupId,
|
||||
'pool_group_name': poolGroupName,
|
||||
@ -114,7 +135,7 @@ class MetaPools(ModelHandler):
|
||||
'fallbackAccess': item.fallbackAccess,
|
||||
'permission': permissions.getEffectivePermission(self._user, item),
|
||||
'calendar_message': item.calendar_message,
|
||||
'transport_grouping': item.transport_grouping
|
||||
'transport_grouping': item.transport_grouping,
|
||||
}
|
||||
|
||||
return val
|
||||
@ -123,30 +144,50 @@ class MetaPools(ModelHandler):
|
||||
def getGui(self, type_: str) -> typing.List[typing.Any]:
|
||||
localGUI = self.addDefaultFields([], ['name', 'short_name', 'comments', 'tags'])
|
||||
|
||||
for field in [{
|
||||
for field in [
|
||||
{
|
||||
'name': 'policy',
|
||||
'values': [gui.choiceItem(k, str(v)) for k, v in MetaPool.TYPES.items()],
|
||||
'values': [
|
||||
gui.choiceItem(k, str(v)) for k, v in MetaPool.TYPES.items()
|
||||
],
|
||||
'label': ugettext('Policy'),
|
||||
'tooltip': ugettext('Service pool policy'),
|
||||
'type': gui.InputField.CHOICE_TYPE,
|
||||
'order': 100,
|
||||
}, {
|
||||
},
|
||||
{
|
||||
'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': gui.DISPLAY_TAB,
|
||||
}, {
|
||||
},
|
||||
{
|
||||
'name': 'servicesPoolGroup_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': gui.DISPLAY_TAB,
|
||||
}, {
|
||||
},
|
||||
{
|
||||
'name': 'visible',
|
||||
'value': True,
|
||||
'label': ugettext('Visible'),
|
||||
@ -154,23 +195,31 @@ class MetaPools(ModelHandler):
|
||||
'type': gui.InputField.CHECKBOX_TYPE,
|
||||
'order': 123,
|
||||
'tab': gui.DISPLAY_TAB,
|
||||
}, {
|
||||
},
|
||||
{
|
||||
'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': 124,
|
||||
'tab': gui.DISPLAY_TAB,
|
||||
}, {
|
||||
},
|
||||
{
|
||||
'name': 'transport_grouping',
|
||||
'values': [gui.choiceItem(k, str(v)) for k, v in MetaPool.TRANSPORT_SELECT.items()],
|
||||
'values': [
|
||||
gui.choiceItem(k, str(v))
|
||||
for k, v in MetaPool.TRANSPORT_SELECT.items()
|
||||
],
|
||||
'label': ugettext('Transport Selection'),
|
||||
'tooltip': ugettext('Transport selection policy'),
|
||||
'type': gui.InputField.CHOICE_TYPE,
|
||||
'order': 125,
|
||||
'tab': gui.DISPLAY_TAB
|
||||
}]:
|
||||
'tab': gui.DISPLAY_TAB,
|
||||
},
|
||||
]:
|
||||
self.addField(localGUI, field)
|
||||
|
||||
return localGUI
|
||||
|
@ -52,12 +52,20 @@ class Networks(ModelHandler):
|
||||
Processes REST requests about networks
|
||||
Implements specific handling for network related requests using GUI
|
||||
"""
|
||||
|
||||
model = Network
|
||||
save_fields = ['name', 'net_string', 'tags']
|
||||
|
||||
table_title = _('Networks')
|
||||
table_fields = [
|
||||
{'name': {'title': _('Name'), 'visible': True, 'type': 'icon', 'icon': 'fa fa-globe text-success'}},
|
||||
{
|
||||
'name': {
|
||||
'title': _('Name'),
|
||||
'visible': True,
|
||||
'type': 'icon',
|
||||
'icon': 'fa fa-globe text-success',
|
||||
}
|
||||
},
|
||||
{'net_string': {'title': _('Range')}},
|
||||
{'networks_count': {'title': _('Used by'), 'type': 'numeric', 'width': '8em'}},
|
||||
{'tags': {'title': _('tags'), 'visible': False}},
|
||||
@ -75,14 +83,17 @@ class Networks(ModelHandler):
|
||||
|
||||
def getGui(self, type_: str) -> typing.List[typing.Any]:
|
||||
return self.addField(
|
||||
self.addDefaultFields([], ['name', 'tags']), {
|
||||
self.addDefaultFields([], ['name', 'tags']),
|
||||
{
|
||||
'name': 'net_string',
|
||||
'value': '',
|
||||
'label': ugettext('Network range'),
|
||||
'tooltip': ugettext('Network range. Accepts most network definitions formats (range, subnet, host, etc...'),
|
||||
'tooltip': ugettext(
|
||||
'Network range. Accepts most network definitions formats (range, subnet, host, etc...'
|
||||
),
|
||||
'type': gui.InputField.TEXT_TYPE,
|
||||
'order': 100, # At end
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
def item_as_dict(self, item: Network) -> typing.Dict[str, typing.Any]:
|
||||
@ -92,5 +103,5 @@ class Networks(ModelHandler):
|
||||
'tags': [tag.tag for tag in item.tags.all()],
|
||||
'net_string': item.net_string,
|
||||
'networks_count': item.transports.count(),
|
||||
'permission': permissions.getEffectivePermission(self._user, item)
|
||||
'permission': permissions.getEffectivePermission(self._user, item),
|
||||
}
|
||||
|
@ -52,6 +52,7 @@ logger = logging.getLogger(__name__)
|
||||
ALLOW = 'ALLOW'
|
||||
DENY = 'DENY'
|
||||
|
||||
|
||||
class AccessCalendars(DetailHandler):
|
||||
@staticmethod
|
||||
def as_dict(item: 'CalendarAccess'):
|
||||
@ -67,7 +68,9 @@ class AccessCalendars(DetailHandler):
|
||||
try:
|
||||
if not item:
|
||||
return [AccessCalendars.as_dict(i) for i in parent.calendarAccess.all()]
|
||||
return AccessCalendars.as_dict(parent.calendarAccess.get(uuid=processUuid(item)))
|
||||
return AccessCalendars.as_dict(
|
||||
parent.calendarAccess.get(uuid=processUuid(item))
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('err: %s', item)
|
||||
raise self.invalidItemException()
|
||||
@ -87,7 +90,9 @@ class AccessCalendars(DetailHandler):
|
||||
uuid = processUuid(item) if item is not None else None
|
||||
|
||||
try:
|
||||
calendar: Calendar = Calendar.objects.get(uuid=processUuid(self._params['calendarId']))
|
||||
calendar: Calendar = Calendar.objects.get(
|
||||
uuid=processUuid(self._params['calendarId'])
|
||||
)
|
||||
access: str = self._params['access'].upper()
|
||||
if access not in (ALLOW, DENY):
|
||||
raise Exception()
|
||||
@ -103,13 +108,24 @@ class AccessCalendars(DetailHandler):
|
||||
calAccess.priority = priority
|
||||
calAccess.save()
|
||||
else:
|
||||
parent.calendarAccess.create(calendar=calendar, access=access, priority=priority)
|
||||
parent.calendarAccess.create(
|
||||
calendar=calendar, access=access, priority=priority
|
||||
)
|
||||
|
||||
log.doLog(parent, log.INFO, "Added access calendar {}/{} by {}".format(calendar.name, access, self._user.pretty_name), log.ADMIN)
|
||||
log.doLog(
|
||||
parent,
|
||||
log.INFO,
|
||||
"Added access calendar {}/{} by {}".format(
|
||||
calendar.name, access, self._user.pretty_name
|
||||
),
|
||||
log.ADMIN,
|
||||
)
|
||||
|
||||
def deleteItem(self, parent: 'ServicePool', item: str) -> None:
|
||||
calendarAccess = parent.calendarAccess.get(uuid=processUuid(self._args[0]))
|
||||
logStr = "Removed access calendar {} by {}".format(calendarAccess.calendar.name, self._user.pretty_name)
|
||||
logStr = "Removed access calendar {} by {}".format(
|
||||
calendarAccess.calendar.name, self._user.pretty_name
|
||||
)
|
||||
|
||||
calendarAccess.delete()
|
||||
|
||||
@ -120,7 +136,10 @@ class ActionsCalendars(DetailHandler):
|
||||
"""
|
||||
Processes the transports detail requests of a Service Pool
|
||||
"""
|
||||
custom_methods = ['execute',]
|
||||
|
||||
custom_methods = [
|
||||
'execute',
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def as_dict(item: 'CalendarAction') -> typing.Dict[str, typing.Any]:
|
||||
@ -131,19 +150,21 @@ class ActionsCalendars(DetailHandler):
|
||||
'calendarId': item.calendar.uuid,
|
||||
'calendar': item.calendar.name,
|
||||
'action': item.action,
|
||||
'actionDescription': action.get('description'),
|
||||
'actionDescription': action.get('description'),
|
||||
'atStart': item.at_start,
|
||||
'eventsOffset': item.events_offset,
|
||||
'params': params,
|
||||
'pretty_params': item.prettyParams,
|
||||
'nextExecution': item.next_execution,
|
||||
'lastExecution': item.last_execution
|
||||
'lastExecution': item.last_execution,
|
||||
}
|
||||
|
||||
def getItems(self, parent: 'ServicePool', item: typing.Optional[str]):
|
||||
try:
|
||||
if item is None:
|
||||
return [ActionsCalendars.as_dict(i) for i in parent.calendaraction_set.all()]
|
||||
return [
|
||||
ActionsCalendars.as_dict(i) for i in parent.calendaraction_set.all()
|
||||
]
|
||||
i = parent.calendaraction_set.get(uuid=processUuid(item))
|
||||
return ActionsCalendars.as_dict(i)
|
||||
except Exception:
|
||||
@ -177,8 +198,12 @@ class ActionsCalendars(DetailHandler):
|
||||
|
||||
# logger.debug('Got parameters: {} {} {} {} ----> {}'.format(calendar, action, eventsOffset, atStart, params))
|
||||
logStr = "Added scheduled action \"{},{},{},{},{}\" by {}".format(
|
||||
calendar.name, action, eventsOffset,
|
||||
atStart and 'Start' or 'End', params, self._user.pretty_name
|
||||
calendar.name,
|
||||
action,
|
||||
eventsOffset,
|
||||
atStart and 'Start' or 'End',
|
||||
params,
|
||||
self._user.pretty_name,
|
||||
)
|
||||
|
||||
if uuid is not None:
|
||||
@ -191,16 +216,26 @@ class ActionsCalendars(DetailHandler):
|
||||
calAction.params = params
|
||||
calAction.save()
|
||||
else:
|
||||
CalendarAction.objects.create(calendar=calendar, service_pool=parent, action=action, at_start=atStart, events_offset=eventsOffset, params=params)
|
||||
CalendarAction.objects.create(
|
||||
calendar=calendar,
|
||||
service_pool=parent,
|
||||
action=action,
|
||||
at_start=atStart,
|
||||
events_offset=eventsOffset,
|
||||
params=params,
|
||||
)
|
||||
|
||||
log.doLog(parent, log.INFO, logStr, log.ADMIN)
|
||||
|
||||
def deleteItem(self, parent: 'ServicePool', item: str) -> None:
|
||||
calendarAction = CalendarAction.objects.get(uuid=processUuid(self._args[0]))
|
||||
logStr = "Removed scheduled action \"{},{},{},{},{}\" by {}".format(
|
||||
calendarAction.calendar.name, calendarAction.action,
|
||||
calendarAction.events_offset, calendarAction.at_start and 'Start' or 'End', calendarAction.params,
|
||||
self._user.pretty_name
|
||||
calendarAction.calendar.name,
|
||||
calendarAction.action,
|
||||
calendarAction.events_offset,
|
||||
calendarAction.at_start and 'Start' or 'End',
|
||||
calendarAction.params,
|
||||
self._user.pretty_name,
|
||||
)
|
||||
|
||||
calendarAction.delete()
|
||||
@ -213,11 +248,14 @@ class ActionsCalendars(DetailHandler):
|
||||
calendarAction: CalendarAction = CalendarAction.objects.get(uuid=uuid)
|
||||
self.ensureAccess(calendarAction, permissions.PERMISSION_MANAGEMENT)
|
||||
logStr = "Launched scheduled action \"{},{},{},{},{}\" by {}".format(
|
||||
calendarAction.calendar.name, calendarAction.action,
|
||||
calendarAction.events_offset, calendarAction.at_start and 'Start' or 'End', calendarAction.params,
|
||||
self._user.pretty_name
|
||||
calendarAction.calendar.name,
|
||||
calendarAction.action,
|
||||
calendarAction.events_offset,
|
||||
calendarAction.at_start and 'Start' or 'End',
|
||||
calendarAction.params,
|
||||
self._user.pretty_name,
|
||||
)
|
||||
|
||||
|
||||
calendarAction.execute()
|
||||
|
||||
log.doLog(parent, log.INFO, logStr, log.ADMIN)
|
||||
|
@ -70,7 +70,7 @@ class OsManagers(ModelHandler):
|
||||
'type_name': type_.name(),
|
||||
'servicesTypes': type_.servicesType,
|
||||
'comments': osm.comments,
|
||||
'permission': permissions.getEffectivePermission(self._user, osm)
|
||||
'permission': permissions.getEffectivePermission(self._user, osm),
|
||||
}
|
||||
|
||||
def item_as_dict(self, item: OSManager) -> typing.Dict[str, typing.Any]:
|
||||
@ -79,7 +79,9 @@ class OsManagers(ModelHandler):
|
||||
def checkDelete(self, item: OSManager) -> None:
|
||||
# Only can delete if no ServicePools attached
|
||||
if item.deployedServices.count() > 0:
|
||||
raise RequestError(ugettext('Can\'t delete an OS Manager with services pools associated'))
|
||||
raise RequestError(
|
||||
ugettext('Can\'t delete an OS Manager with services pools associated')
|
||||
)
|
||||
|
||||
# Types related
|
||||
def enum_types(self) -> typing.Iterable[typing.Type[osmanagers.OSManager]]:
|
||||
@ -88,6 +90,9 @@ class OsManagers(ModelHandler):
|
||||
# Gui related
|
||||
def getGui(self, type_: str) -> typing.List[typing.Any]:
|
||||
try:
|
||||
return self.addDefaultFields(osmanagers.factory().lookup(type_).guiDescription(), ['name', 'comments', 'tags'])
|
||||
return self.addDefaultFields(
|
||||
osmanagers.factory().lookup(type_).guiDescription(), # type: ignore # may raise an exception if lookup fails
|
||||
['name', 'comments', 'tags'],
|
||||
)
|
||||
except:
|
||||
raise NotFound('type not found')
|
||||
|
@ -50,6 +50,7 @@ class Proxies(ModelHandler):
|
||||
"""
|
||||
Processes REST requests about proxys
|
||||
"""
|
||||
|
||||
model = Proxy
|
||||
|
||||
save_fields = ['name', 'host', 'port', 'ssl', 'check_cert', 'comments', 'tags']
|
||||
@ -74,42 +75,49 @@ class Proxies(ModelHandler):
|
||||
'port': item.port,
|
||||
'ssl': item.ssl,
|
||||
'check_cert': item.check_cert,
|
||||
'permission': permissions.getEffectivePermission(self._user, item)
|
||||
'permission': permissions.getEffectivePermission(self._user, item),
|
||||
}
|
||||
|
||||
def getGui(self, type_: str) -> typing.List[typing.Any]:
|
||||
g = self.addDefaultFields([], ['name', 'comments', 'tags'])
|
||||
|
||||
for f in [
|
||||
{
|
||||
'name': 'host',
|
||||
'value': '',
|
||||
'label': ugettext('Host'),
|
||||
'tooltip': ugettext('Server (IP or FQDN) that will serve as proxy.'),
|
||||
'type': gui.InputField.TEXT_TYPE,
|
||||
'order': 110,
|
||||
}, {
|
||||
'name': 'port',
|
||||
'value': '9090',
|
||||
'minValue': '0',
|
||||
'label': ugettext('Port'),
|
||||
'tooltip': ugettext('Port of proxy server'),
|
||||
'type': gui.InputField.NUMERIC_TYPE,
|
||||
'order': 111,
|
||||
}, {
|
||||
'name': 'ssl',
|
||||
'value': True,
|
||||
'label': ugettext('Use SSL'),
|
||||
'tooltip': ugettext('If active, the proxied connections will be done using HTTPS'),
|
||||
'type': gui.InputField.CHECKBOX_TYPE,
|
||||
}, {
|
||||
'name': 'check_cert',
|
||||
'value': True,
|
||||
'label': ugettext('Check Certificate'),
|
||||
'tooltip': ugettext('If active, any SSL certificate will be checked (will not allow self signed certificates on proxy)'),
|
||||
'type': gui.InputField.CHECKBOX_TYPE,
|
||||
},
|
||||
]:
|
||||
{
|
||||
'name': 'host',
|
||||
'value': '',
|
||||
'label': ugettext('Host'),
|
||||
'tooltip': ugettext('Server (IP or FQDN) that will serve as proxy.'),
|
||||
'type': gui.InputField.TEXT_TYPE,
|
||||
'order': 110,
|
||||
},
|
||||
{
|
||||
'name': 'port',
|
||||
'value': '9090',
|
||||
'minValue': '0',
|
||||
'label': ugettext('Port'),
|
||||
'tooltip': ugettext('Port of proxy server'),
|
||||
'type': gui.InputField.NUMERIC_TYPE,
|
||||
'order': 111,
|
||||
},
|
||||
{
|
||||
'name': 'ssl',
|
||||
'value': True,
|
||||
'label': ugettext('Use SSL'),
|
||||
'tooltip': ugettext(
|
||||
'If active, the proxied connections will be done using HTTPS'
|
||||
),
|
||||
'type': gui.InputField.CHECKBOX_TYPE,
|
||||
},
|
||||
{
|
||||
'name': 'check_cert',
|
||||
'value': True,
|
||||
'label': ugettext('Check Certificate'),
|
||||
'tooltip': ugettext(
|
||||
'If active, any SSL certificate will be checked (will not allow self signed certificates on proxy)'
|
||||
),
|
||||
'type': gui.InputField.CHECKBOX_TYPE,
|
||||
},
|
||||
]:
|
||||
self.addField(g, f)
|
||||
|
||||
return g
|
||||
|
@ -41,7 +41,17 @@ from uds import reports
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
VALID_PARAMS = ('authId', 'authSmallName', 'auth', 'username', 'realname', 'password', 'groups', 'servicePool', 'transport')
|
||||
VALID_PARAMS = (
|
||||
'authId',
|
||||
'authSmallName',
|
||||
'auth',
|
||||
'username',
|
||||
'realname',
|
||||
'password',
|
||||
'groups',
|
||||
'servicePool',
|
||||
'transport',
|
||||
)
|
||||
|
||||
|
||||
# Enclosed methods under /actor path
|
||||
@ -49,14 +59,21 @@ class Reports(model.BaseModelHandler):
|
||||
"""
|
||||
Processes actor requests
|
||||
"""
|
||||
|
||||
needs_admin = True # By default, staff is lower level needed
|
||||
|
||||
table_title = _('Available reports')
|
||||
table_fields = [
|
||||
{'group': {'title': _('Group')}},
|
||||
{'name': {'title': _('Name')}}, # Will process this field on client in fact, not sent by server
|
||||
{'description': {'title': _('Description')}}, # Will process this field on client in fact, not sent by server
|
||||
{'mime_type': {'title': _('Generates')}}, # Will process this field on client in fact, not sent by server
|
||||
{
|
||||
'name': {'title': _('Name')}
|
||||
}, # Will process this field on client in fact, not sent by server
|
||||
{
|
||||
'description': {'title': _('Description')}
|
||||
}, # Will process this field on client in fact, not sent by server
|
||||
{
|
||||
'mime_type': {'title': _('Generates')}
|
||||
}, # Will process this field on client in fact, not sent by server
|
||||
]
|
||||
# Field from where to get "class" and prefix for that class, so this will generate "row-state-A, row-state-X, ....
|
||||
table_row_style = {'field': 'state', 'prefix': 'row-state-'}
|
||||
@ -85,7 +102,9 @@ class Reports(model.BaseModelHandler):
|
||||
if self._args[0] == model.OVERVIEW:
|
||||
return list(self.getItems())
|
||||
elif self._args[0] == model.TABLEINFO:
|
||||
return self.processTableFields(self.table_title, self.table_fields, self.table_row_style)
|
||||
return self.processTableFields(
|
||||
self.table_title, self.table_fields, self.table_row_style
|
||||
)
|
||||
|
||||
if nArgs == 2:
|
||||
if self._args[0] == model.GUI:
|
||||
@ -97,7 +116,12 @@ class Reports(model.BaseModelHandler):
|
||||
"""
|
||||
Processes a PUT request
|
||||
"""
|
||||
logger.debug('method PUT for %s, %s, %s', self.__class__.__name__, self._args, self._params)
|
||||
logger.debug(
|
||||
'method PUT for %s, %s, %s',
|
||||
self.__class__.__name__,
|
||||
self._args,
|
||||
self._params,
|
||||
)
|
||||
|
||||
if len(self._args) != 1:
|
||||
raise self.invalidRequestException()
|
||||
@ -112,7 +136,7 @@ class Reports(model.BaseModelHandler):
|
||||
'mime_type': report.mime_type,
|
||||
'encoded': report.encoded,
|
||||
'filename': report.filename,
|
||||
'data': result
|
||||
'data': result,
|
||||
}
|
||||
|
||||
return data
|
||||
@ -126,7 +150,9 @@ class Reports(model.BaseModelHandler):
|
||||
return sorted(report.guiDescription(report), key=lambda f: f['gui']['order'])
|
||||
|
||||
# Returns the list of
|
||||
def getItems(self, *args, **kwargs) -> typing.Generator[typing.Dict[str, typing.Any], None, None]:
|
||||
def getItems(
|
||||
self, *args, **kwargs
|
||||
) -> typing.Generator[typing.Dict[str, typing.Any], None, None]:
|
||||
for i in reports.availableReports:
|
||||
yield {
|
||||
'id': i.getUuid(),
|
||||
@ -134,5 +160,5 @@ class Reports(model.BaseModelHandler):
|
||||
'encoded': i.encoded,
|
||||
'group': i.translated_group(),
|
||||
'name': i.translated_name(),
|
||||
'description': i.translated_description()
|
||||
'description': i.translated_description(),
|
||||
}
|
||||
|
@ -50,6 +50,7 @@ class ServicesPoolGroups(ModelHandler):
|
||||
"""
|
||||
Handles the gallery REST interface
|
||||
"""
|
||||
|
||||
# needs_admin = True
|
||||
|
||||
path = 'gallery'
|
||||
@ -59,7 +60,14 @@ class ServicesPoolGroups(ModelHandler):
|
||||
table_title = _('Services Pool Groups')
|
||||
table_fields = [
|
||||
{'priority': {'title': _('Priority'), 'type': 'numeric', 'width': '6em'}},
|
||||
{'thumb': {'title': _('Image'), 'visible': True, 'type': 'image', 'width': '96px'}},
|
||||
{
|
||||
'thumb': {
|
||||
'title': _('Image'),
|
||||
'visible': True,
|
||||
'type': 'image',
|
||||
'width': '96px',
|
||||
}
|
||||
},
|
||||
{'name': {'title': _('Name')}},
|
||||
{'comments': {'title': _('Comments')}},
|
||||
]
|
||||
@ -79,14 +87,22 @@ class ServicesPoolGroups(ModelHandler):
|
||||
def getGui(self, type_: str) -> typing.List[typing.Any]:
|
||||
localGui = self.addDefaultFields([], ['name', 'comments', 'priority'])
|
||||
|
||||
for field in [{
|
||||
for field in [
|
||||
{
|
||||
'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': 102,
|
||||
}]:
|
||||
}
|
||||
]:
|
||||
self.addField(localGui, field)
|
||||
|
||||
return localGui
|
||||
@ -100,7 +116,9 @@ class ServicesPoolGroups(ModelHandler):
|
||||
'image_id': item.image.uuid if item.image else None,
|
||||
}
|
||||
|
||||
def item_as_dict_overview(self, item: ServicePoolGroup) -> typing.Dict[str, typing.Any]:
|
||||
def item_as_dict_overview(
|
||||
self, item: ServicePoolGroup
|
||||
) -> typing.Dict[str, typing.Any]:
|
||||
return {
|
||||
'id': item.uuid,
|
||||
'priority': item.priority,
|
||||
|
@ -64,16 +64,10 @@ class ServicesUsage(DetailHandler):
|
||||
|
||||
if item.user is None:
|
||||
owner = ''
|
||||
owner_info = {
|
||||
'auth_id': '',
|
||||
'user_id': ''
|
||||
}
|
||||
owner_info = {'auth_id': '', 'user_id': ''}
|
||||
else:
|
||||
owner = item.user.pretty_name
|
||||
owner_info = {
|
||||
'auth_id': item.user.manager.uuid,
|
||||
'user_id': item.user.uuid
|
||||
}
|
||||
owner_info = {'auth_id': item.user.manager.uuid, 'user_id': item.user.uuid}
|
||||
|
||||
return {
|
||||
'id': item.uuid,
|
||||
@ -90,19 +84,30 @@ class ServicesUsage(DetailHandler):
|
||||
'ip': props.get('ip', _('unknown')),
|
||||
'source_host': item.src_hostname,
|
||||
'source_ip': item.src_ip,
|
||||
'in_use': item.in_use
|
||||
'in_use': item.in_use,
|
||||
}
|
||||
|
||||
def getItems(self, parent: 'Provider', item: typing.Optional[str]):
|
||||
try:
|
||||
if item is None:
|
||||
userServicesQuery = UserService.objects.filter(deployed_service__service__provider=parent)
|
||||
userServicesQuery = UserService.objects.filter(
|
||||
deployed_service__service__provider=parent
|
||||
)
|
||||
else:
|
||||
userServicesQuery = UserService.objects.filter(deployed_service__service_uuid=processUuid(item))
|
||||
userServicesQuery = UserService.objects.filter(
|
||||
deployed_service__service_uuid=processUuid(item)
|
||||
)
|
||||
|
||||
return [ServicesUsage.itemToDict(k) for k in userServicesQuery.filter(state=State.USABLE).order_by('creation_date').
|
||||
prefetch_related('deployed_service').prefetch_related('deployed_service__service').prefetch_related('properties').
|
||||
prefetch_related('user').prefetch_related('user__manager')]
|
||||
return [
|
||||
ServicesUsage.itemToDict(k)
|
||||
for k in userServicesQuery.filter(state=State.USABLE)
|
||||
.order_by('creation_date')
|
||||
.prefetch_related('deployed_service')
|
||||
.prefetch_related('deployed_service__service')
|
||||
.prefetch_related('properties')
|
||||
.prefetch_related('user')
|
||||
.prefetch_related('user__manager')
|
||||
]
|
||||
|
||||
except Exception:
|
||||
logger.exception('getItems')
|
||||
@ -131,7 +136,9 @@ class ServicesUsage(DetailHandler):
|
||||
def deleteItem(self, parent: 'Provider', item: str) -> None:
|
||||
userService: UserService
|
||||
try:
|
||||
userService = UserService.objects.get(uuid=processUuid(item), deployed_service__service__provider=parent)
|
||||
userService = UserService.objects.get(
|
||||
uuid=processUuid(item), deployed_service__service__provider=parent
|
||||
)
|
||||
except Exception:
|
||||
raise self.invalidItemException()
|
||||
|
||||
|
@ -59,7 +59,9 @@ USE_MAX = True
|
||||
|
||||
|
||||
def getServicesPoolsCounters(
|
||||
servicePool: typing.Optional[models.ServicePool], counter_type: int, since_days: int = SINCE
|
||||
servicePool: typing.Optional[models.ServicePool],
|
||||
counter_type: int,
|
||||
since_days: int = SINCE,
|
||||
) -> typing.List[typing.Mapping[str, typing.Any]]:
|
||||
try:
|
||||
cacheKey = (
|
||||
@ -142,14 +144,18 @@ class System(Handler):
|
||||
pool: typing.Optional[models.ServicePool] = None
|
||||
if len(self._args) == 3:
|
||||
try:
|
||||
pool = models.ServicePool.objects.get(uuid=processUuid(self._args[2]))
|
||||
pool = models.ServicePool.objects.get(
|
||||
uuid=processUuid(self._args[2])
|
||||
)
|
||||
except Exception:
|
||||
pool = None
|
||||
# If pool is None, needs admin also
|
||||
if not pool and not self._user.is_admin:
|
||||
raise AccessDenied()
|
||||
# Check permission for pool..
|
||||
if not permissions.checkPermissions(self._user, typing.cast('Model', pool), permissions.PERMISSION_READ):
|
||||
if not permissions.checkPermissions(
|
||||
self._user, typing.cast('Model', pool), permissions.PERMISSION_READ
|
||||
):
|
||||
raise AccessDenied()
|
||||
if self._args[0] == 'stats':
|
||||
if self._args[1] == 'assigned':
|
||||
@ -160,9 +166,15 @@ class System(Handler):
|
||||
return getServicesPoolsCounters(pool, counters.CT_CACHED)
|
||||
elif self._args[1] == 'complete':
|
||||
return {
|
||||
'assigned': getServicesPoolsCounters(pool, counters.CT_ASSIGNED, since_days=7),
|
||||
'inuse': getServicesPoolsCounters(pool, counters.CT_INUSE, since_days=7),
|
||||
'cached': getServicesPoolsCounters(pool, counters.CT_CACHED, since_days=7),
|
||||
'assigned': getServicesPoolsCounters(
|
||||
pool, counters.CT_ASSIGNED, since_days=7
|
||||
),
|
||||
'inuse': getServicesPoolsCounters(
|
||||
pool, counters.CT_INUSE, since_days=7
|
||||
),
|
||||
'cached': getServicesPoolsCounters(
|
||||
pool, counters.CT_CACHED, since_days=7
|
||||
),
|
||||
}
|
||||
|
||||
raise RequestError('invalid request')
|
||||
|
@ -42,6 +42,7 @@ from uds.core.util import permissions
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TunnelTokens(ModelHandler):
|
||||
model = TunnelToken
|
||||
|
||||
@ -62,7 +63,7 @@ class TunnelTokens(ModelHandler):
|
||||
'username': item.username,
|
||||
'ip': item.ip,
|
||||
'hostname': item.hostname,
|
||||
'token': item.token
|
||||
'token': item.token,
|
||||
}
|
||||
|
||||
def delete(self) -> str:
|
||||
@ -72,7 +73,9 @@ class TunnelTokens(ModelHandler):
|
||||
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:
|
||||
self.model.objects.get(token=self._args[0]).delete()
|
||||
|
@ -51,6 +51,7 @@ class AssignedService(DetailHandler):
|
||||
"""
|
||||
Rest handler for Assigned Services, wich parent is Service
|
||||
"""
|
||||
|
||||
custom_methods = [
|
||||
'reset',
|
||||
]
|
||||
@ -239,7 +240,10 @@ class CachedService(AssignedService):
|
||||
"""
|
||||
Rest handler for Cached Services, wich parent is Service
|
||||
"""
|
||||
custom_methods: typing.ClassVar[typing.List[str]] = [] # Remove custom methods from assigned services
|
||||
|
||||
custom_methods: typing.ClassVar[
|
||||
typing.List[str]
|
||||
] = [] # Remove custom methods from assigned services
|
||||
|
||||
def getItems(self, parent: models.ServicePool, item: typing.Optional[str]):
|
||||
# Extract provider
|
||||
|
@ -92,15 +92,49 @@ class Users(DetailHandler):
|
||||
# Extract authenticator
|
||||
try:
|
||||
if item is None:
|
||||
values = list(Users.uuid_to_id(parent.users.all().values('uuid', 'name', 'real_name', 'comments', 'state', 'staff_member', 'is_admin', 'last_access', 'parent')))
|
||||
values = list(
|
||||
Users.uuid_to_id(
|
||||
parent.users.all().values(
|
||||
'uuid',
|
||||
'name',
|
||||
'real_name',
|
||||
'comments',
|
||||
'state',
|
||||
'staff_member',
|
||||
'is_admin',
|
||||
'last_access',
|
||||
'parent',
|
||||
)
|
||||
)
|
||||
)
|
||||
for res in values:
|
||||
res['role'] = res['staff_member'] and (res['is_admin'] and _('Admin') or _('Staff member')) or _('User')
|
||||
res['role'] = (
|
||||
res['staff_member']
|
||||
and (res['is_admin'] and _('Admin') or _('Staff member'))
|
||||
or _('User')
|
||||
)
|
||||
return values
|
||||
else:
|
||||
u = parent.users.get(uuid=processUuid(item))
|
||||
res = model_to_dict(u, fields=('name', 'real_name', 'comments', 'state', 'staff_member', 'is_admin', 'last_access', 'parent'))
|
||||
res = model_to_dict(
|
||||
u,
|
||||
fields=(
|
||||
'name',
|
||||
'real_name',
|
||||
'comments',
|
||||
'state',
|
||||
'staff_member',
|
||||
'is_admin',
|
||||
'last_access',
|
||||
'parent',
|
||||
),
|
||||
)
|
||||
res['id'] = u.uuid
|
||||
res['role'] = res['staff_member'] and (res['is_admin'] and _('Admin') or _('Staff member')) or _('User')
|
||||
res['role'] = (
|
||||
res['staff_member']
|
||||
and (res['is_admin'] and _('Admin') or _('Staff member'))
|
||||
or _('User')
|
||||
)
|
||||
usr = aUser(u)
|
||||
res['groups'] = [g.dbGroup().uuid for g in usr.groups()]
|
||||
logger.debug('Item: %s', res)
|
||||
@ -111,17 +145,34 @@ class Users(DetailHandler):
|
||||
|
||||
def getTitle(self, parent):
|
||||
try:
|
||||
return _('Users of {0}').format(Authenticator.objects.get(uuid=processUuid(self._kwargs['parent_id'])).name)
|
||||
return _('Users of {0}').format(
|
||||
Authenticator.objects.get(
|
||||
uuid=processUuid(self._kwargs['parent_id'])
|
||||
).name
|
||||
)
|
||||
except Exception:
|
||||
return _('Current users')
|
||||
|
||||
def getFields(self, parent):
|
||||
return [
|
||||
{'name': {'title': _('Username'), 'visible': True, 'type': 'icon', 'icon': 'fa fa-user text-success'}},
|
||||
{
|
||||
'name': {
|
||||
'title': _('Username'),
|
||||
'visible': True,
|
||||
'type': 'icon',
|
||||
'icon': 'fa fa-user text-success',
|
||||
}
|
||||
},
|
||||
{'role': {'title': _('Role')}},
|
||||
{'real_name': {'title': _('Name')}},
|
||||
{'comments': {'title': _('Comments')}},
|
||||
{'state': {'title': _('state'), 'type': 'dict', 'dict': State.dictionary()}},
|
||||
{
|
||||
'state': {
|
||||
'title': _('state'),
|
||||
'type': 'dict',
|
||||
'dict': State.dictionary(),
|
||||
}
|
||||
},
|
||||
{'last_access': {'title': _('Last access'), 'type': 'datetime'}},
|
||||
]
|
||||
|
||||
@ -139,7 +190,14 @@ class Users(DetailHandler):
|
||||
|
||||
def saveItem(self, parent, item):
|
||||
logger.debug('Saving user %s / %s', parent, item)
|
||||
valid_fields = ['name', 'real_name', 'comments', 'state', 'staff_member', 'is_admin']
|
||||
valid_fields = [
|
||||
'name',
|
||||
'real_name',
|
||||
'comments',
|
||||
'state',
|
||||
'staff_member',
|
||||
'is_admin',
|
||||
]
|
||||
if 'password' in self._params:
|
||||
valid_fields.append('password')
|
||||
self._params['password'] = cryptoManager().hash(self._params['password'])
|
||||
@ -153,7 +211,9 @@ class Users(DetailHandler):
|
||||
try:
|
||||
auth = parent.getInstance()
|
||||
if item is None: # Create new
|
||||
auth.createUser(fields) # this throws an exception if there is an error (for example, this auth can't create users)
|
||||
auth.createUser(
|
||||
fields
|
||||
) # this throws an exception if there is an error (for example, this auth can't create users)
|
||||
user = parent.users.create(**fields)
|
||||
else:
|
||||
auth.modifyUser(fields) # Notifies authenticator
|
||||
@ -161,7 +221,9 @@ class Users(DetailHandler):
|
||||
user.__dict__.update(fields)
|
||||
|
||||
logger.debug('User parent: %s', user.parent)
|
||||
if auth.isExternalSource is False and (user.parent is None or user.parent == ''):
|
||||
if auth.isExternalSource is False and (
|
||||
user.parent is None or user.parent == ''
|
||||
):
|
||||
groups = self.readFieldsFromParams(['groups'])['groups']
|
||||
logger.debug('Groups: %s', groups)
|
||||
logger.debug('Got Groups %s', parent.groups.filter(uuid__in=groups))
|
||||
@ -188,7 +250,9 @@ class Users(DetailHandler):
|
||||
user = parent.users.get(uuid=processUuid(item))
|
||||
if not self._user.is_admin and (user.is_admin or user.staff_member):
|
||||
logger.warn('Removal of user {} denied due to insufficients rights')
|
||||
raise self.invalidItemException('Removal of user {} denied due to insufficients rights')
|
||||
raise self.invalidItemException(
|
||||
'Removal of user {} denied due to insufficients rights'
|
||||
)
|
||||
|
||||
assignedUserService: 'UserService'
|
||||
for assignedUserService in user.userServices.all():
|
||||
@ -216,13 +280,19 @@ class Users(DetailHandler):
|
||||
res = []
|
||||
groups = list(user.getGroups())
|
||||
for i in getPoolsForGroups(groups):
|
||||
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.isRestrained() else _('Ok'),
|
||||
})
|
||||
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.isRestrained() else _('Ok'),
|
||||
}
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
@ -261,19 +331,25 @@ class Groups(DetailHandler):
|
||||
'comments': i.comments,
|
||||
'state': i.state,
|
||||
'type': i.is_meta and 'meta' or 'group',
|
||||
'meta_if_any': i.meta_if_any
|
||||
'meta_if_any': i.meta_if_any,
|
||||
}
|
||||
if i.is_meta:
|
||||
val['groups'] = list(x.uuid for x in i.groups.all().order_by('name'))
|
||||
val['groups'] = list(
|
||||
x.uuid for x in i.groups.all().order_by('name')
|
||||
)
|
||||
res.append(val)
|
||||
if multi or not i:
|
||||
return res
|
||||
# Add pools field if 1 item only
|
||||
res = res[0]
|
||||
if i.is_meta:
|
||||
res['pools'] = [] # Meta groups do not have "assigned "pools, they get it from groups interaction
|
||||
res[
|
||||
'pools'
|
||||
] = (
|
||||
[]
|
||||
) # Meta groups do not have "assigned "pools, they get it from groups interaction
|
||||
else:
|
||||
res['pools'] = [v.uuid for v in i.deployedServices.all()]
|
||||
res['pools'] = [v.uuid for v in i.deployedServices.all()]
|
||||
return res
|
||||
except Exception:
|
||||
logger.exception('REST groups')
|
||||
@ -281,15 +357,35 @@ class Groups(DetailHandler):
|
||||
|
||||
def getTitle(self, parent):
|
||||
try:
|
||||
return _('Groups of {0}').format(Authenticator.objects.get(uuid=processUuid(self._kwargs['parent_id'])).name)
|
||||
return _('Groups of {0}').format(
|
||||
Authenticator.objects.get(
|
||||
uuid=processUuid(self._kwargs['parent_id'])
|
||||
).name
|
||||
)
|
||||
except Exception:
|
||||
return _('Current groups')
|
||||
|
||||
def getFields(self, parent):
|
||||
return [
|
||||
{'name': {'title': _('Group'), 'visible': True, 'type': 'icon_dict', 'icon_dict': {'group': 'fa fa-group text-success', 'meta': 'fa fa-gears text-info'}}},
|
||||
{
|
||||
'name': {
|
||||
'title': _('Group'),
|
||||
'visible': True,
|
||||
'type': 'icon_dict',
|
||||
'icon_dict': {
|
||||
'group': 'fa fa-group text-success',
|
||||
'meta': 'fa fa-gears text-info',
|
||||
},
|
||||
}
|
||||
},
|
||||
{'comments': {'title': _('Comments')}},
|
||||
{'state': {'title': _('state'), 'type': 'dict', 'dict': State.dictionary()}},
|
||||
{
|
||||
'state': {
|
||||
'title': _('state'),
|
||||
'type': 'dict',
|
||||
'dict': State.dictionary(),
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
def getTypes(self, parent, forType):
|
||||
@ -297,12 +393,15 @@ class Groups(DetailHandler):
|
||||
'group': {'name': _('Group'), 'description': _('UDS Group')},
|
||||
'meta': {'name': _('Meta group'), 'description': _('UDS Meta Group')},
|
||||
}
|
||||
types = [{
|
||||
'name': tDct[t]['name'],
|
||||
'type': t,
|
||||
'description': tDct[t]['description'],
|
||||
'icon': ''
|
||||
} for t in tDct]
|
||||
types = [
|
||||
{
|
||||
'name': tDct[t]['name'],
|
||||
'type': t,
|
||||
'description': tDct[t]['description'],
|
||||
'icon': '',
|
||||
}
|
||||
for t in tDct
|
||||
]
|
||||
|
||||
if forType is None:
|
||||
return types
|
||||
@ -327,7 +426,9 @@ class Groups(DetailHandler):
|
||||
auth = parent.getInstance()
|
||||
if item is None: # Create new
|
||||
if not is_meta and not is_pattern:
|
||||
auth.createGroup(fields) # this throws an exception if there is an error (for example, this auth can't create groups)
|
||||
auth.createGroup(
|
||||
fields
|
||||
) # this throws an exception if there is an error (for example, this auth can't create groups)
|
||||
toSave = {}
|
||||
for k in valid_fields:
|
||||
toSave[k] = fields[k]
|
||||
@ -383,13 +484,19 @@ class Groups(DetailHandler):
|
||||
group = parent.groups.get(uuid=processUuid(uuid))
|
||||
res = []
|
||||
for i in getPoolsForGroups((group,)):
|
||||
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.isRestrained() else _('Ok'),
|
||||
})
|
||||
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.isRestrained() else _('Ok'),
|
||||
}
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
@ -403,7 +510,7 @@ class Groups(DetailHandler):
|
||||
'name': user.name,
|
||||
'real_name': user.real_name,
|
||||
'state': user.state,
|
||||
'last_access': user.last_access
|
||||
'last_access': user.last_access,
|
||||
}
|
||||
|
||||
res = []
|
||||
|
@ -37,12 +37,10 @@ from ..handlers import Handler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UDSVersion(Handler):
|
||||
authenticated = False # Version requests are public
|
||||
name = 'version'
|
||||
|
||||
def get(self) -> typing.MutableMapping[str, typing.Any]:
|
||||
return {
|
||||
'version': VERSION,
|
||||
'build': VERSION_STAMP
|
||||
}
|
||||
return {'version': VERSION, 'build': VERSION_STAMP}
|
||||
|
@ -1066,7 +1066,11 @@ class ModelHandler(BaseModelHandler):
|
||||
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 != '']
|
||||
[
|
||||
Tag.objects.get_or_create(tag=val)[0]
|
||||
for val in tags
|
||||
if val != ''
|
||||
]
|
||||
)
|
||||
elif isinstance(
|
||||
tags, list
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright (c) 2012-2019 Virtual Cable S.L.
|
||||
# Copyright (c) 2012-2021 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
@ -12,7 +12,7 @@
|
||||
# * 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. nor the names of its contributors
|
||||
# * 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.
|
||||
#
|
||||
@ -52,6 +52,7 @@ class ContentProcessor:
|
||||
"""
|
||||
Process contents (request/response) so Handlers can manage them
|
||||
"""
|
||||
|
||||
mime_type: typing.ClassVar[str] = ''
|
||||
extensions: typing.ClassVar[typing.Iterable[str]] = []
|
||||
|
||||
@ -81,7 +82,9 @@ class ContentProcessor:
|
||||
Converts an obj to a response of specific type (json, XML, ...)
|
||||
This is done using "render" method of specific type
|
||||
"""
|
||||
return http.HttpResponse(content=self.render(obj), content_type=self.mime_type + "; charset=utf-8")
|
||||
return http.HttpResponse(
|
||||
content=self.render(obj), content_type=self.mime_type + "; charset=utf-8"
|
||||
)
|
||||
|
||||
def render(self, obj: typing.Any):
|
||||
"""
|
||||
@ -98,7 +101,7 @@ class ContentProcessor:
|
||||
return obj
|
||||
|
||||
if isinstance(obj, dict):
|
||||
return {k:ContentProcessor.procesForRender(v) for k, v in obj.items()}
|
||||
return {k: ContentProcessor.procesForRender(v) for k, v in obj.items()}
|
||||
|
||||
if isinstance(obj, (list, tuple, types.GeneratorType)):
|
||||
return [ContentProcessor.procesForRender(v) for v in obj]
|
||||
@ -117,11 +120,15 @@ class MarshallerProcessor(ContentProcessor):
|
||||
If we have a simple marshaller for processing contents
|
||||
this class will allow us to set up a new one simply setting "marshaller"
|
||||
"""
|
||||
|
||||
marshaller: typing.ClassVar[typing.Any] = None
|
||||
|
||||
def processParameters(self) -> typing.MutableMapping[str, typing.Any]:
|
||||
try:
|
||||
if self._request.META.get('CONTENT_LENGTH', '0') == '0' or not self._request.body:
|
||||
if (
|
||||
self._request.META.get('CONTENT_LENGTH', '0') == '0'
|
||||
or not self._request.body
|
||||
):
|
||||
return self.processGetParameters()
|
||||
# logger.debug('Body: >>{}<< {}'.format(self._request.body, len(self._request.body)))
|
||||
res = self.marshaller.loads(self._request.body.decode('utf8'))
|
||||
@ -143,14 +150,16 @@ class JsonProcessor(MarshallerProcessor):
|
||||
"""
|
||||
Provides JSON content processor
|
||||
"""
|
||||
|
||||
mime_type = 'application/json'
|
||||
extensions = ['json']
|
||||
marshaller = json # type: ignore
|
||||
|
||||
|
||||
# ---------------
|
||||
# XML Processor
|
||||
# ---------------
|
||||
#===============================================================================
|
||||
# ===============================================================================
|
||||
# class XMLProcessor(MarshallerProcessor):
|
||||
# """
|
||||
# Provides XML content processor
|
||||
@ -158,12 +167,14 @@ class JsonProcessor(MarshallerProcessor):
|
||||
# mime_type = 'application/xml'
|
||||
# extensions = ['xml']
|
||||
# marshaller = xml_marshaller
|
||||
#===============================================================================
|
||||
# ===============================================================================
|
||||
|
||||
|
||||
processors_list = (JsonProcessor,)
|
||||
default_processor: typing.Type[ContentProcessor] = JsonProcessor
|
||||
available_processors_mime_dict: typing.Dict[str, typing.Type[ContentProcessor]] = {cls.mime_type: cls for cls in processors_list}
|
||||
available_processors_mime_dict: typing.Dict[str, typing.Type[ContentProcessor]] = {
|
||||
cls.mime_type: cls for cls in processors_list
|
||||
}
|
||||
available_processors_ext_dict: typing.Dict[str, typing.Type[ContentProcessor]] = {}
|
||||
for cls in processors_list:
|
||||
for ext in cls.extensions:
|
||||
|
@ -51,6 +51,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
availableReports: typing.List[typing.Type['reports.Report']] = []
|
||||
|
||||
|
||||
def __init__() -> None:
|
||||
"""
|
||||
This imports all packages that are descendant of this package, and, after that,
|
||||
@ -66,7 +67,10 @@ def __init__() -> None:
|
||||
alreadyAdded.add(reportClass.uuid)
|
||||
addReportCls(reportClass)
|
||||
else:
|
||||
logger.debug('Report class %s not added because it lacks of uuid (it is probably a base class)', reportClass)
|
||||
logger.debug(
|
||||
'Report class %s not added because it lacks of uuid (it is probably a base class)',
|
||||
reportClass,
|
||||
)
|
||||
|
||||
subReport: typing.Type[reports.Report]
|
||||
for subReport in reportClass.__subclasses__():
|
||||
|
@ -122,7 +122,7 @@ class ReportAuto(Report, metaclass=ReportAutoType):
|
||||
# Fills datasource
|
||||
fields.source_field_data(self.getModel(), self.data_source, self.source)
|
||||
|
||||
def getModelItems(self) -> typing.Iterable[ReportAutoModel]: # type: ignore
|
||||
def getModelItems(self) -> typing.Iterable[ReportAutoModel]:
|
||||
model = self.getModel()
|
||||
|
||||
filters = (
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright (c) 2020 Virtual Cable S.L.
|
||||
# Copyright (c) 2020-2021 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
@ -12,7 +12,7 @@
|
||||
# * 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. nor the names of its contributors
|
||||
# * 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.
|
||||
#
|
||||
@ -41,22 +41,24 @@ from uds import models
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def start_date_field(order:int) -> gui.DateField:
|
||||
|
||||
def start_date_field(order: int) -> gui.DateField:
|
||||
return gui.DateField(
|
||||
order=order,
|
||||
label=_('Starting date'),
|
||||
tooltip=_('Starting date for report'),
|
||||
defvalue=datetime.date.min,
|
||||
required=True
|
||||
required=True,
|
||||
)
|
||||
|
||||
|
||||
def single_date_field(order: int) -> gui.DateField:
|
||||
return gui.DateField(
|
||||
return gui.DateField(
|
||||
order=order,
|
||||
label=_('Date'),
|
||||
tooltip=_('Date for report'),
|
||||
defvalue=datetime.date.today(),
|
||||
required=True
|
||||
required=True,
|
||||
)
|
||||
|
||||
|
||||
@ -66,9 +68,10 @@ def end_date_field(order: int) -> gui.DateField:
|
||||
label=_('Ending date'),
|
||||
tooltip=_('ending date for report'),
|
||||
defvalue=datetime.date.max,
|
||||
required=True
|
||||
required=True,
|
||||
)
|
||||
|
||||
|
||||
def intervals_field(order: int) -> gui.ChoiceField:
|
||||
return gui.ChoiceField(
|
||||
label=_('Report data interval'),
|
||||
@ -77,14 +80,17 @@ def intervals_field(order: int) -> gui.ChoiceField:
|
||||
'hour': _('Hourly'),
|
||||
'day': _('Daily'),
|
||||
'week': _('Weekly'),
|
||||
'month': _('Monthly')
|
||||
'month': _('Monthly'),
|
||||
},
|
||||
tooltip=_('Interval for report data'),
|
||||
required=True,
|
||||
defvalue='day'
|
||||
defvalue='day',
|
||||
)
|
||||
|
||||
def source_field(order: int, data_source: str, multiple: bool) -> typing.Union[gui.ChoiceField, gui.MultiChoiceField, None]:
|
||||
|
||||
def source_field(
|
||||
order: int, data_source: str, multiple: bool
|
||||
) -> typing.Union[gui.ChoiceField, gui.MultiChoiceField, None]:
|
||||
if not data_source:
|
||||
return None
|
||||
|
||||
@ -102,16 +108,18 @@ def source_field(order: int, data_source: str, multiple: bool) -> typing.Union[g
|
||||
|
||||
logger.debug('Labels: %s, %s', labels, fieldType)
|
||||
|
||||
return fieldType(
|
||||
label=labels[0],
|
||||
order=order,
|
||||
tooltip=labels[1],
|
||||
required=True
|
||||
)
|
||||
return fieldType(label=labels[0], order=order, tooltip=labels[1], required=True)
|
||||
|
||||
def source_field_data(model: typing.Any, data_source: str, field: typing.Union[gui.ChoiceField, gui.MultiChoiceField]) -> None:
|
||||
|
||||
dataList: typing.List[gui.ChoiceType] = [{'id': x.uuid, 'text': x.name} for x in model.objects.all().order_by('name')]
|
||||
def source_field_data(
|
||||
model: typing.Any,
|
||||
data_source: str,
|
||||
field: typing.Union[gui.ChoiceField, gui.MultiChoiceField],
|
||||
) -> None:
|
||||
|
||||
dataList: typing.List[gui.ChoiceType] = [
|
||||
{'id': x.uuid, 'text': x.name} for x in model.objects.all().order_by('name')
|
||||
]
|
||||
|
||||
if isinstance(field, gui.MultiChoiceField):
|
||||
dataList.insert(0, {'id': '0-0-0-0', 'text': _('All')})
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright (c) 2015-2019 Virtual Cable S.L.
|
||||
# Copyright (c) 2015-2021 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
@ -12,7 +12,7 @@
|
||||
# * 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. nor the names of its contributors
|
||||
# * 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.
|
||||
#
|
||||
@ -35,9 +35,9 @@ import typing
|
||||
from django.utils.translation import ugettext_noop as _
|
||||
from uds.core import reports
|
||||
|
||||
|
||||
class ListReport(reports.Report):
|
||||
group = _('Lists') # So we can make submenus with reports
|
||||
|
||||
def generate(self) -> bytes:
|
||||
raise NotImplementedError('ListReport generate invoked and not implemented')
|
||||
|
||||
|
@ -61,7 +61,7 @@ class ListReportUsers(ListReport):
|
||||
label=_("Authenticator"),
|
||||
order=1,
|
||||
tooltip=_('Authenticator from where to list users'),
|
||||
required=True
|
||||
required=True,
|
||||
)
|
||||
|
||||
name = _('Users list') # Report name
|
||||
@ -70,9 +70,7 @@ class ListReportUsers(ListReport):
|
||||
|
||||
def initGui(self) -> None:
|
||||
logger.debug('Initializing gui')
|
||||
vals = [
|
||||
gui.choiceItem(v.uuid, v.name) for v in Authenticator.objects.all()
|
||||
]
|
||||
vals = [gui.choiceItem(v.uuid, v.name) for v in Authenticator.objects.all()]
|
||||
|
||||
self.authenticator.setValues(vals)
|
||||
|
||||
@ -87,7 +85,7 @@ class ListReportUsers(ListReport):
|
||||
'auth': auth.name,
|
||||
},
|
||||
header=ugettext('Users List for {}').format(auth.name),
|
||||
water=ugettext('UDS Report of users in {}').format(auth.name)
|
||||
water=ugettext('UDS Report of users in {}').format(auth.name),
|
||||
)
|
||||
|
||||
|
||||
@ -111,7 +109,9 @@ class ListReportsUsersCSV(ListReportUsers):
|
||||
auth = Authenticator.objects.get(uuid=self.authenticator.value)
|
||||
users = auth.users.order_by('name')
|
||||
|
||||
writer.writerow([ugettext('User ID'), ugettext('Real Name'), ugettext('Last access')])
|
||||
writer.writerow(
|
||||
[ugettext('User ID'), ugettext('Real Name'), ugettext('Last access')]
|
||||
)
|
||||
|
||||
for v in users:
|
||||
writer.writerow([v.name, v.real_name, v.last_access])
|
||||
@ -120,6 +120,7 @@ class ListReportsUsersCSV(ListReportUsers):
|
||||
|
||||
return output.getvalue().encode()
|
||||
|
||||
|
||||
# Sample XLSX report
|
||||
# Just for sampling purposses, not used...
|
||||
# class ListReportsUsersXlsx(ListReportUsers):
|
||||
|
@ -29,9 +29,6 @@
|
||||
"""
|
||||
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
"""
|
||||
import io
|
||||
import csv
|
||||
import datetime
|
||||
import logging
|
||||
import typing
|
||||
|
||||
@ -42,10 +39,15 @@ from uds.core.util.stats import counters
|
||||
|
||||
from .base import StatsReportAuto
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from uds import models
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MAX_ELEMENTS = 10000
|
||||
|
||||
|
||||
class AuthenticatorsStats(StatsReportAuto):
|
||||
dates = 'range'
|
||||
intervals = True
|
||||
@ -54,7 +56,9 @@ class AuthenticatorsStats(StatsReportAuto):
|
||||
|
||||
filename = 'auths_stats.pdf'
|
||||
name = _('Statistics by authenticator') # Report name
|
||||
description = _('Generates a report with the statistics of an authenticator for a desired period') # Report description
|
||||
description = _(
|
||||
'Generates a report with the statistics of an authenticator for a desired period'
|
||||
) # Report description
|
||||
uuid = 'a5a43bc0-d543-11ea-af8f-af01fa65994e'
|
||||
|
||||
def generate(self) -> typing.Any:
|
||||
@ -69,9 +73,34 @@ class AuthenticatorsStats(StatsReportAuto):
|
||||
|
||||
services = 0
|
||||
userServices = 0
|
||||
servicesCounterIter = iter(counters.getCounters(a, counters.CT_AUTH_SERVICES, since=since, interval=interval, limit=MAX_ELEMENTS, use_max=True))
|
||||
usersWithServicesCounterIter = iter(counters.getCounters(a, counters.CT_AUTH_USERS_WITH_SERVICES, since=since, interval=interval, limit=MAX_ELEMENTS, use_max=True))
|
||||
for userCounter in counters.getCounters(a, counters.CT_AUTH_USERS, since=since, interval=interval, limit=MAX_ELEMENTS, use_max=True):
|
||||
servicesCounterIter = iter(
|
||||
counters.getCounters(
|
||||
typing.cast('models.Authenticator', a),
|
||||
counters.CT_AUTH_SERVICES,
|
||||
since=since,
|
||||
interval=interval,
|
||||
limit=MAX_ELEMENTS,
|
||||
use_max=True,
|
||||
)
|
||||
)
|
||||
usersWithServicesCounterIter = iter(
|
||||
counters.getCounters(
|
||||
typing.cast('models.Authenticator', a),
|
||||
counters.CT_AUTH_USERS_WITH_SERVICES,
|
||||
since=since,
|
||||
interval=interval,
|
||||
limit=MAX_ELEMENTS,
|
||||
use_max=True,
|
||||
)
|
||||
)
|
||||
for userCounter in counters.getCounters(
|
||||
typing.cast('models.Authenticator', a),
|
||||
counters.CT_AUTH_USERS,
|
||||
since=since,
|
||||
interval=interval,
|
||||
limit=MAX_ELEMENTS,
|
||||
use_max=True,
|
||||
):
|
||||
try:
|
||||
while True:
|
||||
servicesCounter = next(servicesCounterIter)
|
||||
@ -92,18 +121,18 @@ class AuthenticatorsStats(StatsReportAuto):
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
stats.append({
|
||||
'date': userCounter[0],
|
||||
'users': userCounter[1] or 0,
|
||||
'services': services,
|
||||
'user_services': userServices
|
||||
})
|
||||
stats.append(
|
||||
{
|
||||
'date': userCounter[0],
|
||||
'users': userCounter[1] or 0,
|
||||
'services': services,
|
||||
'user_services': userServices,
|
||||
}
|
||||
)
|
||||
logger.debug('Report Data Done')
|
||||
return self.templateAsPDF(
|
||||
'uds/reports/stats/authenticator_stats.html',
|
||||
dct={
|
||||
'data': stats
|
||||
},
|
||||
dct={'data': stats},
|
||||
header=ugettext('Users usage list'),
|
||||
water=ugettext('UDS Report of users usage')
|
||||
water=ugettext('UDS Report of users usage'),
|
||||
)
|
||||
|
@ -36,6 +36,7 @@ from django.utils.translation import ugettext_noop as _
|
||||
from uds.core import reports
|
||||
from ..auto import ReportAuto
|
||||
|
||||
|
||||
class StatsReport(reports.Report):
|
||||
group = _('Statistics') # So we can make submenus with reports
|
||||
|
||||
|
@ -50,15 +50,14 @@ logger = logging.getLogger(__name__)
|
||||
class UsageSummaryByUsersPool(StatsReport):
|
||||
filename = 'pool_user_usage.pdf'
|
||||
name = _('Pool Usage by users') # Report name
|
||||
description = _('Generates a report with the summary of users usage for a pool') # Report description
|
||||
description = _(
|
||||
'Generates a report with the summary of users usage for a pool'
|
||||
) # Report description
|
||||
uuid = '202c6438-30a8-11e7-80e4-77c1e4cb9e09'
|
||||
|
||||
# Input fields
|
||||
pool = gui.ChoiceField(
|
||||
order=1,
|
||||
label=_('Pool'),
|
||||
tooltip=_('Pool for report'),
|
||||
required=True
|
||||
order=1, label=_('Pool'), tooltip=_('Pool for report'), required=True
|
||||
)
|
||||
|
||||
startDate = gui.DateField(
|
||||
@ -66,7 +65,7 @@ class UsageSummaryByUsersPool(StatsReport):
|
||||
label=_('Starting date'),
|
||||
tooltip=_('starting date for report'),
|
||||
defvalue=datetime.date.min,
|
||||
required=True
|
||||
required=True,
|
||||
)
|
||||
|
||||
endDate = gui.DateField(
|
||||
@ -74,22 +73,32 @@ class UsageSummaryByUsersPool(StatsReport):
|
||||
label=_('Finish date'),
|
||||
tooltip=_('finish date for report'),
|
||||
defvalue=datetime.date.max,
|
||||
required=True
|
||||
required=True,
|
||||
)
|
||||
|
||||
def initGui(self) -> None:
|
||||
logger.debug('Initializing gui')
|
||||
vals = [
|
||||
gui.choiceItem(v.uuid, v.name) for v in ServicePool.objects.all()
|
||||
]
|
||||
vals = [gui.choiceItem(v.uuid, v.name) for v in ServicePool.objects.all()]
|
||||
self.pool.setValues(vals)
|
||||
|
||||
def getPoolData(self, pool) -> typing.Tuple[typing.List[typing.Dict[str, typing.Any]], str]:
|
||||
def getPoolData(
|
||||
self, pool
|
||||
) -> typing.Tuple[typing.List[typing.Dict[str, typing.Any]], str]:
|
||||
start = self.startDate.stamp()
|
||||
end = self.endDate.stamp()
|
||||
logger.debug(self.pool.value)
|
||||
|
||||
items = StatsManager.manager().getEvents(events.OT_DEPLOYED, (events.ET_LOGIN, events.ET_LOGOUT), owner_id=pool.id, since=start, to=end).order_by('stamp')
|
||||
items = (
|
||||
StatsManager.manager()
|
||||
.getEvents(
|
||||
events.OT_DEPLOYED,
|
||||
(events.ET_LOGIN, events.ET_LOGOUT),
|
||||
owner_id=pool.id,
|
||||
since=start,
|
||||
to=end,
|
||||
)
|
||||
.order_by('stamp')
|
||||
)
|
||||
|
||||
logins: typing.Dict[str, int] = {}
|
||||
users: typing.Dict[str, typing.Dict] = {}
|
||||
@ -115,12 +124,15 @@ class UsageSummaryByUsersPool(StatsReport):
|
||||
# })
|
||||
|
||||
# Extract different number of users
|
||||
data = [{
|
||||
'user': k,
|
||||
'sessions': v['sessions'],
|
||||
'hours': '{:.2f}'.format(float(v['time']) / 3600),
|
||||
'average': '{:.2f}'.format(float(v['time']) / 3600 / v['sessions'])
|
||||
} for k, v in users.items()]
|
||||
data = [
|
||||
{
|
||||
'user': k,
|
||||
'sessions': v['sessions'],
|
||||
'hours': '{:.2f}'.format(float(v['time']) / 3600),
|
||||
'average': '{:.2f}'.format(float(v['time']) / 3600 / v['sessions']),
|
||||
}
|
||||
for k, v in users.items()
|
||||
]
|
||||
|
||||
return data, pool.name
|
||||
|
||||
@ -139,7 +151,7 @@ class UsageSummaryByUsersPool(StatsReport):
|
||||
'ending': self.endDate.date(),
|
||||
},
|
||||
header=ugettext('Users usage list for {}').format(poolName),
|
||||
water=ugettext('UDS Report of users in {}').format(poolName)
|
||||
water=ugettext('UDS Report of users in {}').format(poolName),
|
||||
)
|
||||
|
||||
|
||||
@ -160,7 +172,14 @@ class UsageSummaryByUsersPoolCSV(UsageSummaryByUsersPool):
|
||||
|
||||
reportData = self.getData()[0]
|
||||
|
||||
writer.writerow([ugettext('User'), ugettext('Sessions'), ugettext('Hours'), ugettext('Average')])
|
||||
writer.writerow(
|
||||
[
|
||||
ugettext('User'),
|
||||
ugettext('Sessions'),
|
||||
ugettext('Hours'),
|
||||
ugettext('Average'),
|
||||
]
|
||||
)
|
||||
|
||||
for v in reportData:
|
||||
writer.writerow([v['user'], v['sessions'], v['hours'], v['average']])
|
||||
|
Loading…
x
Reference in New Issue
Block a user