1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-01-21 18:03:54 +03:00

Doney Physical Machines

This commit is contained in:
Adolfo Gómez García 2024-04-29 00:24:04 +02:00
parent eb86784c62
commit 7316953e75
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
9 changed files with 297 additions and 40 deletions

View File

@ -258,7 +258,7 @@ 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(
register_username=self._user.name,
register_username=self._user.pretty_name,
register_ip=self._request.ip,
ip=self._params['ip'],
hostname=self._params['hostname'],
@ -291,7 +291,7 @@ class ServersServers(DetailHandler):
try:
models.Server.objects.filter(uuid=process_uuid(item)).update(
# Update register info also on update
register_username=self._user.name,
register_username=self._user.pretty_name,
register_ip=self._request.ip,
hostname=self._params['hostname'],
ip=self._params['ip'],
@ -379,7 +379,7 @@ class ServersServers(DetailHandler):
try:
if parent.servers.filter(Q(ip=ip) | Q(hostname=hostname)).count() == 0:
server = models.Server.objects.create(
register_username=self._user.name,
register_username=self._user.pretty_name,
register_ip=self._request.ip,
ip=ip,
hostname=hostname,

View File

@ -62,7 +62,6 @@ class IPMachineUserService(services.UserService, autoserializable.AutoSerializab
_ip = autoserializable.StringField(default='')
_reason = autoserializable.StringField(default='')
_state = autoserializable.StringField(default=types.states.TaskState.FINISHED)
_name = autoserializable.StringField(default='')
# Utility overrides for type checking...
@ -80,18 +79,17 @@ class IPMachineUserService(services.UserService, autoserializable.AutoSerializab
def get_name(self) -> str:
if not self._name:
# Generate a name with the IP + simple counter
self._name = f'{self.get_ip()}{self.service().get_counter_and_inc()}'
self._name = f'{self.get_ip()}:{self.service().get_counter_and_inc()}'
return self._name
def get_unique_id(self) -> str:
return self._name
return self.get_name()
def set_ready(self) -> types.states.TaskState:
# If single machine, ip is IP~counter,
# If multiple and has a ';' on IP, the values is IP;MAC
self.service().wakeup()
self._state = types.states.TaskState.FINISHED
return self._state
return types.states.TaskState.FINISHED
def _deploy(self) -> types.states.TaskState:
# If not to be managed by a token, autologin user
@ -99,8 +97,7 @@ class IPMachineUserService(services.UserService, autoserializable.AutoSerializab
if userService:
userService.set_in_use(True)
self._state = types.states.TaskState.FINISHED
return self._state
return types.states.TaskState.FINISHED
def deploy_for_user(self, user: 'models.User') -> types.states.TaskState:
logger.debug("Starting deploy of %s for user %s", self._ip, user)
@ -110,13 +107,14 @@ class IPMachineUserService(services.UserService, autoserializable.AutoSerializab
return self._error('Cache deploy not supported')
def _error(self, reason: str) -> types.states.TaskState:
self._state = types.states.TaskState.ERROR
self._ip = ''
self._reason = reason
return self._state
self._reason = reason or 'Unknown error'
return types.states.TaskState.ERROR
def check_state(self) -> types.states.TaskState:
return types.states.TaskState.from_str(self._state)
if self._reason:
return types.states.TaskState.ERROR
return types.states.TaskState.FINISHED
def error_reason(self) -> str:
"""
@ -126,8 +124,9 @@ class IPMachineUserService(services.UserService, autoserializable.AutoSerializab
return self._reason
def destroy(self) -> types.states.TaskState:
self._state = types.states.TaskState.FINISHED
return self._state
self._ip = ''
self._reason = ''
return types.states.TaskState.FINISHED
def cancel(self) -> types.states.TaskState:
return self.destroy()
@ -141,8 +140,10 @@ class IPMachineUserService(services.UserService, autoserializable.AutoSerializab
# Fill own data from restored data
self._ip = _auto_data._ip
self._name = self.db_obj().name or '' # If has a name, use it, else, use the generated one
self._reason = _auto_data._reason
self._state = _auto_data._state
if _auto_data._state == types.states.TaskState.ERROR:
self._reason = self._reason or 'Unknown error'
# Flag for upgrade
self.mark_for_upgrade(True)

View File

@ -30,6 +30,7 @@
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import hashlib
import logging
import typing
@ -69,6 +70,12 @@ class IPMachinesUserService(services.UserService, autoserializable.AutoSerializa
def service(self) -> 'IPMachinesService':
return typing.cast('IPMachinesService', super().service())
def _set_in_use(self) -> None:
if not self.service().get_token():
userservice = self.db_obj()
if userservice:
userservice.set_in_use(True)
def set_ip(self, ip: str) -> None:
logger.debug('Setting IP to %s (ignored)', ip)
@ -76,14 +83,11 @@ class IPMachinesUserService(services.UserService, autoserializable.AutoSerializa
return self._ip
def get_name(self) -> str:
# If single machine, ip is IP~counter,
# If multiple and has a ';' on IP, the values is IP;MAC
return self.get_ip()
def get_unique_id(self) -> str:
if not self._mac:
return self._ip
return self._mac
# Generate a 16 chars string mixing up all _vmid chars
return hashlib.shake_128(self._vmid.encode('utf8')).hexdigest(8)
def set_ready(self) -> types.states.TaskState:
self.service().wakeup(self._ip, self._mac)
@ -92,13 +96,10 @@ class IPMachinesUserService(services.UserService, autoserializable.AutoSerializa
def deploy_for_user(self, user: 'models.User') -> types.states.TaskState:
logger.debug("Starting deploy of %s for user %s", self._ip, user)
self._vmid = self.service().get_unassigned()
self._ip, self._mac = self.service().get_host_mac(self._vmid)
# If not to be managed by a token, autologin user
if not self.service().get_token():
userservice = self.db_obj()
if userservice:
userservice.set_in_use(True)
self._set_in_use()
return types.states.TaskState.FINISHED
@ -111,11 +112,8 @@ class IPMachinesUserService(services.UserService, autoserializable.AutoSerializa
# Update ip & mac
self._ip, self._mac = self.service().get_host_mac(vmid)
if not self.service().get_token():
dbService = self.db_obj()
if dbService:
dbService.set_in_use(True)
dbService.save()
self._set_in_use()
return types.states.TaskState.FINISHED
def _error(self, reason: str) -> types.states.TaskState:
@ -150,4 +148,4 @@ class IPMachinesUserService(services.UserService, autoserializable.AutoSerializa
def cancel(self) -> types.states.TaskState:
return self.destroy()
# Data is migrated on migration 0046, so no unmarshall is needed
# Data is migrated on migration 0046, so no unmarshall is needed

View File

@ -176,6 +176,7 @@ class IPMachinesService(services.Service):
random.shuffle(list_of_servers) # Reorder the list randomly if required
for server in list_of_servers:
if server.locked_until is None or server.locked_until < sql_now():
server.lock(self.get_max_lock_time())
return server.uuid
raise exceptions.services.InsufficientResourcesException()

View File

@ -34,11 +34,19 @@ import datetime
import typing
import uuid
from unittest import mock
from uds import models
from uds.core import environment, types
from uds.core.ui.user_interface import gui
from uds.services.PhysicalMachines import provider, service_single, service_multi
from uds.services.PhysicalMachines import (
provider,
service_single,
service_multi,
deployment as deployment_single,
deployment_multi,
)
SERVER_GROUP_IPS_MACS: typing.Final[list[tuple[str, str]]] = [
(f'127.0.1.{x}', f'{x:02x}:22:{x*2:02x}:44:{x*4:02x}:66') for x in range(1, 32)
@ -178,3 +186,43 @@ def create_service_multi(
)
return service_instance
def create_userservice_single(
service: typing.Optional[service_single.IPSingleMachineService] = None, **kwargs: typing.Any
) -> deployment_single.IPMachineUserService:
"""
Create a user service
"""
uuid_ = str(uuid.uuid4())
userservice_instance = deployment_single.IPMachineUserService(
environment=environment.Environment.private_environment(uuid_),
service=service or create_service_single(),
publication=None,
uuid=uuid_,
)
userservice_instance.db_obj = mock.MagicMock()
return userservice_instance
def create_userservice_multi(
service: typing.Optional[service_single.IPSingleMachineService] = None, **kwargs: typing.Any
) -> deployment_multi.IPMachinesUserService:
"""
Create a user service
"""
uuid_ = str(uuid.uuid4())
userservice_instance = deployment_multi.IPMachinesUserService(
environment=environment.Environment.private_environment(uuid_),
service=service or create_service_multi(),
publication=None,
uuid=uuid_,
)
userservice_instance.db_obj = mock.MagicMock()
return userservice_instance

View File

@ -9,6 +9,7 @@
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
# We use commit/rollback
from unittest import mock
from tests.utils.test import UDSTestCase
from uds.core.util import autoserializable
@ -27,12 +28,12 @@ class IPMachineUserServiceSerializationTest(UDSTestCase):
def _check_fields(instance: deployment.IPMachineUserService) -> None:
self.assertEqual(instance._ip, '1.1.1.1')
self.assertEqual(instance._state, 'state')
self.assertEqual(instance._reason, 'reason')
data = obj.marshal()
instance = deployment.IPMachineUserService(environment=Environment.testing_environment(), service=None) # type: ignore # service is not used
instance.db_obj = mock.MagicMock()
instance.unmarshal(data)
marshaled_data = instance.marshal()

View File

@ -178,8 +178,9 @@ class TestServiceMulti(UDSTransactionTestCase):
unassigned_uuid = service.get_unassigned()
# Must be the first server
self.assertEqual(unassigned_uuid, server.uuid, f'Error on element {num}') # type: ignore # if first is None,raises error
# Lock it, so it's not returned again
service.lock_server(unassigned_uuid)
# Ensure it's locked
server.refresh_from_db()
self.assertIsNotNone(server.locked_until)
# Now, randomized, we must no receive same servers
service.randomize_host.value = True
@ -191,18 +192,21 @@ class TestServiceMulti(UDSTransactionTestCase):
count = 0
for count in range(128):
unassigned_uuid = service.get_unassigned()
self.assertNotEqual(unassigned_uuid, server_list[0].uuid)
# unlock the server, for next iteration or test
service.unlock_server(unassigned_uuid)
if unassigned_uuid != server_list[0].uuid:
break
if count == 127:
self.fail('Randomized server selection failed')
# Unres
# Now ensure we can lock all servers
for num in range(len(server_list)):
unassigned_uuid = service.get_unassigned()
self.assertIsNotNone(unassigned_uuid)
service.lock_server(unassigned_uuid)
self.assertTrue(models.Server.objects.get(uuid=unassigned_uuid).locked_until is not None)
def test_enumerate_assignables(self) -> None:
service = fixtures.create_service_multi()
@ -236,4 +240,4 @@ class TestServiceMulti(UDSTransactionTestCase):
userservice_mock.assign.assert_called_once_with(server.uuid)
# Ensure is locked
server.refresh_from_db()
self.assertIsNotNone(server.locked_until)
self.assertIsNotNone(server.locked_until)

View File

@ -0,0 +1,132 @@
# -*- 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 typing
from unittest import mock
from uds import models
from uds.core import types
from . import fixtures
from ...utils.test import UDSTransactionTestCase
# from ...utils.generators import limited_iterator
# We use transactions on some related methods (storage access, etc...)
class TestUserServiceMulti(UDSTransactionTestCase):
def test_userservice(self) -> None:
"""
Test the user service
"""
userservice = fixtures.create_userservice_multi()
# Does not supports cache
self.assertEqual(
userservice.deploy_for_cache(level=types.services.CacheLevel.L1), types.states.TaskState.ERROR
)
# Ensure check also returns error state
self.assertEqual(userservice.check_state(), types.states.TaskState.ERROR)
# Ensure has no token for this test
self.assertEqual(userservice.deploy_for_user(mock.MagicMock()), types.states.TaskState.FINISHED)
server = models.Server.objects.get(uuid=userservice._vmid)
# Ensure is locked
self.assertIsNotNone(server.locked_until)
# Destroy it
self.assertEqual(userservice.destroy(), types.states.TaskState.FINISHED)
# Ensure is not locked
server.refresh_from_db()
self.assertIsNone(server.locked_until)
def test_userservice_set_ready(self) -> None:
"""
Test the user service
"""
userservice = fixtures.create_userservice_multi()
# Ensure has no token for this test
self.assertEqual(userservice.deploy_for_user(mock.MagicMock()), types.states.TaskState.FINISHED)
# Ensure is locked
server = models.Server.objects.get(uuid=userservice._vmid)
self.assertIsNotNone(server.locked_until)
# Set ready
# patch service wakeup
with mock.patch.object(userservice.service(), 'wakeup') as wakeup:
self.assertEqual(userservice.set_ready(), types.states.TaskState.FINISHED)
wakeup.assert_called_once_with(userservice._ip, userservice._mac)
def test_userservice_assign(self) -> None:
"""
Test the user service
"""
userservice = fixtures.create_userservice_multi()
server = models.Server.objects.first()
if not server:
self.fail('No server found')
# Assign
userservice.assign(server.uuid)
self.assertEqual(userservice._vmid, server.uuid)
# Ensure ip and mac are same as server
self.assertEqual(userservice._ip, server.ip)
self.assertEqual(userservice._mac, server.mac)
def test_userservice_without_token(self) -> None:
"""
Test the user service
"""
userservice = fixtures.create_userservice_multi()
db_obj_mock = typing.cast(mock.MagicMock, userservice.db_obj)
# Ensure has no token for this test
userservice.service().token.value = ''
self.assertEqual(userservice.deploy_for_user(mock.MagicMock()), types.states.TaskState.FINISHED)
self.assertTrue(mock.call().set_in_use(True) in db_obj_mock.mock_calls)
def test_userservice_with_token(self) -> None:
"""
Test the user service
"""
userservice = fixtures.create_userservice_multi()
db_obj_mock = typing.cast(mock.MagicMock, userservice.db_obj)
# Ensure has no token for this test
userservice.service().token.value = 'token_value'
self.assertEqual(userservice.deploy_for_user(mock.MagicMock()), types.states.TaskState.FINISHED)
self.assertFalse(mock.call().set_in_use(True) in db_obj_mock.mock_calls)

View File

@ -0,0 +1,72 @@
# -*- 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
"""
from unittest import mock
from uds.core import types
from . import fixtures
from ...utils.test import UDSTransactionTestCase
# from ...utils.generators import limited_iterator
# We use transactions on some related methods (storage access, etc...)
class TestUserServiceSingle(UDSTransactionTestCase):
def test_userservice(self) -> None:
"""
Test the user service
"""
userservice = fixtures.create_userservice_single()
service = userservice.service()
self.assertEqual(userservice.deploy_for_user(mock.MagicMock()), types.states.TaskState.FINISHED)
self.assertEqual(userservice.check_state(), types.states.TaskState.FINISHED)
self.assertEqual(userservice.get_ip(), service.host.value)
self.assertEqual(userservice.get_name(), f'{userservice.get_ip()}:0')
self.assertEqual(userservice.get_unique_id(), f'{userservice.get_ip()}:0')
# patch service wakeup to ensure it's called
with mock.patch.object(service, 'wakeup') as wakeup:
userservice.set_ready()
wakeup.assert_called_with()
self.assertEqual(userservice.destroy(), types.states.TaskState.FINISHED)
self.assertEqual(userservice.cancel(), types.states.TaskState.FINISHED)
# deploy for cache should return error
state = userservice.deploy_for_cache(level=types.services.CacheLevel.L1)
self.assertEqual(state, types.states.TaskState.ERROR)
self.assertEqual(userservice.check_state(), types.states.TaskState.ERROR)
self.assertEqual(userservice.error_reason(), userservice._reason)