{ "cells": [ { "cell_type": "markdown", "id": "095a25e8", "metadata": {}, "source": [ "# How to load the inventory from an async application\n", "\n", "Use `AInitNornir` when your program already runs an event loop, for example a web service, a bot or an async test suite. `nr = await AInitNornir(...)` takes the same arguments as `InitNornir` and returns the same `Nornir` object, but the event loop keeps serving other work while the inventory loads." ] }, { "cell_type": "markdown", "id": "c3e6e9f7", "metadata": {}, "source": [ "## Build Nornir with an existing inventory plugin\n", "\n", "Existing inventory plugins work with `AInitNornir`, `SimpleInventory` included. A synchronous plugin is constructed and loaded in a worker thread, so a slow inventory source does not stop the event loop. A plugin that must run in the main thread, for example to install a signal handler, fails with `AInitNornir`: keep using `InitNornir` for it, or add an asynchronous `aload` to it. Paths in this example are relative to the notebook working directory, `docs/howto/`." ] }, { "cell_type": "code", "execution_count": 1, "id": "bddf70cb", "metadata": { "execution": { "iopub.execute_input": "2026-10-16T08:26:14.260862Z", "iopub.status.busy": "2026-10-16T08:26:14.260741Z", "iopub.status.idle": "2026-10-16T08:26:14.273046Z", "shell.execute_reply": "2026-10-16T08:26:14.272799Z" } }, "outputs": [], "source": [ "import asyncio\n", "import threading\n", "from typing import Any\n", "\n", "from nornir import AInitNornir, InitNornir\n", "from nornir.core.exceptions import (\n", " InventoryPluginNotSyncError,\n", " TransformFunctionNotSyncError,\n", ")\n", "from nornir.core.inventory import Defaults, Groups, Host, Hosts, Inventory\n", "from nornir.core.plugins.inventory import (\n", " AsyncTransformFunctionRegister,\n", " CapabilityInventoryPluginRegister,\n", " InventoryCapability,\n", ")" ] }, { "cell_type": "code", "execution_count": 2, "id": "f3ddaf5c", "metadata": { "execution": { "iopub.execute_input": "2026-10-16T08:26:14.273776Z", "iopub.status.busy": "2026-10-16T08:26:14.273696Z", "iopub.status.idle": "2026-10-16T08:26:14.448637Z", "shell.execute_reply": "2026-10-16T08:26:14.448347Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "edge-1: 192.0.2.1 (eos, lab-a)\n", "edge-2: 192.0.2.2 (eos, lab-b)\n" ] } ], "source": [ "nr = await AInitNornir(config_file=\"async_inventory/config.yaml\")\n", "for host in nr.inventory.hosts.values():\n", " print(f\"{host.name}: {host.hostname} ({host.platform}, {host['site']})\")" ] }, { "cell_type": "markdown", "id": "108abe3f", "metadata": {}, "source": [ "## Load from an asynchronous source\n", "\n", "An inventory plugin whose API client is asynchronous implements the capability-aware contract: it declares `asyncio` support in `get_capabilities` and implements `aload`. `AInitNornir` awaits `aload` on the running loop. The class below stands in for a plugin that queries an inventory API; a real plugin is registered through the `nornir.plugins.capability_inventory` entry-point group instead of `register`." ] }, { "cell_type": "code", "execution_count": 3, "id": "75df4d97", "metadata": { "execution": { "iopub.execute_input": "2026-10-16T08:26:14.449819Z", "iopub.status.busy": "2026-10-16T08:26:14.449670Z", "iopub.status.idle": "2026-10-16T08:26:14.465242Z", "shell.execute_reply": "2026-10-16T08:26:14.465029Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "core-1: 198.51.100.1 (junos, lab-c)\n", "core-2: 198.51.100.2 (junos, lab-c)\n" ] } ], "source": [ "API_DEVICES = [\n", " {\"name\": \"core-1\", \"address\": \"198.51.100.1\", \"platform\": \"junos\"},\n", " {\"name\": \"core-2\", \"address\": \"198.51.100.2\", \"platform\": \"junos\"},\n", "]\n", "\n", "\n", "class LabApiInventory:\n", " def __init__(self, site: str) -> None:\n", " self.site = site\n", "\n", " def get_capabilities(self) -> frozenset[InventoryCapability]:\n", " return frozenset({\"asyncio\"})\n", "\n", " async def aload(self) -> Inventory:\n", " await asyncio.sleep(0) # an awaited HTTP or GraphQL call in a real plugin\n", " hosts = Hosts(\n", " {\n", " device[\"name\"]: Host(\n", " name=device[\"name\"],\n", " hostname=device[\"address\"],\n", " platform=device[\"platform\"],\n", " data={\"site\": self.site},\n", " )\n", " for device in API_DEVICES\n", " }\n", " )\n", " return Inventory(hosts=hosts, groups=Groups(), defaults=Defaults())\n", "\n", "\n", "CapabilityInventoryPluginRegister.register(\"lab_api\", LabApiInventory)\n", "\n", "api_nr = await AInitNornir(inventory={\"plugin\": \"lab_api\", \"options\": {\"site\": \"lab-c\"}})\n", "for host in api_nr.inventory.hosts.values():\n", " print(f\"{host.name}: {host.hostname} ({host.platform}, {host['site']})\")" ] }, { "cell_type": "markdown", "id": "68420779", "metadata": {}, "source": [ "## Enrich hosts with an async transform function\n", "\n", "A transform function defined with `async def` is awaited once per host, in inventory order, one host at a time. Use it when the data comes from a source with an asynchronous client, such as a secrets store. A synchronous transform function also works with `AInitNornir`; it runs in a worker thread." ] }, { "cell_type": "code", "execution_count": 4, "id": "f115bc72", "metadata": { "execution": { "iopub.execute_input": "2026-10-16T08:26:14.466110Z", "iopub.status.busy": "2026-10-16T08:26:14.466019Z", "iopub.status.idle": "2026-10-16T08:26:14.485806Z", "shell.execute_reply": "2026-10-16T08:26:14.485551Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "password set for core-1\n", "password set for core-2\n", "True\n" ] } ], "source": [ "SECRETS = {\"core-1\": \"s3cr3t-1\", \"core-2\": \"s3cr3t-2\"}\n", "\n", "\n", "async def read_password(host: Host, **kwargs: Any) -> None:\n", " await asyncio.sleep(0) # an awaited call to the secrets store\n", " host.password = SECRETS[host.name]\n", " print(f\"password set for {host.name}\")\n", "\n", "\n", "AsyncTransformFunctionRegister.register(\"read_password\", read_password)\n", "\n", "enriched_nr = await AInitNornir(\n", " inventory={\n", " \"plugin\": \"lab_api\",\n", " \"options\": {\"site\": \"lab-c\"},\n", " \"transform_function\": \"read_password\",\n", " }\n", ")\n", "print(all(host.password for host in enriched_nr.inventory.hosts.values()))" ] }, { "cell_type": "markdown", "id": "c01bca53", "metadata": {}, "source": [ "## Errors from `InitNornir`\n", "\n", "`InitNornir` does not run asynchronous code. When the configured plugin only declares `asyncio` support, or the transform function is defined with `async def`, it raises a named error before anything is loaded. Both errors are subclasses of `SyncAsyncMismatchError`." ] }, { "cell_type": "code", "execution_count": 5, "id": "15fe9e86", "metadata": { "execution": { "iopub.execute_input": "2026-10-16T08:26:14.486460Z", "iopub.status.busy": "2026-10-16T08:26:14.486371Z", "iopub.status.idle": "2026-10-16T08:26:14.513759Z", "shell.execute_reply": "2026-10-16T08:26:14.513517Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "InventoryPluginNotSyncError: Inventory plugin 'lab_api' only declares asyncio support and cannot be loaded by InitNornir(). Use 'nr = await AInitNornir(...)' instead, or ask the plugin author for synchronous support.\n", "TransformFunctionNotSyncError: Transform function 'read_password' is defined with 'async def' and cannot be applied by InitNornir(). Use 'nr = await AInitNornir(...)' instead, or define a synchronous transform function.\n" ] } ], "source": [ "try:\n", " InitNornir(inventory={\"plugin\": \"lab_api\", \"options\": {\"site\": \"lab-c\"}})\n", "except InventoryPluginNotSyncError as exc:\n", " print(f\"{type(exc).__name__}: {exc}\")\n", "\n", "try:\n", " InitNornir(\n", " config_file=\"async_inventory/config.yaml\",\n", " inventory={\"transform_function\": \"read_password\"},\n", " )\n", "except TransformFunctionNotSyncError as exc:\n", " print(f\"{type(exc).__name__}: {exc}\")" ] }, { "cell_type": "markdown", "id": "c2c7e296", "metadata": {}, "source": [ "## Cancellation\n", "\n", "If the application cancels `AInitNornir`, for example on a startup timeout, the cancellation reaches an asynchronous plugin's `aload` immediately. A synchronous plugin running in the worker thread cannot be interrupted: `AInitNornir` returns control at once, but the thread keeps running until `load` returns, and its result is discarded. Synchronous plugins should therefore set timeouts on their network calls." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.14.6" } }, "nbformat": 4, "nbformat_minor": 5 }