Skip to main content

Infrahub Inventory Concepts

This topic provides a deep understanding of how Infrahub's graph database model maps to Nornir's inventory concepts. Understanding these mappings is essential for designing effective automation workflows.

Introduction​

The InfrahubInventory plugin bridges two different paradigms:

  • Infrahub: Graph database with nodes, relationships, and attributes
  • Nornir: Host-centric inventory with groups and connection parameters

This document explores:

  • How graph concepts translate to inventory structures
  • The role of schema mappings in this translation
  • Dynamic group generation from relationships
  • Performance considerations and best practices

Core mapping concepts​

Nodes to hosts​

In Infrahub, devices are nodes in the graph. The inventory plugin transforms these into Nornir hosts:

Infrahub Node Nornir Host
┌─────────────────────┐ ┌─────────────────────┐
│ InfraDevice │ ──▶ │ Host │
│ - id: abc123 │ │ - name: router1 │
│ - name: router1 │ │ - hostname: 10.1.1.1│
│ - primary_address │ │ - platform: ios │
│ └─▶ 10.1.1.1 │ │ - groups: [...] │
└─────────────────────┘ └─────────────────────┘

Attributes to properties​

Node attributes become host properties through schema mappings:

Infrahub AttributeMapping PathNornir Property
Direct attributenamehost.name
Nested relationprimary_address.addresshost.hostname

The inventory loading process​

Step 1: query construction​

The plugin builds a GraphQL query based on your configuration:

query {
InfraDevice(status__value: "active") {
edges {
node {
id
name { value }
primary_address {
node {
address { value }
}
}
site {
node {
name { value }
}
}
member_of_groups {
edges {
node {
name { value }
}
}
}
}
}
}
}

Step 2: data transformation​

Retrieved data undergoes transformation:

  1. Flatten nested structures: Navigate relationships to extract values
  2. Apply type conversions: Convert IP objects to strings
  3. Handle missing data: Use defaults or skip mappings
  4. Preserve metadata: Store full node data for tasks

Step 3: group generation​

Groups are created from:

  1. Infrahub group memberships: Devices assigned to CoreStandardGroup in Infrahub
  2. Dynamic groups: Generated from group_mappings configuration
  3. Hierarchical groups: Groups can have parent groups
config.yaml
group_mappings:
- "site.name"
- "role.name"
# For a device where site.name = "chicago" and role.name = "spine"
# Results in Nornir groups:
host.groups = [
"core_router", # Infrahub group membership
"site__chicago", # dynamic groups from group_mappings
"role__spine" # dynamic groups from group_mappings
]

Advanced inventory concepts​

Filtering at source​

Reduce data transfer by filtering in Infrahub:

host_node:
kind: "InfraDevice"
filters:
status__value: "active"
site__name__values: ["chicago", "dallas"]
role__value: "spine"

This translates to GraphQL filters, ensuring only relevant nodes are fetched. Use __value for single values and __values for lists.

Include optimization​

Minimize query complexity with selective includes:

host_node:
kind: "InfraDevice"
include:
- "name"
- "primary_address"
- "platform"
# Only fetch what you need

Relationship cardinality​

Understanding cardinality is crucial:

  • One-to-One: Device → Primary Address (direct mapping)
  • One-to-Many: Device → Interfaces (requires special handling)
  • Many-to-Many: Devices ↔ Services (complex relationships)

Currently, the plugin handles one-to-one and many-to-one relationships directly.

Performance considerations​

Query optimization​

Large inventories require optimization:

  1. Pagination: Automatic handling of large result sets
  2. Selective fetching: Only request needed attributes
  3. Relationship depth: Limit traversal depth

Inventory loading​

Nornir loads the inventory once during initialization:

# Inventory is fetched from Infrahub during initialization
nr = InitNornir(config_file="config.yaml")

# Subsequent operations use the loaded inventory
nr.filter(platform="ios") # No new query
nr.run(task=my_task) # Uses loaded inventory

Parallel processing​

Inventory loading is optimized for parallel task execution:

  • Host data is independent
  • Groups are pre-computed
  • No shared state between hosts

Dynamic inventory patterns​

Environment-based inventory​

Use branches for environments:

def get_inventory(environment="production"):
branch_map = {
"production": "main",
"staging": "staging",
"development": "dev"
}

return InitNornir(
inventory={
"plugin": "InfrahubInventory",
"options": {
"host_node": {"kind": "InfraDevice"},
"branch": branch_map.get(environment, "main")
}
}
)

Role-based filtering​

Create specialized inventories:

# Core network devices only
core_nr = InitNornir(
inventory={
"plugin": "InfrahubInventory",
"options": {
"host_node": {
"kind": "InfraDevice",
"filters": {
"role__values": ["core", "edge"]
}
}
}
}
)

Multi-platform support​

Handle different device platforms:

config.yaml
schema_mappings:
- name: "hostname"
mapping: "primary_address.address"
- name: "platform"
mapping: "platform.nornir_platform"

# Groups by platform automatically
group_mappings:
- "platform.name"

Troubleshooting inventory issues​

Debug mode​

Enable detailed logging:

import logging
logging.basicConfig(level=logging.DEBUG)

nr = InitNornir(config_file="config.yaml")
# Watch for GraphQL queries and responses

Validation queries​

Test your mappings with GraphQL using the Infrahub GraphQL explorer or SDK:

import asyncio
from infrahub_sdk import InfrahubClient

async def test_query():
client = InfrahubClient(address="http://localhost:8000")
query = """
query {
InfraDevice {
edges {
node {
name { value }
primary_address {
node {
address { value }
}
}
}
}
}
}
"""
result = await client.execute_graphql(query=query)
print(result)

asyncio.run(test_query())

Common issues​

  1. Missing Hosts: Check filters and node kind
  2. Empty Groups: Verify relationship paths exist
  3. Mapping Errors: Ensure attributes are populated
  4. Performance: Reduce included attributes

Integration with Nornir ecosystem​

Transform functions​

Nornir provides a transform_function mechanism to enrich host data during inventory loading. Register a transform function and reference it by name:

from nornir import InitNornir
from nornir.core.plugins.inventory import TransformFunctionRegister

def enrich_host(host):
"""Transform function called for each host during inventory loading."""
# Add computed properties based on naming conventions
host.data["is_core"] = "core" in host.name
host.data["is_edge"] = "edge" in host.name

# Derive datacenter from hostname prefix
if "-" in host.name:
host.data["datacenter"] = host.name.split("-")[0]

# Register the transform function
TransformFunctionRegister.register("enrich_host", enrich_host)

# Use transform_function in InitNornir
nr = InitNornir(
inventory={
"plugin": "InfrahubInventory",
"options": {
"address": "http://localhost:8000",
"host_node": {"kind": "InfraDevice"}
},
"transform_function": "enrich_host"
}
)

# Filter using enriched data
core_devices = nr.filter(filter_func=lambda h: h.data.get("is_core", False))
print(f"Found {len(core_devices.inventory.hosts)} core devices")

Using Infrahub node data in tasks​

Access the full Infrahub node object within tasks for attributes not mapped to Nornir properties:

from nornir.core.task import Task, Result

def device_audit(task: Task) -> Result:
"""Audit device using both Nornir properties and Infrahub data."""
# Access standard Nornir host properties
hostname = task.host.hostname
platform = task.host.platform

# Access the full Infrahub node for additional attributes
infrahub_node = task.host.data.get("InfrahubNode")

audit_info = {
"name": task.host.name,
"hostname": hostname,
"platform": platform,
}

# Extract additional Infrahub attributes if available
if infrahub_node:
if hasattr(infrahub_node, "role") and infrahub_node.role:
audit_info["role"] = infrahub_node.role.value

return Result(host=task.host, result=audit_info)

# Run the audit task
results = nr.run(task=device_audit)

Custom inventory plugins​

Create a custom inventory class to apply transformations during loading:

from nornir_infrahub.plugins.inventory.infrahub import InfrahubInventory
from nornir.core.plugins.inventory import InventoryPluginRegister

class CustomInfrahubInventory(InfrahubInventory):
"""Custom inventory with platform name normalization."""

def load(self):
inventory = super().load()

# Normalize platform names for Nornir/NAPALM compatibility
platform_map = {
"juniper_junos": "junos",
"arista_eos": "eos",
"cisco_ios": "ios",
}

for host in inventory.hosts.values():
if host.platform in platform_map:
host.platform = platform_map[host.platform]

return inventory

# Register and use the custom plugin
InventoryPluginRegister.register("CustomInfrahubInventory", CustomInfrahubInventory)

nr = InitNornir(
inventory={
"plugin": "CustomInfrahubInventory",
"options": {
"address": "http://localhost:8000",
"host_node": {
"kind": "InfraDevice",
"include": ["platform"]
},
"schema_mappings": [
{"name": "platform", "mapping": "platform.nornir_platform"}
]
}
}
)

Further reading​