Skip to content

Commit

Permalink
GraqhQL Inventory: Add ability for users to specify non-foreign key r…
Browse files Browse the repository at this point in the history
…elationships with Null/None (#123)
  • Loading branch information
FragmentedPacket authored Mar 30, 2022
1 parent 40d8f40 commit b5d98c7
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 33 deletions.
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Dockerfile
docker-compose.yml
*.md
.env
.vscode/
.github/
2 changes: 1 addition & 1 deletion ansible.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
force_valid_group_names = always

[inventory]
enable_plugins = networktocode.nautobot.inventory, auto, host_list, yaml, ini, toml, script
enable_plugins = networktocode.nautobot.inventory, networktocode.nautobot.gql_inventory, yaml, ini
83 changes: 83 additions & 0 deletions plugins/filter/graphql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""GraphQL related filter plugins."""
# Copyright (c) 2022, Network to Code (@networktocode) <info@networktocode.com>
# 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

__metaclass__ = type


def build_graphql_filter_string(filter: dict) -> str:
"""Takes a dictionary and builds a graphql filter
Args:
filter (dict): Key/Value pairs to build filter from
Returns:
str: Proper graphQL filter
"""
base_filter = "({0})"
loop_filters = []
for key, value in filter.items():
temp_string = f"{key}: "
value_string = f"{value}"

# GraphQL variables do not need quotes (This isn't completely support with inventory yet, but code added here)
if isinstance(value, str) and not key.startswith("$"):
value_string = "'" + value_string + "'"

loop_filters.append(temp_string + value_string)

return base_filter.format(", ".join(loop_filters))


def convert_to_graphql_string(query: dict, start=0) -> str:
"""Provide a dictionary to convert to a graphQL string.
Args:
query (dict): A dictionary mapping to the graphQL call to be made.
start (int): The starting indentation when compiling string.
Returns:
str: GraphQL query string
"""
graphql_string = """"""
loop_index = 0
query_len = len(query)
for k, v in query.items():
loop_index += 1
loop_string = "".rjust(start * 2, " ")
loop_filter = None

# Determine what to do
if not v:
loop_string += f"{k}"
elif isinstance(v, dict):
if v.get("filters"):
loop_filter = build_graphql_filter_string(v.pop("filters"))
loop_string += "{0} {1}\n".format(k, loop_filter + " {" if loop_filter else "{")
loop_string += convert_to_graphql_string(v, start + 1)
loop_string += "{0}{1}".format(" " * (start * 2), "}")
else:
loop_string += "{0} {1}\n".format(k, "{")
# We want to keep the double spaces, but add 2 more
loop_string += "{0}".format(" " * (start * 2 + 2))
loop_string += f"{v}\n"
loop_string += "{0}{1}".format(" " * (start * 2), "}")

if loop_index != query_len or start > 0:
loop_string += "\n"

graphql_string += loop_string

return graphql_string.replace("'", '"')


class FilterModule:
"""Return graphQL filters."""

def filters(self):
"""Map filter functions to filter names."""
return {
"graphql_string": convert_to_graphql_string,
}
38 changes: 31 additions & 7 deletions plugins/inventory/gql_inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,22 @@
# inventory.yml file in YAML format
# Example command line: ansible-inventory -v --list -i inventory.yml
# Add additional query parameter with query key
# Add additional query parameter with query key and use filters
plugin: networktocode.nautobot.gql_inventory
api_endpoint: http://localhost:8000
validate_certs: True
query:
tags: name
serial:
site:
filters:
tenant: "den"
name:
description:
contact_name:
description:
region:
name:
# To group by use group_by key
# Specify the full path to the data you would like to use to group by.
Expand Down Expand Up @@ -134,7 +144,8 @@
from ansible.module_utils.urls import open_url

from ansible.module_utils.six.moves.urllib import error as urllib_error
from jinja2 import Environment, FileSystemLoader

from ansible_collections.networktocode.nautobot.plugins.filter.graphql import convert_to_graphql_string

try:
from netutils.lib_mapper import ANSIBLE_LIB_MAPPER_REVERSE, NAPALM_LIB_MAPPER
Expand Down Expand Up @@ -242,11 +253,23 @@ def main(self):
if not HAS_NETUTILS:
raise AnsibleError("networktocode.nautobot.gql_inventory requires netutils. Please pip install netutils.")

file_loader = FileSystemLoader(f"{PATH}/../templates")
env = Environment(loader=file_loader, autoescape=True)
template = env.get_template("graphql_default_query.j2")
query = template.render(query=self.gql_query, filters=self.filters)
data = {"query": "query {%s}" % query}
base_query = {
"query": {
"devices": {
"name": None,
"platform": "napalm_driver",
"status": "name",
"primary_ip4": "host",
"device_role": "name",
"site": "name",
}
}
}
base_query["query"]["devices"].update(self.gql_query)
if self.filters:
base_query["query"]["devices"]["filters"] = self.filters
query = convert_to_graphql_string(base_query)
data = {"query": query}

try:
response = open_url(
Expand All @@ -272,6 +295,7 @@ def main(self):
# Need to return mock response data that is empty to prevent any failures downstream
return {"results": [], "next": None}
else:
self.display.display(f"{e.code}", color="red")
self.display.display(
"Something went wrong while executing the query.\nReason: {reason}".format(
reason=json.loads(e.fp.read().decode())["errors"][0]["message"],
Expand Down
5 changes: 0 additions & 5 deletions plugins/templates/graphql_additional_query.j2

This file was deleted.

19 changes: 0 additions & 19 deletions plugins/templates/graphql_default_query.j2

This file was deleted.

1 change: 0 additions & 1 deletion plugins/templates/graphql_filters.j2

This file was deleted.

Empty file added tests/unit/filter/__init__.py
Empty file.
76 changes: 76 additions & 0 deletions tests/unit/filter/test_data/graphql_string.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
[
{
"query": {
"serial": null,
"asset_tag": null
},
"expected": "serial\nasset_tag"
},
{
"query": {
"query": {
"devices": {
"serial": null,
"asset_tag": null
}
}
},
"expected": "query {\n devices {\n serial\n asset_tag\n }\n}"
},
{
"query": {
"serial": null,
"asset_tag": null,
"site": {
"name": null,
"contact_name": null,
"description": null,
"region": {
"name": null,
"parent": {
"name": null
}
}
}
},
"expected": "serial\nasset_tag\nsite {\n name\n contact_name\n description\n region {\n name\n parent {\n name\n }\n }\n}"
},
{
"query": {
"serial": null,
"asset_tag": null,
"site": {
"filters": {
"role": "core",
"tenant": "den"
},
"name": null,
"contact_name": null,
"description": null,
"region": {
"filters": {
"$tenant": "den"
},
"name": null,
"parent": {
"name": null
}
}
}
},
"expected": "serial\nasset_tag\nsite (role: \"core\", tenant: \"den\") {\n name\n contact_name\n description\n region ($tenant: den) {\n name\n parent {\n name\n }\n }\n}"
},
{
"query": {
"device": {
"name": null,
"platform": "napalm_driver",
"status": "name",
"primary_ip4": "address",
"device_role": "name",
"site": "name"
}
},
"expected": "device {\n name\n platform {\n napalm_driver\n }\n status {\n name\n }\n primary_ip4 {\n address\n }\n device_role {\n name\n }\n site {\n name\n }\n}"
}
]
25 changes: 25 additions & 0 deletions tests/unit/filter/test_graphql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Tests for Nautobot GraphQL filter plugins."""

import json
import pytest
from ansible_collections.networktocode.nautobot.plugins.filter.graphql import (
convert_to_graphql_string,
build_graphql_filter_string,
)


def load_test_data(test):
with open(f"tests/unit/filter/test_data/{test}.json", "r") as f:
data = json.loads(f.read())
return data


@pytest.mark.parametrize(("test_data"), load_test_data("graphql_string"))
def test_convert_to_graphql_string(test_data):
result = convert_to_graphql_string(test_data["query"])
assert result == test_data["expected"]


def test_build_graphql_filter_string():
gql_filters = {"role": "core", "$tenant": "den", "device_type": "c9300"}
assert build_graphql_filter_string(gql_filters) == "(role: 'core', $tenant: den, device_type: 'c9300')"

0 comments on commit b5d98c7

Please sign in to comment.