diff --git a/changelogs/fragments/constructed_priority.yml b/changelogs/fragments/constructed_priority.yml new file mode 100644 index 00000000000..25f9e51937e --- /dev/null +++ b/changelogs/fragments/constructed_priority.yml @@ -0,0 +1,3 @@ +minor_changes: + - The ``Constructed`` base class (and plugin) no has a ``vars`` key in the ``keyed_groups`` option, so users can add vars directly w/o need of a 2nd inventory or vars plugin or guessing group names. + - The ``constructed`` inventory plugin also now can have an option ``group_vars`` to allow for assiging vars to groups w/o the need of a 2nd inventory or vars plugin. diff --git a/lib/ansible/plugins/doc_fragments/constructed.py b/lib/ansible/plugins/doc_fragments/constructed.py index 4e10afce100..8e091123f9a 100644 --- a/lib/ansible/plugins/doc_fragments/constructed.py +++ b/lib/ansible/plugins/doc_fragments/constructed.py @@ -58,6 +58,11 @@ options: type: bool default: true version_added: '2.12' + vars: + description: + - Set variables for this group + type: dict + version_added: '2.18' use_extra_vars: version_added: '2.11' description: Merge extra vars into the available variables for composition (highest precedence). diff --git a/lib/ansible/plugins/inventory/__init__.py b/lib/ansible/plugins/inventory/__init__.py index b6f70b3c44f..a2ee54b4c1d 100644 --- a/lib/ansible/plugins/inventory/__init__.py +++ b/lib/ansible/plugins/inventory/__init__.py @@ -298,6 +298,12 @@ class _BaseInventoryPlugin(AnsiblePlugin): return (hostnames, port) + def _set_group_vars(self, group_name, gvars): + + gobj = self.inventory.groups.get(group_name) + for gvar in gvars: + gobj.set_variable(gvar, gvars[gvar]) + class BaseInventoryPlugin(_BaseInventoryPlugin): """ Parses an Inventory Source """ @@ -406,6 +412,11 @@ class Constructable(_BaseInventoryPlugin): for keyed in keys: if keyed and isinstance(keyed, dict): + group_vars = keyed.get('vars', {}) + if group_vars: + # allow using group vars for templating + variables = combine_vars(variables, group_vars) + if fetch_hostvars: variables = combine_vars(variables, self.inventory.get_host(host).get_vars()) try: @@ -414,6 +425,7 @@ class Constructable(_BaseInventoryPlugin): if strict: raise AnsibleParserError("Could not generate group for host %s from %s entry: %s" % (host, keyed.get('key'), to_native(e))) continue + default_value_name = keyed.get('default_value', None) trailing_separator = keyed.get('trailing_separator') if trailing_separator is not None and default_value_name is not None: @@ -467,11 +479,12 @@ class Constructable(_BaseInventoryPlugin): result_gname = self.inventory.add_group(gname) self.inventory.add_host(host, result_gname) + self._set_group_vars(result_gname, group_vars) + if raw_parent_name: parent_name = self._sanitize_group_name(raw_parent_name) self.inventory.add_group(parent_name) self.inventory.add_child(parent_name, result_gname) - else: # exclude case of empty list and dictionary, because these are valid constructions # simply no groups need to be constructed, but are still falsy diff --git a/lib/ansible/plugins/inventory/constructed.py b/lib/ansible/plugins/inventory/constructed.py index 6954e3aeab5..fd9ead7018d 100644 --- a/lib/ansible/plugins/inventory/constructed.py +++ b/lib/ansible/plugins/inventory/constructed.py @@ -31,6 +31,12 @@ DOCUMENTATION = """ default: false type: boolean version_added: '2.11' + group_vars: + description: dictionary of dictionaries, the top keys are group names, the dictionaries withtin are variables for that group. + required: false + type: dict + default: {} + version_added: '2.17' extends_documentation_fragment: - constructed """ @@ -77,6 +83,29 @@ EXAMPLES = r""" # this creates a common parent group for all ec2 availability zones - key: placement.availability_zone parent_group: all_ec2_zones + + # Assuming that there exist `tags` that are assigned to various servers such as 'stuff', 'things', 'status', etc. + # also add vars directly to these groups + - key: tags + prefix: tag + default_value: "running" + priority: "{{ 10 if 'status' in ansible_keyed_group_name else 50 }}" + vars: + ansible_group_priority: '{{ "status" in tags)|ternary(10, 30)}}' + allowed_ssh_groups: qa,ops + group_origin: tags + + group_vars: + # group names: mapped to variables for those groups + all: + ntpserver: "ntp.{{ansible_facts['domain']}}" + webservers: + open_ports: 80,8080,443 + allowed_ssh_groups: webdevs,qa,ops + dbservers: + open_ports: 1283 + allowed_ssh_groups: dbas,qa,ops + ansible_group_priority: 111 """ import os @@ -171,5 +200,10 @@ class InventoryModule(BaseInventoryPlugin, Constructable): # constructed groups based variable values self._add_host_to_keyed_groups(self.get_option('keyed_groups'), hostvars, host, strict=strict, fetch_hostvars=False) + # handle group vars + groups = self.get_option('group_vars') + for group_name in groups.keys(): + self._set_group_vars(group_name, groups[group_name]) + except Exception as ex: raise AnsibleParserError(f"Failed to parse {path!r}.") from ex diff --git a/test/integration/targets/inventory_constructed/invs/3/group_vars/a_keyed_group_priority_3/all.yml b/test/integration/targets/inventory_constructed/invs/3/group_vars/a_keyed_group_priority_3/all.yml new file mode 100644 index 00000000000..cf3a79108fa --- /dev/null +++ b/test/integration/targets/inventory_constructed/invs/3/group_vars/a_keyed_group_priority_3/all.yml @@ -0,0 +1 @@ +override_priority_3: "a_keyed_group_priority_3" diff --git a/test/integration/targets/inventory_constructed/invs/3/group_vars/b_keyed_group_priority_2/all.yml b/test/integration/targets/inventory_constructed/invs/3/group_vars/b_keyed_group_priority_2/all.yml new file mode 100644 index 00000000000..fe004a22ace --- /dev/null +++ b/test/integration/targets/inventory_constructed/invs/3/group_vars/b_keyed_group_priority_2/all.yml @@ -0,0 +1,2 @@ +override_priority_2: "b_keyed_group_priority_2" +override_priority_3: "b_keyed_group_priority_2" diff --git a/test/integration/targets/inventory_constructed/invs/3/group_vars/c_static_group_priority_1/all.yml b/test/integration/targets/inventory_constructed/invs/3/group_vars/c_static_group_priority_1/all.yml new file mode 100644 index 00000000000..c53c1bcaf4d --- /dev/null +++ b/test/integration/targets/inventory_constructed/invs/3/group_vars/c_static_group_priority_1/all.yml @@ -0,0 +1,3 @@ +override_priority_1: "c_static_group_priority_1" +override_priority_2: "c_static_group_priority_1" +override_priority_3: "c_static_group_priority_1" diff --git a/test/integration/targets/inventory_constructed/invs/3/keyed_group_str_default_value_with_priority.yml b/test/integration/targets/inventory_constructed/invs/3/keyed_group_str_default_value_with_priority.yml new file mode 100644 index 00000000000..edfbac0807f --- /dev/null +++ b/test/integration/targets/inventory_constructed/invs/3/keyed_group_str_default_value_with_priority.yml @@ -0,0 +1,13 @@ +plugin: ansible.builtin.constructed +keyed_groups: + - key: keyed_group_1 + default_value: "priority_2" + prefix: b_keyed_group + vars: + ansible_group_priority: 2 + + - key: keyed_group_2 + default_value: "priority_3" + prefix: a_keyed_group + vars: + ansible_group_priority: '{{("priority_3" in keyed_group_2)|ternary(3, 1)}}' diff --git a/test/integration/targets/inventory_constructed/invs/3/priority_inventory.yml b/test/integration/targets/inventory_constructed/invs/3/priority_inventory.yml new file mode 100644 index 00000000000..695dab16d59 --- /dev/null +++ b/test/integration/targets/inventory_constructed/invs/3/priority_inventory.yml @@ -0,0 +1,7 @@ +all: + children: + c_static_group_priority_1: + hosts: + host0: + keyed_group_1: "" + keyed_group_2: "" diff --git a/test/integration/targets/inventory_constructed/runme.sh b/test/integration/targets/inventory_constructed/runme.sh index 8dc9c4f4d43..6799c909df1 100755 --- a/test/integration/targets/inventory_constructed/runme.sh +++ b/test/integration/targets/inventory_constructed/runme.sh @@ -66,3 +66,24 @@ ansible-inventory -i invs/1/one.yml -i invs/2/constructed.yml --graph | tee out. grep '@c_lola' out.txt grep '@c_group4testing' out.txt + +# Testing the priority +ansible-inventory -i invs/3/priority_inventory.yml -i invs/3/keyed_group_str_default_value_with_priority.yml --list | tee out.txt + +# If we expect the variables to be resolved lexicographically, ('c' would override 'b', which would override 'a') +# then we would expect c_static_group_priority_1 to take precedence. +# However, setting the priorities here is what we're testing, and is indicated in the names of the groups. +# +# If you comment out the priorities of the keyed groups, all of the below will return c_static_group_priority_1 + +# override_priority_1 is ONLY set in c_static_group_priority_1 - so it should not get overridden by any other group +grep '"override_priority_1": "c_static_group_priority_1"' out.txt +# override_priority_2 is set in both c_static_group_priority_1 and b_keyed_group_priority_2 which (as advertised) has a priority +# of 2, which should override c_static_group_priority_1. This means that the value from b_keyed_group_priority_2 should return. +grep '"override_priority_2": "b_keyed_group_priority_2"' out.txt +# override_priority_3 is set in all the groups, which means the group with the highest priority set should win. +# Lexicographically, this would otherwise be c_static_group_priority_1. However, since we set the priority of +# a_keyed_group_priority_3 to 3, we will expect it to override the other two groups, and return its variable. +# Note that a_keyed_group_priority_3 has its priority set with a conditional, so here we are testing our ability +# to set this priority with a variable, including the `ansible_keyed_group_name` variable that we expose. +grep '"override_priority_3": "a_keyed_group_priority_3"' out.txt