* Fixed comms to not "annoy" with warnings

* Added generateUuid to allow pass in an object
* renamed "is_meta" to more convenient "is_owned_by_meta
* Fixed 2.x actor rest api small bug (not used that part right now anyway, but for future)
This commit is contained in:
Adolfo Gómez García 2020-11-12 11:48:42 +01:00
parent 637519a162
commit 52f524eb61
4 changed files with 130 additions and 58 deletions

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2019 Virtual Cable S.L.
# Copyright (c) 2014-2020 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -49,17 +49,19 @@ from uds.models import UserService
logger = logging.getLogger(__name__)
# Actor key, configurable in Security Section of administration interface
actorKey = config.Config.section(config.SECURITY_SECTION).value('Master Key',
cryptoManager().uuid(datetime.datetime.now()).replace('-', ''),
type=config.Config.TEXT_FIELD)
actorKey = config.Config.section(config.SECURITY_SECTION).value(
'Master Key',
cryptoManager().uuid(datetime.datetime.now()).replace('-', ''),
type=config.Config.TEXT_FIELD,
)
actorKey.get()
# Error codes:
ERR_INVALID_KEY = 1
ERR_HOST_NOT_MANAGED = 2
ERR_USER_SERVICE_NOT_FOUND = 3
ERR_OSMANAGER_ERROR = 4
ERR_INVALID_KEY = '1'
ERR_HOST_NOT_MANAGED = '2'
ERR_USER_SERVICE_NOT_FOUND = '3'
ERR_OSMANAGER_ERROR = '4'
# Constants for tickets
OWNER = 'ACTOR'
@ -71,6 +73,7 @@ class Actor(Handler):
"""
Processes actor requests
"""
authenticated = False # Actor requests are not authenticated
@staticmethod
@ -104,7 +107,7 @@ class Actor(Handler):
return Actor.result(_('Invalid key'), error=ERR_INVALID_KEY)
return None
def getUserServiceByIds(self) -> UserService:
def getUserServiceByIds(self) -> typing.Optional[UserService]:
"""
This will get the client from the IDs passed from parameters
"""
@ -115,7 +118,9 @@ class Actor(Handler):
except Exception:
raise RequestError('Invalid request: (no id found)')
services = UserService.objects.filter(unique_id__in=clientIds, state__in=[State.USABLE, State.PREPARING])
services = UserService.objects.filter(
unique_id__in=clientIds, state__in=[State.USABLE, State.PREPARING]
)
return services[0] if services else None
@ -176,11 +181,7 @@ class Actor(Handler):
maxIdle = service.deployed_service.osmanager.getInstance().maxIdle()
logger.debug('Max idle: %s', maxIdle)
return Actor.result(
(
service.uuid,
service.unique_id,
0 if maxIdle is None else maxIdle
)
(service.uuid, service.unique_id, 0 if maxIdle is None else maxIdle)
)
raise RequestError('Invalid request')
@ -202,7 +203,9 @@ class Actor(Handler):
try:
service: UserService = UserService.objects.get(uuid=processUuid(uuid))
except Exception:
return Actor.result(_('User service not found'), error=ERR_USER_SERVICE_NOT_FOUND)
return Actor.result(
_('User service not found'), error=ERR_USER_SERVICE_NOT_FOUND
)
if message == 'notifyComms':
logger.debug('Setting comms url to %s', data)
@ -210,7 +213,7 @@ class Actor(Handler):
return Actor.result('ok')
if message == 'ssoAvailable':
logger.debug('Setting that SSO is available')
service.setProperty('sso_available', 1)
service.setProperty('sso_available', '1')
return Actor.result('ok')
if message == 'version':
version = self._params.get('version', 'unknown')
@ -220,7 +223,9 @@ class Actor(Handler):
# "Cook" some messages, common to all clients, such as "log"
if message == 'log':
logger.debug(self._params)
data = '\t'.join((self._params.get('message'), str(self._params.get('level', 10000))))
data = '\t'.join(
(self._params.get('message'), str(self._params.get('level', 10000)))
)
osmanager = service.getInstance().osmanager()
@ -234,7 +239,11 @@ class Actor(Handler):
# Mark for removal...
service.release() # Release for removal
return 'ok'
raise Exception('Unknown message {} for an user service without os manager'.format(message))
raise Exception(
'Unknown message {} for an user service without os manager'.format(
message
)
)
res = osmanager.process(service, message, data, options={'scramble': False})
if not res:
raise Exception('Old Actors not supported by this os Manager!')

View File

@ -1,3 +1,33 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2019-2020 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
import os
import json
import base64
@ -14,18 +44,21 @@ logger = logging.getLogger(__name__)
TIMEOUT = 2
class NoActorComms(Exception):
pass
class OldActorVersion(NoActorComms):
pass
def _requestActor(
userService: 'UserService',
method: str,
data: typing.Optional[typing.MutableMapping[str, typing.Any]] = None,
minVersion: typing.Optional[str] = None
) -> typing.Any:
userService: 'UserService',
method: str,
data: typing.Optional[typing.MutableMapping[str, typing.Any]] = None,
minVersion: typing.Optional[str] = None,
) -> typing.Any:
"""
Makes a request to actor using "method"
if data is None, request is done using GET, else POST
@ -34,14 +67,20 @@ def _requestActor(
"""
url = userService.getCommsUrl()
if not url:
logger.warning('No notification is made because agent does not supports notifications: %s', userService.friendly_name)
raise NoActorComms('No notification urls for {}'.format(userService.friendly_name))
# logger.warning('No notification is made because agent does not supports notifications: %s', userService.friendly_name)
raise NoActorComms(
'No notification urls for {}'.format(userService.friendly_name)
)
minVersion = minVersion or '2.0.0'
version = userService.getProperty('actor_version') or '0.0.0'
if '-' in version or version < minVersion:
logger.warning('Pool %s has old actors (%s)', userService.deployed_service.name, version)
raise OldActorVersion('Old actor version {} for {}'.format(version, userService.friendly_name))
logger.warning(
'Pool %s has old actors (%s)', userService.deployed_service.name, version
)
raise OldActorVersion(
'Old actor version {} for {}'.format(version, userService.friendly_name)
)
url += '/' + method
@ -68,7 +107,7 @@ def _requestActor(
data=json.dumps(data),
headers={'content-type': 'application/json'},
verify=verify,
timeout=TIMEOUT
timeout=TIMEOUT,
)
if verify:
try:
@ -81,34 +120,53 @@ def _requestActor(
js = js['result']
logger.debug('Requested %s to actor. Url=%s', method, url)
except Exception as e:
logger.warning('Request %s failed: %s. Check connection on destination machine: %s', method, e, url)
logger.warning(
'Request %s failed: %s. Check connection on destination machine: %s',
method,
e,
url,
)
js = None
return js
def notifyPreconnect(userService: 'UserService', userName: str, protocol: str) -> None:
'''
"""
Notifies a preconnect to an user service
'''
"""
ip, hostname = userService.getConnectionSource()
try:
_requestActor(userService, 'preConnect', {'user': userName, 'protocol': protocol, 'ip': ip, 'hostname': hostname})
_requestActor(
userService,
'preConnect',
{'user': userName, 'protocol': protocol, 'ip': ip, 'hostname': hostname},
)
except NoActorComms:
pass # If no preconnect, warning will appear on UDS log
def checkUuid(userService: 'UserService') -> bool:
'''
def checkUuid(userService: 'UserService') -> bool:
"""
Checks if the uuid of the service is the same of our known uuid on DB
'''
"""
try:
uuid = _requestActor(userService, 'uuid')
if uuid and uuid != userService.uuid: # Empty UUID means "no check this, fixed pool machine"
logger.info('Machine %s do not have expected uuid %s, instead has %s', userService.friendly_name, userService.uuid, uuid)
if (
uuid and uuid != userService.uuid
): # Empty UUID means "no check this, fixed pool machine"
logger.info(
'Machine %s do not have expected uuid %s, instead has %s',
userService.friendly_name,
userService.uuid,
uuid,
)
return False
except NoActorComms:
pass
return True # Actor does not supports checking
return True # Actor does not supports checking
def requestScreenshot(userService: 'UserService') -> bytes:
"""
@ -116,24 +174,28 @@ def requestScreenshot(userService: 'UserService') -> bytes:
"""
emptyPng = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='
try:
png = _requestActor(userService, 'screenshot', minVersion='3.0.0') # First valid version with screenshot is 3.0
png = _requestActor(
userService, 'screenshot', minVersion='3.0.0'
) # First valid version with screenshot is 3.0
except NoActorComms:
png = None
return base64.b64decode(png or emptyPng)
def sendScript(userService: 'UserService', script: str, forUser: bool = False) -> None:
"""
If allowed, send script to user service
"""
try:
data: typing.MutableMapping[str, typing.Any] = {'script': script }
data: typing.MutableMapping[str, typing.Any] = {'script': script}
if forUser:
data['user'] = forUser
_requestActor(userService, 'script', data=data)
except NoActorComms:
pass
def requestLogoff(userService: 'UserService') -> None:
"""
Ask client to logoff user
@ -143,11 +205,12 @@ def requestLogoff(userService: 'UserService') -> None:
except NoActorComms:
pass
def sendMessage(userService: 'UserService', message: str) -> None:
"""
Sends an screen message to client
"""
try:
_requestActor(userService, 'message', data={'message':message})
_requestActor(userService, 'message', data={'message': message})
except NoActorComms:
pass

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2019 Virtual Cable S.L.
# Copyright (c) 2014-2020 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -31,14 +31,14 @@ import typing
from uds.core.managers import cryptoManager
def generateUuid() -> str:
def generateUuid(obj: typing.Any = None) -> str:
"""
Generates a ramdom uuid for models default
"""
return cryptoManager().uuid().lower()
return cryptoManager().uuid(obj=obj).lower()
def processUuid(uuid: typing.Union[str, bytes]) -> str:
def processUuid(uuid: str) -> str:
if isinstance(uuid, bytes):
uuid = uuid.decode('utf8')
uuid = uuid.decode('utf8') # type: ignore
return uuid.lower()

View File

@ -93,18 +93,17 @@ def getServicesData(request: 'ExtendedHttpRequest') -> typing.Dict[str, typing.A
logger.debug('Checking meta pools: %s', availMetaPools)
services = []
meta: MetaPool
# Preload all assigned user services for this user
# Add meta pools data first
for meta in availMetaPools:
# Check that we have access to at least one transport on some of its children
hasUsablePools = False
in_use = meta.number_in_use > 0 # False
for pool in meta.pools.all():
in_use = typing.cast(typing.Any, meta).number_in_use > 0 # Override, because we used hear annotations
for member in meta.members.all():
# if pool.isInMaintenance():
# continue
for t in pool.transports.all():
for t in member.pool.transports.all():
typeTrans = t.getType()
if t.getType() and t.validForIp(request.ip) and typeTrans.supportsOs(os['OS']) and t.validForOs(os['OS']):
hasUsablePools = True
@ -148,13 +147,12 @@ def getServicesData(request: 'ExtendedHttpRequest') -> typing.Dict[str, typing.A
})
# Now generic user service
svr: ServicePool
for svr in availServicePools:
# Skip pools that are part of meta pools
if svr.is_meta:
if svr.owned_by_meta:
continue
use = str(svr.usage(svr.usage_count)) + '%'
use = str(svr.usage(typing.cast(typing.Any, svr).usage_count)) + '%'
trans = []
for t in sorted(svr.transports.all(), key=lambda x: x.priority): # In memory sort, allows reuse prefetched and not too big array
@ -186,7 +184,7 @@ def getServicesData(request: 'ExtendedHttpRequest') -> typing.Dict[str, typing.A
imageId = 'x'
# Locate if user service has any already assigned user service for this. Use "pre cached" number of assignations in this pool to optimize
in_use = svr.number_in_use > 0
in_use = typing.cast(typing.Any, svr).number_in_use > 0
# if svr.number_in_use: # Anotated value got from getDeployedServicesForGroups(...). If 0, no assignation for this user
# ads = userServiceManager().getExistingAssignationForUser(svr, request.user)
# if ads:
@ -195,15 +193,17 @@ def getServicesData(request: 'ExtendedHttpRequest') -> typing.Dict[str, typing.A
group = svr.servicesPoolGroup.as_dict if svr.servicesPoolGroup else ServicePoolGroup.default().as_dict
# Only add toBeReplaced info in case we allow it. This will generate some "overload" on the services
toBeReplaced = svr.toBeReplaced(request.user) if svr.pubs_active > 0 and GlobalConfig.NOTIFY_REMOVAL_BY_PUB.getBool(False) else None
toBeReplaced = svr.toBeReplaced(request.user) if typing.cast(
typing.Any, svr).pubs_active > 0 and GlobalConfig.NOTIFY_REMOVAL_BY_PUB.getBool(False) else None
# tbr = False
if toBeReplaced:
toBeReplaced = formats.date_format(toBeReplaced, "SHORT_DATETIME_FORMAT")
toBeReplacedTxt = ugettext('This service is about to be replaced by a new version. Please, close the session before {} and save all your work to avoid loosing it.').format(toBeReplaced)
toBeReplacedTxt = ugettext(
'This service is about to be replaced by a new version. Please, close the session before {} and save all your work to avoid loosing it.').format(toBeReplaced)
else:
toBeReplacedTxt = ''
datator = lambda x: x.replace('{use}', use).replace('{total}', str(svr.max_srvs))
def datator(x): return x.replace('{use}', use).replace('{total}', str(svr.max_srvs))
services.append({
'id': 'F' + svr.uuid,