mirror of
https://github.com/dkmstr/openuds.git
synced 2025-10-07 15:33:51 +03:00
Compare commits
49 Commits
dev/janier
...
dev/andres
Author | SHA1 | Date | |
---|---|---|---|
|
398ec1aac0 | ||
|
037b4abad1 | ||
|
839e4c6b1d | ||
|
2fd157e463 | ||
|
7e51c1fd93 | ||
|
b6af59cc44 | ||
|
50072e948e | ||
|
c5e0d0721f | ||
|
afbd4c5355 | ||
|
e4377b83e4 | ||
|
bf97c6f2dc | ||
|
6763de2bab | ||
|
b4ca743d7c | ||
|
20f7ae7fcd | ||
|
8aac4f9aa5 | ||
|
f494c706fc | ||
|
76b488dc1d | ||
|
826cc7aed8 | ||
|
4da15d66fe | ||
|
79495fc3b1 | ||
|
f438a9241e | ||
|
e37b345aff | ||
|
ce1330066f | ||
|
20e86cd8c7 | ||
|
dc52e37abc | ||
|
69fae6a1a6 | ||
|
7c14923afe | ||
|
9e66583b4e | ||
|
34676c817f | ||
|
d17224c9cb | ||
|
b57b00f3fc | ||
|
f82041da1e | ||
|
03a837f865 | ||
|
473dc2577f | ||
|
49dfaf3709 | ||
|
f5afb79a2b | ||
|
bd26fb38d9 | ||
|
95f0b0ab26 | ||
|
28433fc33e | ||
|
fc4e7414df | ||
|
e61cb1f855 | ||
|
689214cf84 | ||
|
d268478767 | ||
|
4a5ad5dc09 | ||
|
7365ee8cc6 | ||
|
5a93aa15e8 | ||
|
1fddc17b75 | ||
|
ca540d7725 | ||
|
fe11b485ed |
2
actor
2
actor
Submodule actor updated: 3c40cb45f0...04ce3fc2d1
@@ -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())
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -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(
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -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):
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
93
server/src/uds/management/commands/maintenance.py
Normal file
93
server/src/uds/management/commands/maintenance.py
Normal 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')
|
@@ -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()]
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
)
|
||||
|
@@ -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'):
|
||||
|
@@ -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:
|
||||
|
@@ -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(
|
||||
|
@@ -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)
|
||||
|
@@ -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))
|
||||
|
@@ -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}')
|
||||
|
@@ -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),
|
||||
|
@@ -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()
|
||||
|
||||
|
@@ -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
@@ -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>
|
||||
|
@@ -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,
|
||||
}
|
||||
)
|
||||
|
@@ -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)'
|
||||
),
|
||||
|
@@ -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'
|
||||
|
||||
|
@@ -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 '',
|
||||
}
|
||||
)
|
||||
|
@@ -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")
|
||||
|
@@ -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=
|
@@ -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])
|
||||
|
@@ -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=
|
@@ -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
|
||||
|
@@ -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=
|
@@ -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
|
||||
|
@@ -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=
|
@@ -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)
|
||||
|
@@ -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:
|
||||
|
@@ -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:
|
||||
|
Reference in New Issue
Block a user