diff --git a/changelogs/fragments/52045-keyed-group-features.yaml b/changelogs/fragments/52045-keyed-group-features.yaml new file mode 100644 index 00000000000..5620567646f --- /dev/null +++ b/changelogs/fragments/52045-keyed-group-features.yaml @@ -0,0 +1,2 @@ +minor_changes: +- keyed_groups now has a 'parent_group' keyword that allows assigning all generated groups to the same parent group diff --git a/lib/ansible/plugins/inventory/__init__.py b/lib/ansible/plugins/inventory/__init__.py index e3cd33f2ae1..582a361de39 100644 --- a/lib/ansible/plugins/inventory/__init__.py +++ b/lib/ansible/plugins/inventory/__init__.py @@ -335,7 +335,6 @@ class Constructable(object): def _add_host_to_keyed_groups(self, keys, variables, host, strict=False): ''' helper to create groups for plugins based on variable values and add the corresponding hosts to it''' if keys and isinstance(keys, list): - groups = [] for keyed in keys: if keyed and isinstance(keyed, dict): @@ -349,26 +348,33 @@ class Constructable(object): if key: prefix = keyed.get('prefix', '') sep = keyed.get('separator', '_') + raw_parent_name = keyed.get('parent_group', None) + new_raw_group_names = [] if isinstance(key, string_types): - groups.append('%s%s%s' % (prefix, sep, key)) + new_raw_group_names.append(key) elif isinstance(key, list): for name in key: - groups.append('%s%s%s' % (prefix, sep, name)) + new_raw_group_names.append(name) elif isinstance(key, Mapping): for (gname, gval) in key.items(): name = '%s%s%s' % (gname, sep, gval) - groups.append('%s%s%s' % (prefix, sep, name)) + new_raw_group_names.append(name) else: raise AnsibleParserError("Invalid group name format, expected a string or a list of them or dictionary, got: %s" % type(key)) + + for bare_name in new_raw_group_names: + gname = to_safe_group_name('%s%s%s' % (prefix, sep, bare_name)) + self.inventory.add_group(gname) + self.inventory.add_child(gname, host) + + if raw_parent_name: + parent_name = to_safe_group_name(raw_parent_name) + self.inventory.add_group(parent_name) + self.inventory.add_child(parent_name, gname) + else: if strict: raise AnsibleParserError("No key or key resulted empty, invalid entry") else: raise AnsibleParserError("Invalid keyed group entry, it must be a dictionary: %s " % keyed) - - # now actually add any groups - for group_name in groups: - gname = to_safe_group_name(group_name) - self.inventory.add_group(gname) - self.inventory.add_child(gname, host) diff --git a/lib/ansible/plugins/inventory/constructed.py b/lib/ansible/plugins/inventory/constructed.py index 8b68d59d8d7..7ab6dea588e 100644 --- a/lib/ansible/plugins/inventory/constructed.py +++ b/lib/ansible/plugins/inventory/constructed.py @@ -53,9 +53,19 @@ EXAMPLES = r''' - prefix: distro key: ansible_distribution + # the following examples assume the first inventory is from contrib/inventory/ec2.py # this creates a group per ec2 architecture and assign hosts to the matching ones (arch_x86_64, arch_sparc, etc) - prefix: arch key: ec2_architecture + + # this creates a group per ec2 region like "us_west_1" + - prefix: "" + separator: "" + key: ec2_region + + # this creates a common parent group for all ec2 availability zones + - key: ec2_placement + parent_group: all_ec2_zones ''' import os diff --git a/test/units/plugins/inventory/test_constructed.py b/test/units/plugins/inventory/test_constructed.py new file mode 100644 index 00000000000..ba09692bba9 --- /dev/null +++ b/test/units/plugins/inventory/test_constructed.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +# Copyright 2019 Alan Rominger +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import pytest + +from ansible.plugins.inventory.constructed import InventoryModule +from ansible.inventory.data import InventoryData +from ansible.template import Templar + + +@pytest.fixture(scope="module") +def inventory_module(): + r = InventoryModule() + r.inventory = InventoryData() + r.templar = Templar(None) + return r + + +def test_group_by_value_only(inventory_module): + inventory_module.inventory.add_host('foohost') + inventory_module.inventory.set_variable('foohost', 'bar', 'my_group_name') + host = inventory_module.inventory.get_host('foohost') + keyed_groups = [ + { + 'prefix': '', + 'separator': '', + 'key': 'bar' + } + ] + inventory_module._add_host_to_keyed_groups( + keyed_groups, host.vars, host.name, strict=False + ) + assert 'my_group_name' in inventory_module.inventory.groups + group = inventory_module.inventory.groups['my_group_name'] + assert group.hosts == [host] + + +def test_keyed_group_separator(inventory_module): + inventory_module.inventory.add_host('farm') + inventory_module.inventory.set_variable('farm', 'farmer', 'mcdonald') + inventory_module.inventory.set_variable('farm', 'barn', {'cow': 'betsy'}) + host = inventory_module.inventory.get_host('farm') + keyed_groups = [ + { + 'prefix': 'farmer', + 'separator': '_old_', + 'key': 'farmer', + 'unsafe': True + }, + { + 'separator': 'mmmmmmmmmm', + 'key': 'barn', + 'unsafe': True + } + ] + inventory_module._add_host_to_keyed_groups( + keyed_groups, host.vars, host.name, strict=False + ) + for group_name in ('farmer_old_mcdonald', 'mmmmmmmmmmcowmmmmmmmmmmbetsy'): + assert group_name in inventory_module.inventory.groups + group = inventory_module.inventory.groups[group_name] + assert group.hosts == [host] + + +def test_keyed_parent_groups(inventory_module): + inventory_module.inventory.add_host('web1') + inventory_module.inventory.add_host('web2') + inventory_module.inventory.set_variable('web1', 'region', 'japan') + inventory_module.inventory.set_variable('web2', 'region', 'japan') + host1 = inventory_module.inventory.get_host('web1') + host2 = inventory_module.inventory.get_host('web2') + keyed_groups = [ + { + 'prefix': 'region', + 'key': 'region', + 'parent_group': 'region_list' + } + ] + for host in [host1, host2]: + inventory_module._add_host_to_keyed_groups( + keyed_groups, host.vars, host.name, strict=False + ) + assert 'region_japan' in inventory_module.inventory.groups + assert 'region_list' in inventory_module.inventory.groups + region_group = inventory_module.inventory.groups['region_japan'] + all_regions = inventory_module.inventory.groups['region_list'] + assert all_regions.child_groups == [region_group] + assert region_group.hosts == [host1, host2]