More formating & minor typing fixes

This commit is contained in:
Adolfo Gómez García 2021-08-13 15:11:22 +02:00
parent 03bfb3efbb
commit 8285e2daad
35 changed files with 951 additions and 358 deletions

View File

@ -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()

View File

@ -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)

View File

@ -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]:

View File

@ -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

View File

@ -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()

View File

@ -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')

View File

@ -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]:

View File

@ -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))

View File

@ -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

View File

@ -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'])

View File

@ -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,
}

View File

@ -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):

View File

@ -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

View File

@ -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),
}

View File

@ -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)

View File

@ -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')

View File

@ -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

View File

@ -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(),
}

View File

@ -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,

View File

@ -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()

View File

@ -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')

View File

@ -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()

View File

@ -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

View File

@ -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 = []

View File

@ -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}

View File

@ -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

View File

@ -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:

View File

@ -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__():

View File

@ -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 = (

View File

@ -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')})

View File

@ -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')

View File

@ -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):

View File

@ -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'),
)

View File

@ -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

View File

@ -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']])