1
0
mirror of https://github.com/dkmstr/openuds.git synced 2024-12-22 13:34:04 +03:00

Renamed some fields of servers to better understand what they are

Adding physical machines fixtures, tests, ,,,
This commit is contained in:
Adolfo Gómez García 2024-04-27 22:31:39 +02:00
parent 30bbf1ef0f
commit 6694b9d5bc
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
12 changed files with 287 additions and 63 deletions

View File

@ -78,9 +78,9 @@ class ActorTokens(ModelHandler):
log_level = LogLevel(log_level_int).name
return {
'id': item.token,
'name': str(_('Token isued by {} from {}')).format(item.username, item.hostname or item.ip),
'name': str(_('Token isued by {} from {}')).format(item.register_username, item.hostname or item.ip),
'stamp': item.stamp,
'username': item.username,
'username': item.register_username,
'ip': item.ip,
'host': f'{item.ip} - {data.get("mac")}',
'hostname': item.hostname,

View File

@ -282,8 +282,8 @@ class Register(ActorV3Action):
# This means that we can invoke its API from user_service, but not from server (The actor token is transformed as soon as initialized to a user service token)
if actor_token:
# Update parameters
actor_token.username = self._user.pretty_name
actor_token.ip_from = self._request.ip
actor_token.register_username = self._user.pretty_name
actor_token.register_ip = self._request.ip
actor_token.ip = self._params['ip']
actor_token.hostname = self._params['hostname']
actor_token.log_level = self._params['log_level']
@ -301,8 +301,8 @@ class Register(ActorV3Action):
if not found:
kwargs = {
'username': self._user.pretty_name,
'ip_from': self._request.ip,
'register_username': self._user.pretty_name,
'register_ip': self._request.ip,
'ip': self._params['ip'],
'hostname': self._params['hostname'],
'log_level': self._params['log_level'],

View File

@ -101,10 +101,10 @@ class ServerRegisterBase(Handler):
serverToken = serverTokens[0]
# Update parameters
# serverToken.hostname = self._params['hostname']
serverToken.username = self._user.pretty_name
serverToken.register_username = self._user.pretty_name
serverToken.certificate = certificate
# Ensure we do not store zone if IPv6 and present
serverToken.ip_from = self._request.ip.split('%')[0]
serverToken.register_ip = self._request.ip.split('%')[0]
serverToken.listen_port = port
serverToken.ip = ip
serverToken.stamp = now
@ -116,8 +116,8 @@ class ServerRegisterBase(Handler):
except Exception:
try:
serverToken = models.Server.objects.create(
username=self._user.pretty_name,
ip_from=self._request.ip.split('%')[0], # Ensure we do not store zone if IPv6 and present
register_username=self._user.pretty_name,
register_ip=self._request.ip.split('%')[0], # Ensure we do not store zone if IPv6 and present
ip=ip,
listen_port=port,
hostname=self._params['hostname'],

View File

@ -75,9 +75,9 @@ class ServersTokens(ModelHandler):
item = typing.cast('models.Server', item) # We will receive for sure
return {
'id': item.uuid,
'name': str(_('Token isued by {} from {}')).format(item.username, item.ip),
'name': str(_('Token isued by {} from {}')).format(item.register_username, item.ip),
'stamp': item.stamp,
'username': item.username,
'username': item.register_username,
'ip': item.ip,
'hostname': item.hostname,
'listen_port': item.listen_port,
@ -257,7 +257,8 @@ class ServersServers(DetailHandler):
raise self.invalid_request_response('Invalid MAC address')
# Create a new one, and add it to group
server = models.Server.objects.create(
ip_from='::1',
register_username=self._user.name,
register_ip=self._request.ip,
ip=self._params['ip'],
hostname=self._params['hostname'],
listen_port=0,
@ -288,6 +289,9 @@ class ServersServers(DetailHandler):
raise self.invalid_request_response('Invalid MAC address')
try:
models.Server.objects.filter(uuid=process_uuid(item)).update(
# Update register info also on update
register_username=self._user.name,
register_ip=self._request.ip,
ip=self._params['ip'],
hostname=self._params['hostname'],
mac=mac,

View File

@ -203,7 +203,7 @@ class ServerManager(metaclass=singleton.Singleton):
service_type: Type of service to assign
min_memory_mb: Minimum memory required for server in MB, does not apply to unmanaged servers
lock_interval: If not None, lock server for this time
server: If not None, use this server instead of selecting one from serverGroup. (Used on manual assign)
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.
@ -280,24 +280,24 @@ class ServerManager(metaclass=singleton.Singleton):
# Update counter
info = types.servers.ServerCounter(info.server_uuid, info.counter + 1)
props[prop_name] = info
bestServer = models.Server.objects.get(uuid=info.server_uuid)
best_server = models.Server.objects.get(uuid=info.server_uuid)
# Ensure next assignation will have updated stats
# This is a simple simulation on cached stats, will be updated on next stats retrieval
# (currently, cache time is 1 minute)
bestServer.interpolate_new_assignation()
best_server.interpolate_new_assignation()
# Notify assgination in every case, even if reassignation to same server is made
# This lets the server to keep track, if needed, of multi-assignations
self.notify_assign(bestServer, userservice, service_type, info.counter)
self.notify_assign(best_server, userservice, service_type, info.counter)
return info
def release(
self,
userService: 'models.UserService',
serverGroup: 'models.ServerGroup',
userservice: 'models.UserService',
server_group: 'models.ServerGroup',
unlock: bool = False,
userUuid: typing.Optional[str] = None,
user_uuid: typing.Optional[str] = None,
) -> types.servers.ServerCounter:
"""
Unassigns a server from an user
@ -308,13 +308,13 @@ class ServerManager(metaclass=singleton.Singleton):
unlock: If True, unlock server, even if it has more users assigned to it
userUuid: If not None, use this uuid instead of userService.user.uuid
"""
userUuid = userUuid if userUuid else userService.user.uuid if userService.user else None
user_uuid = user_uuid if user_uuid else userservice.user.uuid if userservice.user else None
if userUuid is None:
if user_uuid is None:
return types.servers.ServerCounter.null() # No user is assigned to this service, nothing to do
prop_name = self.property_name(userService.user)
with serverGroup.properties as props:
prop_name = self.property_name(userservice.user)
with server_group.properties as props:
with transaction.atomic():
resetCounter = False
# ServerCounterType
@ -353,39 +353,39 @@ class ServerManager(metaclass=singleton.Singleton):
# (currently, cache time is 1 minute)
server.interpolate_new_release()
self.notify_release(server, userService)
self.notify_release(server, userservice)
return types.servers.ServerCounter(serverCounter.server_uuid, serverCounter.counter - 1)
def notify_preconnect(
self,
serverGroup: 'models.ServerGroup',
userService: 'models.UserService',
server_group: 'models.ServerGroup',
userservice: 'models.UserService',
info: types.connections.ConnectionData,
server: typing.Optional[
'models.Server'
] = None, # Forced server instead of selecting one from serverGroup
] = None, # Forced server instead of selecting one from server_group
) -> None:
"""
Notifies preconnect to server
"""
if not server:
server = self.server_assignation_for(userService, serverGroup)
server = self.server_assignation_for(userservice, server_group)
if server:
requester.ServerApiRequester(server).notify_preconnect(userService, info)
requester.ServerApiRequester(server).notify_preconnect(userservice, info)
def notify_assign(
self,
server: 'models.Server',
userService: 'models.UserService',
serviceType: types.services.ServiceType,
userservice: 'models.UserService',
service_type: types.services.ServiceType,
counter: int,
) -> None:
"""
Notifies assign to server
"""
requester.ServerApiRequester(server).notify_assign(userService, serviceType, counter)
requester.ServerApiRequester(server).notify_assign(userservice, service_type, counter)
def notify_release(
self,
@ -397,19 +397,19 @@ class ServerManager(metaclass=singleton.Singleton):
"""
requester.ServerApiRequester(server).notify_release(userService)
def assignation_info(self, serverGroup: 'models.ServerGroup') -> dict[str, int]:
def assignation_info(self, server_group: 'models.ServerGroup') -> dict[str, int]:
"""
Get usage information for a server group
Args:
serverGroup: Server group to get current usage from
server_group: Server group to get current usage from
Returns:
Dict of current usage (user uuid, counter for assignations to that user)
"""
res: dict[str, int] = {}
for k, v in serverGroup.properties.items():
for k, v in server_group.properties.items():
if k.startswith(self.BASE_PROPERTY_NAME):
kk = k[len(self.BASE_PROPERTY_NAME) :] # Skip base name
res[kk] = res.get(kk, 0) + v[1]
@ -417,24 +417,24 @@ class ServerManager(metaclass=singleton.Singleton):
def server_assignation_for(
self,
userService: 'models.UserService',
serverGroup: 'models.ServerGroup',
userservice: 'models.UserService',
server_group: 'models.ServerGroup',
) -> typing.Optional['models.Server']:
"""
Returns the server assigned to an user service
Args:
userService: User service to get server from
serverGroup: Server group to get server from
server_group: Server group to get server from
Returns:
Server assigned to user service, or None if no server is assigned
"""
if not userService.user:
if not userservice.user:
raise exceptions.UDSException(_('No user assigned to service'))
prop_name = self.property_name(userService.user)
with serverGroup.properties as props:
prop_name = self.property_name(userservice.user)
with server_group.properties as props:
info: typing.Optional[types.servers.ServerCounter] = types.servers.ServerCounter.from_iterable(
props.get(prop_name)
)
@ -444,21 +444,21 @@ class ServerManager(metaclass=singleton.Singleton):
def sorted_server_list(
self,
serverGroup: 'models.ServerGroup',
server_group: 'models.ServerGroup',
excluded_servers_uuids: typing.Optional[typing.Set[str]] = None,
) -> list['models.Server']:
"""
Returns a list of servers sorted by usage
Args:
serverGroup: Server group to get servers from
server_group: Server group to get servers from
Returns:
List of servers sorted by usage
"""
with transaction.atomic():
now = model_utils.sql_datetime()
fltrs = serverGroup.servers.filter(maintenance_mode=False)
fltrs = server_group.servers.filter(maintenance_mode=False)
fltrs = fltrs.filter(Q(locked_until=None) | Q(locked_until__lte=now)) # Only unlocked servers
if excluded_servers_uuids:
fltrs = fltrs.exclude(uuid__in=excluded_servers_uuids)
@ -468,23 +468,23 @@ class ServerManager(metaclass=singleton.Singleton):
# Sort by weight, lower first (lower is better)
return [s[1] for s in sorted(serverStats, key=lambda x: x[0].weight() if x[0] else 999999999)]
def perform_maintenance(self, serverGroup: 'models.ServerGroup') -> None:
def perform_maintenance(self, server_group: 'models.ServerGroup') -> None:
"""Realizes maintenance on server group
Maintenace operations include:
* Clean up removed users from server counters
Args:
serverGroup: Server group to realize maintenance on
server_group: Server group to realize maintenance on
"""
for k, _ in serverGroup.properties.items():
for k, _ in server_group.properties.items():
if k.startswith(self.BASE_PROPERTY_NAME):
uuid = k[len(self.BASE_PROPERTY_NAME) :]
try:
models.User.objects.get(uuid=uuid)
except Exception:
# User does not exists, remove it from counters
del serverGroup.properties[k]
del server_group.properties[k]
def process_event(self, server: 'models.Server', data: dict[str, typing.Any]) -> typing.Any:
"""

View File

@ -120,7 +120,6 @@ def server_group_field(
_server_group_values, valid_types, subtype
), # So it gets evaluated at runtime
tab=tab,
old_field_name='serverGroup',
)

View File

@ -33,8 +33,8 @@ def migrate_old_data(apps: typing.Any, schema_editor: typing.Any) -> None:
# Now append actors to registered servers, with "unknown" os type (legacy)
for token in ActorToken.objects.all():
Server.objects.create(
username=token.username,
ip_from=token.ip_from,
register_username=token.username,
register_ip=token.ip_from,
ip=token.ip,
hostname=token.hostname,
token=token.token,
@ -69,8 +69,8 @@ def rollback_old_data(apps: typing.Any, schema_editor: typing.Any) -> None:
if not server.data:
continue # Skip servers without data, they are not actors!!
ActorToken.objects.create(
username=server.username,
ip_from=server.ip_from,
username=server.register_username,
ip_from=server.register_ip,
ip=server.ip,
hostname=server.hostname,
token=server.token,
@ -127,6 +127,16 @@ class Migration(migrations.Migration):
'TunnelToken',
'Server',
),
migrations.RenameField(
model_name="server",
old_name="ip_from",
new_name="register_ip",
),
migrations.RenameField(
model_name="server",
old_name="username",
new_name="register_username",
),
migrations.CreateModel(
name="ServerGroup",
fields=[
@ -269,7 +279,7 @@ class Migration(migrations.Migration):
# Add server group to transports
migrations.AddField(
model_name="transport",
name="serverGroup",
name="server_group",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,

View File

@ -122,8 +122,8 @@ def migrate(
for ip, hostname, mac in server_ip_hostname_mac:
registeredServerGroup.servers.create(
token=secrets.token_urlsafe(36),
username='migration',
ip_from='127.0.0.1',
register_username='migration',
register_ip='127.0.0.1',
ip=ip,
os_type=types.os.KnownOS.WINDOWS.os_name(),
hostname=hostname,

View File

@ -166,9 +166,9 @@ class Server(UUIDModel, TaggingMixin, properties.PropertiesMixin):
"""
# Username that registered the server
username = models.CharField(max_length=128)
register_username = models.CharField(max_length=128)
# Ip from where the server was registered, can be IPv4 or IPv6
ip_from = models.CharField(max_length=consts.system.MAX_IPV6_LENGTH)
register_ip = models.CharField(max_length=consts.system.MAX_IPV6_LENGTH)
# Ip of the server, can be IPv4 or IPv6 (used to communicate with it)
ip = models.CharField(max_length=consts.system.MAX_IPV6_LENGTH)
@ -413,7 +413,7 @@ class Server(UUIDModel, TaggingMixin, properties.PropertiesMixin):
return f'https://{pre}{self.ip}{post}:{self.listen_port}/{self.server_type.path()}/v1/{path}'
def __str__(self) -> str:
return f'<RegisterdServer {self.token} of type {self.server_type.name} created on {self.stamp} by {self.username} from {self.ip}/{self.hostname}>'
return f'<RegisterdServer {self.token} of type {self.server_type.name} created on {self.stamp} by {self.register_username} from {self.ip}/{self.hostname}>'
properties.PropertiesMixin.setup_signals(Server)

View File

@ -64,7 +64,7 @@ class Transport(ManagedObjectModel, TaggingMixin):
# Label, to group transports on meta pools
label = models.CharField(max_length=32, default='', db_index=True)
serverGroup = models.ForeignKey(
server_group = models.ForeignKey(
'ServerGroup',
related_name='transports',
null=True,

View File

@ -47,8 +47,8 @@ def create_server(
) -> 'models.Server':
# Token is created by default on record creation
return models.Server.objects.create(
username=generators.random_string(),
ip_from=ip or '127.0.0.1',
register_username=generators.random_string(),
register_ip=ip or '127.0.0.1',
ip=ip or '127.0.0.1',
hostname=generators.random_string(),
listen_port=listen_port,

View File

@ -0,0 +1,211 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2024 Virtual Cable S.L.U.
# 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 contextlib
import typing
import uuid
from unittest import mock
from uds import models
from uds.core import types
from uds.core.ui.user_interface import gui
SERVER_GROUP_IPS = [
f'127.0.1.{x}' for x in range(1, 32)
]
PROVIDER_VALUES_DICT: typing.Final[gui.ValuesDictType] = {
'config': '[wol]\n127.0.0.1/16=http://127.0.0.1:8000/test',
}
SERVICE_SINGLE_VALUES_DICT: typing.Final[gui.ValuesDictType] = {
'host': '127.0.0.1',
}
SERVICE_MULTI_VALUES_DICT: typing.Final[gui.ValuesDictType] = {
'token': 'MULTI_TOKEN',
'server_group': '899bcad9-1b4d-59e3-90c7-bdd28b7674fa',
'port': 1000, # Chekcin port
'ignore_minutes_on_failure': 1,
'max_session_hours': 2,
'lock_on_external_access': True,
'randomize_host': True,
}
def create_server_group():
server_group = models.ServerGroup.objects.create(
name='Test Server Group',
type=types.servers.ServerType.UNMANAGED,
subtype=types.servers.IP_SUBTYPE,
)
for ip in SERVER_GROUP_IPS:
models.Server.objects.create(
username='test',
name=ip,
ip=ip,
group=server_group,
)
@contextlib.contextmanager
def patched_provider(
**kwargs: typing.Any,
) -> typing.Generator[provider.OpenStackProvider, None, None]:
client = create_client_mock()
provider = create_provider(**kwargs)
with mock.patch.object(provider, 'api') as api:
provider.do_log = mock.MagicMock() # Avoid logging
api.return_value = client
yield provider
@contextlib.contextmanager
def patched_provider_legacy(
**kwargs: typing.Any,
) -> typing.Generator[provider_legacy.OpenStackProviderLegacy, None, None]:
client = create_client_mock()
provider = create_provider_legacy(**kwargs)
with mock.patch.object(provider, 'api') as api:
api.return_value = client
yield provider
def create_provider(**kwargs: typing.Any) -> provider.OpenStackProvider:
"""
Create a provider
"""
values = PROVIDER_VALUES_DICT.copy()
values.update(kwargs)
uuid_ = str(uuid.uuid4())
return provider.OpenStackProvider(
environment=environment.Environment.private_environment(uuid_), values=values, uuid=uuid_
)
def create_provider_legacy(**kwargs: typing.Any) -> provider_legacy.OpenStackProviderLegacy:
"""
Create a provider legacy
"""
values = PROVIDER_LEGACY_VALUES_DICT.copy()
values.update(kwargs)
uuid_ = str(uuid.uuid4())
return provider_legacy.OpenStackProviderLegacy(
environment=environment.Environment.private_environment(uuid_), values=values, uuid=uuid_
)
def create_live_service(provider: AnyOpenStackProvider, **kwargs: typing.Any) -> service.OpenStackLiveService:
"""
Create a service
"""
values = SERVICE_VALUES_DICT.copy()
values.update(kwargs)
uuid_ = str(uuid.uuid4())
return service.OpenStackLiveService(
provider=provider,
environment=environment.Environment.private_environment(uuid_),
values=values,
uuid=uuid_,
)
def create_publication(service: service.OpenStackLiveService) -> publication.OpenStackLivePublication:
"""
Create a publication
"""
uuid_ = str(uuid.uuid4())
return publication.OpenStackLivePublication(
environment=environment.Environment.private_environment(uuid_),
service=service,
revision=1,
servicepool_name='servicepool_name',
uuid=uuid_,
)
def create_live_userservice(
service: service.OpenStackLiveService,
publication: typing.Optional[publication.OpenStackLivePublication] = None,
) -> deployment.OpenStackLiveUserService:
"""
Create a linked user service
"""
uuid_ = str(uuid.uuid4())
return deployment.OpenStackLiveUserService(
environment=environment.Environment.private_environment(uuid_),
service=service,
publication=publication or create_publication(service),
uuid=uuid_,
)
def create_fixed_service(
provider: AnyOpenStackProvider, **kwargs: typing.Any
) -> service_fixed.OpenStackServiceFixed:
"""
Create a fixed service
"""
values = SERVICES_FIXED_VALUES_DICT.copy()
values.update(kwargs)
uuid_ = str(uuid.uuid4())
return service_fixed.OpenStackServiceFixed(
provider=provider,
environment=environment.Environment.private_environment(uuid_),
values=values,
uuid=uuid_,
)
# Fixed has no publications
def create_fixed_userservice(
service: service_fixed.OpenStackServiceFixed,
) -> deployment_fixed.OpenStackUserServiceFixed:
"""
Create a linked user service
"""
uuid_ = str(uuid.uuid4())
return deployment_fixed.OpenStackUserServiceFixed(
environment=environment.Environment.private_environment(uuid_),
service=service,
uuid=uuid_,
)