Skip to content

Commit

Permalink
inventory: Implement aggregation of multiple variable values
Browse files Browse the repository at this point in the history
This commit fixes the aggregation of multiple host variables values.
Let's say SNow returns something like this:
```
{'u_name': 'First_app', 'u_parent': 'tomcat_aqwidmvs', 'sys_id': 'a7dc8d0633a8d210392d0e570e5c7bb4', 'u_child': 'i101', 'u_level': '1', 'u_child.name': 'i101', 'u_child.ip_address': '10.1.0.101', 'u_parent.support_group': 'Application Development', 'u_parent.name': 'tomcat_aqwidmvs'}
{'u_name': 'Second app', 'u_parent': 'tomcat_pxgnodig', 'sys_id': 'f9fc8d0633a8d210392d0e570e5c7bff', 'u_child': 'i101', 'u_level': '1', 'u_child.name': 'i101', 'u_child.ip_address': '10.1.0.101', 'u_parent.support_group': 'Application Development', 'u_parent.name': 'tomcat_pxgnodig'}
```
For the same host, we have two records with different values: u_parent,
u_parent.name, u_parent.support_group, u_parent.name.

This change will aggreate these values as follows:
```
all:
  children:
    ungrouped:
      hosts:
        i101:
          sys_id: a7dc8d0633a8d210392d0e570e5c7bb4
          u_child:
          - ip_address: 10.1.0.101
            name: i101
          u_level: '1'
          u_parent:
          - name: tomcat_aqwidmvs
            support_group: Application Development
            u_parent: tomcat_aqwidmvs
          - name: tomcat_pxgnodig
            support_group: Application Development
            u_parent: tomcat_pxgnodig
          u_type: active
```

To enable this aggregation, the inventory must have:
```
experimental_aggregation: true
```
  • Loading branch information
tupyy committed Oct 3, 2024
1 parent 0872e1e commit 423e008
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 3 deletions.
100 changes: 97 additions & 3 deletions plugins/inventory/now.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf8- -*-
# Copyright: (c) 2021, XLAB Steampunk <[email protected]>
#
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
Expand Down Expand Up @@ -120,6 +120,12 @@
type: bool
default: false
version_added: 1.3.0
experimental_aggregation:
description:
- Enable multiple variable values aggregations.
type: bool
default: false
version_added: 2.7.0
inventory_hostname_source:
type: str
description:
Expand Down Expand Up @@ -329,6 +335,7 @@


import os
import hashlib

from ansible.errors import AnsibleParserError
from ansible.inventory.group import to_safe_group_name as orig_safe
Expand All @@ -352,6 +359,76 @@
from ..module_utils.table import TableClient


class Aggregator:
def __init__(self, columns):
self.data = dict()
self.tmp = None

def add(self, host, key, value):
parent, child = self._split(key)
if not self.tmp:
self.tmp = dict()

if not child:
self.tmp[parent] = value
else:
tmp_parent_data = self.tmp.get(parent)
if not tmp_parent_data:
tmp_parent_data = dict()

parent_data = dict()
parent_data[child] = value
if isinstance(tmp_parent_data, str):
parent_data[parent] = tmp_parent_data
else:
for k, v in tmp_parent_data.items():
parent_data[k] = v

self.tmp[parent] = parent_data

def commit(self, host):
if not self.tmp:
return
host_data = self.data.get(host, dict())
for k, v in self.tmp.items():
if k in host_data:
vv = host_data.get(k)
if isinstance(vv, list):
if not self._is_exists(vv, v):
vv.append(v)
continue
if isinstance(v, dict):
host_data[k] = [v]
continue
host_data[k] = v
self.tmp = None
self.data[host] = host_data

def aggregate(self, inventory):
for host, data in self.data.items():
for k, v in data.items():
inventory.set_variable(host, k, v)

def _split(self, column):
if "." not in column:
return column, ""
parts = column.split(".")
return parts[0], parts[1]

def _is_exists(self, items, item):
hash_v = self._hash_dict(item)
for i in items:
if self._hash_dict(i) == hash_v:
return True
return False

def _hash_dict(self, d):
h = hashlib.sha256()
d_sorted = str(dict(sorted(d.items())))
h.update(d_sorted.encode())
return h.hexdigest()


def construct_sysparm_query(query, is_encoded_query):
if is_encoded_query:
return query
Expand Down Expand Up @@ -445,6 +522,11 @@ def set_hostvars(self, host, record, columns):
for k in columns:
self.inventory.set_variable(host, k.replace(".", "_"), record[k])

def set_host_vars_aggregated(self, host, record, columns, aggregator):
for k in columns:
aggregator.add(host, k, record[k])
aggregator.commit(host)

def fill_constructed(
self,
records,
Expand All @@ -455,16 +537,26 @@ def fill_constructed(
keyed_groups,
strict,
enhanced,
exp_aggregation,
):
if exp_aggregation:
aggregator = Aggregator(columns)

for record in records:
host = self.add_host(record, name_source)
if host:
self.set_hostvars(host, record, columns)
if exp_aggregation:
self.set_host_vars_aggregated(host, record, columns, aggregator)
else:
self.set_hostvars(host, record, columns)

self._set_composite_vars(compose, record, host, strict)
self._add_host_to_composed_groups(groups, record, host, strict)
self._add_host_to_keyed_groups(keyed_groups, record, host, strict)
if enhanced:
self.fill_enhanced_auto_groups(record, host)
if exp_aggregation:
aggregator.aggregate(self.inventory)

def fill_enhanced_auto_groups(self, record, host):
for rel_group in record["relationship_groups"]:
Expand Down Expand Up @@ -569,6 +661,7 @@ def parse(self, inventory, loader, path, cache=True):
raise AnsibleParserError(e)

enhanced = self.get_option("enhanced")
exp_aggregation = self.get_option("experimental_aggregation")

sysparm_limit = self.get_option("sysparm_limit")
if sysparm_limit:
Expand Down Expand Up @@ -636,7 +729,7 @@ def parse(self, inventory, loader, path, cache=True):
)
enhance_records_with_rel_groups(records, rel_records)

self._cache[self.cache_key] = {cache_sub_key: records}
self._cache[self.cache_key] = {cache_sub_key: records}

self.fill_constructed(
records,
Expand All @@ -647,4 +740,5 @@ def parse(self, inventory, loader, path, cache=True):
self.get_option("keyed_groups"),
self.get_option("strict"),
enhanced,
exp_aggregation,
)
63 changes: 63 additions & 0 deletions tests/unit/plugins/inventory/test_now.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ def test_construction_empty(self, inventory_plugin):
keyed_groups = []
strict = False
enhanced = False
exp_aggregation = False

inventory_plugin.fill_constructed(
records,
Expand All @@ -361,6 +362,7 @@ def test_construction_empty(self, inventory_plugin):
keyed_groups,
strict,
enhanced,
exp_aggregation,
)

assert set(inventory_plugin.inventory.groups) == set(("all", "ungrouped"))
Expand All @@ -385,6 +387,7 @@ def test_construction_host(self, inventory_plugin):
keyed_groups = []
strict = False
enhanced = False
exp_aggregation = False

inventory_plugin.fill_constructed(
records,
Expand All @@ -395,6 +398,7 @@ def test_construction_host(self, inventory_plugin):
keyed_groups,
strict,
enhanced,
exp_aggregation,
)

assert set(inventory_plugin.inventory.groups) == set(("all", "ungrouped"))
Expand Down Expand Up @@ -425,6 +429,7 @@ def test_construction_hostvars(self, inventory_plugin):
keyed_groups = []
strict = False
enhanced = False
exp_aggregation = False

inventory_plugin.fill_constructed(
records,
Expand All @@ -435,6 +440,7 @@ def test_construction_hostvars(self, inventory_plugin):
keyed_groups,
strict,
enhanced,
exp_aggregation,
)

assert set(inventory_plugin.inventory.groups) == set(("all", "ungrouped"))
Expand Down Expand Up @@ -493,6 +499,7 @@ def test_construction_composite_vars(self, inventory_plugin):
keyed_groups = []
strict = False
enhanced = False
exp_aggregation = False

inventory_plugin.fill_constructed(
records,
Expand All @@ -503,6 +510,7 @@ def test_construction_composite_vars(self, inventory_plugin):
keyed_groups,
strict,
enhanced,
exp_aggregation,
)

assert set(inventory_plugin.inventory.groups) == set(("all", "ungrouped"))
Expand Down Expand Up @@ -547,6 +555,7 @@ def test_construction_composite_vars_strict(self, inventory_plugin):
keyed_groups = []
strict = True
enhanced = False
exp_aggregation = False

with pytest.raises(AnsibleError, match="non_existing"):
inventory_plugin.fill_constructed(
Expand All @@ -558,6 +567,7 @@ def test_construction_composite_vars_strict(self, inventory_plugin):
keyed_groups,
strict,
enhanced,
exp_aggregation,
)

def test_construction_composite_vars_ansible_host(self, inventory_plugin):
Expand All @@ -579,6 +589,7 @@ def test_construction_composite_vars_ansible_host(self, inventory_plugin):
keyed_groups = []
strict = False
enhanced = False
exp_aggregation = False

inventory_plugin.fill_constructed(
records,
Expand All @@ -589,6 +600,7 @@ def test_construction_composite_vars_ansible_host(self, inventory_plugin):
keyed_groups,
strict,
enhanced,
exp_aggregation,
)

assert set(inventory_plugin.inventory.groups) == set(("all", "ungrouped"))
Expand Down Expand Up @@ -631,6 +643,7 @@ def test_construction_composed_groups(self, inventory_plugin):
keyed_groups = []
strict = False
enhanced = False
exp_aggregation = False

inventory_plugin.fill_constructed(
records,
Expand All @@ -641,6 +654,7 @@ def test_construction_composed_groups(self, inventory_plugin):
keyed_groups,
strict,
enhanced,
exp_aggregation,
)

assert set(inventory_plugin.inventory.groups) == set(
Expand Down Expand Up @@ -678,6 +692,7 @@ def test_construction_composed_groups_strict(self, inventory_plugin):
keyed_groups = []
strict = True
enhanced = False
exp_aggregation = False

with pytest.raises(AnsibleError, match="cost_usd"):
inventory_plugin.fill_constructed(
Expand All @@ -689,6 +704,7 @@ def test_construction_composed_groups_strict(self, inventory_plugin):
keyed_groups,
strict,
enhanced,
exp_aggregation,
)

def test_construction_keyed_groups(self, inventory_plugin):
Expand All @@ -710,6 +726,7 @@ def test_construction_keyed_groups(self, inventory_plugin):
]
strict = False
enhanced = False
exp_aggregation = False

inventory_plugin.fill_constructed(
records,
Expand All @@ -720,6 +737,7 @@ def test_construction_keyed_groups(self, inventory_plugin):
keyed_groups,
strict,
enhanced,
exp_aggregation,
)

assert set(inventory_plugin.inventory.groups) == set(
Expand Down Expand Up @@ -760,6 +778,7 @@ def test_construction_keyed_groups_with_parent(self, inventory_plugin):
]
strict = False
enhanced = False
exp_aggregation = False

inventory_plugin.fill_constructed(
records,
Expand All @@ -770,6 +789,7 @@ def test_construction_keyed_groups_with_parent(self, inventory_plugin):
keyed_groups,
strict,
enhanced,
exp_aggregation,
)

assert set(inventory_plugin.inventory.groups) == set(
Expand Down Expand Up @@ -815,6 +835,7 @@ def test_construction_enhanced(self, inventory_plugin):
keyed_groups = []
strict = False
enhanced = True
exp_aggregation = False

inventory_plugin.fill_constructed(
records,
Expand All @@ -825,6 +846,7 @@ def test_construction_enhanced(self, inventory_plugin):
keyed_groups,
strict,
enhanced,
exp_aggregation,
)

assert set(inventory_plugin.inventory.groups) == set(
Expand Down Expand Up @@ -853,6 +875,47 @@ def test_construction_enhanced(self, inventory_plugin):

assert a2.vars == dict(inventory_file=None, inventory_dir=None)

def test_exp_aggragation(self, inventory_plugin):
records = [
{"sys_id": "1", "app": "tomcat1", "app.env": "dev", "fqdn": "a1"},
{"sys_id": "2", "app": "tomcat2", "app.env": "prod", "fqdn": "a1"},
{"sys_id": "3", "app": "tomcat3", "app.env": "staging", "fqdn": "a1"},
{"sys_i": "4", "app": "tomcat4", "app.env": "dev", "fqdn": "a2"},
]

columns = [
"app",
"app.env",
"fqdn",
]
name_source = "fqdn"
compose = {}
groups = {}
keyed_groups = []
strict = False
enhanced = False
exp_aggregation = True

inventory_plugin.fill_constructed(
records,
columns,
name_source,
compose,
groups,
keyed_groups,
strict,
enhanced,
exp_aggregation,
)

a1 = inventory_plugin.inventory.get_host("a1")
assert isinstance(a1.vars["app"], list)
for val in a1.vars["app"]:
assert "env" in val.keys()
assert "app" in val.keys()
assert val["env"] in ["prod", "dev", "staging"]
assert val["app"] in ["tomcat1", "tomcat2", "tomcat3"]


class TestConstructCacheSuffix:
def test_from_query(self, inventory_plugin, mocker):
Expand Down

0 comments on commit 423e008

Please sign in to comment.