Sync VRFs¤
The sync_vrfs task reconciles VRFs from live devices with global NetBox VRF
objects. A VRF name is its identity. The task synchronizes its description and
adds live import and export route targets to the existing NetBox associations.
Route-target and device associations are additive.
The task creates missing NetBox route targets before it creates or updates VRFs. Route distinguishers and import/export route policies returned by the TTP getter are not stored in NetBox.
Inputs¤
| Input | Default | Description |
|---|---|---|
instance |
Worker default | NetBox instance name. |
dry_run |
False |
Calculate the diff without writing to NetBox. |
with_approval |
False |
Present the prepared plan for approval before writing. Ignored during dry-run. |
timeout |
600 |
Timeout in seconds for host resolution and TTP parsing. |
devices |
None |
Explicit NetBox and Nornir device names. |
branch |
None |
NetBox Branching plugin branch name. |
device_custom_field |
devices |
Multi-object VRF custom field related to dcim.device. |
| Nornir filters | None |
FO, FB, FH, FC, FR, FG, FP, FL, FM, FX, and FN. |
Provide devices or at least one Nornir host filter.
NetBox VRF Device Custom Field¤
By default, the task uses a VRF custom field named devices. Configure it as a
multi-object field assigned to ipam.vrf and related to dcim.device. Use
device_custom_field to select another field with the same configuration.
The task adds devices on which the VRF was observed and preserves existing associations. It does not remove devices during a scoped run. If the configured field exists but has no value on an existing VRF, it is treated as an empty association list. If the configured field does not exist, the task continues without device association changes.
For example, when device_custom_field="vrf_devices" is requested but NetBox
does not have a custom field named vrf_devices, VRFs and route targets are
still synchronized. The task does not fail and does not add a device custom
field value to the VRF.
Live data¤
The task runs Nornir parse_ttp with get="vrfs", which returns:
- name: TENANT_A
description: Tenant A services
rd: 65000:201
rt_import:
- 65000:201
- 65000:301
rt_export:
- 65000:201
- 65000:302
route_policy_import: TENANT_A_IMPORT
route_policy_export: TENANT_A_EXPORT
Only name, description, rt_import, and rt_export are used. Description
text and route-target values are used without string normalization, and null
descriptions become an empty string.
For each VRF, devices are ordered alphabetically. The first non-empty description in that order becomes the NetBox description; the description is empty when no device supplies one. Different descriptions from multiple devices are resolved by this rule and are not reported as conflicts.
Import and export route-target lists from every reporting device are
concatenated to form the live aggregate. The shared make_diff method compares
that aggregate with NetBox and ignores route-target order. If it finds an
import or export route-target difference for an existing VRF, the task combines
the current NetBox list with live targets that are not already in that list and
places the combined list in the update. For a new VRF, or an existing VRF with
an empty target set, the live aggregate populates the association. Route-target
differences are not reported as errors, and all reporting devices are added to
the device custom field.
Multi-device aggregation example¤
Assume the selected devices return the following records for the same VRF:
- name: TENANT_A
description:
rt_import:
- 65000:100
- 65000:200
rt_export:
- 65000:100
- name: TENANT_A
description: Tenant A services
rt_import:
- 65000:100
- 65000:300
rt_export:
- 65000:100
- 65000:400
The task processes edge-a before edge-b because device names are sorted.
The empty description from edge-a is skipped, so Tenant A services from
edge-b becomes the desired description. It concatenates the live lists as
follows:
TENANT_A:
description: Tenant A services
import_targets:
- 65000:100
- 65000:200
- 65000:100
- 65000:300
export_targets:
- 65000:100
- 65000:100
- 65000:400
devices:
- edge-a
- edge-b
The task does not sort or deduplicate these aggregate lists. NetBox stores VRF
route targets as object relationships, so the resulting VRF is associated with
import targets 65000:100, 65000:200, and 65000:300, and export targets
65000:100 and 65000:400. Repeated live references do not create duplicate
route-target objects or relationships.
If NetBox initially associates TENANT_A with import target 65000:999 and
export targets 65000:100 and 65000:999, the task produces this state:
| NetBox item | Result |
|---|---|
TENANT_A description |
Changed to Tenant A services. |
| Import associations | Extended to 65000:999, 65000:100, 65000:200, and 65000:300. |
| Export associations | Extended to 65000:100, 65000:999, and 65000:400. |
| Missing route-target objects | Created before the VRF is updated; existing objects are reused. |
Existing 65000:999 associations |
Retained on TENANT_A. |
Route-target object 65000:999 |
Retained in NetBox. |
| Device associations | edge-a and edge-b are added; devices already recorded on the VRF are retained. |
The shared make_diff method initially detects the difference between the raw
live aggregate and the current NetBox VRF. The task then replaces the
route-target value in that update plan with the combined list. An order-only
difference does not update the VRF. A missing live membership extends the
existing relationship and does not add an error to the result. A NetBox-only
membership is retained in the merged update.
Because the comparison uses the raw live aggregate before the merge, a NetBox-only route target remains a difference on later runs. The task can report the VRF as updated again, but the NetBox-only association remains attached and is never removed.
Output¤
Results use one global scope. Dry-run returns the standard sync diff:
{
"global": {
"create": ["TENANT_A"],
"update": {
"TENANT_B": {
"export_targets": {
"old_value": ["65000:999"],
"new_value": ["65000:999", "65000:202"]
}
}
},
"delete": [],
"in_sync": ["CONTROL_PLANE"]
}
}
Live runs use completed-action verbs. The prepared plan remains available in
the top-level diff field:
{
"global": {
"created": ["TENANT_A"],
"updated": ["TENANT_B"],
"deleted": [],
"in_sync": ["CONTROL_PLANE"]
}
}
Deletions¤
The task does not delete VRFs, route-target objects, route-target associations,
or device associations. A selected device set does not prove that existing
NetBox data is stale, so delete and deleted remain empty.
| Condition | Action |
|---|---|
| Live VRF is missing from NetBox | Create the VRF. |
| Live route target is missing from NetBox | Create the route-target object and associate it with the VRF. |
| NetBox VRF association is absent from the live aggregate | Keep the existing association on the VRF. |
| Existing route-target object is not reported by live data | Keep the route-target object. |
| NetBox VRF is absent from the selected devices | Keep the NetBox VRF. |
Branches¤
The task obtains its NetBox client with the requested branch. Reads and writes therefore remain inside that branch. The NetBox Branching plugin must be installed and configured for branch use.
Examples¤
Preview VRF changes:
nf# netbox sync vrfs devices fn-ceos-lf-1 dry-run
Select devices with a Nornir filter and use another custom field:
nf# netbox sync vrfs FC leaf device-custom-field vrf_devices
from norfab.core.nfapi import NorFab
with NorFab(inventory="./inventory.yaml") as nf:
client = nf.make_client()
result = client.run_job(
"netbox",
"sync_vrfs",
workers="any",
kwargs={
"devices": ["fn-ceos-lf-1", "fn-ceos-lf-2"],
"dry_run": True,
"device_custom_field": "devices",
},
)
print(result)
Troubleshooting¤
Missing parser data¤
Confirm the device platform is supported by the TTP vrfs getter and that the
Nornir worker can run the getter's command. Missing or malformed results are
reported as errors; valid results from other devices and workers still proceed.
Live VRF aggregation¤
Every selected device contributes its import and export route targets. Check the selected device set when the aggregate contains an unexpected target. For descriptions, the first non-empty value in sorted device-name order is used.
Device custom field¤
Confirm that the field is assigned to ipam.vrf, uses the multi-object type,
and relates to dcim.device. The field name must match device_custom_field
exactly.
NetBox write failures¤
Confirm route-target values are valid NetBox route-target names and that the custom field accepts device object IDs. Correct the validation error and rerun the dry-run before applying changes.
Task command shell reference¤
nf# man tree netbox.sync.vrfs
R - required field, M - supports multiline input, D - dynamic key
root
└── netbox: Netbox service
└── sync: Sync Netbox data
└── vrfs: Sync live VRF configuration with NetBox
├── instance: Netbox instance name to target
├── dry-run: Calculate the VRF diff without writing to NetBox, default 'False'
├── branch: NetBox branching plugin branch name to use
├── FO: Filter hosts using Filter Object
├── FB: Filter hosts by name using Glob Patterns
├── FH: Filter hosts by hostname
├── FC: Filter hosts containment of pattern in name
├── FR: Filter hosts by name using Regular Expressions
├── FG: Filter hosts by group
├── FP: Filter hosts by hostname using IP Prefix
├── FL: Filter hosts by names list
├── FM: Filter hosts by platform
├── FX: Filter hosts excluding them by name
├── FN: Negate the match
├── devices: List of NetBox devices to collect VRFs from
├── timeout: Job timeout
├── with-approval: Preview VRF changes and ask for review before writing to NetBox, default 'False'
├── device-custom-field: VRF custom field that stores associated NetBox devices, default 'devices'
├── workers: Filter worker to target, default 'any'
├── verbose-result: Control output details, default 'False'
└── nowait: Do not wait for job to complete, default 'False'
nf#
Python API reference¤
Synchronize live VRFs and their route targets with NetBox.
VRFs have global scope and are identified by name. Descriptions are synchronized, while live import/export route targets extend the existing NetBox associations. Route distinguishers and route policies returned by the parser are not stored. The selected VRF custom field records devices on which each VRF was observed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job
|
Job
|
NorFab job object. |
required |
instance
|
Union[None, str]
|
NetBox instance name. Uses the default instance when omitted. |
None
|
dry_run
|
bool
|
Return the calculated diff without writing to NetBox. |
False
|
with_approval
|
bool
|
Ask for approval before applying the prepared diff. |
False
|
timeout
|
int
|
Timeout in seconds for Nornir host resolution and parsing. |
600
|
devices
|
Union[None, list]
|
Explicit NetBox and Nornir device names. |
None
|
branch
|
Union[None, str]
|
NetBox Branching plugin branch name. |
None
|
device_custom_field
|
str
|
VRF custom field containing associated devices. |
'devices'
|
**kwargs
|
Any
|
Nornir FFun host filters. |
{}
|
Returns:
| Name | Type | Description |
|---|---|---|
Result |
Result
|
Global VRF synchronization actions. |
Source code in norfab\workers\netbox_worker\vrf_tasks.py
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 | |