1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-10-07 15:33:51 +03:00

49 Commits

Author SHA1 Message Date
aschumann-virtualcable
398ec1aac0 Merge remote-tracking branch 'origin/v4.0' into dev/andres/v4.0 2025-09-25 12:13:11 +02:00
Adolfo Gómez García
037b4abad1 Add enumerate_servers method to filter out servers in maintenance mode 2025-09-23 19:26:52 +02:00
Adolfo Gómez García
839e4c6b1d Fix access check in ServersServers class to use parent object instead of item 2025-09-23 19:10:49 +02:00
aschumann-virtualcable
2fd157e463 Merge remote-tracking branch 'origin/v4.0' into dev/andres/v4.0 2025-09-22 17:36:09 +02:00
Adolfo Gómez García
7e51c1fd93 Handle InvalidServiceException in enable_service function and log a warning 2025-09-22 16:53:45 +02:00
Adolfo Gómez García
b6af59cc44 Refactor MAC address handling in IPMachinesUserService to use constant for unknown MACs 2025-09-22 16:27:57 +02:00
Adolfo Gómez García
50072e948e Fix token assignment in IPMachinesService to use the correct attribute 2025-09-22 16:22:25 +02:00
Adolfo Gómez García
c5e0d0721f Add max_users attribute to ServerStatsWeights for load calculation 2025-09-19 16:03:16 +02:00
aschumann-virtualcable
afbd4c5355 Fixes incorrect parameter usage for macOS RDP connections
Updates logic to use the correct macOS-specific custom parameters
instead of Linux parameters when generating RDP connection settings.
Adds type ignore comments to improve compatibility with type checkers
and prevent related runtime issues.
2025-09-19 13:54:36 +02:00
aschumann-virtualcable
e4377b83e4 Corrects Mac RDP file usage and field mapping
Aligns Mac-specific RDP file logic to use the appropriate configuration and updates legacy field naming for better clarity and migration. Ensures Mac connections consistently respect intended custom parameter and file options, reducing potential confusion with Linux settings.
2025-09-18 13:59:48 +02:00
Adolfo Gómez García
bf97c6f2dc Reduce default max items to 200 and enhance service pool export logic to include additional user services, while adding server group serialization to the tree command output. 2025-09-17 17:57:41 +02:00
aschumann-virtualcable
6763de2bab Merge remote-tracking branch 'origin/v4.0' into dev/andres/v4.0 2025-09-17 12:35:22 +02:00
Adolfo Gómez García
b4ca743d7c Refactor server management to use atomic transactions for server updates and improve code formatting in service pool model
Fixed is_usable on ServicePool to include locked as a velid is_usable state
2025-09-16 17:45:35 +02:00
Adolfo Gómez García
20f7ae7fcd Update script and preload links in admin index.html with new integrity and timestamp values 2025-09-16 17:34:38 +02:00
Adolfo Gómez García
8aac4f9aa5 Improve error handling in UserServiceManager for non-active elements and task errors 2025-09-16 16:00:37 +02:00
aschumann-virtualcable
f494c706fc Updates RDP signature files for macOS with new parameters 2025-09-15 12:34:00 +02:00
aschumann-virtualcable
76b488dc1d Extends RDP custom parameter support for macOS clients
Unifies logic for applying custom RDP parameters to macOS alongside Windows and Linux, improving compatibility and flexibility for connecting from Apple platforms.

Refactors script handling to better support Thincast and MSRDC clients on macOS, allowing password injection into RDP files and debugging RDP file content. Adds consistent type hints to suppress type checking warnings in subprocess and file operations.

Enhances tunnel scripts to properly apply RDP file logic for Thincast and improves debugging output.

No issue reference provided.
2025-09-15 11:23:47 +02:00
aschumann-virtualcable
826cc7aed8 Add macOS support for RDP file usage in Thincast connections
Adds macOS RDP file support for Thincast connections

Introduces a configurable option to use RDP files for Thincast and xfreerdp on macOS, enabling seamless file-based connections. Updates logic to open Thincast with the RDP file when the option is enabled, improving compatibility and user experience for macOS users.
2025-09-12 15:38:14 +02:00
aschumann-virtualcable
4da15d66fe Improves Thincast client detection and launch on macOS
Switches Thincast detection from file to directory check to match macOS app bundle structure.

Updates Thincast launch logic to use the 'open' command with appropriate arguments, improving compatibility and reliability.

Removes unused code for opening .rdp files with Thincast and applies consistent resolution handling.

Ensures signature files are updated accordingly.
2025-09-12 12:09:54 +02:00
aschumann-virtualcable
79495fc3b1 Enables Thincast support for RDP transport on macOS
Uncomments and activates logic for launching Thincast client,
allowing users to initiate RDP sessions via Thincast.

Updates the related signature file for integrity validation.
2025-09-12 11:24:31 +02:00
Adolfo Gómez García
f438a9241e Remove unused import of DynamicUserService in Xen service module 2025-09-11 17:12:34 +02:00
aschumann-virtualcable
e37b345aff Adds support for RDP file custom params on Linux
Enables the use of Windows custom parameters in RDP file generation when specified for Linux targets, aligning Linux behavior with Windows.

Improves flexibility for custom connection settings across platforms.
2025-09-11 13:15:07 +02:00
aschumann-virtualcable
ce1330066f Enhance XFREERDP and Thincast support to conditionally use RDP files, improving parameter handling and logging.
Improves RDP client handling with conditional file usage

Allows XFREERDP and Thincast to use RDP files when provided, enhancing parameter management and execution flexibility.
Refines logging for better traceability of client launch logic.
2025-09-10 19:22:37 +02:00
aschumann-virtualcable
20e86cd8c7 Refactor Thincast support: rename lnx_thincast_rdp_file to lnx_use_rdp_file, update related logic in RDPTransport and BaseRDPTransport, and enhance RDP file handling in direct.py and tunnel.py.
Refactors Thincast RDP file support for Linux clients

Renames and consolidates configuration for using RDP files with Thincast and xfreerdp, streamlines related logic, and enhances RDP file handling in Linux scripts. Improves clarity, maintainability, and user experience for Linux RDP connections.
2025-09-10 18:33:59 +02:00
Adolfo Gómez García
dc52e37abc Fix proxy handling in secure_requests_session to check for None instead of truthiness 2025-09-09 21:40:17 +02:00
Adolfo Gómez García
69fae6a1a6 Refactor access denial handling in blocker decorator and update frequency for DeployedServiceInfoItemsCleaner 2025-09-09 18:58:13 +02:00
Adolfo Gómez García
7c14923afe Add string representation method to Environment class 2025-09-09 18:07:02 +02:00
Adolfo Gómez García
9e66583b4e Enhance MAC address handling in Proxmox and Xen services; add maintenance command for cleaning unused MACs 2025-09-09 16:58:49 +02:00
aschumann-virtualcable
34676c817f Enhance Thincast support by updating RDPTransport to conditionally handle 'as_file' and improve logging in direct.py for better debugging. 2025-09-09 11:16:52 +02:00
aschumann-virtualcable
d17224c9cb Merge branch 'dev/andres/v4.0' of github.com:VirtualCable/openuds into dev/andres/v4.0 2025-09-09 10:43:02 +02:00
aschumann-virtualcable
b57b00f3fc Add lnx_thincast_rdp_file field to RDPTransport and BaseRDPTransport for Thincast support 2025-09-09 10:42:40 +02:00
aschumann-virtualcable
f82041da1e Add debug logging for Thincast RDP file processing and update signatures 2025-09-08 13:10:04 +02:00
aschumann-virtualcable
03a837f865 Add Thincast support and improve logging in RDP scripts 2025-09-08 13:08:12 +02:00
Adolfo Gómez García
473dc2577f Add MAC Address field to ServersTokens API response 2025-09-05 16:42:31 +02:00
Adolfo Gómez García
49dfaf3709 Add NO_MORE_MACS constant and update error handling in DynamicUserService and UniqueMacGenerator 2025-09-04 22:01:18 +02:00
Adolfo Gómez García
f5afb79a2b Limit length of server group name and comments in migrate function for consistency 2025-09-04 20:18:43 +02:00
Adolfo Gómez García
bd26fb38d9 Add error logging for unavailable IDs in UniqueGenerator and UniqueMacGenerator 2025-09-04 19:20:35 +02:00
aschumann-virtualcable
95f0b0ab26 Update tunnel.py.signature with new signature data 2025-09-04 11:59:05 +02:00
aschumann-virtualcable
28433fc33e Add support for Thincast in RDP scripts and improve executable search logic 2025-09-04 11:55:41 +02:00
aschumann-virtualcable
fc4e7414df Update subproject commits for actor and client modules 2025-09-04 11:26:01 +02:00
aschumann-virtualcable
e61cb1f855 Add logging for client discovery in RDP scripts 2025-09-04 11:25:24 +02:00
Adolfo Gómez García
689214cf84 Refactor code formatting in ServerManager and Server classes for improved readability 2025-09-04 02:05:41 +02:00
Adolfo Gómez García
d268478767 Add server stats weights handling and update load calculation 2025-09-04 02:04:17 +02:00
Adolfo Gómez García
4a5ad5dc09 Update max user limit in ServerStats comment from 1000 to 100 for accuracy 2025-09-03 16:06:41 +02:00
Adolfo Gómez García
7365ee8cc6 Fix CryptoManager call in generate_uuid function to use manager method 2025-09-03 15:44:54 +02:00
Adolfo Gómez García
5a93aa15e8 Add HA group handling for Proxmox version 9 and update tests accordingly 2025-08-25 17:29:08 +02:00
aschumann-virtualcable
1fddc17b75 initial dev enviroment 2025-08-21 18:04:11 +02:00
Adolfo Gómez García
ca540d7725 Improve logging message in RadiusClient and change secret field to PasswordField in RadiusOTP for better security 2025-08-21 16:30:39 +02:00
Adolfo Gómez García
fe11b485ed Rename _ensure_local_db_exists method to ensure_local_db_exists for clarity 2025-08-19 17:16:38 +02:00
46 changed files with 747 additions and 177 deletions

2
actor

Submodule actor updated: 3c40cb45f0...04ce3fc2d1

View File

@@ -34,6 +34,7 @@ import typing
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from django.db import transaction
from uds import models
from uds.core import consts, types, ui
@@ -69,6 +70,7 @@ class ServersTokens(ModelHandler):
{'os': {'title': _('OS')}},
{'username': {'title': _('Issued by')}},
{'stamp': {'title': _('Date'), 'type': 'datetime'}},
{'mac': {'title': _('MAC Address')}},
]
def item_as_dict(self, item: 'Model') -> dict[str, typing.Any]:
@@ -304,9 +306,14 @@ class ServersServers(DetailHandler):
raise self.invalid_item_response() from None
else:
# Remove current server and add the new one in a single transaction
try:
server = models.Server.objects.get(uuid=process_uuid(item))
parent.servers.add(server)
with transaction.atomic():
current_server = models.Server.objects.get(uuid=process_uuid(item))
new_server = models.Server.objects.get(uuid=process_uuid(self._params['server']))
parent.servers.remove(current_server)
parent.servers.add(new_server)
item = new_server.uuid
except Exception:
raise self.invalid_item_response() from None
return {'id': item}
@@ -331,7 +338,7 @@ class ServersServers(DetailHandler):
:param item:
"""
item = models.Server.objects.get(uuid=process_uuid(id))
self.ensure_has_access(item, types.permissions.PermissionType.MANAGEMENT)
self.ensure_has_access(parent, types.permissions.PermissionType.MANAGEMENT)
item.maintenance_mode = not item.maintenance_mode
item.save()
return 'ok'
@@ -528,7 +535,8 @@ class ServersGroups(ModelHandler):
'hostname': s[1].hostname,
'mac': s[1].mac if s[1].mac != consts.MAC_UNKNOWN else '',
'ip': s[1].ip,
'load': s[0].load() if s[0] else 0,
'load': s[0].load(weights=item.weights) if s[0] else 0,
'weights': item.weights.as_dict(),
},
}
for s in ServerManager.manager().get_server_stats(item.servers.all())

View File

@@ -197,7 +197,7 @@ class RadiusClient:
if i.startswith(groupclass_prefix)
]
else:
logger.info('No "Class (25)" attribute found')
logger.info('No "Class (25)" attribute found: %s', reply)
return ([], '', b'')
# ...and mfa code

View File

@@ -74,3 +74,5 @@ UNLIMITED: typing.Final[int] = -1
# Constant marking no more names available
NO_MORE_NAMES: typing.Final[str] = 'NO-NAME-ERROR'
# For convenience, same as MAC_UNKNOWN, but different meaning
NO_MORE_MACS: typing.Final[str] = MAC_UNKNOWN

View File

@@ -215,6 +215,9 @@ class Environment:
def __exit__(self, exc_type: typing.Any, exc_value: typing.Any, traceback: typing.Any) -> None:
if self._key == TEST_ENV or (self._key.startswith('#_#') and self._key.endswith('#^#')):
self.clean_related_data()
def __str__(self) -> str:
return f'Environment: {self._key}'
class Environmentable:

View File

@@ -51,7 +51,7 @@ class NotificationsManager(metaclass=singleton.Singleton):
_initialized: bool = False
def _ensure_local_db_exists(self) -> bool:
def ensure_local_db_exists(self) -> bool:
if not apps.ready:
return False
@@ -85,7 +85,7 @@ class NotificationsManager(metaclass=singleton.Singleton):
from uds.models.notifications import Notification # pylint: disable=import-outside-toplevel
# Due to use of local db, we must ensure that it exists (and cannot do it on ready)
if self._ensure_local_db_exists() is False:
if self.ensure_local_db_exists() is False:
return # Not initialized apps yet, so we cannot do anything
# logger.debug(

View File

@@ -119,9 +119,9 @@ class ServerManager(metaclass=singleton.Singleton):
Returns:
An iterator of servers with activity in the last last_activity_delta time
"""
op = operator.gt if with_activity else operator.le
activity_limit = model_utils.sql_now() - last_activity_delta
# Get all servers with activity in the last 10 minutes
for server in server_group.servers.filter(maintenance_mode=False):
@@ -181,7 +181,7 @@ class ServerManager(metaclass=singleton.Singleton):
weight_threshold_f = weight_threshold / 100
def _real_weight(stats: 'types.servers.ServerStats') -> float:
stats_weight = stats.load()
stats_weight = stats.load(weights=server_group.weights)
if weight_threshold == 0:
return stats_weight
@@ -545,7 +545,12 @@ class ServerManager(metaclass=singleton.Singleton):
# Get the stats for all servers, but in parallel
server_stats = self.get_server_stats(fltrs)
# Sort by load, lower first (lower is better)
return [s[1] for s in sorted(server_stats, key=lambda x: x[0].load() if x[0] else 999999999)]
return [
s[1]
for s in sorted(
server_stats, key=lambda x: x[0].load(weights=server_group.weights) if x[0] else 999999999
)
]
def perform_maintenance(self, server_group: 'models.ServerGroup') -> None:
"""Realizes maintenance on server group

View File

@@ -489,7 +489,13 @@ class UserServiceManager(metaclass=singleton.Singleton):
operations_logger.info('Removing userservice %a', userservice.name)
if userservice.is_usable() is False and State.from_str(userservice.state).is_removable() is False:
if not forced:
raise OperationException(_('Can\'t remove a non active element') + ': ' + userservice.name + ', ' + userservice.state)
raise OperationException(
_('Can\'t remove a non active element')
+ ': '
+ userservice.name
+ ', '
+ userservice.state
)
userservice.set_state(State.REMOVING)
logger.debug("***** The state now is %s *****", State.from_str(userservice.state).localized)
userservice.set_in_use(False) # For accounting, ensure that it is not in use right now
@@ -772,6 +778,11 @@ class UserServiceManager(metaclass=singleton.Singleton):
logger.warning('Could not check readyness of %s: %s', user_service, e)
return False
if state == types.states.TaskState.ERROR:
user_service.update_data(userservice_instance)
user_service.set_state(State.ERROR)
raise InvalidServiceException('Service missing or in error state')
logger.debug('State: %s', state)
if state == types.states.TaskState.FINISHED:

View File

@@ -33,6 +33,7 @@ import time
import logging
import typing
from uds.core.managers.notifications import NotificationsManager
from uds.core.managers.task import BaseThread
from uds.models import Notifier, Notification
@@ -43,13 +44,12 @@ from .config import DO_NOT_REPEAT
logger = logging.getLogger(__name__)
# Note that this thread will be running on the scheduler process
class MessageProcessorThread(BaseThread):
_keep_running: bool = True
_cached_providers: typing.Optional[
list[tuple[int, NotificationProviderModule]]
]
_cached_providers: typing.Optional[list[tuple[int, NotificationProviderModule]]]
_cached_stamp: float
def __init__(self) -> None:
@@ -73,12 +73,14 @@ class MessageProcessorThread(BaseThread):
return self._cached_providers
def run(self) -> None:
while NotificationsManager.manager().ensure_local_db_exists() is False:
logger.info('Waiting for local notifications database to be ready...')
time.sleep(1)
while self._keep_running:
# Locate all notifications from "persistent" and try to process them
# If no notification can be fully resolved, it will be kept in the database
not_before = sql_now() - datetime.timedelta(
seconds=DO_NOT_REPEAT.as_int()
)
not_before = sql_now() - datetime.timedelta(seconds=DO_NOT_REPEAT.as_int())
for n in Notification.get_persistent_queryset().all():
# If there are any other notification simmilar to this on default db, skip it
# Simmilar means that group, identificator and message are already been logged less than DO_NOT_REPEAT seconds ago
@@ -119,7 +121,7 @@ class MessageProcessorThread(BaseThread):
# logger.warning(
# 'Could not save notification %s to main DB, trying notificators',
# n,
#)
# )
if notify:
for p in (i[1] for i in self.providers if i[0] >= n.level):

View File

@@ -566,6 +566,14 @@ class DynamicUserService(services.UserService, autoserializable.AutoSerializable
If you override this method, you should take care yourself of removing duplicated machines
(maybe only calling "super().op_initialize()" method)
"""
# By default, should return a VALID username and unique_id
# Note that valid is anything different from consts.NO_MORE_NAMES or consts.NO_MORE_MACS
if self.get_name() == consts.NO_MORE_NAMES:
self.error('No more names available') # Will mark as error and check will note it
return
if self.get_unique_id() == consts.NO_MORE_MACS:
self.error('No more MACs available') # Will mark as error and check will note it
return
self.remove_duplicates()
@abc.abstractmethod

View File

@@ -137,6 +137,38 @@ class ServerDiskInfo:
}
@dataclasses.dataclass
class ServerStatsWeights:
cpu: float = 0.3
memory: float = 0.6
users: float = 0.1
max_users: int = 100 # Max users to consider in load calculation
def normalize(self) -> 'ServerStatsWeights':
total = self.cpu + self.memory + self.users
self.cpu /= total
self.memory /= total
self.users /= total
return self
def as_dict(self) -> dict[str, float]:
return {
'cpu': self.cpu,
'memory': self.memory,
'users': self.users,
'max_users': self.max_users,
}
@staticmethod
def from_dict(data: dict[str, float]) -> 'ServerStatsWeights':
return ServerStatsWeights(
data.get('cpu', 0.3),
data.get('memory', 0.6),
data.get('users', 0.1),
int(data.get('max_users', 100)),
).normalize()
@dataclasses.dataclass
class ServerStats:
memused: int = 0 # In bytes
@@ -165,21 +197,23 @@ class ServerStats:
return self.stamp > sql_stamp() - consts.cache.DEFAULT_CACHE_TIMEOUT
def load(self, min_memory: int = 0) -> float:
def load(self, *, min_memory: int = 0, weights: ServerStatsWeights | None = None) -> float:
# Loads are calculated as:
# 30% cpu usage
# 60% memory usage
# 10% current users, with a max of 1000 users
# 10% current users, with a max of 100 users
# Loads are normalized to 0-1
# Lower weight is better
weights = (weights or ServerStatsWeights()).normalize()
if self.memtotal - self.memused < min_memory:
return 1000000000 # At the end of the list
w = (
0.3 * self.cpuused
+ 0.6 * (self.memused / (self.memtotal or 1))
+ 0.1 * (min(1.0, self.current_users / 100.0))
weights.cpu * self.cpuused
+ weights.memory * (self.memused / (self.memtotal or 1))
+ weights.users * (min(1.0, self.current_users / weights.max_users))
)
return min(max(0.0, w), 1.0)

View File

@@ -373,28 +373,30 @@ def blocker(
try:
return f(*args, **kwargs)
except uds.core.exceptions.rest.BlockAccess:
raise exceptions.rest.AccessDenied
raise exceptions.rest.AccessDenied()
request: typing.Optional[typing.Any] = getattr(args[0], request_attr or '_request', None)
request: typing.Any = getattr(args[0], request_attr or '_request', None)
# No request object, so we can't block
if request is None or not isinstance(request, types.requests.ExtendedHttpRequest):
if request is None or getattr(request, 'ip', None) is None:
logger.debug('No request object, so we can\'t block: (value is %s)', request)
return f(*args, **kwargs)
request = typing.cast(types.requests.ExtendedHttpRequest, request)
ip = request.ip
# if ip is blocked, raise exception
failures_count: int = mycache.get(ip, 0)
if failures_count >= max_failures:
raise exceptions.rest.AccessDenied
raise exceptions.rest.AccessDenied()
try:
result = f(*args, **kwargs)
except uds.core.exceptions.rest.BlockAccess:
# Increment
mycache.put(ip, failures_count + 1, GlobalConfig.LOGIN_BLOCK.as_int())
raise exceptions.rest.AccessDenied
raise exceptions.rest.AccessDenied()
# Any other exception will be raised
except Exception:
raise
@@ -473,7 +475,7 @@ def retry_on_exception(
raise e
time.sleep(wait_seconds * (2 ** min(i, 4))) # Exponential backoff until 16x
# retries == 0 allowed, but only use it for testing purposes
# because it's a nonsensical decorator otherwise
return fnc(*args, **kwargs)

View File

@@ -136,7 +136,7 @@ def generate_uuid(obj: typing.Any = None) -> str:
"""
Generates a ramdom uuid for models default
"""
return CryptoManager().uuid(obj=obj).lower()
return CryptoManager.manager().uuid(obj=obj).lower()
def process_uuid(uuid: str) -> str:

View File

@@ -222,7 +222,7 @@ def secure_requests_session(*, verify: 'str|bool' = True, proxies: 'dict[str, st
session = requests.Session()
session.mount("https://", UDSHTTPAdapter())
if proxies:
if proxies is not None:
session.proxies = proxies
# Add user agent header to session

View File

@@ -111,6 +111,7 @@ class UniqueGenerator:
seq = range_start
if seq > range_end:
logger.error('No more ids available in range %s - %s', range_start, range_end)
return -1 # No ids free in range
# May ocurr on some circustance that a concurrency access gives same item twice, in this case, we

View File

@@ -33,6 +33,8 @@ Author: Adolfo Gómez, dkmaster at dkmon dot com
import logging
import re
from uds.core import consts
from .unique_id_generator import UniqueGenerator
logger = logging.getLogger(__name__)
@@ -48,8 +50,9 @@ class UniqueMacGenerator(UniqueGenerator):
return int(mac.replace(':', ''), 16)
def _to_mac_addr(self, seq: int) -> str:
if seq == -1: # No mor macs available
return '00:00:00:00:00:00'
if seq == -1: # No more macs available
logger.error('No more MAC addresses available')
return consts.NO_MORE_MACS
return re.sub(r"(..)", r"\1:", f'{seq:012X}')[:-1]
# Mac Generator rewrites the signature of parent class, so we need to redefine it here

View File

@@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import logging
import typing
from django.core.management.base import BaseCommand
from uds import models
from uds.core.util import unique_mac_generator
logger = logging.getLogger(__name__)
MIN_VERBOSITY: typing.Final[int] = 1 # Minimum verbosity to print freed macs
class Command(BaseCommand):
help = "Execute maintenance tasks for UDS broker"
def clean_unused_service_macs(self, service: models.Service) -> int:
# Get all userservices from this service, extract their "unique_id" (the mac)
# And store it in a set for later use
self.stdout.write(f'Cleaning unused macs for service {service.name} (id: {service.id})\n')
def mac_to_int(mac: str) -> int:
try:
return int(mac.replace(':', ''), 16)
except Exception:
return -1
mac_gen = unique_mac_generator.UniqueMacGenerator(f't-service-{service.id}')
used_macs = {
mac_to_int(us.unique_id) for us in models.UserService.objects.filter(deployed_service__service=service)
}
counter = 0
for seq in (
models.UniqueId.objects.filter(basename='\tmac', assigned=True, owner=f't-service-{service.id}')
.exclude(seq__in=used_macs)
.values_list('seq', flat=True)
):
counter += 1
self.stdout.write(f'Freeing mac {mac_gen._to_mac_addr(seq)} for service {service.name}\n')
mac_gen.free(mac_gen._to_mac_addr(seq))
self.stdout.write(f'Freed {counter} macs for service {service.name}\n')
logger.info('Freed %d macs for service %s', counter, service.name)
return counter
def handle(self, *args: typing.Any, **options: typing.Any) -> None:
logger.debug('Maintenance called with args: %s, options: %s', args, options)
counter = 0
for service in models.Service.objects.all():
try:
counter += self.clean_unused_service_macs(service)
except Exception as e:
logger.error('Error doing maintenance for service %s: %s', service.name, e)
self.stdout.write(f'Error doing maintenance for service {service.name}: {e}\n')
logger.info('Maintenance finished, total freed macs: %d', counter)
self.stdout.write(f'Total freed macs: {counter}\n')

View File

@@ -130,7 +130,7 @@ class Command(BaseCommand):
'--max-items',
action='store',
dest='maxitems',
default=400,
default=200,
help='Maximum elements exported for groups and user services',
)
@@ -166,7 +166,15 @@ class Command(BaseCommand):
fltr = servicepool.userServices.all()
if not options['alluserservices']:
fltr = fltr.filter(state=types.states.State.ERROR)
for item in fltr[:max_items]: # at most max_items items
fltr_list = list(fltr)[:max_items]
if len(fltr_list) < max_items:
# Append rest of userservices, if there is space
fltr_list += list(
servicepool.userServices.exclude(
pk__in=[u.pk for u in fltr_list]
)[: max_items - len(fltr_list)]
)
for item in fltr_list[:max_items]: # at most max_items items
logs = [
f'{l["date"]}: {types.log.LogLevel.from_int(l["level"])} [{l["source"]}] - {l["message"]}'
for l in log.get_logs(item)
@@ -241,8 +249,22 @@ class Command(BaseCommand):
'_': get_serialized_from_managed_object(provider),
'services': services,
}
tree[counter('PROVIDERS')] = providers
# Get server groups
server_groups: dict[str, typing.Any] = {}
for server_group in models.ServerGroup.objects.all():
servers: dict[str, typing.Any] = {}
for server in server_group.servers.all()[:max_items]: # at most max_items items
servers[server.hostname] = get_serialized_from_model(server, exclude_uuid=False)
server_groups[server_group.name] = {
'_': get_serialized_from_model(server_group, exclude_uuid=False),
'servers': servers,
}
tree[counter('SERVICES')] = {
'providers': providers,
'server_groups': server_groups
}
# authenticators
authenticators: dict[str, typing.Any] = {}
@@ -380,12 +402,12 @@ class Command(BaseCommand):
tree[counter('CONFIG')] = cfg
# Last 7 days of logs
# Last 7 days of logs or 500 entries, whichever is less
logs = [
get_serialized_from_model(log_entry)
for log_entry in models.Log.objects.filter(
created__gt=now - datetime.timedelta(days=7)
).order_by('-created')
).order_by('-created')[:500]
]
# Cluster nodes
cluster_nodes: list[dict[str, str]] = [node.as_dict() for node in cluster.enumerate_cluster_nodes()]

View File

@@ -81,7 +81,7 @@ class RadiusOTP(mfas.MFA):
tooltip=_('Radius authentication port (usually 1812)'),
required=True,
)
secret = gui.TextField(
secret = gui.PasswordField(
length=64,
label=_('Secret'),
order=3,

View File

@@ -113,8 +113,8 @@ def migrate(
logger.error('Server %s on %s not found on DNS', server, record.name)
registered_server_group = ServerGroup.objects.create(
name=f'{server_group_prefix} for {record.name}',
comments='Migrated from {}'.format(record.name),
name=f'{server_group_prefix} for {record.name}'[:64],
comments='Migrated from {}'.format(record.name)[:255],
type=types.servers.ServerType.UNMANAGED,
subtype=subtype,
)

View File

@@ -109,7 +109,7 @@ class IPMachinesService(services.Service):
self.ipList.value = [_as_identifier(i) for i in _ips]
if values[0] != b'v1':
self._token = values[1].decode()
self.token.value = values[1].decode()
if values[0] in (b'v3', b'v4', b'v5', b'v6', b'v7'):
self.port.value = int(values[2].decode())
if values[0] in (b'v4', b'v5', b'v6', b'v7'):

View File

@@ -109,6 +109,25 @@ class ServerGroup(UUIDModel, TaggingMixin, properties.PropertiesMixin):
"""Sets the server type of this server"""
self.type = value
@property
def weights(self) -> types.servers.ServerStatsWeights:
"""Returns the server stats weights for this server group"""
weights_dict = self.properties.get('weights', None)
if weights_dict:
return types.servers.ServerStatsWeights.from_dict(weights_dict)
return types.servers.ServerStatsWeights()
@weights.setter
def weights(self, value: types.servers.ServerStatsWeights) -> None:
"""Sets the server stats weights for this server group"""
self.properties['weights'] = value.as_dict()
@weights.deleter
def weights(self) -> None:
"""Deletes the server stats weights for this server group"""
if 'weights' in self.properties:
del self.properties['weights']
def is_managed(self) -> bool:
"""Returns if this server group is managed or not"""
return self.server_type != types.servers.ServerType.UNMANAGED
@@ -149,7 +168,6 @@ class ServerGroup(UUIDModel, TaggingMixin, properties.PropertiesMixin):
return None
def _create_token() -> str:
return secrets.token_urlsafe(36)
@@ -250,7 +268,7 @@ class Server(UUIDModel, TaggingMixin, properties.PropertiesMixin):
def server_type(self, value: types.servers.ServerType) -> None:
"""Sets the server type of this server"""
self.type = value
def is_managed(self) -> bool:
"""Returns if this server is managed or not"""
return self.server_type != types.servers.ServerType.UNMANAGED
@@ -296,15 +314,15 @@ class Server(UUIDModel, TaggingMixin, properties.PropertiesMixin):
def lock(self, duration: typing.Optional[datetime.timedelta]) -> None:
"""Locks this server for a duration
Args:
duration: Duration to lock the server. If None, it will be unlocked
Note:
If duration is None, the server will be unlocked
The lock time will be calculated from current time on sql server
"""
if duration is None:
self.locked_until = None
else:

View File

@@ -245,7 +245,7 @@ class ServicePool(UUIDModel, TaggingMixin):
@property
def owned_by_meta(self) -> bool:
return self.memberOfMeta.count() > 0
@property
def uses_cache(self) -> bool:
return self.cache_l1_srvs > 0 or self.cache_l2_srvs > 0 or self.initial_srvs > 0
@@ -256,7 +256,7 @@ class ServicePool(UUIDModel, TaggingMixin):
if self.short_name and str(self.short_name).strip():
return str(self.short_name.strip())
return str(self.name)
def can_create_userservices(self) -> bool:
"""
If the service pool is in a state that allows to create user services
@@ -291,13 +291,12 @@ class ServicePool(UUIDModel, TaggingMixin):
return True
return False
def is_locked(self) -> bool:
"""
Returns true if the service pool is locked
"""
return self.state == types.states.State.LOCKED
def remaining_restraint_time(self) -> int:
from uds.core.util.config import GlobalConfig
@@ -327,7 +326,7 @@ class ServicePool(UUIDModel, TaggingMixin):
def is_usable(self) -> bool:
return (
self.state == types.states.State.ACTIVE
self.state in (types.states.State.ACTIVE, types.states.State.LOCKED)
and not self.is_in_maintenance()
and not self.is_restrained()
)
@@ -487,7 +486,7 @@ class ServicePool(UUIDModel, TaggingMixin):
Args:
active_publication: Active publication used as "current" publication to make checks
skip_assigned: If true, assigned services will not be marked as removable
"""
now = sql_now()
non_active_publication: 'ServicePoolPublication'
@@ -500,9 +499,9 @@ class ServicePool(UUIDModel, TaggingMixin):
for userservice in non_active_publication.userServices.filter(state=types.states.State.PREPARING):
userservice.cancel()
with transaction.atomic():
non_active_publication.userServices.exclude(cache_level=0).filter(state=types.states.State.USABLE).update(
state=types.states.State.REMOVABLE, state_date=now
)
non_active_publication.userServices.exclude(cache_level=0).filter(
state=types.states.State.USABLE
).update(state=types.states.State.REMOVABLE, state_date=now)
if not skip_assigned:
non_active_publication.userServices.filter(
cache_level=0, state=types.states.State.USABLE, in_use=False
@@ -572,7 +571,9 @@ class ServicePool(UUIDModel, TaggingMixin):
"""
from uds.core import services # pylint: disable=import-outside-toplevel
services_not_needing_publication = [t.mod_type() for t in services.factory().services_not_needing_publication()]
services_not_needing_publication = [
t.mod_type() for t in services.factory().services_not_needing_publication()
]
# Get services that HAS publications
query = (
ServicePool.objects.filter(

View File

@@ -33,7 +33,7 @@ Author: Adolfo Gómez, dkmaster at dkmon dot com
import logging
import typing
from uds.core import services, types
from uds.core import consts, services, types
from uds.core.util import autoserializable
# Not imported at runtime, just for type checking
@@ -73,7 +73,7 @@ class IPMachinesUserService(services.UserService, autoserializable.AutoSerializa
def get_unique_id(self) -> str:
# Generate a 16 chars string mixing up all _vmid chars
return self._mac or self._ip
return self._mac if self._mac and self._mac != consts.MAC_UNKNOWN else self._ip
def set_ready(self) -> types.states.TaskState:
self.service().wakeup(self._ip, self._mac)

View File

@@ -141,6 +141,9 @@ class IPMachinesService(services.Service):
services_type_provided = types.services.ServiceType.VDI
def enumerate_servers(self) -> typing.Iterable['models.Server']:
return fields.get_server_group_from_field(self.server_group).servers.filter(maintenance_mode=False)
def get_token(self) -> typing.Optional[str]:
return self.token.as_str() or None
@@ -153,7 +156,7 @@ class IPMachinesService(services.Service):
now = sql_now()
return [
gui.choice_item(server.uuid, f'{server.host}|{server.mac}')
for server in fields.get_server_group_from_field(self.server_group).servers.all()
for server in self.enumerate_servers()
if server.locked_until is None or server.locked_until < now
]
@@ -175,14 +178,15 @@ class IPMachinesService(services.Service):
'''
Returns an unassigned machine
'''
list_of_servers = list(fields.get_server_group_from_field(self.server_group).servers.all())
# Get all servers in the group, not in maintenance mode
list_of_servers = list(self.enumerate_servers())
if self.randomize_host.as_bool() is True:
random.shuffle(list_of_servers) # Reorder the list randomly if required
for server in list_of_servers:
# If not locked or lock expired
if server.locked_until is None or server.locked_until < sql_now():
# if port check enabled, check
# if port check enabled, check
if self.port.value != 0:
if not net.test_connectivity(server.host, self.port.value):
server.lock(datetime.timedelta(minutes=self.ignore_minutes_on_failure.value))

View File

@@ -461,6 +461,10 @@ class ProxmoxClient:
@cached('hagrps', consts.CACHE_DURATION, key_helper=caching_key_helper)
def list_ha_groups(self, **kwargs: typing.Any) -> list[str]:
version = self.get_version()
# Version 9 does not have the security groups
if version[0] >= '9':
return []
return [g['group'] for g in self.do_get('cluster/ha/groups')['data']]
def enable_vm_ha(self, vmid: int, started: bool = False, group: typing.Optional[str] = None) -> None:
@@ -484,6 +488,10 @@ class ProxmoxClient:
+ ([('group', group)] if group else []), # Append ha group if present
)
@cached('ha_resources', consts.CACHE_DURATION, key_helper=caching_key_helper)
def list_ha_resources(self, **kwargs: typing.Any) -> list[str]:
return [r['sid'] for r in self.do_get('cluster/ha/resources')['data']]
def disable_vm_ha(self, vmid: int) -> None:
try:
self.do_delete(f'cluster/ha/resources/vm%3A{vmid}')

View File

@@ -58,7 +58,7 @@ class Node:
id: str
@staticmethod
def from_dict(dictionary: collections.abc.MutableMapping[str, typing.Any]) -> 'Node':
def from_dict(dictionary: dict[str, typing.Any]) -> 'Node':
return Node(
name=dictionary.get('name', ''),
online=dictionary.get('online', False),

View File

@@ -281,6 +281,8 @@ class ProxmoxServiceLinked(DynamicService):
) -> str:
# If vmid is empty, we are requesting a new mac
if not vmid or for_unique_id:
if isinstance(caller_instance, DynamicUserService):
return caller_instance.mac_generator().get(self.get_macs_range())
return self.mac_generator().get(self.get_macs_range())
return self.provider().api.get_vm_config(int(vmid)).networks[0].macaddr.lower()

View File

@@ -37,6 +37,7 @@ import typing
from django.utils.translation import gettext_noop as _
from uds.core import exceptions, types
from uds.core.services.generics.dynamic.service import DynamicService
from uds.core.services.generics.dynamic.userservice import DynamicUserService
from uds.core.util import validators
from uds.core.ui import gui
@@ -49,7 +50,6 @@ from .xen import exceptions as xen_exceptions
if typing.TYPE_CHECKING:
from .provider import XenProvider
from uds.core.services.generics.dynamic.publication import DynamicPublication
from uds.core.services.generics.dynamic.userservice import DynamicUserService
logger = logging.getLogger(__name__)
@@ -326,8 +326,14 @@ class XenLinkedService(DynamicService): # pylint: disable=too-many-public-metho
*,
for_unique_id: bool = False,
) -> str:
return self.mac_generator().get(self.provider().get_macs_range())
if not vmid or for_unique_id:
if isinstance(caller_instance, DynamicUserService):
return caller_instance.mac_generator().get(self.provider().get_macs_range())
return self.mac_generator().get(self.provider().get_macs_range())
with self.provider().get_connection() as api:
return api.get_first_mac(vmid)
def is_running(
self, caller_instance: typing.Optional['DynamicUserService | DynamicPublication'], vmid: str
) -> bool:

File diff suppressed because one or more lines are too long

View File

@@ -102,6 +102,6 @@
</svg>
</div>
</uds-root>
<link rel="modulepreload" href="/uds/res/admin/chunk-2F3F2YC2.js?stamp=1751031500" integrity="sha384-VVOra5xy5Xg9fYkBmK9MLhX7vif/MexRAaLIDBsQ4ZlkF31s/U6uWWrj+LAnvX/q"><script src="/uds/res/admin/polyfills.js?stamp=1751031500" type="module" crossorigin="anonymous" integrity="sha384-TVRkn44wOGJBeCKWJBHWLvXubZ+Julj/yA0OoEFa3LgJHVHaPeeATX6NcjuNgsIA"></script><script src="/uds/res/admin/main.js?stamp=1751031500" type="module" crossorigin="anonymous" integrity="sha384-D7v1bHwPV9S5WsYYbKeGYE4w8s2/dBI1mXgJa+Pptf5X3LLXWUEkBBT30PTosind"></script></body>
<link rel="modulepreload" href="/uds/res/admin/chunk-2F3F2YC2.js?stamp=1758035900" integrity="sha384-VVOra5xy5Xg9fYkBmK9MLhX7vif/MexRAaLIDBsQ4ZlkF31s/U6uWWrj+LAnvX/q"><script src="/uds/res/admin/polyfills.js?stamp=1758035900" type="module" crossorigin="anonymous" integrity="sha384-TVRkn44wOGJBeCKWJBHWLvXubZ+Julj/yA0OoEFa3LgJHVHaPeeATX6NcjuNgsIA"></script><script src="/uds/res/admin/main.js?stamp=1758035900" type="module" crossorigin="anonymous" integrity="sha384-3lZTkxMTxP/KgMLbdC/mjFUbq3YXJmMoJbgPH6az7gjQiWFJvikEtKXNxd9gQYI7"></script></body>
</html>

View File

@@ -98,6 +98,9 @@ class RDPTransport(BaseRDPTransport):
mac_custom_parameters = BaseRDPTransport.mac_custom_parameters
wnd_custom_parameters = BaseRDPTransport.wnd_custom_parameters
lnx_use_rdp_file = BaseRDPTransport.lnx_use_rdp_file
mac_use_rdp_file = BaseRDPTransport.mac_use_rdp_file
def get_transport_script( # pylint: disable=too-many-locals
self,
userservice: 'models.UserService',
@@ -165,20 +168,27 @@ class RDPTransport(BaseRDPTransport):
}
)
elif os.os == types.os.KnownOS.LINUX:
r.custom_parameters = self.lnx_custom_parameters.value
if self.lnx_use_rdp_file.as_bool():
r.custom_parameters = self.wnd_custom_parameters.value
else:
r.custom_parameters = self.lnx_custom_parameters.value
sp.update(
{
'as_new_xfreerdp_params': r.as_new_xfreerdp_params,
'address': r.address,
'as_file': r.as_file if self.lnx_use_rdp_file.as_bool() else '',
}
)
elif os.os == types.os.KnownOS.MAC_OS:
r.custom_parameters = self.mac_custom_parameters.value
if self.mac_use_rdp_file.as_bool():
r.custom_parameters = self.wnd_custom_parameters.value
else:
r.custom_parameters = self.mac_custom_parameters.value
sp.update(
{
'as_new_xfreerdp_params': r.as_new_xfreerdp_params,
'as_rdp_url': r.as_rdp_url if self.mac_allow_msrdc.as_bool() else '',
'as_file': r.as_file if self.mac_allow_msrdc.as_bool() else '',
'as_file': r.as_file if self.mac_use_rdp_file.as_bool() else '',
'address': r.address,
}
)

View File

@@ -297,6 +297,14 @@ class BaseRDPTransport(transports.Transport):
tab='Linux Client',
old_field_name='alsa',
)
lnx_use_rdp_file = gui.CheckBoxField(
label=_('Use RDP file for connections'),
order=42,
tooltip=_('If marked, an RDP file will be used for connections with Thincast or xfreerdp on Linux.'),
tab='Linux Client',
default=True,
old_field_name='lnx_thincastRdpFile',
)
lnx_printer_string = gui.TextField(
label=_('Printer string'),
order=43,
@@ -333,9 +341,18 @@ class BaseRDPTransport(transports.Transport):
old_field_name='allowMacMSRDC',
)
mac_use_rdp_file = gui.CheckBoxField(
label=_('Use RDP file for connections'),
order=51,
tooltip=_('If marked, an RDP file will be used for connections with Thincast or xfreerdp on Mac OS X.'),
tab='Mac OS X',
default=True,
old_field_name='mac_thincastRdpFile',
)
mac_custom_parameters = gui.TextField(
label=_('Custom parameters'),
order=51,
order=52,
tooltip=_(
'If not empty, extra parameter to include for Mac OS X Freerdp Client (for example /usb:id,dev:054c:0268, or aything compatible with your xfreerdp client)'
),

View File

@@ -295,8 +295,8 @@ class RDPFile:
# Camera?
# res += 'camerastoredirect:s:*\n'
# If target is windows, add customParameters
if self.target == types.os.KnownOS.WINDOWS:
# If target is windows or linux or macOS, add customParameters
if self.target == types.os.KnownOS.WINDOWS or self.target == types.os.KnownOS.LINUX or self.target == types.os.KnownOS.MAC_OS:
if self.custom_parameters and self.custom_parameters.strip() != '':
res += self.custom_parameters.strip() + '\n'

View File

@@ -114,6 +114,9 @@ class TRDPTransport(BaseRDPTransport):
lnx_custom_parameters = BaseRDPTransport.lnx_custom_parameters
mac_custom_parameters = BaseRDPTransport.mac_custom_parameters
wnd_custom_parameters = BaseRDPTransport.wnd_custom_parameters
lnx_use_rdp_file = BaseRDPTransport.lnx_use_rdp_file
mac_use_rdp_file = BaseRDPTransport.mac_use_rdp_file
# optimizeTeams = BaseRDPTransport.optimizeTeams
def initialize(self, values: 'types.core.ValuesType') -> None:
@@ -201,18 +204,25 @@ class TRDPTransport(BaseRDPTransport):
}
)
elif os.os == types.os.KnownOS.LINUX:
r.custom_parameters = self.lnx_custom_parameters.value
if self.lnx_use_rdp_file.as_bool():
r.custom_parameters = self.wnd_custom_parameters.value
else:
r.custom_parameters = self.lnx_custom_parameters.value
sp.update(
{
'as_new_xfreerdp_params': r.as_new_xfreerdp_params,
'as_file': r.as_file if self.lnx_use_rdp_file.as_bool() else '',
}
)
elif os.os == types.os.KnownOS.MAC_OS:
r.custom_parameters = self.mac_custom_parameters.value
if self.mac_use_rdp_file.as_bool():
r.custom_parameters = self.wnd_custom_parameters.value
else:
r.custom_parameters = self.mac_custom_parameters.value
sp.update(
{
'as_new_xfreerdp_params': r.as_new_xfreerdp_params,
'as_file': r.as_file if self.mac_allow_msrdc.as_bool() else '',
'as_file': r.as_file if self.mac_use_rdp_file.as_bool() else '',
'as_rdp_url': r.as_rdp_url if self.mac_allow_msrdc.as_bool() else '',
}
)

View File

@@ -1,4 +1,12 @@
import typing
import logging
import subprocess
import os.path
import shutil
import os
logger = logging.getLogger(__name__)
# On older client versions, need importing globally to allow inner functions to work
import subprocess # type: ignore
@@ -14,38 +22,105 @@ if 'sp' not in globals():
globals()['sp'] = sp # type: ignore # pylint: disable=undefined-variable
def exec_udsrdp(udsrdp: str) -> None:
import subprocess
import os.path
def _prepare_rdp_file(theFile: str, extension: str = '.rdp') -> str:
"""Save RDP file to user's home directory with the given extension and return its path."""
filename = tools.saveTempFile(theFile)
home_dir = os.path.expanduser("~")
base_name = os.path.basename(filename)
dest_filename = os.path.join(home_dir, base_name + extension)
temp_rdp_filename = filename + extension
logger.debug(f'Renaming temp file {filename} to {temp_rdp_filename}')
os.rename(filename, temp_rdp_filename)
logger.debug(f'Moving temp file {temp_rdp_filename} to {dest_filename}')
shutil.move(temp_rdp_filename, dest_filename)
logger.debug(f'RDP file content (forced): {theFile}')
return dest_filename
params: typing.List[str] = [os.path.expandvars(i) for i in [udsrdp] + sp['as_new_xfreerdp_params'] + ['/v:{}'.format(sp['address'])]] # type: ignore
def _exec_client_with_params(executable: str, params: typing.List[str], unlink_file: typing.Optional[str] = None) -> None:
logger.info(f'Executing {executable} with params: {params}')
tools.addTaskToWait(subprocess.Popen(params))
if unlink_file:
tools.addFileToUnlink(unlink_file)
def exec_udsrdp(udsrdp: str) -> None:
params = [os.path.expandvars(i) for i in [udsrdp] + sp['as_new_xfreerdp_params'] + [f'/v:{sp["address"]}']] # type: ignore
_exec_client_with_params(udsrdp, params)
def exec_new_xfreerdp(xfreerdp: str) -> None:
import subprocess # @Reimport
import os.path
if sp.get('as_file', ''): # type: ignore
dest_filename = _prepare_rdp_file(sp['as_file'], '.uds.rdp') # type: ignore
params = [xfreerdp, dest_filename, f'/p:{sp.get("password", "")}'] # type: ignore
_exec_client_with_params(xfreerdp, params, unlink_file=dest_filename)
else:
params = [os.path.expandvars(i) for i in [xfreerdp] + sp['as_new_xfreerdp_params'] + [f'/v:{sp["address"]}']] # type: ignore
_exec_client_with_params(xfreerdp, params)
params: typing.List[str] = [os.path.expandvars(i) for i in [xfreerdp] + sp['as_new_xfreerdp_params'] + ['/v:{}'.format(sp['address'])]] # type: ignore
tools.addTaskToWait(subprocess.Popen(params))
def exec_thincast(thincast: str) -> None:
if sp.get('as_file', ''): # type: ignore
dest_filename = _prepare_rdp_file(sp['as_file'], '.rdp') # type: ignore
params = [thincast, dest_filename, f'/p:{sp.get("password", "")}'] # type: ignore
_exec_client_with_params(thincast, params, unlink_file=dest_filename)
else:
params = [os.path.expandvars(i) for i in [thincast] + sp['as_new_xfreerdp_params'] + [f'/v:{sp["address"]}']] # type: ignore
_exec_client_with_params(thincast, params)
# Typical Thincast Routes on Linux
thincast_list = [
'/usr/bin/thincast-remote-desktop-client',
'/usr/bin/thincast',
'/opt/thincast/thincast-remote-desktop-client',
'/opt/thincast/thincast',
'/snap/bin/thincast-remote-desktop-client',
'/snap/bin/thincast',
'/snap/bin/thincast-client'
]
# Try to locate a xfreerdp and udsrdp. udsrdp will be used if found.
xfreerdp: typing.Optional[str] = tools.findApp('xfreerdp3') or tools.findApp('xfreerdp') or tools.findApp('xfreerdp2')
udsrdp: typing.Optional[str] = tools.findApp('udsrdp')
fnc, app = None, None
# Search Thincast first
executable = None
kind = ''
for thincast in thincast_list:
if os.path.isfile(thincast) and os.access(thincast, os.X_OK):
executable = thincast
kind = 'thincast'
break
if xfreerdp:
fnc, app = exec_new_xfreerdp, xfreerdp
# If you don't find Thincast, search UDSRDP and XFREERDP
if not executable:
udsrdp: typing.Optional[str] = tools.findApp('udsrdp')
xfreerdp: typing.Optional[str] = tools.findApp('xfreerdp3') or tools.findApp('xfreerdp') or tools.findApp('xfreerdp2')
if udsrdp:
executable = udsrdp
kind = 'udsrdp'
elif xfreerdp:
executable = xfreerdp
kind = 'xfreerdp'
if udsrdp is not None:
fnc, app = exec_udsrdp, udsrdp
if app is None or fnc is None:
if not executable:
raise Exception(
'''<p>You need to have xfreerdp (>= 2.0) installed on your systeam, and have it your PATH in order to connect to this UDS service.</p>
'''<p>You need to have Thincast Remote Desktop Client or xfreerdp (>= 2.0) installed on your system, and have it in your PATH in order to connect to this UDS service.</p>
<p>Please, install the proper package for your system.</p>
<ul>
<li>Thincast: <a href="https://thincast.com/en/products/client">Download</a></li>
<li>xfreerdp: <a href="https://github.com/FreeRDP/FreeRDP">Download</a></li>
</ul>
'''
)
else:
logging.debug(f'RDP client found: {executable} of kind {kind}')
fnc(app)
# Execute the client found
if kind == 'thincast':
if isinstance(executable, str):
exec_thincast(executable)
else:
raise TypeError("Executable must be a string for exec_thincast")
elif kind == 'udsrdp':
if isinstance(executable, str):
exec_udsrdp(executable)
else:
raise TypeError("Executable must be a string for exec_udsrdp")
elif kind == 'xfreerdp':
if isinstance(executable, str):
exec_new_xfreerdp(executable)
else:
raise TypeError("Executable must be a string for exec_new_xfreerdp")

View File

@@ -1 +1 @@
fGtGXYFIwNgr7B2h23tZSTRTZZzuUjrRqphmqgpRAS+hQ3FKqZJIoNIO7qxHh2ibA9BUyMHN21mjQvtVvAnv7ic0HfYPfJQPGro/yAJooMIuZPvqZS6e1hOBdd50Z3FKuqHMyHvQZhMu9tdoE06gyArwcSE++PZoT8dptOhwm5ogSCf2yfPA+bPxm9ACC3OmHTvKjZExFlnWLec/idASdGBxWnqHoWrXpBR3N8V/CMS4/QZZ3I+e+hJ8I2Sz2hINH0X2TIVVr2CTe3j4TzkxkCDAC5JrRmgj35vkiaOHKpW6drRopLiOxE2DC4mshL0wwUw0wHExeP2W03tobSZpK8bRmNBe8s7bjUlQ7df2V0dB8W/G10ez3rIJnUGzeXhOjUy/f3T0KFP0wAJzQo1LNjTFfc3XEZq+IyqyIHjxDiN9yG/rsZP8vAZHc/kNCwNtuVjRE1K1hmUckVa5RD1TtATY8c5h2JkIL2pcPgFJvJh9s2CbK2VCfF2U3VUJu0icDqRREqY542/y84aZW7Pz+kIicBI6blwpCA6DxgCJ3fs/pgQYqueF+cJ7UteBEUaDOyvxvttYr02xio/izs7vRJsL0Gpve5WUHIl1+9QTuoDRi8w6l92AtrnNPN9QooOuS6VbrTW9up0nkHkUz3zm7QuXulennCFgO8je81FhxOk=
Jk6itcePhQyNebx65AQanMLRapBn/cfN066shvowwMWN5dKn+YONY1ydxObj1p645AWXYVoVMJNwxWrlL4orqo6VkPOVBZjzT0pWKplRHcdxVuAnCvNyiprjiLGSgm401kp5F4jSIIm+nADb2a0gdlADBwVok2+Dqps19ebP1j3hvX7f4/sUEIPAl9xGAS4MCrOuEde8SnGvDnLJfTGzq5Kt4guSEhM81s037PK/RgkQQIxBIyeinTmaklFsMhmnKI9csQ5QW7Zkig5BSwARe/m2SWuf/WJfFE9L9l5ObroXjw4wV4MLp1EpWusVwjel+OEeNdU7lanKxxlqBXPwqeVRDyh1ag43O30TQHWkdAZOdFYGUxL7ft6AVuAewMwNp76px65M00dYL5QbQ2PXPmuAjGnZskTshCvyo82G5kYJgXireHieim8eW0fULA5P4PHv/dH1+RwhuO71An1tq34ngeOVFkskIWmvDQqlbkoHz6Bfk2JhkLDU88voa1toxeRXm1ZXdRPdUPrD91HRvEOUTlm4VJxiYiEWOH4jJGy/na6EKAFm4rM+xptAebp73y6KQbcwBZRocRKpMvnd3EG3qvV2l37HMTJyg8Rn7an9yad3j6iFq3tKBeTp8ExNo0bPUV4UV+ymDzA49f5DsizJxu6+qCT/Ee4O4sMotok=

View File

@@ -1,4 +1,11 @@
import typing
import shutil
import os
import logging
import subprocess
import os.path
logger = logging.getLogger(__name__)
# On older client versions, need importing globally to allow inner functions to work
import subprocess # type: ignore
@@ -20,40 +27,75 @@ if 'sp' not in globals():
# Inject local passed sp into globals for inner functions if not already there
globals()['sp'] = sp # type: ignore # pylint: disable=undefined-variable
def _prepare_rdp_file(theFile: str, port: int, extension: str = '.rdp') -> str:
"""Save RDP file to user's home directory with the given extension and return its path."""
# Replace the address in the RDP file with 127.0.0.1:{port}
# Replace any line starting with "full address:s:" with the desired value
theFile = theFile.format(
address='127.0.0.1:{}'.format(port)
)
logger.info(f'Preparing RDP file with address 127.0.0.1:{port}')
logger.debug(f'RDP file content (forced): {theFile}')
filename = tools.saveTempFile(theFile)
home_dir = os.path.expanduser("~")
base_name = os.path.basename(filename)
dest_filename = os.path.join(home_dir, base_name + extension)
temp_rdp_filename = filename + extension
logger.debug(f'Renaming temp file {filename} to {temp_rdp_filename}')
os.rename(filename, temp_rdp_filename)
logger.debug(f'Moving temp file {temp_rdp_filename} to {dest_filename}')
shutil.move(temp_rdp_filename, dest_filename)
logger.debug(f'RDP file content (forced): {theFile}')
return dest_filename
def _exec_client_with_params(executable: str, params: typing.List[str], unlink_file: typing.Optional[str] = None) -> None:
logger.info(f'Executing {executable} with params: {params}')
tools.addTaskToWait(subprocess.Popen(params))
if unlink_file:
tools.addFileToUnlink(unlink_file)
def exec_udsrdp(udsrdp: str, port: int) -> None:
import subprocess # @Reimport
import os.path
params: typing.List[str] = [os.path.expandvars(i) for i in [udsrdp] + sp['as_new_xfreerdp_params'] + ['/v:127.0.0.1:{}'.format(port)]] # type: ignore
tools.addTaskToWait(subprocess.Popen(params))
logging.debug('UDSRDP client will use command line parameters')
params: typing.List[str] = [os.path.expandvars(i) for i in [app] + sp['as_new_xfreerdp_params'] + [f'/v:127.0.0.1:{port}']] # type: ignore
_exec_client_with_params(udsrdp, params)
def exec_new_xfreerdp(xfreerdp: str, port: int) -> None:
import subprocess # @Reimport
import os.path
if sp.get('as_file', ''): # type: ignore
logger.debug('XFREERDP client will use RDP file')
dest_filename = _prepare_rdp_file(sp['as_file'], port, '.rdp') # type: ignore
params = [xfreerdp, dest_filename, f'/p:{sp.get("password", "")}'] # type: ignore
_exec_client_with_params(xfreerdp, params, unlink_file=dest_filename)
else:
logging.debug('XFREERDP client will use command line parameters')
params: typing.List[str] = [os.path.expandvars(i) for i in [app] + sp['as_new_xfreerdp_params'] + [f'/v:127.0.0.1:{port}']] # type: ignore
_exec_client_with_params(xfreerdp, params)
params: typing.List[str] = [os.path.expandvars(i) for i in [xfreerdp] + sp['as_new_xfreerdp_params'] + ['/v:127.0.0.1:{}'.format(port)]] # type: ignore
tools.addTaskToWait(subprocess.Popen(params))
def exec_thincast(thincast: str, port: int) -> None:
if sp.get('as_file', ''): # type: ignore
logger.debug('Thincast client will use RDP file')
dest_filename = _prepare_rdp_file(sp['as_file'], port, '.rdp') # type: ignore
params = [thincast, dest_filename, f'/p:{sp.get("password", "")}'] # type: ignore
_exec_client_with_params(thincast, params, unlink_file=dest_filename)
else:
logging.debug('Thincast client will use command line parameters')
params: typing.List[str] = [os.path.expandvars(i) for i in [app] + sp['as_new_xfreerdp_params'] + [f'/v:127.0.0.1:{port}']] # type: ignore
_exec_client_with_params(thincast, params)
# Try to locate a xfreerdp and udsrdp. udsrdp will be used if found.
xfreerdp: typing.Optional[str] = tools.findApp('xfreerdp3') or tools.findApp('xfreerdp') or tools.findApp('xfreerdp2')
udsrdp = tools.findApp('udsrdp')
fnc, app = None, None
if xfreerdp:
fnc, app = exec_new_xfreerdp, xfreerdp
if udsrdp:
fnc, app = exec_udsrdp, udsrdp
if app is None or fnc is None:
raise Exception(
'''<p>You need to have xfreerdp (>= 2.0) installed on your systeam, and have it your PATH in order to connect to this UDS service.</p>
<p>Please, install the proper package for your system.</p>
'''
)
# Add thinclast support
thincast_list = [
'/usr/bin/thincast-remote-desktop-client',
'/usr/bin/thincast',
'/opt/thincast/thincast-remote-desktop-client',
'/opt/thincast/thincast',
'/snap/bin/thincast-remote-desktop-client',
'/snap/bin/thincast',
'/snap/bin/thincast-client'
]
thincast_executable = None
for thincast in thincast_list:
if os.path.isfile(thincast) and os.access(thincast, os.X_OK):
thincast_executable = thincast
break
# Open tunnel and connect
fs = forward(remote=(sp['tunHost'], int(sp['tunPort'])), ticket=sp['ticket'], timeout=sp['tunWait'], check_certificate=sp['tunChk']) # type: ignore
@@ -64,4 +106,29 @@ if fs.check() is False:
'<p>Could not connect to tunnel server.</p><p>Please, check your network settings.</p>'
)
fnc(app, fs.server_address[1])
# If thincast exists, use it. If not, continue with UDSRDP/XFREERDP as before
if thincast_executable:
logging.debug('Thincast client found, using it')
#logging.debug(f'RDP file params: {sp.get("as_file", "")}')
fnc, app = exec_thincast, thincast_executable
else:
xfreerdp: typing.Optional[str] = tools.findApp('xfreerdp3') or tools.findApp('xfreerdp') or tools.findApp('xfreerdp2')
udsrdp = tools.findApp('udsrdp')
fnc, app = None, None
if xfreerdp:
fnc, app = exec_new_xfreerdp, xfreerdp
if udsrdp:
fnc, app = exec_udsrdp, udsrdp
if app is None or fnc is None:
raise Exception(
'''<p>You need to have Thincast Remote Desktop Client o xfreerdp (>= 2.0) installed on your system, y tenerlo en tu PATH para conectar con este servicio UDS.</p>
<p>Please install the right package for your system.</p>
<ul>
<li>Thincast: <a href="https://thincast.com/en/products/client">Download</a></li>
<li>xfreerdp: <a href="https://github.com/FreeRDP/FreeRDP">Download</a></li>
</ul>
'''
)
if fnc is not None and app is not None:
fnc(app, fs.server_address[1])

View File

@@ -1 +1 @@
Tqjv2NoZjghIsF/sAHDgawag9vu/A+YEmr59t6HzER8TB13/UqZnCPJpyJrOL643h+ssN51HEoBjj3fAYBlleNjmr1Nkle9/VaFWNhuruRQDMUp41GoGzICkF4dNMWhdEgpZ80xGxcv+0AZzmo4eCZsIAZjMNDYJKq7N/M77I2kl++K79VU9OB/npjArVKUBiROvS5Y6E9dOXhUAFgpO5zKLPPLuFzdLyNnGpIOmu3ei5bG8arVq8S1nF4aBjVXgSP9bZ15VKHgT5vBkOIK0TIKKS+qfP7Hb1+QqxLeV5BKeVn4jLYBj+TZmqtUmIJQG226PdzsFTnjpPH33twdhTFFb8aCyoNma/U+xOOJZNSqouUPQV7KvHQuzYqrLB9Zg1rA9O2cz/sp4seePfrit8dw+H8Wq4dAlvgB05/zfoVMhttjuYuI46C69XrK5M8SFsnyD7faFRwNqZtdeJt7XwYtg1/TUMH6JM4UxTuL49aXjJQ3aOrNA9r6ukNe7lPMEbGBx4mtlGqlg+2ZCbqG1HZRITnUfuHy5GklU2scBGoEgoz6YeguGAgNbiwLohrWYAA1IGZEhSJc7Fx+nn5IRuYJdCof2dO/o0ntGi4pfVDL91sqmafEn/f90A5lBsoFotlbSG67tI7CcWs+QR0P0T6KspBv/TIFSNu/Igs1c0hY=
rHy0885L4XPOrWLCKgn/A+7LYfPpuymRo5Pq3zcDKpW9lUmBdBryTJAGYuaEDiJWq3+Lp0y3W5GIhE11xHtR/XpF70GW/GW7YGRgbDvYlvDz06jNoQWi6tzFnAsA+cvcVPd18PLaC6rDWJpx55PEa8Aq+kqZeGIjiLBRFeTMiv4BXGz60ba9vNNfn+dCHbqOXjXk+zOKHkaGE3hHz3CIrb3IkLYI0KfEODgySZs39yNqfcnRmFgdnH3sM+V09ZUm5hJNgP41Nc8UJgwidBwQXnZXMk0VQMv5BBX9NWdnwuO/nI0JKanpnWd0QLQ991i4TDjWDkchXVWAIwune7djgKy9b0yVkfWYxykJXT2EEzBtSGqm4NETj4q3BIzHJNSaL2HZkPSY9/5olTCaPZY5JfmUPVNJK8wSMzSTPeinn1NkGfSQDgcc28yu6M+/xSrQV6XB9kt9k5Q0TacXfONr5q5F4ReDqasD/EPLJdcVc0f5DI+t7+/no+xNk+f0NrfmjvTNKTqCmTBaJXC+gX72rzTQslxyFokIPJN+luLYasZureRFHCnXlrQn579B4eLQ1+QcPVFKU0SZChkbNbBgpTABkAI3j9Kg41NLp5f1/Sx28ACTW3cSRJbNQKe3s5XXCCn1Fg5XC49MinrSwNk34HfafgQ+X/G1HNqeydtHn1M=

View File

@@ -2,6 +2,9 @@ import typing
import shutil
import os
import os.path
import logging
logger = logging.getLogger(__name__)
# On older client versions, need importing globally to allow inner functions to work
import subprocess # type: ignore
@@ -43,7 +46,7 @@ msrdc_list = [
]
thincast_list = [
'/Applications/Thincast Remote Desktop Client.app/Contents/MacOS/Thincast Remote Desktop Client',
'/Applications/Thincast Remote Desktop Client.app',
]
xfreerdp_list = [
@@ -58,21 +61,28 @@ executable = None
kind = ''
# Check first thincast (better option right now, prefer it)
logger.debug('Searching for Thincast in: %s', thincast_list)
for thincast in thincast_list:
if os.path.isfile(thincast):
if os.path.isdir(thincast):
logger.debug('Thincast found: %s', thincast)
executable = thincast
kind = 'thincast'
break
if not executable:
logger.debug('Searching for xfreerdp in: %s', xfreerdp_list)
found_xfreerdp = False
for xfreerdp_executable in xfreerdp_list:
xfreerdp: str = tools.findApp(xfreerdp_executable)
if xfreerdp and os.path.isfile(xfreerdp):
executable = xfreerdp
xfreerdp = tools.findApp(xfreerdp_executable) # type: ignore
logger.debug('tools.findApp(%s) result: %s', xfreerdp_executable, xfreerdp) # type: ignore
if xfreerdp and os.path.isfile(xfreerdp): # type: ignore
logger.debug('xfreerdp found: %s', xfreerdp) # type: ignore
executable = xfreerdp # type: ignore
# Ensure that the kind is 'xfreerdp' and not 'xfreerdp3' or 'xfreerdp2'
kind = xfreerdp_executable.rstrip('3').rstrip('2')
break
else:
if not found_xfreerdp:
logger.debug('Searching for MSRDC in: %s', msrdc_list)
for msrdc in msrdc_list:
if os.path.isdir(msrdc) and sp['as_file']: # type: ignore
executable = msrdc
@@ -80,6 +90,7 @@ if not executable:
break
if not executable:
logger.debug('No compatible executable found (Thincast, xfreerdp, MSRDC)')
msrd = msrd_li = ''
if sp['as_rdp_url']: # type: ignore
msrd = ', Microsoft Remote Desktop'
@@ -110,26 +121,90 @@ if not executable:
'''
)
logger.debug('Using %s client of kind %s', executable, kind) # type: ignore
if kind == 'msrdc':
theFile = sp['as_file'] # type: ignore
filename = tools.saveTempFile(theFile)
filename = tools.saveTempFile(theFile) # type: ignore
# Rename as .rdp, so open recognizes it
shutil.move(filename, filename + '.rdp')
shutil.move(filename, filename + '.rdp') # type: ignore
# tools.addTaskToWait(subprocess.Popen(['open', filename + '.rdp']))
# Force MSRDP to be used with -a (thanks to Dani Torregrosa @danitorregrosa (https://github.com/danitorregrosa) )
tools.addTaskToWait(
# Force MSRDP to be used with -a (thanks to Dani Torregrosa @danitorregrosa (https://github.com/danitorregrosa))
tools.addTaskToWait( # type: ignore
subprocess.Popen(
[
'open',
'-a',
executable,
filename + '.rdp',
]
] # type: ignore
)
)
tools.addFileToUnlink(filename + '.rdp')
else: # thincast, udsrdp, freerdp
tools.addFileToUnlink(filename + '.rdp') # type: ignore
if kind == 'thincast':
if sp['as_file']: # type: ignore
logger.debug('Opening Thincast with RDP file %s', sp['as_file']) # type: ignore
theFile = sp['as_file'] # type: ignore
filename = tools.saveTempFile(theFile) # type: ignore
# # add to file the encrypted password for RDP
# import win32crypt
# import binascii
# def encrypt_password_rdp(plain_text_password):
# # Convert password to UTF-16-LE (Unicode string used by RDP)
# data = plain_text_password.encode('utf-16-le')
# # Encrypt with DPAPI (CryptProtectData)
# encrypted_data = win32crypt.CryptProtectData(data, None, None, None, None, 0)
# # Convert bytes to hexadecimal for RDP
# encrypted_hex = binascii.hexlify(encrypted_data).decode('ascii')
# return encrypted_hex
# filename_handle = open(filename, 'a') # type: ignore
# if sp.get('password', ''): # type: ignore
# encrypted_password = encrypt_password_rdp(sp["password"])
# filename_handle.write(f'password 51:b:{encrypted_password}\n') # type: ignore
# filename_handle.close()
# add to file the password without encryption (Thincast will encrypt it)
filename_handle = open(filename, 'a') # type: ignore
if sp.get('password', ''): # type: ignore
filename_handle.write(f'password 51:b:{sp["password"]}\n') # type: ignore
filename_handle.close()
# Rename as .rdp, so open recognizes it
shutil.move(filename, filename + '.rdp') # type: ignore
params = [ # type: ignore
'open',
'-a',
executable,
filename + '.rdp', # type: ignore
]
logger.debug('Opening Thincast with RDP file with params: %s', ' '.join(params)) # type: ignore
tools.addTaskToWait( # type: ignore
subprocess.Popen(params) # type: ignore
)
tools.addFileToUnlink(filename + '.rdp') # type: ignore
else:
logger.debug('Opening Thincast with xfreerdp parameters')
# Fix resolution...
try:
xfparms = fix_resolution()
except Exception as e:
xfparms = list(map(lambda x: x.replace('#WIDTH#', '1400').replace('#HEIGHT#', '800'), sp['as_new_xfreerdp_params'])) # type: ignore
params = [ # type: ignore
'open',
'-a',
executable,
'--args',
] + [os.path.expandvars(i) for i in xfparms + ['/v:{}'.format(sp['address'])]] # type: ignore
#logger.debug('Executing: %s', ' '.join(params))
subprocess.Popen(params) # type: ignore
else: # for now, both xfreerdp or udsrdp
# Fix resolution...
try:
xfparms = fix_resolution()
@@ -137,4 +212,5 @@ else: # thincast, udsrdp, freerdp
xfparms = list(map(lambda x: x.replace('#WIDTH#', '1400').replace('#HEIGHT#', '800'), sp['as_new_xfreerdp_params'])) # type: ignore
params = [os.path.expandvars(i) for i in [executable] + xfparms + ['/v:{}'.format(sp['address'])]] # type: ignore
subprocess.Popen(params)
logger.debug('Executing: %s', ' '.join(params)) # type: ignore
subprocess.Popen(params) # type: ignore

View File

@@ -1 +1 @@
kIL9OXr/AnhhqE+5ln8YpmUhgCDxK1r7yuHjb5j+n1VJOLHtJwH0Rm5Dh5SrhNkCFo2suHTVb0f1nbkQ4mkzlszBNaCSvLkwxpTIZrO8P9aOpEJDmq5aGRHNHLPNzmhBzUfhK+ILGlsCElJzP/19kNxifoanH9xyZkybHAuxNy4QjRt8dkIZkxj0qxsRFRAt8R2yZARdjUkfufiztNAdTuHLVUG+JF5OnTQDJi6vIqOrbPiAIn/vweHuL4zFH4UJXHipvgXJiO9nq4ZvXCAy9+ASxEDG5ql/iwOFoQyGekXK5XtkSbT9F+fDejVoxKP/qp4lIrEHQ/Y9WMJnpGBt2ko7FeyDP8msx45svLQYBNeqxGqIUi0yzfzktglLfexInisHBtz0lb7Uuz/mdPscUoKU+j/5/ZgFS0yFD+NelFmG1q52X7ndkXUXaLb3tPtNDd1ZdgsyEkTiVuo3jW+kT1SWlAeK+YEyZinC8/Df7PLCX7fOl+WPm8MAnncfmbHO654HHgYiqDh+I93IqqReytepxWgK+bwxdGz/z0BPzLXSCy2im6UjEdNFSFRBEywXAdMTxgC2YD2XOl7SHAtcPT2kpgJtlxT1V7+wwv2tyzWf0bL0GA0EIDEAP6cga+l4wJGSIDnYnxiJHaF6aF3XYL8yh4ghHTD7IAJFpU6vWjM=
rP6Eur9PlTONUNLjIRAVL/CdtT7ATNYC8l0AzvU57tqyFDFa/C8nNyq3Aaepf+SSYaYzxg9TnWUge8jpcnM20ERV6H2IA2aN3Hrg0+q76OPNlH1UmygyT1+UxxccPemnGAcVVnBXOHwONHvpE8FqdOFZn6P2CWWojOLUMB2yj/kO0l+bZDmDRlihg5sIpSd4Wkt4ezyz9j7Cjsz6JuFDQjVdaIDEFeGcqfEJIDKlpIY6GJgJYbGMx0C0uayNtQlFO653EcS7mnXhlIQwGg4YJl3fjKksjDWL2H65MsddRvZubIIrBU6jQnIj2W+gl1/xT8mRom48SogBJWzjzjT/X7sN6QRfvKCMfLwhfqHw7p0MYVV1Tcpjzn1sFMyrR4zPXGaH80+2hn9yf2HGVb6QVmir0x0VKRy0eQAEqYtb3TeMU0lmXShkuSogiOfdpqd65NKpboUuv/cVttpa8qzZhroBQXyufSEi1gmVTc6tp2PeQIXFZLrL6SOP263HXOWPirmIuLri8k3qK2L4BiuD7ZTiwursqCytoFjBCVpWPhnI3c6Q81WpzESBs6E8Kyvanr/jMX4T95i9m/kZBdLELLA7uj2dTaxsdUHJrs1fO7/hMGdgxdmWzXwXJX9VzJ+ZyF69KP0w4oZd+bazxFK0aaHqttxS2ZjATJ5rlOARtzs=

View File

@@ -3,6 +3,9 @@ import typing
import shutil
import os
import os.path
import logging
logger = logging.getLogger(__name__)
# On older client versions, need importing globally to allow inner functions to work
import subprocess # type: ignore
@@ -50,7 +53,7 @@ msrdc_list = [
]
thincast_list = [
'/Applications/Thincast Remote Desktop Client.app/Contents/MacOS/Thincast Remote Desktop Client',
'/Applications/Thincast Remote Desktop Client.app',
]
xfreerdp_list = [
@@ -65,25 +68,31 @@ executable = None
kind = ''
# Check first thincast (better option right now, prefer it)
logger.debug('Searching for Thincast in: %s', thincast_list)
for thincast in thincast_list:
if os.path.isfile(thincast):
if os.path.isdir(thincast):
executable = thincast
kind = 'thincast'
logger.debug('Found Thincast client at %s', thincast)
break
if not executable:
logger.debug('Searching for xfreerdp in: %s', xfreerdp_list)
for xfreerdp_executable in xfreerdp_list:
xfreerdp: str = tools.findApp(xfreerdp_executable)
if xfreerdp and os.path.isfile(xfreerdp):
executable = xfreerdp
xfreerdp: str = tools.findApp(xfreerdp_executable) # type: ignore
if xfreerdp and os.path.isfile(xfreerdp): # type: ignore
executable = xfreerdp # type: ignore
# Ensure that the kind is 'xfreerdp' and not 'xfreerdp3' or 'xfreerdp2'
kind = xfreerdp_executable.rstrip('3').rstrip('2')
logger.debug('Found xfreerdp client: %s (kind: %s)', xfreerdp, kind) # type: ignore
break
else:
logger.debug('Searching for Microsoft Remote Desktop in: %s', msrdc_list)
for msrdc in msrdc_list:
if os.path.isdir(msrdc) and sp['as_file']: # type: ignore
executable = msrdc
kind = 'msrdc'
logger.debug('Found Microsoft Remote Desktop client at %s', msrdc)
break
if not executable:
@@ -91,6 +100,7 @@ if not executable:
if sp['as_rdp_url']: # type: ignore
msrd = ', Microsoft Remote Desktop'
msrd_li = '<li><p><b>{}</b> from Apple Store</p></li>'.format(msrd)
logger.debug('as_rdp_url is set, will suggest Microsoft Remote Desktop')
raise Exception(
f'''<p><b>xfreerdp{msrd} or thincast client not found</b></p>
@@ -119,38 +129,90 @@ if not executable:
# Open tunnel
fs = forward(remote=(sp['tunHost'], int(sp['tunPort'])), ticket=sp['ticket'], timeout=sp['tunWait'], check_certificate=sp['tunChk']) # type: ignore
address = '127.0.0.1:{}'.format(fs.server_address[1])
address = '127.0.0.1:{}'.format(fs.server_address[1]) # type: ignore
# Check that tunnel works..
if fs.check() is False:
if fs.check() is False: # type: ignore
logger.debug('Tunnel check failed, could not connect to tunnel server')
raise Exception('<p>Could not connect to tunnel server.</p><p>Please, check your network settings.</p>')
else:
logger.debug('Tunnel check succeeded, connection to tunnel server established')
logger.debug('Using %s client of kind %s', executable, kind) # type: ignore
if kind == 'msrdc':
theFile = theFile = sp['as_file'].format(address=address) # type: ignore
filename = tools.saveTempFile(theFile)
filename = tools.saveTempFile(theFile) # type: ignore
# Rename as .rdp, so open recognizes it
shutil.move(filename, filename + '.rdp')
shutil.move(filename, filename + '.rdp') # type: ignore
# tools.addTaskToWait(subprocess.Popen(['open', filename + '.rdp']))
# Force MSRDP to be used with -a (thanks to Dani Torregrosa @danitorregrosa (https://github.com/danitorregrosa) )
tools.addTaskToWait(
tools.addTaskToWait( # type: ignore
subprocess.Popen(
[
'open',
'-a',
executable,
filename + '.rdp',
]
)
] # type: ignore
)
)
tools.addFileToUnlink(filename + '.rdp')
else: # freerdp, thincast or udsrdp
tools.addFileToUnlink(filename + '.rdp') # type: ignore
if kind == 'thincast':
if sp['as_file']: # type: ignore
logger.debug('Opening Thincast with RDP file %s', sp['as_file']) # type: ignore
theFile = sp['as_file'] # type: ignore
theFile = theFile.format( # type: ignore
address='{}'.format(address)
)
filename = tools.saveTempFile(theFile) # type: ignore
# filename_handle = open(filename, 'a') # type: ignore
# if sp.get('password', ''): # type: ignore
# filename_handle.write(f'password 51:b:{sp["password"]}\n') # type: ignore
# filename_handle.close()
# Rename as .rdp, so open recognizes it
shutil.move(filename, filename + '.rdp') # type: ignore
# show filename content in log for debug
with open(filename + '.rdp', 'r') as f: # type: ignore
logger.debug('RDP file content:\n%s', f.read()) # type: ignore
params = [ # type: ignore
'open',
'-a',
executable,
filename + '.rdp', # type: ignore
]
logger.debug('Opening Thincast with RDP file with params: %s', ' '.join(params)) # type: ignore
tools.addTaskToWait( # type: ignore
subprocess.Popen(params) # type: ignore
)
tools.addFileToUnlink(filename + '.rdp') # type: ignore
else:
logger.debug('Opening Thincast with xfreerdp parameters')
# Fix resolution...
try:
xfparms = fix_resolution()
except Exception as e:
xfparms = list(map(lambda x: x.replace('#WIDTH#', '1400').replace('#HEIGHT#', '800'), sp['as_new_xfreerdp_params'])) # type: ignore
params = [ # type: ignore
'open',
'-a',
executable,
'--args',
] + [os.path.expandvars(i) for i in xfparms + ['/v:{}'.format(address)]] # type: ignore
#logger.debug('Executing: %s', ' '.join(params))
subprocess.Popen(params) # type: ignore
else: # freerdp or udsrdp
# Fix resolution...
try:
xfparms = fix_resolution()
except Exception as e:
xfparms = list(map(lambda x: x.replace('#WIDTH#', '1400').replace('#HEIGHT#', '800'), sp['as_new_xfreerdp_params'])) # type: ignore
params = [os.path.expandvars(i) for i in [executable] + xfparms + ['/v:{}'.format(address)]]
subprocess.Popen(params)
params = [os.path.expandvars(i) for i in [executable] + xfparms + ['/v:{}'.format(address)]] # type: ignore
subprocess.Popen(params) # type: ignore

View File

@@ -1 +1 @@
fLDRyAJrjER+znvjbhAuUa+XJ0itaLRspgQzk5AKMz/YpNZgdaHBtOw25XUsYrszsjSq6AIPB2VnT45Bhg7/GB8CRpTuCkpeoDpUA2rNR24DW7i5urmhjfPjtuMmSOBKPdXkiYPIhavFeD+kRWrrm6X1lolTnGz+pie2IXxsEhsot4gbg6eOv1ieloRHnTptO2qRr99I35BmSTxCKnyhrV6AWJBb6lHhuT04fhk5+X/ZA4kQewaI0ncqBdsqZoxcdSDgiOllF8kUsDgBL91wpYAB9s5eQpnI5VUPY/7Gzd47guRNCf24EHswd1lJrGAGta1p/e3hvtU0whdaaAiuhG/U6zHhpDpE5SPV66MuPfo1kKyIeNTMTC50Dkahu+j19cVbubdSJIUj42+nXKHDy5Bojotd/IYYSSR68K0eblpHDId7YxXpYmKY2QyA2cWDUrGD4glRWinKR/Hoi1oNuuBItMQUcDCPxxPWks4OsYqwpbAAkdFYRXzUR18TKndtwmHKox5AfXuQkuHpICRRnEMzD5pfNerUtEk3Zy0BHZYtoD7BQu2OEh9e3jC5bRHaQvf/sGBGMWfKchBwtPiBKlji6nPw6pqLPW5npD3GHYkcxI8tgzlTzIZS6YkOXDI8lHMYV5lvHxOPYFrAecfNpK8pj2uAeGoEFvBRmZZG79g=
NKfVtsTQWErfjpdrXd7qGLp6c0ScKMcbj4o5QTDAfQfDEuuj1/Qcg9gx+1NCyIF0hxy9ZIKbvwaYBYu9rARxz3XYsidxpgsnhZPyshdPN236M+zRo9SBFY3Ug0aNBZSewSZ6MSfrCZkMUW0NJOOpGu41KQNUVE5+DciC618rMoD0V//zJhz4SFy7dscjLg8cm69KNS6jC1trJkX7Ep19TF5DG6s0P9lQGaMLSj2UYTsF5gaZZY1jwZcCSw0QGrXon/mxR4i/t3BiARekyUB/ygM5DfzG5BMnQIMtLPNZu89vdJ+vAU5bUq1MrQjpAI3dzhdJwiUWTSVbQ3rHAoQHyBL4yE8bvuLIB0sUD5Jqd3EkTlVBJ0mmo1dHJZj0dzbXd5cHDlK+0Ms+vDSm/Qw8rqKatSBZlavcdQ+ScIEHlj0bEf6teKD3VsMUKFDxpdIBivkSz0KIZW+HXQiiFprdeN5tnAVj8ItlxFZlItNw3Zz6CrNqHpfSdyW0uzmuN3mD1iyQhkW6+JzWC+G9hmMY9X7eFv/xzwXQHUiB8rrt5QK7PJHlK1uRC2MFQRg7s83bYeZBEhAynuyjwLAT6mo+RsINgqF3hkNUHfh7E4IvK09ynl5Dnv3ypAfYKGvlWbpH8Moo+Pp45r+8sQPa83br9+y52I8JxDxqkdHLrzmXqc4=

View File

@@ -42,6 +42,7 @@ from uds.core.auths.auth import get_webpassword
from uds.core.managers.crypto import CryptoManager
from uds.core.managers.userservice import UserServiceManager
from uds.core.exceptions.services import (
InvalidServiceException,
MaxServicesReachedError,
ServiceAccessDeniedByCalendar,
ServiceNotReadyError,
@@ -466,6 +467,9 @@ def enable_service(
except ServiceAccessDeniedByCalendar:
logger.info('Access tried to a calendar limited access pool "%s"', service_id)
error = types.errors.Error.SERVICE_CALENDAR_DENIED.message
except InvalidServiceException as e:
logger.warning('Invalid service: %s', e)
error = types.errors.Error.INVALID_SERVICE.message
except Exception as e:
logger.exception('Error')
error = str(e)

View File

@@ -46,10 +46,10 @@ MAX_REMOVING_TIME = 3600 * 24 * 1 # 2 days, in seconds
class DeployedServiceInfoItemsCleaner(Job):
frecuency = 3607
frecuency_cfg = (
GlobalConfig.CLEANUP_CHECK
) # Request run cache "info" cleaner every configured seconds. If config value is changed, it will be used at next reload
frecuency = 600
# frecuency_cfg = (
# GlobalConfig.CLEANUP_CHECK
# ) # Request run cache "info" cleaner every configured seconds. If config value is changed, it will be used at next reload
friendly_name = 'Deployed Service Info Cleaner'
def run(self) -> None:

View File

@@ -62,7 +62,7 @@ class TestProxmoxClient(UDSTransactionTestCase):
hagroup: str = ''
def setUp(self) -> None:
v = vars.get_vars('proxmox_cluster')
v = vars.get_vars('proxmox9_alone')
if not v:
self.skipTest('No proxmox vars')
@@ -96,10 +96,13 @@ class TestProxmoxClient(UDSTransactionTestCase):
if self.storage.is_null():
self.skipTest('No valid storage found')
self.hagroup = v['test_ha_group']
# Ensure we have a valid pool, storage and ha group
if self.hagroup not in self.pclient.list_ha_groups():
self.skipTest('No valid ha group found')
if v.get('version', '8') < '9':
self.hagroup = v['test_ha_group']
# Ensure we have a valid pool, storage and ha group
if self.hagroup not in self.pclient.list_ha_groups():
self.skipTest('No valid ha group found')
else:
self.hagroup = ''
def _get_new_vmid(self) -> int:
MAX_RETRIES: typing.Final[int] = 512 # So we don't loop forever, just in case...
@@ -253,6 +256,9 @@ class TestProxmoxClient(UDSTransactionTestCase):
pass
def test_list_ha_groups(self) -> None:
if self.hagroup == '':
self.skipTest('No ha groups in this version of proxmox')
groups = self.pclient.list_ha_groups()
self.assertIsInstance(groups, list)
for group in groups:
@@ -261,15 +267,25 @@ class TestProxmoxClient(UDSTransactionTestCase):
self.assertIn(self.hagroup, groups)
def test_enable_disable_vm_ha(self) -> None:
# Should enable HA, but with no ha group because no ha groups are available
with self._create_test_vm() as vm:
self.pclient.enable_vm_ha(vm.id, started=False, group=self.hagroup)
# Ensure it's enabled
vminfo = self.pclient.get_vm_info(vm.id, force=True)
self.assertEqual(vminfo.ha.group, self.hagroup)
# Ensure it's enabled. Only works for version 9
ha_resources = self.pclient.list_ha_resources(force=True)
self.assertIn(f'vm:{vm.id}', ha_resources)
# On < 9, groups can be enabled. On 9 >, no ha groups exists
if self.hagroup:
vminfo = self.pclient.get_vm_info(vm.id, force=True)
self.assertEqual(vminfo.ha.group, self.hagroup)
# Disable it
self.pclient.disable_vm_ha(vm.id)
vminfo = self.pclient.get_vm_info(vm.id, force=True)
self.assertEqual(vminfo.ha.group, '')
ha_resources = self.pclient.list_ha_resources(force=True)
self.assertNotIn(f'vm:{vm.id}', ha_resources)
if self.hagroup:
vminfo = self.pclient.get_vm_info(vm.id, force=True)
self.assertEqual(vminfo.ha.group, '')
def test_set_vm_protection(self) -> None:
with self._create_test_vm() as vm: