From e287ab5526b6ab2d83ac9580ffbec4da288835aa Mon Sep 17 00:00:00 2001 From: "Mark J. Horninger" Date: Tue, 20 Feb 2024 13:47:41 -0500 Subject: [PATCH] Getting closer to where I want to be --- plugins/inventory/netbird.py | 67 +++++++++++++------- requirements.txt | 1 + tests/unit/plugins/inventory/test_netbird.py | 28 +++++++- 3 files changed, 71 insertions(+), 25 deletions(-) diff --git a/plugins/inventory/netbird.py b/plugins/inventory/netbird.py index 0c484b2..b2fd6df 100644 --- a/plugins/inventory/netbird.py +++ b/plugins/inventory/netbird.py @@ -3,22 +3,21 @@ # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import (absolute_import, division, print_function) - __metaclass__ = type -DOCUMENTATION = r""" - name: netbird - author: Mark J. Horninger (@spam-n-eggs) - version_added: "0.0.2" - requirements: +DOCUMENTATION = r''' +name: netbird +author: Mark J. Horninger (@spam-n-eggs) +version_added: 0.0.2 +requirements: - requests>=2.31.0 - short_description: Get inventory from the Netbird API - description: +short_description: Get inventory from the Netbird API +description: - Get inventory from the Netbird API. Allows for filtering based on Netbird Tags / Groups. - extends_documentation_fragment: +extends_documentation_fragment: - constructed - inventory_cache - options: +options: cache: description: Cache plugin output to a file type: boolean @@ -28,22 +27,22 @@ DOCUMENTATION = r""" required: true choices: ['netbird', 'dominion_solutions.netbird'] api_key: - description: The API Key for the Netbird API. - required: true - type: string - env: + description: The API Key for the Netbird API. + required: true + type: string + env: - name: NETBIRD_API_KEY api_url: - description: The URL for the Netbird API. - required: true - type: string - env: + description: The URL for the Netbird API. + required: true + type: string + env: - name: NETBIRD_API_URL - disconnected: + include_disconnected: description: Whether or not to include disconnected peers in the inventory type: boolean default: false -""" +''' EXAMPLES = r""" """ @@ -51,9 +50,11 @@ EXAMPLES = r""" from ansible.errors import AnsibleError from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable +from ansible.utils.display import Display # Specific for the NetbirdAPI Class import json + try: import requests except ImportError: @@ -61,13 +62,19 @@ except ImportError: else: HAS_NETBIRD_API_LIBS = True +display = Display() + class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): NAME = "dominion_solutions.netbird" + _redirected_names = ["netbird", "dominion_solutions.netbird"] + + _load_name = NAME + def _build_client(self, loader): """Build the Netbird API Client""" - + display.v("Building the Netbird API Client.") api_key = self.get_option('api_key') api_url = self.get_option('api_url') if self.templar.is_template(api_key): @@ -80,13 +87,17 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): if api_url is None: raise AnsibleError("Could not retrieve the Netbird API URL from the configuration sources.") + display.v(f"Set up the Netbird API Client with the URL: {api_url}") self.client = NetbirdApi(api_key, api_url) def _get_peer_inventory(self): """Get the inventory from the Netbird API""" - if self.disconnected is False: + self.display.v("Getting the inventory from the Netbird API.") + if self.include_disconnected is False: + self.display.vv("Filtering out disconnected peers.") self.peers = [peer for peer in self.client.ListPeers() if peer.data["connected"] is True] else: + display.vv("Including disconnected peers.") self.peers = self.client.ListPeers() def _filter_by_config(self): @@ -98,16 +109,23 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): if any(group in peer.groups for group in groups) ] + def verify_file(self, path): + """Verify the Linode configuration file.""" + if super(InventoryModule, self).verify_file(path): + endings = ('netbird.yaml', 'netbird.yml') + if any((path.endswith(ending) for ending in endings)): + return True + return False + def parse(self, inventory, loader, path, cache=True): """Dynamically parse the inventory from the Netbird API""" super(InventoryModule, self).parse(inventory, loader, path) if not HAS_NETBIRD_API_LIBS: raise AnsibleError("the Netbird Dynamic inventory requires Requests.") + self._options = self._read_config_data(path) self.peers = None - self.disconnected = self.get_option('disconnected') - self._read_config_data(path) cache_key = self.get_cache_key(path) if cache: @@ -122,6 +140,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): # Check for None rather than False in order to allow # for empty sets of cached instances if self.peers is None: + self.include_disconnected = self.get_option('include_disconnected') self._build_client(loader) self._get_peer_inventory() diff --git a/requirements.txt b/requirements.txt index 091ef92..7cf94d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ ansible>=9.2.0 ansible-core>=2.16.3 cffi==1.16.0 cryptography==42.0.2 +epdb==0.15.1 Jinja2==3.1.3 MarkupSafe==2.1.5 packaging==23.2 diff --git a/tests/unit/plugins/inventory/test_netbird.py b/tests/unit/plugins/inventory/test_netbird.py index 33e67a9..47bc559 100644 --- a/tests/unit/plugins/inventory/test_netbird.py +++ b/tests/unit/plugins/inventory/test_netbird.py @@ -7,13 +7,16 @@ __metaclass__ = type import pytest + # TODO: Reenable this if needed. # import sys from ansible.errors import AnsibleError from ansible.parsing.dataloader import DataLoader from ansible.template import Templar -from ansible_collections.dominion_solutions.netbird.plugins.inventory.netbird import InventoryModule +from ansible_collections.dominion_solutions.netbird.plugins.inventory.netbird import InventoryModule, NetbirdApi, Peer +from unittest.mock import MagicMock +import json @pytest.fixture(scope="module") @@ -23,6 +26,20 @@ def inventory(): return plugin +@pytest.fixture(scope="module") +def netbird_api(): + mock_netbird_api = NetbirdApi(None, None) + response_data = [] + with open('tests/unit/module_utils/inventories/fixtures/peers.json') as peers_file: + peers_map = json.load(peers_file) + for data in peers_map: + response_data.append(Peer(data['hostname'], data['id'], data)) + + mock_netbird_api.ListPeers = MagicMock(return_value=response_data) + + return mock_netbird_api + + def test_missing_access_token_lookup(inventory): loader = DataLoader() inventory._options = {'api_key': None, 'api_url': None} @@ -39,3 +56,12 @@ def test_verify_file(tmp_path, inventory): def test_verify_file_bad_config(inventory): assert inventory.verify_file('foobar.netbird.yml') is False + + +def test_get_peer_data(inventory, netbird_api): + loader = DataLoader() + path = 'tests/unit/module_utils/inventories/fixtures/netbird.yml' + inventory._build_client = MagicMock() + inventory.client = netbird_api + inventory.parse(dict(), loader, path, False) + assert inventory.peers is not None