diff --git a/OpenStack-Rabbit-Consumer/rabbit_consumer/aq_api.py b/OpenStack-Rabbit-Consumer/rabbit_consumer/aq_api.py index fa753ea9..90f7b88b 100644 --- a/OpenStack-Rabbit-Consumer/rabbit_consumer/aq_api.py +++ b/OpenStack-Rabbit-Consumer/rabbit_consumer/aq_api.py @@ -191,23 +191,23 @@ def delete_host(hostname: str) -> None: setup_requests(url, "delete", "Host Delete") -def delete_address(address: OpenstackAddress, machine_name: str) -> None: +def delete_address(address: str, machine_name: str) -> None: """ Deletes an address in Aquilon """ - logger.debug("Attempting to delete address for %s ", address.addr) + logger.debug("Attempting to delete address for %s ", address) url = ConsumerConfig().aq_url + "/interface_address" - params = {"ip": address.addr, "machine": machine_name, "interface": "eth0"} + params = {"ip": address, "machine": machine_name, "interface": "eth0"} setup_requests(url, "delete", "Address Delete", params=params) -def delete_interface(address: OpenstackAddress) -> None: +def delete_interface(machine_name: str) -> None: """ Deletes a host interface in Aquilon """ - logger.debug("Attempting to delete interface for %s ", address.mac_addr) + logger.debug("Attempting to delete interface for %s ", machine_name) url = ConsumerConfig().aq_url + "/interface/command/del" - params = {"mac": address.mac_addr} + params = {"interface": "eth0", "machine": machine_name} setup_requests(url, "post", "Interface Delete", params=params) @@ -247,13 +247,27 @@ def set_interface_bootable(machine_name: str, interface_name: str) -> None: setup_requests(url, "post", "Update Machine Interface") -def search_machine(mac_addr: str) -> Optional[str]: +def search_machine_by_serial(vm_data: VmData) -> Optional[str]: """ - Searches for a machine in Aquilon based on a MAC address + Searches for a machine in Aquilon based on a serial number """ - logger.debug("Searching for host with MAC %s", mac_addr) + logger.debug("Searching for host with serial %s", vm_data.virtual_machine_id) url = ConsumerConfig().aq_url + "/find/machine" - params = {"mac": mac_addr} + params = {"serial": vm_data.virtual_machine_id} + response = setup_requests(url, "get", "Search Host", params=params).strip() + + if response: + return response + return None + + +def search_host_by_machine(machine_name: str) -> Optional[str]: + """ + Searches for a host in Aquilon based on a machine name + """ + logger.debug("Searching for host with machine name %s", machine_name) + url = ConsumerConfig().aq_url + "/find/host" + params = {"machine": machine_name} response = setup_requests(url, "get", "Search Host", params=params).strip() if response: diff --git a/OpenStack-Rabbit-Consumer/rabbit_consumer/image_metadata.py b/OpenStack-Rabbit-Consumer/rabbit_consumer/image_metadata.py index 7e8db5c9..e7d7e7b2 100644 --- a/OpenStack-Rabbit-Consumer/rabbit_consumer/image_metadata.py +++ b/OpenStack-Rabbit-Consumer/rabbit_consumer/image_metadata.py @@ -1,10 +1,7 @@ -import dataclasses import logging from dataclasses import dataclass, field -from typing import Optional, Type from mashumaro import DataClassDictMixin, field_options -from mashumaro.mixins.json import T logger = logging.getLogger(__name__) @@ -15,29 +12,9 @@ class ImageMetadata(DataClassDictMixin): Deserialised metadata that is set on OpenStack images """ - aq_archetype: Optional[str] = field(metadata=field_options(alias="AQ_ARCHETYPE")) - aq_domain: Optional[str] = field(metadata=field_options(alias="AQ_DOMAIN")) + aq_archetype: str = field(metadata=field_options(alias="AQ_ARCHETYPE")) + aq_domain: str = field(metadata=field_options(alias="AQ_DOMAIN")) - aq_personality: Optional[str] = field( - metadata=field_options(alias="AQ_PERSONALITY") - ) - aq_os_version: Optional[str] = field(metadata=field_options(alias="AQ_OSVERSION")) - aq_os: Optional[str] = field(metadata=field_options(alias="AQ_OS")) - - @classmethod - def __post_deserialize__( - cls: Type[T], obj: "ImageMetadata" - ) -> Optional["ImageMetadata"]: - """ - Post de-serialisation hook to check for missing fields - """ - fields = dataclasses.fields(obj) - if not any(getattr(obj, f.name) for f in fields): - # This doesn't have any fields set, so we can't validate it - return None - - if not all(getattr(obj, f.name) for f in fields): - logger.error("Missing data for on object %s", obj) - return None - - return obj + aq_personality: str = field(metadata=field_options(alias="AQ_PERSONALITY")) + aq_os_version: str = field(metadata=field_options(alias="AQ_OSVERSION")) + aq_os: str = field(metadata=field_options(alias="AQ_OS")) diff --git a/OpenStack-Rabbit-Consumer/rabbit_consumer/message_consumer.py b/OpenStack-Rabbit-Consumer/rabbit_consumer/message_consumer.py index 75dbe3b1..0d51bbe8 100644 --- a/OpenStack-Rabbit-Consumer/rabbit_consumer/message_consumer.py +++ b/OpenStack-Rabbit-Consumer/rabbit_consumer/message_consumer.py @@ -1,5 +1,6 @@ import json import logging +import socket from typing import Optional, List import rabbitpy @@ -25,12 +26,12 @@ def is_aq_managed_image(rabbit_message: RabbitMessage) -> Optional[ImageMetadata Check to see if the metadata in the message contains entries that suggest it is for an Aquilon VM. """ - image = openstack_api.get_image_name(VmData.from_message(rabbit_message)) - image_meta = ImageMetadata.from_dict(image.metadata) - - if not image_meta: + image = openstack_api.get_image(VmData.from_message(rabbit_message)) + if "AQ_OS" not in image.metadata: logger.debug("Skipping non-Aquilon image: %s", image.name) return None + + image_meta = ImageMetadata.from_dict(image.metadata) return image_meta @@ -49,49 +50,86 @@ def consume(message: RabbitMessage) -> None: raise ValueError(f"Unsupported message type: {message.event_type}") -def delete_machine(addresses: List[OpenstackAddress]): - for address in addresses: - if aq_api.check_host_exists(address.hostname): - logger.info("Host exists for %s. Deleting old", address.hostname) - aq_api.delete_host(address.hostname) +def delete_machine( + vm_data: VmData, network_details: Optional[OpenstackAddress] = None +) -> None: + """ + Deletes a machine in Aquilon and all associated addresses based on + the serial, MAC and hostname provided. This is the best effort attempt + to clean-up, since we can have partial or incorrect information. + """ + # First handle hostnames + if network_details and aq_api.check_host_exists(network_details.hostname): + logger.info("Deleting host %s", network_details.hostname) + aq_api.delete_host(network_details.hostname) + + machine_name = aq_api.search_machine_by_serial(vm_data) + if not machine_name: + logger.info("No existing record found for %s", vm_data.virtual_machine_id) + return + + # We have to do this manually because AQ has neither a: + # - Just delete the machine please + # - Delete this if it exists + # So alas we have to do everything by hand, whilst adhering to random rules + # of deletion orders which it enforces... - machine_name = aq_api.search_machine(address.mac_addr) - if not machine_name: - # Nothing else to do at this point - return + hostname = aq_api.search_host_by_machine(machine_name) + machine_details = aq_api.get_machine_details(machine_name) + # We have to clean-up all the interfaces and addresses first + if hostname: + if aq_api.check_host_exists(hostname): + # This is a different hostname to the one we have in the message + # so, we need to delete it + logger.info("Host exists for %s. Deleting old", hostname) + aq_api.delete_host(hostname) - logger.info("Machine exists for MAC: %s. Deleting old", address.mac_addr) - machine_details = aq_api.get_machine_details(machine_name) + # First delete the interfaces + ipv4_address = socket.gethostbyname(hostname) + if ipv4_address in machine_details: + aq_api.delete_address(ipv4_address, machine_name) - # We have to do this manually because AQ has neither a: - # - Just delete the machine please - # - Delete this if it exists - # So alas we have to do everything by hand, whilst adhering to random rules - # of deletion orders which it enforces... + if "eth0" in machine_details: + aq_api.delete_interface(machine_name) - if address.addr in machine_details: - aq_api.delete_address(address, machine_name) - if address.mac_addr in machine_details: - aq_api.delete_interface(address) - aq_api.delete_machine(machine_name) + logger.info("Machine exists for %s. Deleting old", vm_data.virtual_machine_id) + # Then delete the machine + aq_api.delete_machine(machine_name) -def handle_create_machine(rabbit_message: RabbitMessage) -> None: + +def check_machine_valid(rabbit_message: RabbitMessage) -> bool: """ - Handles the creation of a machine in Aquilon. This includes - creating the machine, adding the nics, and managing the host. + Checks to see if the machine is valid for creating in Aquilon. """ - logger.info("=== Received Aquilon VM create message ===") - _print_debug_logging(rabbit_message) - vm_data = VmData.from_message(rabbit_message) if not openstack_api.check_machine_exists(vm_data): # User has likely deleted the machine since we got here logger.warning( "Machine %s does not exist, skipping creation", vm_data.virtual_machine_id ) + return False + + if not is_aq_managed_image(rabbit_message): + logger.debug("Ignoring non AQ Image: %s", rabbit_message) + return False + + return True + + +def handle_create_machine(rabbit_message: RabbitMessage) -> None: + """ + Handles the creation of a machine in Aquilon. This includes + creating the machine, adding the nics, and managing the host. + """ + logger.info("=== Received Aquilon VM create message ===") + _print_debug_logging(rabbit_message) + + if not check_machine_valid(rabbit_message): return + vm_data = VmData.from_message(rabbit_message) + image_meta = is_aq_managed_image(rabbit_message) network_details = openstack_api.get_server_networks(vm_data) @@ -100,7 +138,7 @@ def handle_create_machine(rabbit_message: RabbitMessage) -> None: logger.info("Skipping novalocal only host: %s", vm_name) return - delete_machine(network_details) + delete_machine(vm_data, network_details[0]) # Configure networking machine_name = aq_api.create_machine(rabbit_message, vm_data) @@ -128,7 +166,7 @@ def _print_debug_logging(rabbit_message: RabbitMessage) -> None: logger.debug( "Project Name: %s (%s)", rabbit_message.project_name, vm_data.project_id ) - logger.debug( + logger.info( "VM Name: %s (%s) ", rabbit_message.payload.vm_name, vm_data.virtual_machine_id ) logger.debug("Username: %s", rabbit_message.user_name) @@ -143,14 +181,7 @@ def handle_machine_delete(rabbit_message: RabbitMessage) -> None: _print_debug_logging(rabbit_message) vm_data = VmData.from_message(rabbit_message) - network_data = openstack_api.get_server_networks(vm_data) - - if not network_data or not network_data[0].hostname: - vm_name = rabbit_message.payload.vm_name - logger.debug("No hostnames found for %s, skipping delete", vm_name) - return - - delete_machine(addresses=network_data) + delete_machine(vm_data=vm_data) logger.info( "=== Finished Aquilon deletion hook for VM %s ===", vm_data.virtual_machine_id @@ -193,11 +224,6 @@ def on_message(message: rabbitpy.Message) -> None: decoded = RabbitMessage.from_json(body) logger.debug("Decoded message: %s", decoded) - if not is_aq_managed_image(decoded): - logger.debug("Ignoring non AQ Image: %s", decoded) - message.ack() - return - consume(decoded) message.ack() diff --git a/OpenStack-Rabbit-Consumer/rabbit_consumer/openstack_api.py b/OpenStack-Rabbit-Consumer/rabbit_consumer/openstack_api.py index 4ecd48cb..05f6a381 100644 --- a/OpenStack-Rabbit-Consumer/rabbit_consumer/openstack_api.py +++ b/OpenStack-Rabbit-Consumer/rabbit_consumer/openstack_api.py @@ -18,8 +18,7 @@ class OpenstackConnection: in subsequent functions. """ - def __init__(self, project_name: str): - self.project_name = project_name + def __init__(self): self.conn = None def __enter__(self): @@ -41,7 +40,7 @@ def check_machine_exists(vm_data: VmData) -> bool: """ Checks to see if the machine exists in Openstack. """ - with OpenstackConnection(vm_data.project_id) as conn: + with OpenstackConnection() as conn: return bool(conn.compute.find_server(vm_data.virtual_machine_id)) @@ -49,10 +48,12 @@ def get_server_details(vm_data: VmData) -> Server: """ Gets the server details from Openstack with details included """ - with OpenstackConnection(vm_data.project_id) as conn: + with OpenstackConnection() as conn: # Workaround for details missing from find_server # on the current version of openstacksdk - found = list(conn.compute.servers(vm_data.virtual_machine_id)) + found = list( + conn.compute.servers(uuid=vm_data.virtual_machine_id, all_projects=True) + ) if not found: raise ValueError(f"Server not found for id: {vm_data.virtual_machine_id}") return found[0] @@ -64,6 +65,9 @@ def get_server_networks(vm_data: VmData) -> List[OpenstackAddress]: of deserialized OpenstackAddresses. """ server = get_server_details(vm_data) + if "Internal" not in server.addresses: + logger.warning("No internal network found for server %s", server.name) + return [] return OpenstackAddress.get_internal_networks(server.addresses) @@ -75,13 +79,13 @@ def get_metadata(vm_data: VmData) -> dict: return server.metadata -def get_image_name(vm_data: VmData) -> Image: +def get_image(vm_data: VmData) -> Image: """ Gets the image name from Openstack for the virtual machine. """ server = get_server_details(vm_data) uuid = server.image.id - with OpenstackConnection(vm_data.project_id) as conn: + with OpenstackConnection() as conn: image = conn.compute.find_image(uuid) return image @@ -91,7 +95,7 @@ def update_metadata(vm_data: VmData, metadata) -> None: Updates the metadata for the virtual machine. """ server = get_server_details(vm_data) - with OpenstackConnection(vm_data.project_id) as conn: + with OpenstackConnection() as conn: conn.compute.set_server_metadata(server, **metadata) logger.debug("Setting metadata successful") diff --git a/OpenStack-Rabbit-Consumer/test/test_aq_api.py b/OpenStack-Rabbit-Consumer/test/test_aq_api.py index b64d3e35..8ddee039 100644 --- a/OpenStack-Rabbit-Consumer/test/test_aq_api.py +++ b/OpenStack-Rabbit-Consumer/test/test_aq_api.py @@ -25,6 +25,8 @@ check_host_exists, AquilonError, add_machine_nics, + search_machine_by_serial, + search_host_by_machine, ) @@ -359,3 +361,67 @@ def test_check_host_exists_returns_false(config, setup): setup.side_effect = AquilonError(f"Error:\n Host {hostname} not found.") assert not check_host_exists(hostname) + + +@patch("rabbit_consumer.aq_api.setup_requests") +@patch("rabbit_consumer.aq_api.ConsumerConfig") +def test_search_machine_by_serial(config, setup, vm_data): + """ + Test that search_machine_by_serial calls the correct URL with the correct parameters + """ + config.return_value.aq_url = "https://example.com" + response = search_machine_by_serial(vm_data) + + expected_url = "https://example.com/find/machine" + expected_args = {"serial": vm_data.virtual_machine_id} + setup.assert_called_once_with(expected_url, "get", mock.ANY, params=expected_args) + assert response == setup.return_value.strip.return_value + + +@patch("rabbit_consumer.aq_api.setup_requests") +@patch("rabbit_consumer.aq_api.ConsumerConfig") +def test_search_machine_by_serial_not_found(config, setup, vm_data): + """ + Test that search_machine_by_serial calls the correct URL with the correct parameters + """ + config.return_value.aq_url = "https://example.com" + setup.return_value = "" + response = search_machine_by_serial(vm_data) + + expected_url = "https://example.com/find/machine" + expected_args = {"serial": vm_data.virtual_machine_id} + setup.assert_called_once_with(expected_url, "get", mock.ANY, params=expected_args) + assert response is None + + +@patch("rabbit_consumer.aq_api.setup_requests") +@patch("rabbit_consumer.aq_api.ConsumerConfig") +def test_search_host_by_machine(config, setup): + """ + Test that search_host_by_machine calls the correct URL with the correct parameters + to return the host name + """ + config.return_value.aq_url = "https://example.com" + response = search_host_by_machine("machine_name") + + expected_url = "https://example.com/find/host" + expected_args = {"machine": "machine_name"} + setup.assert_called_once_with(expected_url, "get", mock.ANY, params=expected_args) + assert response == setup.return_value.strip.return_value + + +@patch("rabbit_consumer.aq_api.setup_requests") +@patch("rabbit_consumer.aq_api.ConsumerConfig") +def test_search_host_by_machine_not_found(config, setup): + """ + Test that search_host_by_machine calls the correct URL with the correct parameters + to return the host name + """ + config.return_value.aq_url = "https://example.com" + setup.return_value = "" + response = search_host_by_machine("machine_name") + + expected_url = "https://example.com/find/host" + expected_args = {"machine": "machine_name"} + setup.assert_called_once_with(expected_url, "get", mock.ANY, params=expected_args) + assert response is None diff --git a/OpenStack-Rabbit-Consumer/test/test_message_consumer.py b/OpenStack-Rabbit-Consumer/test/test_message_consumer.py index 39c6070a..a2b2bea6 100644 --- a/OpenStack-Rabbit-Consumer/test/test_message_consumer.py +++ b/OpenStack-Rabbit-Consumer/test/test_message_consumer.py @@ -19,7 +19,10 @@ handle_create_machine, handle_machine_delete, SUPPORTED_MESSAGE_TYPES, + check_machine_valid, + is_aq_managed_image, ) +from rabbit_consumer.vm_data import VmData @pytest.fixture(name="valid_event_type") @@ -90,41 +93,14 @@ def test_on_message_accepts_event_types(message_event_type, consume, event_type) with ( patch("rabbit_consumer.message_consumer.RabbitMessage"), patch("rabbit_consumer.message_consumer.json"), - patch("rabbit_consumer.message_consumer.is_aq_managed_image") as is_managed, ): - is_managed.return_value = True message = Mock() on_message(message) - is_managed.assert_called_once() consume.assert_called_once() message.ack.assert_called_once() -@patch("rabbit_consumer.message_consumer.is_aq_managed_image") -@patch("rabbit_consumer.message_consumer.consume") -def test_on_message_ignores_non_aq(consume_mock, aq_message_mock, valid_event_type): - """ - Test that the function ignores non-AQ messages and acks them - """ - message = Mock() - aq_message_mock.return_value = False - - with ( - patch("rabbit_consumer.message_consumer.json"), - patch("rabbit_consumer.message_consumer.RabbitMessage"), - patch( - "rabbit_consumer.message_consumer.MessageEventType" - ) as message_event_type, - ): - message_event_type.from_json.return_value = valid_event_type - on_message(message) - - aq_message_mock.assert_called_once() - consume_mock.assert_not_called() - message.ack.assert_called_once() - - # pylint: disable=too-few-public-methods class MockedConfig(ConsumerConfig): """ @@ -207,23 +183,39 @@ def test_add_hostname_to_metadata_machine_does_not_exist(openstack_api, vm_data) openstack_api.update_metadata.assert_not_called() +@patch("rabbit_consumer.message_consumer.check_machine_valid") +@patch("rabbit_consumer.message_consumer.openstack_api") +def test_handle_create_machine_skips_invalid(openstack_api, machine_valid): + """ + Test that the function skips invalid machines + """ + machine_valid.return_value = False + vm_data = Mock() + + handle_create_machine(vm_data) + + machine_valid.assert_called_once_with(vm_data) + openstack_api.get_server_networks.assert_not_called() + + @patch("rabbit_consumer.message_consumer.openstack_api") @patch("rabbit_consumer.message_consumer.aq_api") @patch("rabbit_consumer.message_consumer.add_hostname_to_metadata") # pylint: disable=too-many-arguments def test_consume_create_machine_hostnames_good_path( - metadata, aq_api, openstack, rabbit_message, valid_event_type, image_metadata + metadata, aq_api, openstack, rabbit_message, image_metadata ): """ Test that the function calls the correct functions in the correct order to register a new machine """ with ( patch("rabbit_consumer.message_consumer.VmData") as data_patch, + patch("rabbit_consumer.message_consumer.check_machine_valid") as check_machine, patch("rabbit_consumer.message_consumer.is_aq_managed_image") as is_managed, - patch("rabbit_consumer.message_consumer.MessageEventType") as message_type, + patch("rabbit_consumer.message_consumer.delete_machine") as delete_machine, ): + check_machine.return_value = True is_managed.return_value = image_metadata - message_type.from_json.return_value = valid_event_type handle_create_machine(rabbit_message) @@ -234,6 +226,7 @@ def test_consume_create_machine_hostnames_good_path( openstack.get_server_networks.assert_called_with(vm_data) # Check main Aq Flow + delete_machine.assert_called_once_with(vm_data, network_details[0]) aq_api.create_machine.assert_called_once_with(rabbit_message, vm_data) machine_name = aq_api.create_machine.return_value @@ -253,25 +246,94 @@ def test_consume_create_machine_hostnames_good_path( @patch("rabbit_consumer.message_consumer.delete_machine") -def test_consume_delete_machine_good_path( - delete_machine, rabbit_message, openstack_address_list -): +def test_consume_delete_machine_good_path(delete_machine, rabbit_message): """ Test that the function calls the correct functions in the correct order to delete a machine """ rabbit_message.payload.metadata.machine_name = "AQ-HOST1" - with ( - patch("rabbit_consumer.message_consumer.VmData") as data_patch, - patch("rabbit_consumer.message_consumer.openstack_api") as openstack, - ): - openstack.get_server_networks.return_value = openstack_address_list - + with patch("rabbit_consumer.message_consumer.VmData") as data_patch: handle_machine_delete(rabbit_message) - data_patch.from_message.assert_called_with(rabbit_message) - openstack.get_server_networks.assert_called_with( - data_patch.from_message.return_value - ) + delete_machine.assert_called_once_with(vm_data=data_patch.from_message.return_value) + + +@patch("rabbit_consumer.message_consumer.is_aq_managed_image") +@patch("rabbit_consumer.message_consumer.openstack_api") +def test_check_machine_valid(openstack_api, is_aq_managed): + """ + Test that the function returns True when the machine is valid + """ + mock_message = NonCallableMock() + is_aq_managed.return_value = True + openstack_api.check_machine_exists.return_value = True + + assert check_machine_valid(mock_message) + is_aq_managed.assert_called_once_with(mock_message) + openstack_api.check_machine_exists.assert_called_once_with( + VmData.from_message(mock_message) + ) + + +@patch("rabbit_consumer.message_consumer.is_aq_managed_image") +@patch("rabbit_consumer.message_consumer.openstack_api") +def test_check_machine_invalid_image(openstack_api, is_aq_managed): + """ + Test that the function returns False when the image is not AQ managed + """ + mock_message = NonCallableMock() + is_aq_managed.return_value = False + openstack_api.check_machine_exists.return_value = True + + assert not check_machine_valid(mock_message) + + openstack_api.check_machine_exists.assert_called_once_with( + VmData.from_message(mock_message) + ) + is_aq_managed.assert_called_once_with(mock_message) + + +@patch("rabbit_consumer.message_consumer.is_aq_managed_image") +@patch("rabbit_consumer.message_consumer.openstack_api") +def test_check_machine_invalid_machine(openstack_api, is_aq_managed): + """ + Test that the function returns False when the machine does not exist + """ + mock_message = NonCallableMock() + openstack_api.check_machine_exists.return_value = False + + assert not check_machine_valid(mock_message) + + is_aq_managed.assert_not_called() + openstack_api.check_machine_exists.assert_called_once_with( + VmData.from_message(mock_message) + ) + + +@patch("rabbit_consumer.message_consumer.VmData") +@patch("rabbit_consumer.message_consumer.ImageMetadata") +@patch("rabbit_consumer.message_consumer.openstack_api") +def test_is_aq_managed_image(openstack_api, image_meta, vm_data): + """ + Test that the function returns True when the image is AQ managed + """ + mock_message = NonCallableMock() + openstack_api.get_image.return_value.metadata = {"AQ_OS": "True"} + + assert is_aq_managed_image(mock_message) == image_meta.from_dict.return_value + openstack_api.get_image.assert_called_once_with(vm_data.from_message.return_value) + + +@patch("rabbit_consumer.message_consumer.VmData") +@patch("rabbit_consumer.message_consumer.ImageMetadata") +@patch("rabbit_consumer.message_consumer.openstack_api") +def test_is_aq_managed_image_missing_key(openstack_api, image_meta, vm_data): + """ + Test that the function returns False when the image is not AQ managed + """ + mock_message = NonCallableMock() + openstack_api.get_image.return_value.metadata = {} - delete_machine.assert_called_once_with(addresses=openstack_address_list) + assert not is_aq_managed_image(mock_message) + openstack_api.get_image.assert_called_once_with(vm_data.from_message.return_value) + image_meta.from_dict.assert_not_called() diff --git a/OpenStack-Rabbit-Consumer/test/test_openstack_api.py b/OpenStack-Rabbit-Consumer/test/test_openstack_api.py index 68e55f0c..a150eee1 100644 --- a/OpenStack-Rabbit-Consumer/test/test_openstack_api.py +++ b/OpenStack-Rabbit-Consumer/test/test_openstack_api.py @@ -18,8 +18,7 @@ def test_openstack_connection(mock_connect, mock_config): """ Test that the OpenstackConnection context manager calls the correct functions """ - mock_project = NonCallableMock() - with OpenstackConnection(mock_project) as conn: + with OpenstackConnection() as conn: mock_connect.assert_called_once_with( auth_url=mock_config.return_value.openstack_auth_url, username=mock_config.return_value.openstack_username, @@ -44,7 +43,7 @@ def test_check_machine_exists_existing_machine(conn, vm_data): context.compute.find_server.return_value = NonCallableMock() found = check_machine_exists(vm_data) - conn.assert_called_once_with(vm_data.project_id) + conn.assert_called_once_with() context.compute.find_server.assert_called_with(vm_data.virtual_machine_id) assert isinstance(found, bool) and found @@ -58,7 +57,7 @@ def test_check_machine_exists_deleted_machine(conn, vm_data): context.compute.find_server.return_value = None found = check_machine_exists(vm_data) - conn.assert_called_once_with(vm_data.project_id) + conn.assert_called_once_with() context = conn.return_value.__enter__.return_value context.compute.find_server.assert_called_with(vm_data.virtual_machine_id) assert isinstance(found, bool) and not found @@ -75,7 +74,7 @@ def test_update_metadata(server_details, conn, vm_data): server_details.assert_called_once_with(vm_data) - conn.assert_called_once_with(vm_data.project_id) + conn.assert_called_once_with() context = conn.return_value.__enter__.return_value context.compute.set_server_metadata.assert_called_once_with( server_details.return_value, **{"key": "value"} @@ -92,7 +91,9 @@ def test_get_server_details(conn, vm_data): result = get_server_details(vm_data) - context.compute.servers.assert_called_once_with(vm_data.virtual_machine_id) + context.compute.servers.assert_called_once_with( + uuid=vm_data.virtual_machine_id, all_projects=True + ) assert result == context.compute.servers.return_value[0] @@ -103,9 +104,21 @@ def test_get_server_networks(address, server_details, vm_data): """ Test that the function calls the correct functions to get the networks of a VM """ - server_details.return_value = NonCallableMock() + server_details.return_value.addresses = {"Internal": []} get_server_networks(vm_data) address.get_internal_networks.assert_called_once_with( server_details.return_value.addresses ) + + +@patch("rabbit_consumer.openstack_api.get_server_details") +def test_get_server_networks_no_internal(server_details, vm_data): + """ + Tests that an empty list is returned when there are no internal networks + """ + server_details.return_value = NonCallableMock() + server_details.return_value.addresses = {"public": []} + + result = get_server_networks(vm_data) + assert not result diff --git a/OpenStack-Rabbit-Consumer/version.txt b/OpenStack-Rabbit-Consumer/version.txt index 38f77a65..7ec1d6db 100644 --- a/OpenStack-Rabbit-Consumer/version.txt +++ b/OpenStack-Rabbit-Consumer/version.txt @@ -1 +1 @@ -2.0.1 +2.1.0 diff --git a/charts/rabbit-consumer/Chart.yaml b/charts/rabbit-consumer/Chart.yaml index ab610744..cd143118 100644 --- a/charts/rabbit-consumer/Chart.yaml +++ b/charts/rabbit-consumer/Chart.yaml @@ -6,10 +6,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.1.1 +version: 1.2.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v2.0.1" +appVersion: "v2.1.0"