Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove plugin option from inventory config for compatibility with passthrough plugins #674

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

vitropy
Copy link

@vitropy vitropy commented Mar 15, 2025

One niche but useful feature of Ansible inventory plugins is the ability to call one plugin from another. For example, you can write a custom plugin (my.plugin.multi_cloud) that acts as a sort of normalization interface between project inventories deployed on multiple cloud platforms. This makes it feasible to create a single set of Ansible Playbooks that can act on hosts across cloud platforms because they can expect managed node (inventory host) data to be consistent across those cloud platform vendors.

For example, my.plugin.multi_cloud inventory plugin might look something like the following code, which simply dynamically loads one or another underlying (upstream) vendor'ed inventory plugins based on the value of the shell environment variable called CLOUD_PLATFORM:

#!/usr/bin/env python3

from ansible.errors import AnsibleError, AnsibleParserError
from ansible.plugins.inventory import BaseInventoryPlugin
from ansible.plugins.loader import inventory_loader
import os

class InventoryModule(BaseInventoryPlugin):
    NAME = 'my.plugin.multi_cloud'

    def parse(self, inventory, loader, path, cache=True):
        super(InventoryModule, self).parse(inventory, loader, path)

        # Map the cloud platform to the appropriate vendor's inventory plugin.
        cloud_platform = os.environ.get('CLOUD_PLATFORM', '').lower()
        if cloud_platform == 'aws':
            plugin_fqcn = 'amazon.aws.aws_ec2'
        elif cloud_platform == 'gcp':
            plugin_fqcn = 'google.cloud.gcp_compute'
        else:
            raise AnsibleParserError(
                    f"Error: Unrecognized or unset cloud platform '{cloud_platform}'. "
                    f"Set CLOUD_PLATFORM to 'aws' or 'gcp'.\n"
            )

        if not inventory_loader.has_plugin(plugin_fqcn):
            raise AnsibleParserError(f"Error: '{plugin_fqcn}' inventory plugin not found.\n")

        # Load the requested plugin.
        self.inventory_plugin = inventory_loader.get(plugin_fqcn)

In the above example, we see code loading either this google.cloud.gcp_compute inventory plugin or Amazon's amazon.aws.aws_ec2 inventory plugin.

Later, we can invoke the underlying (upstream) plugin in a passthrough manner like this:

        # Passthrough to the underlying plugin.
        if self.inventory_plugin.verify_file(path):
            self.inventory_plugin.parse(inventory, loader, path)
        else:
            raise AnsibleParserError(f"Error: Inventory configuration file '{path}' failed verification by plugin '{plugin_fqcn}'")

However, in order for this to work, we must supply the underlying plugin with knowledge of the fact that we are calling it from a different actual plugin. This is facilitated via Ansible's built in _redirected_names class property. Before calling the underlying plugin's parse() method, we must first do:

        self.inventory_plugin._redirected_names.append(InventoryModule.NAME)

Now the underlying plugin will be permitted to run because the underlying plugin is informing Ansible that one of the names it is permitted to use is this "redirected" (aliased) name of the calling plugin. We have effectively monkey-patched the plugin during runtime, which is exactly what we want.

Unfortunately, for this google.cloud.gcp_compute inventory plugin, that's not enough, because of the fact that the plugin option in its configuration file is also checked and compared against this same name. That's something that, for example, the amazon.aws.aws_ec2 inventory plugin doesn't do, and for good reason: enforcing this check with hardcoded options breaks the built-in functionality of the Ansible module loading alias features.

That's why I'm suggesting we remove this option. It isn't needed for the auto inventory plugin to load the plugin correctly, nor does it ever really need to be checked once this plugin is actually running; it's already running! But its presence does break existing Ansible features, and makes the above use case of a pass-through plugin, for example, infeasible.

Thanks for considering this proposal.

SUMMARY
ISSUE TYPE
  • Bugfix Pull Request
  • Docs Pull Request
  • Feature Pull Request
  • New Module Pull Request
COMPONENT NAME
ADDITIONAL INFORMATION

…assthrough plugins

One niche but useful feature of Ansible inventory plugins is the ability to call one plugin from another. For example, you can write a custom plugin (`my.plugin.multi_cloud`) that acts as a sort of normalization interface between project inventories deployed on multiple cloud platforms. This makes it feasible to create a single set of Ansible Playbooks that can act on hosts across cloud platforms because they can expect managed node (inventory host) data to be consistent across those cloud platform vendors.

For example, `my.plugin.multi_cloud` inventory plugin might look something like the following code, which simply dynamically loads one or another underlying (upstream) vendor'ed inventory plugins based on the value of the shell environment variable called `CLOUD_PLATFORM`:

```python
#!/usr/bin/env python3

from ansible.errors import AnsibleError, AnsibleParserError
from ansible.plugins.inventory import BaseInventoryPlugin
from ansible.plugins.loader import inventory_loader
import os

class InventoryModule(BaseInventoryPlugin):

    def parse(self, inventory, loader, path, cache=True):
        super(InventoryModule, self).parse(inventory, loader, path)

        # Map the cloud platform to the appropriate vendor's inventory plugin.
        cloud_platform = os.environ.get('CLOUD_PLATFORM', '').lower()
        if cloud_platform == 'aws':
            plugin_fqcn = 'amazon.aws.aws_ec2'
        elif cloud_platform == 'gcp':
            plugin_fqcn = 'google.cloud.gcp_compute'
        else:
            raise AnsibleParserError(
                    f"Error: Unrecognized or unset cloud platform '{cloud_platform}'. "
                    f"Set ENTROPY_CLOUD_PLATFORM to 'aws' or 'gcp'.\n"
            )

        if not inventory_loader.has_plugin(plugin_fqcn):
            raise AnsibleParserError(f"Error: '{plugin_fqcn}' inventory plugin not found.\n")

        # Load the requested plugin.
        self.inventory_plugin = inventory_loader.get(plugin_fqcn)
```

In the above example, we see code loading either this `google.cloud.gcp_compute` inventory plugin or Amazon's `amazon.aws.aws_ec2` inventory plugin.

Later, we can invoke the underlying (upstream) plugin in a passthrough manner like this:

```python
        # Passthrough to the underlying plugin.
        if self.inventory_plugin.verify_file(path):
            self.inventory_plugin.parse(inventory, loader, path)
        else:
            raise AnsibleParserError(f"Error: Inventory configuration file '{path}' failed verification by plugin '{plugin_fqcn}'")
```

However, in order for this to work, we must supply the underlying plugin with knowledge of the fact that we are calling it from a different actual plugin. This is facilitated via Ansible's built in `_redirected_names` class property. Before calling the underlying plugin's `parse()` method, we must first do:

```python
        self.inventory_plugin._redirected_names.append(InventoryModule.NAME)
```

Now the underlying plugin will be permitted to run because the underlying plugin is informing Ansible that one of the names it is permitted to use is this "redirected" (aliased) name of the calling plugin. We have effectively monkey-patched the plugin during runtime, which is exactly what we want.

Unfortunately, for _this_ `google.cloud.gcp_compute` inventory plugin, that's not enough, because of the fact that the `plugin` option in its configuration file is also checked and compared against this same name. That's something that, for example, the `amazon.aws.aws_ec2` inventory plugin _doesn't_ do, and for good reason: enforcing this check with hardcoded options breaks the built-in functionality of the Ansible module loading alias features.

That's why I'm suggesting we remove this option. It isn't needed for the `auto` inventory plugin to load the plugin correctly, nor does it ever really need to be checked once this plugin is actually running; it's already running! But its presence _does_ break existing Ansible features, and makes the above use case of a pass-through plugin, for example, infeasible.

Thanks for considering this proposal.
@vitropy vitropy changed the title Remove plugin option from inventory config for compatibility with p… Remove plugin option from inventory config for compatibility with passthrough plugins Mar 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant