Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions docs/docs/adapters/aci.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
---
title: Cisco ACI adapter
---
import ReferenceLink from "../../src/components/Card";

## What is Cisco ACI?

The *Cisco ACI* is a software-defined networking (SDN) solution that provides a policy-based, application-centric approach to managing and orchestrating network infrastructure. It is commonly used in data centers for scalable, policy-driven networking.

## Sync direction

Cisco ACI → Infrahub

:::note
Currently, the Cisco ACI adapter supports only **one-way synchronization** from ACI to Infrahub. Syncing data back into ACI is not yet supported.
:::

## Configuration

The adapter reads connection settings from the sync config and can be overridden by environment variables. Credentials should be provided from a secret manager or environment variables in production.

### Configuration parameters

```yaml
---
name: from-cisco-aci
source:
name: aci
settings:
url: "https://<APIC_CONTROLLER>"
username: "<USER>"
password: "<PASSWORD>"
api_endpoint: "api" # optional, default: api
verify: true # boolean or string ("false","0") accepted
```

### Environment variables

- CISCO_APIC_URL: overrides settings.url
- CISCO_APIC_USERNAME: overrides settings.username
- CISCO_APIC_PASSWORD: overrides settings.password
- CISCO_APIC_VERIFY: overrides settings.verify; accepts true/false/0/1 (strings are normalized)

Notes:

- Credentials must come from environment variables or a secret manager in production. Never commit secrets.
- The adapter normalizes verify to a boolean (strings like false, 0, no are treated as False).
- The adapter records login timestamps in UTC to avoid timezone related issues and ensure correct token refresh behavior.

### Schema mapping examples

#### Basic device mapping

```yaml
- name: DcimPhysicalDevice
mapping: "class/fabricNode.json"
identifiers: ["name"]
fields:
- name: name
mapping: "fabricNode.attributes.name"
- name: serial
mapping: "fabricNode.attributes.serial"
- name: role
mapping: "fabricNode.attributes.role"
filters:
- field: "fabricNode.attributes.fabricSt"
operation: "=="
value: "active"
```

#### Interface mapping with ACI Jinja filter

```yaml
- name: DcimPhysicalInterface
mapping: "class/l1PhysIf.json"
identifiers: ["device", "name"]
fields:
- name: name
mapping: "l1PhysIf.attributes.id"
- name: device
mapping: "l1PhysIf.attributes.dn"
reference: DcimPhysicalDevice
- name: description
mapping: "l1PhysIf.attributes.descr"
transforms:
- field: device
expression: "{{ l1PhysIf.attributes.dn.split('/')[2].replace('node-', '') | aci_device_name }}"
- field: status
expression: "{{ 'active' if l1PhysIf.attributes.adminSt == 'up' else 'free' }}"
filters:
- field: "l1PhysIf.attributes.id"
operation: "contains"
value: "eth"
```

## ACI-specific Jinja filters

The ACI adapter provides custom Jinja filters for data transformation:

### `aci_device_name` filter

The `aci_device_name` filter resolves ACI node IDs to device names automatically. This is particularly useful when mapping physical interfaces to their parent devices.

**Usage:**

```jinja2
{{ node_id | aci_device_name }}
```

**Example:**

- Input: `"102"` (ACI node ID)
- Output: `"spine-102"` (actual device name from ACI)

**Common use case in transforms:**

```yaml
transforms:
- field: device
expression: "{{ l1PhysIf.attributes.dn.split('/')[2].replace('node-', '') | aci_device_name }}"
```

This transform:

1. Extracts the node ID from the ACI Distinguished Name (DN)
2. Removes the `"node-"` prefix (e.g., `"node-102"` → `"102"`)
3. Uses the `aci_device_name` filter to resolve the node ID to the actual device name

**How it works:**

- The adapter automatically queries ACI's `fabricNode` class during initialization
- Builds a mapping of node IDs to device names
- The filter performs a simple lookup with fallback to the original node ID if not found

## Generating the models

Use the generate command to produce models from the schema mapping and examples:

```bash
poetry run infrahub-sync generate --name from-cisco-aci --directory examples/
```

## Common issues and troubleshooting

### Authentication and connectivity

- If you see token refresh errors, ensure the APIC response includes refreshTimeoutSeconds; the adapter forces re-login when refresh data is unavailable.
- For TLS verification problems, set CISCO_APIC_VERIFY to false in a secure environment (use with caution).

### Device reference resolution

- **Interface-device relationship errors**: If you see "Unable to locate the node device" errors, ensure:
- The `DcimPhysicalDevice` mapping runs before `DcimPhysicalInterface` in the `order` configuration
- The device transform uses the `aci_device_name` filter correctly: `{{ node_id | aci_device_name }}`
- The ACI fabricNode query is successful (check logs for "Built ACI device mapping" messages)

### Jinja filter issues

- **`aci_device_name` filter not found**: Ensure you're using the ACI adapter and the filter is correctly spelled
- **Filter returns node ID instead of device name**: Check that the fabricNode query was successful during adapter initialization
- **Transform expression errors**: Verify the DN parsing logic extracts the correct node ID:

```yaml
expression: "{{ l1PhysIf.attributes.dn.split('/')[2].replace('node-', '') | aci_device_name }}"
```

### General debugging

- Enable DEBUG logging for the adapter to see raw fetched objects and mapping decisions. Logs will not include secrets.
- Check the device mapping build process in logs: look for "Built ACI device mapping with X entries"
119 changes: 119 additions & 0 deletions docs/docs/adapters/local-adapters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,132 @@ For a more complete example, refer to the example in the repository:

<ReferenceLink title="Custom Adapter Example" url="https://github.com/opsmill/infrahub-sync/tree/main/examples/custom_adapter" openInNewTab />

### Adding custom Jinja filters

Custom adapters can provide their own Jinja filters for use in transform expressions. This is particularly useful for adapter-specific data transformations.

#### Implementing custom filters

To add custom filters to your adapter, implement the `_add_custom_filters` class method in your DiffSync model class:

```python
from typing import Any, ClassVar
from diffsync import DiffSyncModel
from infrahub_sync import DiffSyncModelMixin

class MyCustomModel(DiffSyncModelMixin, DiffSyncModel):
# Store any data needed by filters as class variables
_my_mapping: ClassVar[dict[str, str]] = {}

@classmethod
def set_my_mapping(cls, mapping: dict[str, str]) -> None:
"""Set mapping data for use in filters."""
cls._my_mapping = mapping

@classmethod
def _add_custom_filters(cls, native_env: Any, item: dict[str, Any]) -> None:
"""Add custom filters to the Jinja environment."""

def my_custom_filter(value: str) -> str:
"""Custom filter that transforms values using stored mapping."""
return cls._my_mapping.get(str(value), value)

def format_identifier(value: str) -> str:
"""Another custom filter for formatting identifiers."""
return f"ID-{value.upper()}"

# Register filters with the Jinja environment
native_env.filters["my_custom_filter"] = my_custom_filter
native_env.filters["format_identifier"] = format_identifier
```

#### Setting up filter data

If your filters need data (like mappings, lookups, etc.), initialize it in your adapter:

```python
class MyCustomAdapter(DiffSyncMixin, Adapter):
def __init__(self, target, adapter, config, *args, **kwargs):
super().__init__(*args, **kwargs)
# ... other initialization

# Build data needed by filters
my_mapping = self._build_custom_mapping()

# Pass data to model class for filter use
MyCustomModel.set_my_mapping(my_mapping)

def _build_custom_mapping(self) -> dict[str, str]:
"""Build mapping data from your data source."""
# Implementation depends on your data source
return {"key1": "value1", "key2": "value2"}
```

#### Using custom filters in configuration

Once implemented, use your custom filters in transform expressions:

```yaml
schema_mapping:
- name: MyModel
mapping: "api/endpoint"
fields:
- name: identifier
mapping: "raw_id"
- name: formatted_name
mapping: "name"
transforms:
- field: identifier
expression: "{{ raw_id | my_custom_filter | format_identifier }}"
- field: status
expression: "{{ 'active' if enabled else 'inactive' }}"
```

#### Filter implementation guidelines

1. **Keep filters simple**: Each filter should do one specific transformation
2. **Handle edge cases**: Always provide fallback values for missing data
3. **Use class variables**: Store filter data as class variables for efficient access
4. **Document your filters**: Add docstrings explaining what each filter does
5. **Test thoroughly**: Ensure filters work with various input types and edge cases

#### Real-world example: ACI device name filter

Here's how the ACI adapter implements the `aci_device_name` filter:

```python
class AciModel(DiffSyncModelMixin, DiffSyncModel):
_device_mapping: ClassVar[dict[str, str]] = {}

@classmethod
def set_device_mapping(cls, device_mapping: dict[str, str]) -> None:
cls._device_mapping = device_mapping

@classmethod
def _add_custom_filters(cls, native_env: Any, item: dict[str, Any]) -> None:
def aci_device_name(node_id: str) -> str:
"""Resolve ACI node IDs to device names."""
return cls._device_mapping.get(str(node_id), node_id)

native_env.filters["aci_device_name"] = aci_device_name
```

Used in configuration:

```yaml
transforms:
- field: device
expression: "{{ l1PhysIf.attributes.dn.split('/')[2].replace('node-', '') | aci_device_name }}"
```

### Best practices

1. **Package Structure**: Organize complex adapters as packages with `__init__.py`
2. **Testing**: Include test data and documentation with your adapter
3. **Configuration**: Use settings to make your adapter configurable
4. **Error Handling**: Implement proper error handling and logging
5. **Type Annotations**: Use type hints to make your code more maintainable
6. **Custom Filters**: Implement adapter-specific Jinja filters for complex transformations

### Local adapter example

Expand Down
3 changes: 3 additions & 0 deletions docs/docs/reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ $ infrahub-sync diff [OPTIONS]
* `--directory TEXT`: Base directory to search for sync configurations
* `--branch TEXT`: Branch to use for the diff.
* `--show-progress / --no-show-progress`: Show a progress bar during diff [default: show-progress]
* `--adapter-path TEXT`: Paths to look for adapters. Can be specified multiple times.
* `--help`: Show this message and exit.

## `infrahub-sync generate`
Expand All @@ -54,6 +55,7 @@ $ infrahub-sync generate [OPTIONS]
* `--config-file TEXT`: File path to the sync configuration YAML file
* `--directory TEXT`: Base directory to search for sync configurations
* `--branch TEXT`: Branch to use for the sync.
* `--adapter-path TEXT`: Paths to look for adapters. Can be specified multiple times.
* `--help`: Show this message and exit.

## `infrahub-sync list`
Expand Down Expand Up @@ -89,4 +91,5 @@ $ infrahub-sync sync [OPTIONS]
* `--branch TEXT`: Branch to use for the sync.
* `--diff / --no-diff`: Print the differences between the source and the destination before syncing [default: diff]
* `--show-progress / --no-show-progress`: Show a progress bar during syncing [default: show-progress]
* `--adapter-path TEXT`: Paths to look for adapters. Can be specified multiple times.
* `--help`: Show this message and exit.
3 changes: 2 additions & 1 deletion docs/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ const sidebars: SidebarsConfig = {
'adapters/observium',
'adapters/peering-manager',
'adapters/slurpit',
'adapters/aci',
],
},
},
{
type: 'category',
label: 'Reference',
Expand Down
1 change: 1 addition & 0 deletions examples/aci_to_infrahub/aci/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""ACI sync adapter module."""
28 changes: 28 additions & 0 deletions examples/aci_to_infrahub/aci/sync_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from __future__ import annotations

from infrahub_sync.plugin_loader import PluginLoader

from .sync_models import (
DcimPhysicalDevice,
DcimPhysicalInterface,
LocationBuilding,
LocationMetro,
OrganizationCustomer,
)

# Load adapter class dynamically at runtime

_AdapterBaseClass = PluginLoader().resolve("aci")


# -------------------------------------------------------
# AUTO-GENERATED FILE, DO NOT MODIFY
# This file has been generated with the command `infrahub-sync generate`
# All modifications will be lost the next time you reexecute this command
# -------------------------------------------------------
class AciSync(_AdapterBaseClass):
DcimPhysicalDevice = DcimPhysicalDevice
DcimPhysicalInterface = DcimPhysicalInterface
LocationBuilding = LocationBuilding
LocationMetro = LocationMetro
OrganizationCustomer = OrganizationCustomer
Loading
Loading