From 418ae11e37861f39c0435179c9e61fe29b11aea6 Mon Sep 17 00:00:00 2001 From: Toni Kangas Date: Tue, 31 Mar 2026 15:14:26 +0300 Subject: [PATCH] feat(inventory): allow list of methods in `connect_with` --- .github/workflows/unit-tests.yml | 3 +- CHANGELOG.md | 4 + plugins/inventory/servers.py | 109 +++-- tests/sanity/ignore-2.22.txt | 2 + tests/unit/plugins/inventory/test_upcloud.py | 438 ++++++++++--------- 5 files changed, 319 insertions(+), 237 deletions(-) create mode 100644 tests/sanity/ignore-2.22.txt diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index f885ceb..0992a1b 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -38,8 +38,7 @@ jobs: - name: Install ansible-base (${{ matrix.version.ansible }}) run: pip install https://github.com/ansible/ansible/archive/${{ matrix.version.ansible }}.tar.gz --disable-pip-version-check - # Run the unit tests - - name: Run unit test + - name: Run unit tests run: ansible-test units -v --color --docker --coverage working-directory: ./ansible_collections/${{env.NAMESPACE}}/${{env.COLLECTION_NAME}} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d24473..492884d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Accept list of methods as value to `connect_with`. This allows defining multiple connection methods in order of preference to configure fallback. For example, V(["public_ipv4", "private_ipv4"]) would try to find public IPv4 and fall back to private IPv4 if public IPv4 is not available. + ## [0.9.0] - 2025-11-27 ### Added diff --git a/plugins/inventory/servers.py b/plugins/inventory/servers.py index d3496e6..09a8b0d 100644 --- a/plugins/inventory/servers.py +++ b/plugins/inventory/servers.py @@ -44,9 +44,13 @@ type: str required: false connect_with: - description: Connect to the server with the specified choice. Server is skipped if chosen type is not available. + description: > + Connect to the server with the specified method. Define multiple methods in order of preference to configure fallback. For example, + V(["public_ipv4", "private_ipv4"]) would try to find public IPv4 and fall back to private IPv4 if public IPv4 is not available. Server is + skipped if none of the chosen types are available. default: public_ipv4 - type: str + type: list + elements: str choices: - public_ipv4 - public_ipv6 @@ -126,7 +130,7 @@ import os from typing import List from ansible.errors import AnsibleError -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.plugins.inventory import BaseInventoryPlugin, Constructable from ansible.utils.display import Display @@ -268,6 +272,44 @@ def _filter_servers(self): self.servers = tmp + def _get_ansible_host(self, public_ipv4, public_ipv6, util_addrs, server, server_details): + connect_with = _ensure_list(self.get_option("connect_with")) + + for method in connect_with: + display.vv(f'Trying to find {method} connection method for server {server.uuid} ({server.hostname})') + + if method == "public_ipv4": + if len(public_ipv4) > 0: + return public_ipv4[0] + else: + display.v( + f"No available public IPv4 addresses for server {server.uuid} ({server.hostname})") + + if method == "public_ipv6": + if len(public_ipv6) > 0: + return public_ipv6[0] + else: + display.v( + f"No available public IPv6 addresses for server {server.uuid} ({server.hostname})") + if method == "utility_ipv4": + if len(util_addrs) > 0: + return util_addrs[0] + else: + display.v( + f"No available utility addresses for server {server.uuid} ({server.hostname})") + if method == "hostname": + return server.hostname + if method == "private_ipv4": + if self.get_option("network"): + for iface in server_details.networking["interfaces"]["interface"]: + if iface["network"] == self.network.uuid: + return iface["ip_addresses"]["ip_address"][0].get("address") + else: + raise AnsibleError("You can only connect with private IPv4 if you specify a network") + + raise NoAvailableAddressException( + f"None of the requested connection types {connect_with} are available for server {server.uuid} ({server.hostname})") + def _get_server_attributes(self, server): server_details = self._fetch_server_details(server.uuid) @@ -302,47 +344,20 @@ def _new_attribute(key, attribute): if iface.get("type") == "utility": util_addrs.append(address) - public_ipv4 = list(set(ipv4_addrs) & set(publ_addrs)) - public_ipv6 = list(set(ipv6_addrs) & set(publ_addrs)) + public_ipv4 = _ordered_intersection(ipv4_addrs, publ_addrs) + public_ipv6 = _ordered_intersection(ipv6_addrs, publ_addrs) # We default to IPv4 when available if len(public_ipv4) > 0: attributes.append(_new_attribute("public_ip", to_native(public_ipv4[0]))) elif len(public_ipv6) > 0: - attributes.append(_new_attribute("public_ip", to_native(public_ipv4[0]))) + attributes.append(_new_attribute("public_ip", to_native(public_ipv6[0]))) if len(util_addrs) > 0: attributes.append(_new_attribute("utility_ip", to_native(util_addrs[0]))) - connect_with = self.get_option("connect_with") - if connect_with == "public_ipv4": - if len(public_ipv4) == 0: - raise NoAvailableAddressException( - f"No available public IPv4 addresses for server {server.uuid} ({server.hostname})") - attributes.append(_new_attribute("ansible_host", to_native(public_ipv4[0]))) - - elif connect_with == "public_ipv6": - if len(public_ipv6) == 0: - raise NoAvailableAddressException( - f"No available public IPv6 addresses for server {server.uuid} ({server.hostname})") - attributes.append(_new_attribute("ansible_host", to_native(public_ipv6[0]))) - elif connect_with == "utility_ipv4": - if len(util_addrs) == 0: - raise NoAvailableAddressException( - f"No available utility addresses for server {server.uuid} ({server.hostname})") - attributes.append(_new_attribute("ansible_host", to_native(util_addrs[0]))) - elif connect_with == "hostname": - attributes.append(_new_attribute("ansible_host", to_native(server.hostname))) - elif connect_with == "private_ipv4": - if self.get_option("network"): - for iface in server_details.networking["interfaces"]["interface"]: - if iface["network"] == self.network.uuid: - attributes.append(_new_attribute( - "ansible_host", - to_native(iface["ip_addresses"]["ip_address"][0].get("address"))) - ) - else: - raise AnsibleError("You can only connect with private IPv4 if you specify a network") + ansible_host = self._get_ansible_host(public_ipv4, public_ipv6, util_addrs, server, server_details) + attributes.append(_new_attribute("ansible_host", to_native(ansible_host))) return attributes @@ -390,19 +405,35 @@ def _populate(self): # Create groups based on variable values and add the corresponding hosts to it self._add_host_to_keyed_groups(self.get_option('keyed_groups'), {}, server.hostname, strict=strict) - def parse(self, inventory, loader, path, cache=True): - super(InventoryModule, self).parse(inventory, loader, path, cache) - + def _check_upcloud_api_installed(self): if not UC_AVAILABLE: raise AnsibleError( "UpCloud dynamic inventory plugin requires upcloud-api Python module, " + "see https://pypi.org/project/upcloud-api/") - self._read_config_data(path) + def parse(self, inventory, loader, path, cache=True): + super(InventoryModule, self).parse(inventory, loader, path, cache) + self._check_upcloud_api_installed() + self._read_config_data(path) self._populate() +def _ensure_list(value) -> List: + if value is None: + return [] + + if isinstance(value, list): + return value + + return [value] + + +def _ordered_intersection(a, b): + b_dict = {i: True for i in b} + return [i for i in a if i in b_dict] + + def _parse_server_labels(labels: List): processed = [] diff --git a/tests/sanity/ignore-2.22.txt b/tests/sanity/ignore-2.22.txt new file mode 100644 index 0000000..9ef6dbd --- /dev/null +++ b/tests/sanity/ignore-2.22.txt @@ -0,0 +1,2 @@ +plugins/inventory/servers.py validate-modules:missing-gplv3-license +plugins/modules/loadbalancer_backend_member.py validate-modules:missing-gplv3-license diff --git a/tests/unit/plugins/inventory/test_upcloud.py b/tests/unit/plugins/inventory/test_upcloud.py index 23bc9dc..515ab43 100644 --- a/tests/unit/plugins/inventory/test_upcloud.py +++ b/tests/unit/plugins/inventory/test_upcloud.py @@ -75,6 +75,12 @@ def __init__(self, **entries): def inventory(): r = InventoryModule() r.inventory = InventoryData() + + r._check_upcloud_api_installed = lambda: None + + r._load_name = "upcloud.cloud.servers" + r._redirected_names = "upcloud.cloud.servers" + return r @@ -83,7 +89,7 @@ def test_verify_file_bad_config(inventory): def get_servers(): - servers_response = [ + servers = [ { 'core_number': '2', 'created': 1599136169, @@ -96,6 +102,7 @@ def get_servers(): 'plan': '2xCPU-4GB', 'plan_ipv4_bytes': '0', 'plan_ipv6_bytes': '0', + 'server_group': '', 'simple_backup': 'no', 'state': 'started', 'title': 'Server #1', @@ -120,6 +127,7 @@ def get_servers(): 'plan': '1xCPU-2GB', 'plan_ipv4_bytes': '0', 'plan_ipv6_bytes': '0', + 'server_group': '', 'simple_backup': 'no', 'state': 'stopped', 'title': 'Server #2', @@ -148,6 +156,7 @@ def get_servers(): 'plan': '1xCPU-2GB', 'plan_ipv4_bytes': '0', 'plan_ipv6_bytes': '0', + 'server_group': '', 'simple_backup': 'no', 'state': 'started', 'title': 'Server #3', @@ -157,192 +166,206 @@ def get_servers(): } ] - server_list = list() - for server in servers_response: - server_list.append(Server(**server)) - - return server_list + return [Server(**i) for i in servers] -def get_server1_details(): - return Server(**{ - 'boot_order': 'disk', - 'core_number': '2', - 'created': 1599136169, - 'firewall': 'on', - 'hostname': 'server1', - 'labels': { - 'label': [] - }, - 'license': 0, - 'memory_amount': '4096', - 'metadata': 'no', - 'networking': { - 'interfaces': { - 'interface': [ - { - 'bootable': 'no', - 'index': 1, - 'ip_addresses': { - 'ip_address': [ - { - 'address': '1.1.1.10', - 'family': 'IPv4', - 'floating': 'no' - }, - { - 'address': '1.1.1.11', - 'family': 'IPv4', - 'floating': 'yes' - } - ] +def get_server_details(uuid): + details = { + '00229adf-0e46-49b5-a8f7-cbd638d11f6a': Server(**{ + 'boot_order': 'disk', + 'core_number': '2', + 'created': 1599136169, + 'firewall': 'on', + 'hostname': 'server1', + 'labels': { + 'label': [] + }, + 'license': 0, + 'memory_amount': '4096', + 'metadata': 'no', + 'networking': { + 'interfaces': { + 'interface': [ + { + 'bootable': 'no', + 'index': 1, + 'ip_addresses': { + 'ip_address': [ + { + 'address': '1.1.1.10', + 'family': 'IPv4', + 'floating': 'no' + }, + { + 'address': '1.1.1.11', + 'family': 'IPv4', + 'floating': 'yes' + } + ] + }, + 'mac': '3b:a6:ba:4a:13:01', + 'network': '031437b4-0f8c-483c-96f2-eca5be02909c', + 'source_ip_filtering': 'yes', + 'type': 'public' }, - 'mac': '3b:a6:ba:4a:13:01', - 'network': '031437b4-0f8c-483c-96f2-eca5be02909c', - 'source_ip_filtering': 'yes', - 'type': 'public' - } - ] - } - }, - 'nic_model': 'virtio', - 'plan': '2xCPU-4GB', - 'plan_ipv4_bytes': '0', - 'plan_ipv6_bytes': '0', - 'remote_access_enabled': 'no', - 'remote_access_password': 'barFoo5', - 'remote_access_type': 'vnc', - 'simple_backup': 'no', - 'state': 'started', - 'timezone': 'UTC', - 'title': 'Server #1', - 'uuid': '00229adf-0e46-49b5-a8f7-cbd638d11f6a', - 'video_model': 'vga', - 'zone': 'de-fra1', - 'tags': ['foo', 'bar'] - }) - - -def get_server2_details(): - return Server(**{ - 'boot_order': 'disk', - 'core_number': '1', - 'created': 1598526425, - 'firewall': 'on', - 'hostname': 'server2', - 'labels': { - 'label': [ - { - 'key': 'foo', - 'value': 'bar' + { + 'bootable': 'no', + 'index': 2, + 'ip_addresses': { + 'ip_address': [ + { + 'address': '172.16.0.4', + 'family': 'IPv4', + 'floating': 'no' + } + ] + }, + 'mac': '3b:a6:ba:4a:4b:10', + 'network': '035146a5-7a85-408b-b1f8-21925164a7d3', + 'source_ip_filtering': 'yes', + 'type': 'private' + } + ] } - ] - }, - 'license': 0, - 'memory_amount': '2048', - 'metadata': 'no', - 'networking': { - 'interfaces': { - 'interface': [ + }, + 'nic_model': 'virtio', + 'plan': '2xCPU-4GB', + 'plan_ipv4_bytes': '0', + 'plan_ipv6_bytes': '0', + 'remote_access_enabled': 'no', + 'remote_access_password': 'barFoo5', + 'remote_access_type': 'vnc', + 'server_group': '', + 'simple_backup': 'no', + 'state': 'started', + 'timezone': 'UTC', + 'title': 'Server #1', + 'uuid': '00229adf-0e46-49b5-a8f7-cbd638d11f6a', + 'video_model': 'vga', + 'zone': 'de-fra1', + 'tags': ['foo', 'bar'] + }), + '004d5201-e2ff-4325-7ac6-a274f1c517b7': Server(**{ + 'boot_order': 'disk', + 'core_number': '1', + 'created': 1598526425, + 'firewall': 'on', + 'hostname': 'server2', + 'labels': { + 'label': [ { - 'bootable': 'no', - 'index': 1, - 'ip_addresses': { - 'ip_address': [ - { - 'address': '1.1.1.12', - 'family': 'IPv4', - 'floating': 'no' - } - ] - }, - 'mac': '3b:a6:ba:4a:2c:d6', - 'network': '031437b4-0f8c-483c-96f2-eca5be02909c', - 'source_ip_filtering': 'yes', - 'type': 'public' + 'key': 'foo', + 'value': 'bar' } ] - } - }, - 'nic_model': 'virtio', - 'plan': '1xCPU-2GB', - 'plan_ipv4_bytes': '0', - 'plan_ipv6_bytes': '0', - 'remote_access_enabled': 'no', - 'remote_access_password': 'fooBar', - 'remote_access_type': 'vnc', - 'simple_backup': 'no', - 'state': 'stopped', - 'timezone': 'UTC', - 'title': 'Server #2', - 'uuid': '004d5201-e2ff-4325-7ac6-a274f1c517b7', - 'video_model': 'vga', - 'zone': 'nl-ams1', - 'tags': [], - }) - - -def get_server3_details(): - return Server(**{ - 'boot_order': 'disk', - 'core_number': '1', - 'created': 1598526319, - 'firewall': 'on', - 'hostname': 'server3', - 'labels': { - 'label': [ - { - 'key': 'foo', - 'value': 'bar' - }, - { - 'key': 'private', - 'value': 'yes' + }, + 'license': 0, + 'memory_amount': '2048', + 'metadata': 'no', + 'networking': { + 'interfaces': { + 'interface': [ + { + 'bootable': 'no', + 'index': 1, + 'ip_addresses': { + 'ip_address': [ + { + 'address': '1.1.1.12', + 'family': 'IPv4', + 'floating': 'no' + } + ] + }, + 'mac': '3b:a6:ba:4a:2c:d6', + 'network': '031437b4-0f8c-483c-96f2-eca5be02909c', + 'source_ip_filtering': 'yes', + 'type': 'public' + } + ] } - ] - }, - 'license': 0, - 'memory_amount': '2048', - 'metadata': 'yes', - 'networking': { - 'interfaces': { - 'interface': [ + }, + 'nic_model': 'virtio', + 'plan': '1xCPU-2GB', + 'plan_ipv4_bytes': '0', + 'plan_ipv6_bytes': '0', + 'remote_access_enabled': 'no', + 'remote_access_password': 'fooBar', + 'remote_access_type': 'vnc', + 'server_group': '', + 'simple_backup': 'no', + 'state': 'stopped', + 'timezone': 'UTC', + 'title': 'Server #2', + 'uuid': '004d5201-e2ff-4325-7ac6-a274f1c517b7', + 'video_model': 'vga', + 'zone': 'nl-ams1', + 'tags': [], + }), + '0003295f-343a-44a2-8080-fb8196a6802a': Server(**{ + 'boot_order': 'disk', + 'core_number': '1', + 'created': 1598526319, + 'firewall': 'on', + 'hostname': 'server3', + 'labels': { + 'label': [ { - 'bootable': 'no', - 'index': 1, - 'ip_addresses': { - 'ip_address': [ - { - 'address': '172.16.0.3', - 'family': 'IPv4', - 'floating': 'no' - } - ] - }, - 'mac': '3b:a6:ba:4a:4b:10', - 'network': '035146a5-7a85-408b-b1f8-21925164a7d3', - 'source_ip_filtering': 'yes', - 'type': 'private' + 'key': 'foo', + 'value': 'bar' + }, + { + 'key': 'private', + 'value': 'yes' } ] - } - }, - 'nic_model': 'virtio', - 'plan': '1xCPU-2GB', - 'plan_ipv4_bytes': '0', - 'plan_ipv6_bytes': '0', - 'remote_access_enabled': 'no', - 'remote_access_password': 'fooBar', - 'remote_access_type': 'vnc', - 'simple_backup': 'no', - 'state': 'started', - 'timezone': 'UTC', - 'title': 'Server #3', - 'uuid': '0003295f-343a-44a2-8080-fb8196a6802a', - 'video_model': 'vga', - 'zone': 'nl-ams1', - 'tags': [], - }) + }, + 'license': 0, + 'memory_amount': '2048', + 'metadata': 'yes', + 'networking': { + 'interfaces': { + 'interface': [ + { + 'bootable': 'no', + 'index': 1, + 'ip_addresses': { + 'ip_address': [ + { + 'address': '172.16.0.3', + 'family': 'IPv4', + 'floating': 'no' + } + ] + }, + 'mac': '3b:a6:ba:4a:4b:10', + 'network': '035146a5-7a85-408b-b1f8-21925164a7d3', + 'source_ip_filtering': 'yes', + 'type': 'private' + } + ] + } + }, + 'nic_model': 'virtio', + 'plan': '1xCPU-2GB', + 'plan_ipv4_bytes': '0', + 'plan_ipv6_bytes': '0', + 'remote_access_enabled': 'no', + 'remote_access_password': 'fooBar', + 'remote_access_type': 'vnc', + 'server_group': '', + 'simple_backup': 'no', + 'state': 'started', + 'timezone': 'UTC', + 'title': 'Server #3', + 'uuid': '0003295f-343a-44a2-8080-fb8196a6802a', + 'video_model': 'vga', + 'zone': 'nl-ams1', + 'tags': [], + }), + } + + return details[uuid] def get_network_details(uuid): @@ -369,7 +392,8 @@ def get_network_details(uuid): "labels": [], "servers": { "server": [ - {"uuid": "0003295f-343a-44a2-8080-fb8196a6802a", "title": "Server #2"} + {"uuid": "0003295f-343a-44a2-8080-fb8196a6802a", "title": "Server #2"}, + {"uuid": "00229adf-0e46-49b5-a8f7-cbd638d11f6a", "title": "Server #1"} ] } }) @@ -386,6 +410,7 @@ def _mock_test_credentials(): def get_option(option): options = { 'plugin': 'upcloud.cloud.servers', + 'connect_with': 'public_ipv4', 'strict': False, } return options.get(option) @@ -393,9 +418,7 @@ def get_option(option): def test_populate_hostvars(inventory, mocker): inventory._fetch_servers = mocker.MagicMock(side_effect=get_servers) - inventory._fetch_server_details = mocker.MagicMock( - side_effects=[get_server1_details, get_server2_details, get_server3_details] - ) + inventory._fetch_server_details = mocker.MagicMock(side_effect=get_server_details) inventory.get_option = mocker.MagicMock(side_effect=get_option) inventory._initialize_upcloud_client = _mock_initialize_client @@ -405,7 +428,6 @@ def test_populate_hostvars(inventory, mocker): host1 = inventory.inventory.get_host('server1') host2 = inventory.inventory.get_host('server2') - host3 = inventory.inventory.get_host('server3') assert host1.vars['id'] == "00229adf-0e46-49b5-a8f7-cbd638d11f6a" assert host1.vars['state'] == "started" @@ -413,13 +435,12 @@ def test_populate_hostvars(inventory, mocker): assert host2.vars['plan'] == "1xCPU-2GB" assert len(host2.vars['labels']) == 1 assert host2.vars['labels'][0] == "foo=bar" - assert host3.vars['id'] == "0003295f-343a-44a2-8080-fb8196a6802a" - assert len(host3.vars['labels']) == 2 def get_filtered_labeled_option(option): options = { 'plugin': 'upcloud.cloud.servers', + 'connect_with': ['public_ipv4'], 'labels': ['foo=bar'], } return options.get(option) @@ -427,9 +448,7 @@ def get_filtered_labeled_option(option): def test_filtering_with_labels(inventory, mocker): inventory._fetch_servers = mocker.MagicMock(side_effect=get_servers) - inventory._fetch_server_details = mocker.MagicMock( - side_effects=[get_server1_details, get_server2_details, get_server3_details] - ) + inventory._fetch_server_details = mocker.MagicMock(side_effect=get_server_details) inventory.get_option = mocker.MagicMock(side_effect=get_filtered_labeled_option) inventory._initialize_upcloud_client = _mock_initialize_client @@ -437,15 +456,12 @@ def test_filtering_with_labels(inventory, mocker): inventory._populate() - assert len(inventory.inventory.hosts) == 2 + assert len(inventory.inventory.hosts) == 1 host2 = inventory.inventory.get_host('server2') - host3 = inventory.inventory.get_host('server3') + # host3 has the label, but it is filtered out by connect_with assert host2.vars['id'] == "004d5201-e2ff-4325-7ac6-a274f1c517b7" assert host2.vars['labels'][0] == "foo=bar" - assert host3.vars['id'] == "0003295f-343a-44a2-8080-fb8196a6802a" - assert len(host3.vars['labels']) == 2 - assert host3.vars['labels'][1] == "private=yes" def get_filtered_connect_with_option(option): @@ -459,9 +475,7 @@ def get_filtered_connect_with_option(option): def test_filtering_with_connect_with(inventory, mocker): inventory._fetch_servers = mocker.MagicMock(side_effect=get_servers) - inventory._fetch_server_details = mocker.MagicMock( - side_effects=[get_server1_details, get_server2_details, get_server3_details] - ) + inventory._fetch_server_details = mocker.MagicMock(side_effect=get_server_details) inventory.get_option = mocker.MagicMock(side_effect=get_filtered_connect_with_option) inventory._initialize_upcloud_client = _mock_initialize_client @@ -471,7 +485,39 @@ def test_filtering_with_connect_with(inventory, mocker): inventory._populate() - assert len(inventory.inventory.hosts) == 1 + assert len(inventory.inventory.hosts) == 2 + host1 = inventory.inventory.get_host('server1') + host3 = inventory.inventory.get_host('server3') + + assert host1.vars['ansible_host'] == "172.16.0.4" + assert host3.vars['ansible_host'] == "172.16.0.3" + + +def get_connect_with_fallback_option(option): + options = { + 'plugin': 'upcloud.cloud.servers', + 'connect_with': ['public_ipv4', 'private_ipv4'], + 'network': '035146a5-7a85-408b-b1f8-21925164a7d3' + } + return options.get(option) + + +def test_connect_with_fallback(inventory, mocker): + inventory._fetch_servers = mocker.MagicMock(side_effect=get_servers) + inventory._fetch_server_details = mocker.MagicMock(side_effect=get_server_details) + inventory.get_option = mocker.MagicMock(side_effect=get_connect_with_fallback_option) + + inventory._initialize_upcloud_client = _mock_initialize_client + inventory._test_upcloud_credentials = _mock_test_credentials + + inventory._fetch_network_details = get_network_details + + inventory._populate() + + assert len(inventory.inventory.hosts) == 2 + + host1 = inventory.inventory.get_host('server1') host3 = inventory.inventory.get_host('server3') - assert host3.vars['id'] == "0003295f-343a-44a2-8080-fb8196a6802a" + assert host1.vars['ansible_host'] == "1.1.1.10" + assert host3.vars['ansible_host'] == "172.16.0.3"