1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-03-20 06:50:23 +03:00

old_field_name and servers improvement.

Improved old_field_name to allow more than 1 "old field name", just in case the future. Also, improved servers manager to allow a "threshold" value, so we can get intead of the less loaded server, one with some load, but not too loaded...
This commit is contained in:
Adolfo Gómez García 2024-07-04 00:41:16 +02:00
parent d60f47aa7a
commit 43389248c8
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
8 changed files with 103 additions and 45 deletions

View File

@ -319,3 +319,13 @@ class TestingUserInterfaceFieldName(UserInterface):
default='', # Will be loaded from orig
old_field_name='strField',
)
class TestingUserInterfaceFieldNameSeveral(UserInterface):
str2_field = gui.TextField(
label='Text Field',
order=0,
tooltip='This is a text field',
required=True,
default='', # Will be loaded from orig
old_field_name=['str_field', 'strField'],
)

View File

@ -46,6 +46,7 @@ from .fixtures import (
TestingOldUserInterface,
DEFAULTS,
TestingUserInterfaceFieldName,
TestingUserInterfaceFieldNameSeveral,
TestingUserInterfaceFieldNameOrig,
)
@ -156,7 +157,9 @@ class UserinterfaceTest(UDSTestCase):
ui2.randomize_values() # Ensure data is different
ui2.deserialize_from_old_format(data)
self.assertEqual(ui2, ui) # Important, TestingUserInterface has __eq__ method, but TestingOldUserInterface has not
self.assertEqual(
ui2, ui
) # Important, TestingUserInterface has __eq__ method, but TestingOldUserInterface has not
self.ensure_values_fine(ui2)
# Now deserialize old data with new method, (will internally call oldUnserializeForm)
@ -183,27 +186,36 @@ class UserinterfaceTest(UDSTestCase):
# This test is to ensure that new serialized data can be loaded
# mock logging warning
ui = TestingUserInterfaceFieldNameOrig()
data = ui.serialize_fields()
data_ui = ui.serialize_fields()
ui2 = TestingUserInterfaceFieldName()
self.assertFalse(ui2.deserialize_fields(data)) # Should not need upgrade
ui3 = TestingUserInterfaceFieldNameSeveral()
self.assertFalse(ui2.deserialize_fields(data_ui)) # Should not need upgrade
self.assertEqual(ui.strField.value, ui2.str_field.value)
# Now, we will try to load data from ui to ui3, that has 2 "old_field_names" on the field
self.assertFalse(ui3.deserialize_fields(data_ui)) # Should not need upgrade
self.assertEqual(ui.strField.value, ui3.str2_field.value)
data_ui2 = ui2.serialize_fields()
self.assertFalse(ui3.deserialize_fields(data_ui2)) # Should not need upgrade
self.assertEqual(ui2.str_field.value, ui3.str2_field.value)
# On current version, we do noallow backwards compatibility, so a warning is issued
# and the field is not loaded
with mock.patch('logging.Logger.warning') as mock_warning:
ui2.str_field.value = 'new value'
data = ui2.serialize_fields() # Should store str_field as strField
data_ui = ui2.serialize_fields() # Should store str_field as strField
self.assertFalse(ui.deserialize_fields(data)) # Should need upgrade, current format serialized
self.assertFalse(ui.deserialize_fields(data_ui)) # Should need upgrade, current format serialized
# Logger.warning should has been called (because there is an unknown field)
mock_warning.assert_called()
# And field is not loaded
self.assertNotEqual(ui.strField.value, ui2.str_field.value)
# Previously:
# Previously:
# mock_warning.assert_not_called()
# And strField should be loaded from str_field

View File

@ -148,12 +148,14 @@ class ServerManager(metaclass=singleton.Singleton):
stats_and_servers = self.get_server_stats(fltrs)
def _weight_threshold(stats: 'types.servers.ServerStats') -> float:
def _real_weight(stats: 'types.servers.ServerStats') -> float:
if weight_threshold == 0:
return stats.weight()
# Values under threshold are better, weight is in between 0 and 1, lower is better
# To values over threshold, we will add 1, so they are always worse than any value under threshold
return stats.weight() if stats.weight() < weight_threshold else 1 + stats.weight()
# No matter if over threshold is overcalculed, it will be always worse than any value under threshold
# and all values over threshold will be affected in the same way
return weight_threshold - stats.weight() if stats.weight() < weight_threshold else 1 + stats.weight()
# Now, cachedStats has a list of tuples (stats, server), use it to find the best server
for stats, server in stats_and_servers:
@ -166,7 +168,7 @@ class ServerManager(metaclass=singleton.Singleton):
if best is None:
best = (server, stats)
if _weight_threshold(stats) < _weight_threshold(best[1]):
if _real_weight(stats) < _real_weight(best[1]):
best = (server, stats)
# stats.weight() < best[1].weight()
@ -206,6 +208,7 @@ class ServerManager(metaclass=singleton.Singleton):
lock_interval: typing.Optional[datetime.timedelta] = None,
server: typing.Optional['models.Server'] = None, # If not note
excluded_servers_uuids: typing.Optional[typing.Set[str]] = None,
weight_threshold: int = 0,
) -> typing.Optional[types.servers.ServerCounter]:
"""
Select a server for an userservice to be assigned to
@ -219,6 +222,20 @@ class ServerManager(metaclass=singleton.Singleton):
server: If not None, use this server instead of selecting one from server_group. (Used on manual assign)
excluded_servers_uuids: If not None, exclude this servers from selection. Used in case we check the availability of a server
with some external method and we want to exclude it from selection because it has already failed.
weight_threshold: If not 0, basically will prefer values below an near this value
Note:
weight_threshold is used to select a server with a weight as near as possible, without going over, to this value.
If none is found, the server with the lowest weight will be selected.
If 0, no weight threshold is applied.
The calculation is done as follows (with weight_threshold > 0 ofc):
* if weight is below threshold, (threshold - weight) is returned (so nearer to threshold, better)
* if weight is over threshold, 1 + weight is returned (so, all values over threshold are worse than any value under threshold)
that is:
real_weight = weight_threshold - weight if weight < weight_threshold else 1 + weight
The idea behind this is to be able to select a server not fully empty, but also not fully loaded, so it can be used
to leave servers empty as soon as possible, but also to not overload servers that are near to be full.
Returns:
uuid of server assigned
@ -281,6 +298,7 @@ class ServerManager(metaclass=singleton.Singleton):
now=now,
min_memory_mb=min_memory_mb,
excluded_servers_uuids=excluded_servers_uuids,
weight_threshold=weight_threshold,
)
info = types.servers.ServerCounter(best[0].uuid, 0)

View File

@ -60,7 +60,7 @@ class FixedService(services.Service, abc.ABC): # pylint: disable=too-many-publi
needs_osmanager = False # If the service needs a s.o. manager (managers are related to agents provided by services, i.e. virtual machines with agent)
# can_reset = True
# If machine has an alternate field with it, it will be used instead of "machines" field
# If machines has an alternate field with it, it will be used instead of "machines" field
alternate_machines_field: typing.Optional[str] = None
# : Types of publications (preparated data for deploys)
@ -213,6 +213,13 @@ class FixedService(services.Service, abc.ABC): # pylint: disable=too-many-publi
Defaults to True
"""
return True
def is_running(self, vmid: str) -> bool:
"""
Returns if the machine is running
Defaults to self.is_ready()
"""
return self.is_ready(vmid)
@abc.abstractmethod
def get_mac(self, vmid: str) -> str:

View File

@ -432,8 +432,11 @@ class FixedUserService(services.UserService, autoserializable.AutoSerializable,
def op_start_checker(self) -> types.states.TaskState:
"""
Checks if machine has started
Defaults to is_ready method from service
"""
return types.states.TaskState.FINISHED
if self.service().is_running(self._vmid):
return types.states.TaskState.FINISHED
return types.states.TaskState.RUNNING
def op_stop(self) -> None:
"""
@ -444,8 +447,11 @@ class FixedUserService(services.UserService, autoserializable.AutoSerializable,
def op_stop_checker(self) -> types.states.TaskState:
"""
Checks if machine has stoped
Default to is_ready method from service
"""
return types.states.TaskState.FINISHED
if not self.service().is_running(self._vmid):
return types.states.TaskState.FINISHED
return types.states.TaskState.RUNNING
# Not abstract methods, defaults to stop machine
def op_shutdown(self) -> None:

View File

@ -36,6 +36,8 @@ import collections.abc
from django.utils.translation import gettext_noop
# Old Field name type
OldFieldNameType = typing.Union[str,list[str],None]
class Tab(enum.StrEnum):
ADVANCED = gettext_noop('Advanced')
@ -144,7 +146,7 @@ class FieldInfo:
tooltip: str
order: int
type: FieldType
old_field_name: typing.Optional[str] = None
old_field_name: OldFieldNameType = None
readonly: typing.Optional[bool] = None
value: typing.Union[collections.abc.Callable[[], typing.Any], typing.Any] = None
default: typing.Optional[typing.Union[collections.abc.Callable[[], str], str]] = None

View File

@ -292,7 +292,7 @@ class gui:
self,
label: str,
type: types.ui.FieldType,
old_field_name: typing.Optional[str],
old_field_name: types.ui.OldFieldNameType,
order: int = 0,
tooltip: str = '',
length: typing.Optional[int] = None,
@ -342,11 +342,13 @@ class gui:
def is_serializable(self) -> bool:
return True
def old_field_name(self) -> typing.Optional[str]:
def old_field_name(self) -> list[str]:
"""
Returns the name of the field
"""
return self._fields_info.old_field_name
if isinstance(self._fields_info.old_field_name, list):
return self._fields_info.old_field_name
return [self._fields_info.old_field_name] if self._fields_info.old_field_name else []
@property
def value(self) -> typing.Any:
@ -505,7 +507,7 @@ class gui:
value: typing.Optional[str] = None,
pattern: typing.Union[str, types.ui.FieldPatternType] = types.ui.FieldPatternType.NONE,
lines: int = 0,
old_field_name: typing.Optional[str] = None,
old_field_name: types.ui.OldFieldNameType = None,
) -> None:
super().__init__(
old_field_name=old_field_name,
@ -617,7 +619,7 @@ class gui:
dict[str, str],
None,
] = None,
old_field_name: typing.Optional[str] = None,
old_field_name: types.ui.OldFieldNameType = None,
) -> None:
super().__init__(
label=label,
@ -676,7 +678,7 @@ class gui:
value: typing.Optional[int] = None,
min_value: typing.Optional[int] = None,
max_value: typing.Optional[int] = None,
old_field_name: typing.Optional[str] = None,
old_field_name: types.ui.OldFieldNameType = None,
) -> None:
super().__init__(
old_field_name=old_field_name,
@ -728,7 +730,7 @@ class gui:
typing.Union[collections.abc.Callable[[], datetime.date], datetime.date]
] = None,
value: typing.Optional[typing.Union[str, datetime.date]] = None,
old_field_name: typing.Optional[str] = None,
old_field_name: types.ui.OldFieldNameType = None,
) -> None:
super().__init__(
old_field_name=old_field_name,
@ -825,7 +827,7 @@ class gui:
tab: typing.Optional[typing.Union[str, types.ui.Tab]] = None,
default: typing.Union[collections.abc.Callable[[], str], str] = '',
value: typing.Optional[str] = None,
old_field_name: typing.Optional[str] = None,
old_field_name: types.ui.OldFieldNameType = None,
):
super().__init__(
old_field_name=old_field_name,
@ -906,7 +908,7 @@ class gui:
default: typing.Any = None, # May be also callable
value: typing.Any = None,
serializable: bool = False,
old_field_name: typing.Optional[str] = None,
old_field_name: types.ui.OldFieldNameType = None,
) -> None:
super().__init__(
old_field_name=old_field_name,
@ -960,7 +962,7 @@ class gui:
tab: typing.Optional[typing.Union[str, types.ui.Tab]] = None,
default: typing.Union[collections.abc.Callable[[], bool], bool] = False,
value: typing.Optional[bool] = None,
old_field_name: typing.Optional[str] = None,
old_field_name: types.ui.OldFieldNameType = None,
):
super().__init__(
old_field_name=old_field_name,
@ -1101,7 +1103,7 @@ class gui:
tab: typing.Optional[typing.Union[str, types.ui.Tab]] = None,
default: typing.Union[collections.abc.Callable[[], str], str, None] = None,
value: typing.Optional[str] = None,
old_field_name: typing.Optional[str] = None,
old_field_name: types.ui.OldFieldNameType = None,
) -> None:
super().__init__(
old_field_name=old_field_name,
@ -1168,7 +1170,7 @@ class gui:
tab: typing.Optional[typing.Union[str, types.ui.Tab]] = None,
default: typing.Union[collections.abc.Callable[[], str], str, None] = None,
value: typing.Optional[str] = None,
old_field_name: typing.Optional[str] = None,
old_field_name: types.ui.OldFieldNameType = None,
):
super().__init__(
old_field_name=old_field_name,
@ -1261,7 +1263,7 @@ class gui:
collections.abc.Callable[[], str], collections.abc.Callable[[], list[str]], list[str], str, None
] = None,
value: typing.Optional[collections.abc.Iterable[str]] = None,
old_field_name: typing.Optional[str] = None,
old_field_name: types.ui.OldFieldNameType = None,
):
super().__init__(
old_field_name=old_field_name,
@ -1355,7 +1357,7 @@ class gui:
collections.abc.Callable[[], str], collections.abc.Callable[[], list[str]], list[str], str, None
] = None,
value: typing.Optional[collections.abc.Iterable[str]] = None,
old_field_name: typing.Optional[str] = None,
old_field_name: types.ui.OldFieldNameType = None,
) -> None:
super().__init__(
old_field_name=old_field_name,
@ -1413,7 +1415,7 @@ class gui:
label: str,
title: str,
help: str,
old_field_name: typing.Optional[str] = None,
old_field_name: types.ui.OldFieldNameType = None,
) -> None:
super().__init__(
label=label, default=[title, help], type=types.ui.FieldType.INFO, old_field_name=old_field_name
@ -1574,8 +1576,8 @@ class UserInterface(metaclass=UserInterfaceType):
# Any unexpected type will raise an exception
# Note that we always store CURRENT field name, so once migrated forward
# we cannot reverse it to original...
# if required, we can use field.old_field_name(), but better not
# we cannot reverse it to original... (unless we reverse old_field_name to current, and then current to old)
# but this is not recommended :)
fields = [
(field_name, field.field_type.name, FIELDS_ENCODERS[field.field_type](field))
for field_name, field in self._all_serializable_fields()
@ -1645,17 +1647,16 @@ class UserInterface(metaclass=UserInterfaceType):
field.value = field.default
for field_name, field_type, field_value in fields:
if field_name in field_names_translations:
field_name = field_names_translations[field_name] # Convert old field name to new one if needed
field_name = field_names_translations.get(field_name, field_name)
if field_name not in self._gui:
logger.warning('Field %s not found in form', field_name)
continue
internal_field_type = self._gui[field_name].field_type
if internal_field_type not in FIELD_DECODERS:
logger.warning('Field %s has no converter', field_name)
logger.warning('Field %s has no decoder', field_name)
continue
if field_type != internal_field_type.name:
logger.warning('Field %s has different type than expected', field_name)
logger.warning('Field %s has different type than expected: %s != %s', field_name, field_type, internal_field_type.name)
continue
self._gui[field_name].value = FIELD_DECODERS[internal_field_type](field_value)
@ -1764,11 +1765,13 @@ class UserInterface(metaclass=UserInterfaceType):
def _get_fieldname_translations(self) -> dict[str, str]:
# Dict of translations from old_field_name to field_name
# Note that if an old_field_name is repeated on different fields, only the FIRST will be used
# Also, order of fields is not guaranteed, so we we cannot assure that the first one will be chosen
field_names_translations: dict[str, str] = {}
for fld_name, fld in self._all_serializable_fields():
fld_old_field_name = fld.old_field_name()
if fld_old_field_name and fld_old_field_name != fld_name:
field_names_translations[fld_old_field_name] = fld_name
for fld_old_field_name in fld.old_field_name():
if fld_old_field_name != fld_name:
field_names_translations[fld_old_field_name] = fld_name
return field_names_translations

View File

@ -189,17 +189,17 @@ def user_service_status(
@web_login_required(admin=False)
@never_cache
def action(request: 'ExtendedHttpRequestWithUser', service_id: str, action_string: str) -> HttpResponse:
userService = UserServiceManager().locate_meta_service(request.user, service_id)
userService = UserServiceManager.manager().locate_meta_service(request.user, service_id)
if not userService:
userService = UserServiceManager().locate_user_service(request.user, service_id, create=False)
response: typing.Any = None
rebuild: bool = False
if userService:
if action_string == 'release' and userService.deployed_service.allow_users_remove:
if action_string == 'release' and userService.service_pool.allow_users_remove:
rebuild = True
log.log(
userService.deployed_service,
userService.service_pool,
types.log.LogLevel.INFO,
"Removing User Service {} as requested by {} from {}".format(
userService.friendly_name, request.user.pretty_name, request.ip
@ -210,12 +210,12 @@ def action(request: 'ExtendedHttpRequestWithUser', service_id: str, action_strin
userService.release()
elif (
action_string == 'reset'
and userService.deployed_service.allow_users_reset
and userService.deployed_service.service.get_type().can_reset
and userService.service_pool.allow_users_reset
and userService.service_pool.service.get_type().can_reset
):
rebuild = True
log.log(
userService.deployed_service,
userService.service_pool,
types.log.LogLevel.INFO,
"Reseting User Service {} as requested by {} from {}".format(
userService.friendly_name, request.user.pretty_name, request.ip