diff --git a/server/src/tests/services/openstack/test_client.py b/server/src/tests/services/openstack/test_client.py index f54217121..c028a3f75 100644 --- a/server/src/tests/services/openstack/test_client.py +++ b/server/src/tests/services/openstack/test_client.py @@ -120,6 +120,17 @@ class TestOpenStackClient(UDSTransactionTestCase): msg='Timeout waiting for snapshot to be available', ) + def wait_for_server( + self, + server: openstack_types.ServerInfo, + power_state: openstack_types.PowerState = openstack_types.PowerState.RUNNING, + ) -> None: + helpers.waiter( + lambda: self.oclient.get_server_info(server.id, force=True).power_state == power_state, + timeout=30, + msg='Timeout waiting for server to be running', + ) + @contextlib.contextmanager def create_test_volume(self) -> typing.Iterator[openstack_types.VolumeInfo]: volume = self.oclient.t_create_volume( @@ -164,7 +175,11 @@ class TestOpenStackClient(UDSTransactionTestCase): ) @contextlib.contextmanager - def create_test_server(self) -> typing.Iterator[openstack_types.ServerInfo]: + def create_test_server( + self, + ) -> typing.Iterator[ + tuple[openstack_types.ServerInfo, openstack_types.VolumeInfo, openstack_types.SnapshotInfo] + ]: with self.create_test_volume() as volume: with self.create_test_snapshot(volume) as snapshot: server = self.oclient.create_server_from_snapshot( @@ -177,14 +192,10 @@ class TestOpenStackClient(UDSTransactionTestCase): ) try: # Wait for server to be running - helpers.waiter( - lambda: self.oclient.get_server_info(server.id, force=True).power_state.is_running(), - timeout=30, - msg='Timeout waiting for server to be running', - ) + self.wait_for_server(server) # Reget server info to complete all data server = self.oclient.get_server_info(server.id, force=True) - yield server + yield server, volume, snapshot finally: self.oclient.delete_server(server.id) @@ -199,8 +210,8 @@ class TestOpenStackClient(UDSTransactionTestCase): self.assertIn(self._regionid, [r.id for r in regions]) def test_list_servers(self) -> None: - with self.create_test_server() as server1: - with self.create_test_server() as server2: + with self.create_test_server() as (server1, _, _): + with self.create_test_server() as (server2, _, _): servers = self.oclient.list_servers(force=True) self.assertGreaterEqual(len(servers), 2) self.assertIn( @@ -262,7 +273,7 @@ class TestOpenStackClient(UDSTransactionTestCase): self.assertIn(self._security_group_name, [sg.name for sg in security_groups]) def test_get_server_info(self) -> None: - with self.create_test_server() as server: + with self.create_test_server() as (server, _, _): server_info = self.oclient.get_server_info(server.id) self.assertEqual(server.id, server_info.id) self.assertEqual(server.name, server_info.name) @@ -303,7 +314,7 @@ class TestOpenStackClient(UDSTransactionTestCase): self.oclient.create_snapshot(volume_id='non-existing-volume', name='non-existing-snapshot') def test_create_server_from_snapshot(self) -> None: - with self.create_test_server() as server: + with self.create_test_server() as (server, _, _): self.assertIsNotNone(server.id) # Trying to create a server from a non existing snapshot should raise an exceptions.NotFoundException @@ -330,23 +341,16 @@ class TestOpenStackClient(UDSTransactionTestCase): self.oclient.delete_snapshot('non-existing-snapshot') def test_operations_server(self) -> None: - with self.create_test_server() as server: + with self.create_test_server() as (server, _, _): # Server is already running, first stop it self.oclient.stop_server(server.id) - helpers.waiter( - lambda: self.oclient.get_server_info(server.id, force=True).power_state.is_stopped(), - timeout=30, - msg='Timeout waiting for server to be stopped', - ) + self.wait_for_server(server, openstack_types.PowerState.SHUTDOWN) self.oclient.start_server(server.id) - helpers.waiter( - lambda: self.oclient.get_server_info(server.id, force=True).power_state.is_running(), - timeout=30, - msg='Timeout waiting for server to be running', - ) + self.wait_for_server(server) self.oclient.reset_server(server.id) + # Here we need to wait for the server to be active again helpers.waiter( lambda: self.oclient.get_server_info(server.id, force=True).status.is_active(), timeout=30, @@ -355,19 +359,11 @@ class TestOpenStackClient(UDSTransactionTestCase): # Suspend self.oclient.suspend_server(server.id) - helpers.waiter( - lambda: self.oclient.get_server_info(server.id, force=True).power_state.is_suspended(), - timeout=30, - msg='Timeout waiting for server to be suspended', - ) + self.wait_for_server(server, openstack_types.PowerState.SUSPENDED) # Resume self.oclient.resume_server(server.id) - helpers.waiter( - lambda: self.oclient.get_server_info(server.id, force=True).power_state.is_running(), - timeout=30, - msg='Timeout waiting for server to be running', - ) + self.wait_for_server(server) # Reboot self.oclient.reboot_server(server.id) @@ -399,6 +395,25 @@ class TestOpenStackClient(UDSTransactionTestCase): def test_test_connection(self) -> None: self.assertTrue(self.oclient.test_connection()) - + def test_is_available(self) -> None: - self.assertTrue(self.oclient.is_available()) \ No newline at end of file + self.assertTrue(self.oclient.is_available()) + + # Some useful tests + def test_duplicated_server_name(self) -> None: + with self.create_test_server() as (server, _volume, snapshot): + res = self.oclient.create_server_from_snapshot( + snapshot_id=snapshot.id, + name=server.name, + flavor_id=self._flavorid, + network_id=self._networkid, + security_groups_names=[], + availability_zone=self._availability_zone_id, + ) + # Has been created, and no problem at all + self.assertIsNotNone(res) + + # Now, delete it + # wait for server to be running + self.wait_for_server(res) + self.oclient.delete_server(res.id) diff --git a/server/src/uds/core/services/generics/dynamic/service.py b/server/src/uds/core/services/generics/dynamic/service.py index 0c2a79615..495769ddd 100644 --- a/server/src/uds/core/services/generics/dynamic/service.py +++ b/server/src/uds/core/services/generics/dynamic/service.py @@ -77,7 +77,6 @@ class DynamicService(services.Service, abc.ABC): # pylint: disable=too-many-pub order=102, tab=types.ui.Tab.ADVANCED, ) - maintain_on_error = fields.maintain_on_error_field( order=103, tab=types.ui.Tab.ADVANCED, @@ -110,7 +109,7 @@ class DynamicService(services.Service, abc.ABC): # pylint: disable=too-many-pub """ return name - # overridable + # overridable, but not needed if no remove_duplicates is used def find_duplicates(self, name: str, mac: str) -> collections.abc.Iterable[str]: """ Checks if a machine with the same name or mac exists @@ -125,6 +124,8 @@ class DynamicService(services.Service, abc.ABC): # pylint: disable=too-many-pub Note: Maybe we can only check name or mac, or both, depending on the service + This method must be be provided if the field remove_duplicates is used + If not, will raise a NotImplementedError """ raise NotImplementedError(f'{self.__class__}: find_duplicates must be implemented if remove_duplicates is used!') diff --git a/server/src/uds/core/services/generics/dynamic/userservice.py b/server/src/uds/core/services/generics/dynamic/userservice.py index 17c8cd710..684bd6914 100644 --- a/server/src/uds/core/services/generics/dynamic/userservice.py +++ b/server/src/uds/core/services/generics/dynamic/userservice.py @@ -519,6 +519,7 @@ class DynamicUserService(services.UserService, autoserializable.AutoSerializable def remove_duplicated_names(self) -> None: name = self.get_vmname() try: + retry = False for vmid in self.service().perform_find_duplicates(name, self.get_unique_id()): userservice = self.db_obj() log.log( @@ -528,7 +529,12 @@ class DynamicUserService(services.UserService, autoserializable.AutoSerializable types.log.LogSource.SERVICE, ) self.service().delete(self, vmid) + retry = True + if retry: # Retry again in a while if duplicated machines where found, until we remove all of them + # Note that this way, can request the deletion of the same machine multiple times + # but this is not a problem, as the service will simply ignore the request if the machine is not there + # when the deletion is requested self.retry_later() except Exception as e: logger.warning('Locating duplicated machines: %s', e) diff --git a/server/src/uds/core/services/generics/exceptions.py b/server/src/uds/core/services/generics/exceptions.py index f1ab2f517..3bbc69fa2 100644 --- a/server/src/uds/core/services/generics/exceptions.py +++ b/server/src/uds/core/services/generics/exceptions.py @@ -1,11 +1,14 @@ from uds.core import exceptions as core_exceptions + class Error(core_exceptions.UDSException): """ Base exception for this module """ + pass + class RetryableError(Error): """ Exception that is raised when an error is detected that can be retried @@ -31,3 +34,12 @@ class NotFoundError(Error): def __init__(self, message: str): super().__init__(message) + + +class AlreadyExistsError(Error): + """ + Exception that is raised when an object already exists + """ + + def __init__(self, message: str): + super().__init__(message) diff --git a/server/src/uds/core/util/fields.py b/server/src/uds/core/util/fields.py index 8fc851c44..60d110dba 100644 --- a/server/src/uds/core/util/fields.py +++ b/server/src/uds/core/util/fields.py @@ -198,7 +198,7 @@ def get_certificates_from_field( # Timeout def timeout_field( - default: int = 3, + default: int = 5, order: int = 90, tab: 'types.ui.Tab|str|None' = types.ui.Tab.ADVANCED, old_field_name: typing.Optional[str] = None, diff --git a/server/src/uds/services/OpenStack/service.py b/server/src/uds/services/OpenStack/service.py index a83e77bb8..d04518e15 100644 --- a/server/src/uds/services/OpenStack/service.py +++ b/server/src/uds/services/OpenStack/service.py @@ -31,6 +31,7 @@ Author: Adolfo Gómez, dkmaster at dkmon dot com """ import logging +import collections.abc import typing from django.utils.translation import gettext_noop as _ @@ -178,6 +179,7 @@ class OpenStackLiveService(DynamicService): maintain_on_error = DynamicService.maintain_on_error try_soft_shutdown = DynamicService.try_soft_shutdown + remove_duplicates = DynamicService.remove_duplicates prov_uuid = gui.HiddenField() @@ -233,6 +235,12 @@ class OpenStackLiveService(DynamicService): def sanitized_name(self, name: str) -> str: return self.provider().sanitized_name(name) + def find_duplicates(self, name: str, mac: str) -> collections.abc.Iterable[str]: + # Only looks for name duplicates, the mac is created by openstack, so it should be unique + for i in self.api.list_servers(): + if i.name == name: + yield i.id + def get_ip(self, caller_instance: typing.Optional['DynamicUserService | DynamicPublication'], vmid: str) -> str: return self.api.get_server_info(vmid).validated().addresses[0].ip