Validating inventory against argument_specs

Hi, I have been trying to figure out if there’s any way to validate that I have all the necessary variables defined when I include roles from a playbook. If the required arguments for a role are defined in the argument_specs, then it seems like it should be possible for a tool to check that, given a specific playbook and inventory, all the hosts have the necessary variables defined at the time the role is invoked.

I am aware that ansible-lint is able to check that required arguments to modules are provided, but I’m not able to get similar warnings to ensure arguments are defined when calling a role. I could understand how this might not be easy to do in the general case since one role could include another role, and the variables defined at that point in time could be dynamic in nature (so it may be impossible to know without actually running the whole thing), but for simple single level setups where a playbook includes a role and roles don’t include each other, this could be pretty useful for ensuring that new variables are properly defined on all hosts that need it.

Are there any tools that would help validate this? Or is there some other way to structure my project besides using roles such that I could get this kind of validation? Or maybe if the role validation tasks could be tagged and then I only run that tag with ansible-playbook (to prevent the rest of the role tasks from being run)?

Welcome to the forum, @sourcherry

You can test that required arguments in a role argument_specs are defined by running the playbook in check mode:

$ ansible-playbook playbook.yml --check-mode

The playbook will fail when calling the role if you are missing a required argument at the very first task when verifying the arguments, but if it passes check mode will not make any changes to your systems.

Calling this example role in a playbook will fail for host2, but not host1:

$ cat roles/example/meta/argument_specs.yml
---
argument_specs:  
  main:              
    options:                                                   

      example_required_arg:
        description: "A unique argument that should be defined per host in your inventory."
        type: str
        required: true

      example_default_arg:
        description: "A common argument defined in defaults."
        type: str

$ cat roles/example/defaults/main.yml
---
# example_required_arg: 192.0.2.10
example_default_arg: "Example"

$ cat inventory/host_vars/host1/example.yml
---
example_required_arg: 192.0.2.111

$ cat inventory/host_vars/host2/example.yml
---
example_default_arg: "Test"
2 Likes

Thanks for the thoughts. I think check mode is somewhat broken right now (it always fails on certain tasks) and I haven’t bothered to fix it yet but maybe this would be good motivation. This would also require the hosts I’m checking to be online though I think? That wouldn’t be ideal but I might be able to tweak it to still help in some cases.

You should strive to make check mode and idempotency work for all your tasks.
Yes, check mode requires your hosts to be online.

1 Like

I strongly disagree with this. If you need check mode to work it can be reasonable to expend the large amount of effort that requires; if you don’t require it (which many, if not most, people don’t), your time is better spent elsewhere.

Since check mode is merely a simulation, I guess it is never required unless you write code to simulate running it. However, since Ansible is often used to describe infrastructure as code, it certainly is nice that most modules support check mode so that configuration drift can be discovered before changing it.

Yes idempotency is of very high importance to me, but so far check mode has mostly been a nice to have. It’s definitely nice that ansible has that functionality built in but I’ve been prioritizing other testing mechanisms above check mode up until now. Was just asking about the variable definition checks since that would be one more thing to help catch errors early on along with other tests, ansible lint, etc.

I can answer your initial questions straight to the point.

meta/argument_specs.yml is the only “tool” that will validate the parameters supplied for the role and stop execution if some are missing.

Not to my knowledge.

When an argument specification is defined, a new task is inserted at the beginning of role execution. This task will run regardless of tags, but when you are testing you can add tags to the rest of your tasks and skip those, set the first task in the role to fail, or comment out your tasks.

2 Likes

One thing to note with the argument specification is that it will only validate variables that are defined in it, in other words if the arg spec contains the following:

argument_specs:
  main:
    author: Chris Croome
    description: Ansible role for installing and configuring PHP on Debian.
    short_description: The main entry point for the PHP role.
    options:
      php:
        type: bool
        required: true
        description: Run the tasks in this role.

And then a php_foo variable is used in an inventory this won’t trigger an error, however you can add an additional task to your role that runs in addition to the default arg spec task to catch things like this (see the example here and the ansible.builtin.validate_argument_spec module which are combined to achieve this here and here).

Also you can use .ansible-lint to limit the variable names used, for example:

var_naming_pattern: "^[php|molecule]_?[a-z0-9_]*$"

However the argument spec limits are such that I find I often need to add tasks to check variables, eg:

    # https://github.com/composer/composer/issues/12153#issuecomment-2419452358
    - name: Check that the opcache.jit_buffer_size is set to 0 or >= 40961 or >= 1K or >= 1M or >= 1G
      ansible.builtin.assert:
        that:
          - php_conf_opcache_jit_buffer_size is ansible.builtin.regex('^[0-9]{1,20}[G|K|M]?$')
          - >-
            ( php_conf_opcache_jit_buffer_size == "0" ) or
            ( ( php_conf_opcache_jit_buffer_size is ansible.builtin.regex('^[0-9]{1,20}$') ) and
            ( php_conf_opcache_jit_buffer_size | int >= 40961 ) ) or
            ( php_conf_opcache_jit_buffer_size is ansible.builtin.regex('^[1-9][0-9]{0,20}[G|K|M]$') )
        quiet: "{% if ansible_verbosity == 0 %}true{% else %}false{% endif %}"
      loop: "{{ php_conf_opcache_jit_buffer_sizes }}"
      loop_control:
        loop_var: php_conf_opcache_jit_buffer_size
      when: php_conf_opcache_jit_buffer_sizes != []
1 Like