Skip to content

Commit

Permalink
Update "group by" process in inventory_graphql (nautobot#110)
Browse files Browse the repository at this point in the history
  • Loading branch information
qduk authored Nov 29, 2021
1 parent 0f4ae29 commit 4039c6b
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 74 deletions.
10 changes: 6 additions & 4 deletions docs/plugins/gql_inventory_inventory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ Parameters
<td>
</td>
<td>
<div>List of group names to group the hosts</div>
<div>List of data paths to group the hosts.</div>
</td>
</tr>
<tr>
Expand Down Expand Up @@ -289,7 +289,7 @@ Examples

.. code-block:: yaml+jinja


# inventory.yml file in YAML format
# Example command line: ansible-inventory -v --list -i inventory.yml

Expand All @@ -309,12 +309,14 @@ Examples
- platform

# To group by use group_by key
# Please see choices for supported group_by options.
# Specify the full path to the data you would like to use to group by.
# Note. If you pass in a single string rather than a path, the plugin will automatically try to find a name or slug value.
plugin: networktocode.nautobot.gql_inventory
api_endpoint: http://localhost:8000
validate_certs: True
group_by:
- platform
- tenant.name
- status.slug


# Add additional variables
Expand Down
148 changes: 78 additions & 70 deletions plugins/inventory/gql_inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
from collections.abc import Mapping

__metaclass__ = type

Expand Down Expand Up @@ -63,10 +64,12 @@
group_by:
required: False
description:
- List of group names to group the hosts
- List of dot-sparated paths to index graphql query results (e.g. `platform.slug`)
- The final value returned by each path is used to derive group names and then group the devices into these groups.
- Valid group names must be string, so indexing the dotted path should return a string (i.e. `platform.slug` instead of `platform`)
- If value returned by the defined path is a dictionary, an attempt will first be made to access the `name` field, and then the `slug` field. (i.e. `platform` would attempt to lookup `platform.name`, and if that data was not returned, it would then try `platform.slug`)
type: list
default: []
choices: ["platform", "status", "device_role", "site"]
filters:
required: false
description:
Expand All @@ -87,21 +90,13 @@
tags: name
# To group by use group_by key
# Please see choices for supported group_by options.
# Specify the full path to the data you would like to use to group by.
plugin: networktocode.nautobot.gql_inventory
api_endpoint: http://localhost:8000
validate_certs: True
group_by:
- platform
# To group by use group_by key
# Please see choices for supported group_by options.
plugin: networktocode.nautobot.gql_inventory
api_endpoint: http://localhost:8000
validate_certs: True
group_by:
- platform
- tenant.name
- status.slug
# Add additional variables
plugin: networktocode.nautobot.gql_inventory
Expand Down Expand Up @@ -169,16 +164,6 @@ def verify_file(self, path):

return False

def create_inventory(self, group: str, host: str):
"""Creates Ansible inventory.
Args:
group (str): Name of the group
host (str): Hostname
"""
self.inventory.add_group(group)
self.inventory.add_host(host, group)

def add_variable(self, host: str, var: str, var_type: str):
"""Adds variables to group or host.
Expand All @@ -189,6 +174,69 @@ def add_variable(self, host: str, var: str, var_type: str):
"""
self.inventory.set_variable(host, var_type, var)

def add_ipv4_address(self, device):
"""Add primary IPv4 address to host."""
if device["primary_ip4"]:
self.add_variable(device["name"], device["primary_ip4"]["address"], "ansible_host")
else:
self.add_variable(device["name"], device["name"], "ansible_host")

def add_ansible_platform(self, device):
"""Add network platform to host"""
if device["platform"] and "napalm_driver" in device["platform"]:
self.add_variable(
device["name"],
ANSIBLE_LIB_MAPPER_REVERSE.get(NAPALM_LIB_MAPPER.get(device["platform"]["napalm_driver"])), # Convert napalm_driver to ansible_network_os value
"ansible_network_os",
)

def populate_variables(self, device):
"""Add specified variables to device."""
for var in self.variables:
if var in device and device[var]:
self.add_variable(device["name"], device[var], var)

def create_groups(self, device):
"""Create groups specified and add device to group."""
device_name = device["name"]
for group_by_path in self.group_by:
parent_attr, *chain = group_by_path.split(".")
device_attr = device.get(parent_attr)
if device_attr is None:
self.display.display(f"Could not find value for {parent_attr} on device {device_name}")
continue

if not chain:
group_name = device_attr

while chain:
group_name = chain.pop(0)
if isinstance(device_attr.get(group_name), Mapping):
device_attr = device_attr.get(group_name)
continue
else:
try:
group_name = device_attr[group_name]
except KeyError:
self.display.display(f"Could not find value for {group_name} in {group_by_path} on device {device_name}.")
break

if isinstance(group_name, Mapping):
if "name" in group_name:
group_name = group_name["name"]
elif "slug" in group_name:
group_name = group_name["slug"]
else:
self.display.display(f"No slug or name value for {group_name} in {group_by_path} on device {device_name}.")

if isinstance(group_name, str):
self.inventory.add_group(group_name)
self.inventory.add_child(group_name, device_name)
else:
self.display.display(
f"Groups must be a string. {group_name} is not a string. Please make sure your group_by path specified resolves to a string value."
)

def main(self):
"""Main function."""
if not HAS_NETUTILS:
Expand Down Expand Up @@ -243,53 +291,12 @@ def main(self):
# Need to return mock response data that is empty to prevent any failures downstream
return {"results": [], "next": None}

groups = {}
if self.group_by:
for group_by in self.group_by:
if not GROUP_BY.get(group_by):
self.display.display(
"WARNING: '{0}' is not supported as a 'group_by' option. Supported options are: {1} ".format(
group_by,
" ".join("'{0}',".format(str(x)) for x in GROUP_BY.keys()),
),
color="yellow",
)
continue
for device in json_data["data"]["devices"]:
groups[device["site"]["name"]] = "site"
if device.get(group_by) and GROUP_BY.get(group_by):
groups[device[group_by][GROUP_BY.get(group_by)]] = group_by
else:
groups["unknown"] = "unknown"

else:
for device in json_data["data"]["devices"]:
groups[device["site"]["name"]] = "site"

for key, value in groups.items():
for device in json_data["data"]["devices"]:
if device.get(value) and key == device[value][GROUP_BY[value]]:
self.create_inventory(key, device["name"])
if device["primary_ip4"]:
self.add_variable(
device["name"],
device["primary_ip4"]["address"],
"ansible_host",
)
else:
self.add_variable(device["name"], device["name"], "ansible_host")
if device["platform"] and "napalm_driver" in device["platform"]:
self.add_variable(
device["name"],
ANSIBLE_LIB_MAPPER_REVERSE.get(
NAPALM_LIB_MAPPER.get(device["platform"]["napalm_driver"]) # Convert napalm_driver to ansible_network_os value
),
"ansible_network_os",
)

for var in self.variables:
if var in device and device[var]:
self.add_variable(device["name"], device[var], var)
for device in json_data["data"]["devices"]:
self.inventory.add_host(device["name"])
self.add_ipv4_address(device)
self.add_ansible_platform(device)
self.populate_variables(device)
self.create_groups(device)

def parse(self, inventory, loader, path, cache=True):
super(InventoryModule, self).parse(inventory, loader, path)
Expand All @@ -308,6 +315,7 @@ def parse(self, inventory, loader, path, cache=True):
}
if token:
self.headers.update({"Authorization": "Token %s" % token})

self.gql_query = self.get_option("query")
self.group_by = self.get_option("group_by")
self.filters = self.get_option("filters")
Expand Down
25 changes: 25 additions & 0 deletions tests/unit/inventory/test_data/graphql_groups/device_data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "mydevice",
"platform": {
"napalm_driver": "asa"
},
"status": {
"name": "Active"
},
"primary_ip4": {
"address": "10.10.10.10/32"
},
"device_role": {
"name": "edge",
"color_category": {
"primary": "red"
}
},
"site": {
"name": "ATL01"
},
"tenant": {
"slug": "mytenant",
"type": "local"
}
}
Loading

0 comments on commit 4039c6b

Please sign in to comment.