Nautobot v3.2
This document describes all new features and changes in Nautobot 3.2.
+Security Note: Several CVE fixes in this release introduce Breaking Changes. These changes most commonly surface as unexpected TypeError, AttributeError, or KeyError responses for REST API and GraphQL clients.
Upgrade Actions
Administrators
Migrate Job Execution and Scheduled Jobs
This release introduces a behavioral change to job execution APIs. The job_kwargs parameter is now required for the following functions: create_schedule, enqueue_job, execute_job and run_job_for_testing.
Previously, job arguments could be omitted or passed implicitly. This behavior is deprecated and will be removed in a future release. A temporary backward-compatible fallback remains in place but will emit warnings when used.
Action Required:
- Update all job invocations to explicitly pass
job_kwargs(e.gjob_kwargs={}) - Recreate all Scheduled jobs which has
kwargs=None, because now may fail at runtime due to stricter validation.
!!! tip
Treat any warnings "Using deprecated **job_kwargs pattern, please instead switch to passing job_kwargs as a single parameter" as indicators that your code should be updated to the new explicit pattern.
App Developers
Review Device Component Lookups
The behavior of the Device component device foreign key has been changed, such that components (Interface, etc.) belonging to a Module installed in a Device's Module Bay now automatically set each component's device foreign key to point to the Device, instead of leaving it as NULL/None as it was in previous Nautobot versions. As a result, queries such as Device.interfaces.all() will now return records that belong to child Modules as well as those that belong directly to the Device, whereas in previous versions this query would only return records that belong directly to the Device. In many cases the new behavior will be more desirable and more intuitive to App and Job authors, but maintainers of existing App and Job code should review their logic to identify possible impacts.
Migrate Cable Termination Queries
To support breakout cables, the association between a Cable and its terminations has been re-implemented. The cable ForeignKey that previously existed on each CableTermination subclass (Interface, FrontPort, RearPort, CircuitTermination, PowerPort, etc.) has been removed in favor of a new CableToCableTermination join model, exposed on each termination via the cable_termination reverse one-to-one relationship. App and Job code that queries or traverses cables may need to be updated.
The cable attribute is preserved as a read-only property on each termination instance (e.g. interface.cable), so attribute access continues to work. Assignments to Cable.termination_a / Cable.termination_b (and their *_type / *_type_id / *_id counterparts) on unsaved Cable instances also continue to work and are materialized into CableToCableTermination rows on save.
ORM queries filtering CableTermination subclasses by cable are automatically translated to the new cable_termination__cable[...] paths, emitting a DeprecationWarning for each. The following patterns are translated:
| Deprecated | Translated to |
|---|---|
Interface.objects.filter(cable=...)
| cable_termination__cable=...
|
.filter(cable=None)
| cable_termination__isnull=True
|
.filter(cable_id=...), .filter(cable__isnull=...), .filter(cable__<lookup>=...)
| cable_termination__cable... equivalents
|
.select_related("cable"), .select_related("cable__<field>")
| cable_termination__cable... equivalents
|
The following patterns are not translated and will break — rewrite them explicitly:
| Not translated | Use instead |
|---|---|
Q(cable=...)
| Q(cable_termination__cable=...)
|
.order_by("cable")
| .order_by("cable_termination__cable")
|
.values("cable") / .values_list("cable")
| cable_termination__cable
|
Querying Cable by its terminations: in the same way, the legacy termination_a_type / termination_a_id (and b-side) fields are no longer database columns on Cable — the terminations now live on the terminations (CableToCableTermination) relation. Cable.objects translates lookups using the old *_type / *_id names into the equivalent terminations__... join paths (constrained to connector 1 to match the properties), emitting a DeprecationWarning for each, so Cable.objects.filter(...), .get(...), and .get_or_create(...) continue to work for legacy callers. The *_type ContentType may be supplied either as a ContentType instance (termination_a_type=<ct>) or as an integer ContentType PK (termination_a_type_id=<ct.pk>). The lookup half is translated by the queryset and, for get_or_create(), the same kwargs flow through Cable(...) and are materialized into CableToCableTermination rows on save.
| Deprecated | Translated to |
|---|---|
Cable.objects.get(termination_a_type=<ct>, termination_a_id=<pk>)
| terminations__<fk>_id=<pk> with terminations__cable_end="A"
|
.get(termination_a_type_id=<ct.pk>, termination_a_id=<pk>) (integer ContentType PK)
| same as above |
.filter(termination_a_id=<pk>) (no _type / _type_id given)
| <pk> matched against every per-type termination FK
|
Cable.objects.get_or_create(termination_a_type=..., termination_a_id=..., ...)
| lookup translated as above; creation flows through Cable(...)
|
The following are not translated — rewrite them against the terminations relation explicitly:
| Not translated | Use instead |
|---|---|
Q(termination_a_id=...), .order_by("termination_a_id"), .values("termination_a_id")
| terminations__... equivalents
|
transformed lookups, e.g. .filter(termination_a_id__in=[...])
| terminations__<fk>_id__in=[...] with terminations__cable_end="A"
|
.exclude(termination_a_id=..., termination_b_id=...) combining both ends
| separate .exclude() calls, or an explicit terminations__... Q. The shim applies each end independently (exclude(A) AND exclude(B)), which is not equivalent to negating the combined condition, because the A-side and B-side match different CableToCableTermination rows. Single-end exclude() is exact.
|
!!! warning
Queries using termination_[a|b]_[id|type] only match the first connector on each side of a Cable by design. Code that needs to support any additional connectors on a breakout cable must use the new access patterns.
Cable paths: the private _path ForeignKey on PathEndpoint has been replaced with a cable_paths GenericRelation (resolving through CablePath.origin). The public path, trace(), and connected_endpoint accessors are unchanged. Rewrite any _path__... query usages as cable_paths__...; because this is now a multi-row reverse relation (one CablePath per breakout lane), distinct() is typically required on filter() / count() / exclude().
Other notes:
- The private
_cable_peer,_cable_peer_type, and_cable_peer_idcache fields have been removed fromCableTermination. The public peer accessors (get_cable_peer(), RESTcable_peer/cable_peer_type, GraphQLcable_peer_*) are unchanged;get_cable_peer()now accepts an optionalpeer_connectorargument for breakout-lane-specific lookups. - New helpers are available for working with multi-termination cables:
Cable.add_termination(termination, cable_end, connector=1), the typed many-to-many reverse accessors onCable(cable.interfaces,cable.front_ports, etc.), the singularcable_terminationreverse accessor on each termination, andPathEndpoint.get_connected_endpoints()(returning the resolved destinations of all cable paths, one per breakout lane).
Release Overview
Breaking Changes
REST API Permissions Enforcement on Related Objects
As a part of the fix for security advisory GHSA-h8rv-c7c8-cvmx, the REST API now enforces object-level view permissions when traversing from a requested object to its related objects via the ?depth query parameter. At ?depth=1 and beyond, a related object that the requesting user does not have permission to view is no longer serialized in full, but is instead restricted to a brief representation containing only its id, object_type, url, and display values. See REST API Object Permissions for full details.
REST API clients operating with limited permissions may now receive the brief representation where a fully serialized related object was previously returned, client code that assumes the presence of other nested fields will typically surface this as a TypeError, AttributeError, or KeyError.
For example, consider a user with view permission on Devices but not on Locations requesting GET /api/dcim/devices/<uuid>/?depth=1. Previously, the device's location was serialized in full:
{
"name": "device1",
"location": {
"id": "8badf00d-0000-0000-0000-000000000000",
"object_type": "dcim.location",
"url": "https://nautobot.example.com/api/dcim/locations/8badf00d-0000-0000-0000-000000000000/",
"name": "AMER",
"description": "North and South America",
"...": "..."
}
}Now, because the user lacks permission to view the Location, only the brief representation is returned:
{
"name": "device1",
"location": {
"id": "8badf00d-0000-0000-0000-000000000000",
"object_type": "dcim.location",
"url": "https://nautobot.example.com/api/dcim/locations/8badf00d-0000-0000-0000-000000000000/",
"display": "AMER"
}
}Client code such as response["location"]["name"] that previously worked will now raise a KeyError. To restore the previous behavior, grant the requesting user view permission on the related model (in this example, dcim.location), with constraints as appropriate. Alternatively, update the client code to tolerate the brief representation, for example by falling back to the display value or by retrieving the related object separately via its url.
GraphQL Permissions Enforcement on Related Objects
As a part of the fix for security advisory GHSA-mfwj-pjgx-22v2, GraphQL queries now enforce object-level view permissions when traversing the object graph into related models, not just at the root of the query. A query field resolving to a single related object now returns null if the user lacks permission to view that object, and a field resolving to a list of objects now includes only the objects the user is permitted to view. See GraphQL Permissions Enforcement for full details.
GraphQL clients operating with limited permissions may now receive null values or shorter lists where full data was previously returned. Client code that assumes a related object is present will typically surface as a TypeError, AttributeError, or KeyError.
For example, consider a user with view permission on Devices but not on Locations or Interfaces running the following query:
query {
devices {
name
location {
name
}
interfaces {
name
}
}
}Previously, the related objects were returned in full:
{
"data": {
"devices": [
{
"name": "device1",
"location": {
"name": "AMER"
},
"interfaces": [
{"name": "Ethernet1"},
{"name": "Ethernet2"}
]
}
]
}
}Now, the single related object resolves to null and the list contains only viewable objects:
{
"data": {
"devices": [
{
"name": "device1",
"location": null,
"interfaces": []
}
]
}
}Client code such as devices[0]["location"]["name"] that previously worked will now raise a TypeError. To restore the previous behavior, grant the requesting user view permission on the related models (in this example, dcim.location and dcim.interface), with constraints as appropriate. Alternatively, update the client code to handle null related objects and filtered lists.
Jinja2 Template Rendering Restrictions
As a part of the fix for security advisory GHSA-6jmc-h6f2-46j4, Nautobot's Jinja rendering of user-configurable Jinja2 templates (computed fields, custom links, webhooks, etc.) is now more restrictive. Jinja template snippets may no longer read arbitrary values from the Django settings object.
Cable Data Model Changes
To support breakout cables (see below), the way a Cable associates to its terminations has changed. The cable ForeignKey previously present on each CableTermination (Interface, FrontPort, etc.) has been replaced by a new CableToCableTermination join model, allowing a cable to have more than two terminations. Similarly, the Cable.termination_a and Cable.termination_b GenericForeignKey fields have been similarly migrated to use the reverse-foreign-key-relation Cable.terminations to CableToCableTermination records.
Backward-compatibility shims are provided for the most common access patterns (including ongoing support for the REST API and UI query parameters ?termination_a_type=, ?termination_a_id=, etc.), but App and Job authors who interact with cables programmatically may need to make updates to support the updated data model. See Upgrade Actions for App Developers above for details.
Added
Breakout Cables
Nautobot now models breakout cables — multi-lane cable assemblies where a single physical cable splits into multiple individual connections (for example a 400G QSFP-DD port broken out into 4×100G SFP lanes). A new CableType model defines the physical structure of a cable (connectors per side, internal lanes, and the connector-to-lane mapping), and a Cable assigned a breakout cable type may have more than two terminations, each recorded as a CableToCableTermination. Breakout cables are fully supported across the UI (cable type and cable forms, connection tables, SVG lane-mapping and trace diagrams), the REST API, and lane-aware cable path tracing. Interface records gain an optional breakout_position field to map a subinterface to a position on its parent interface's breakout trunk connector.
Partially-Connected, Disconnected, and Repurposed Cables
A Cable is no longer required to have both of its endpoints defined, and its terminations are no longer fixed at creation time. A cable may now be partially-connected (a termination on only one side, or on a subset of a breakout cable's connectors) or fully-disconnected (no terminations at all), and a cable's terminations may be added, changed, or removed after the cable is created — through the UI, the REST API, or programmatically — without deleting and recreating the cable.
IP Address Ranges
A new IPAddressRange model represents a contiguous span of IP addresses within a parent Prefix without needing to create an individual IPAddress record for each address in the span. Views include standard (list, detail, edit, delete), "IP Address Ranges" tab on the Prefix detail view, and inline rendering of ranges within the Prefix "IP Addresses" tab.
IPAddress.clean() now rejects addresses that fall within an "exclusive" range, Prefix.clean() rejects network edits that would orphan a contained range, and Prefix.save() reparents contained ranges to the closest fully containing Prefix. Prefix utilization calculations now account for IP address ranges when "mark as utilized" is set.
!!! tip
Setting a range to "exclusive" may change your workflow — for example, you will no longer be able to create an IP address that falls within an exclusive IP address range. This is by design, but may be surprising if the implications of enabling exclusivity were not considered.
Job Cancel
Jobs that are actively running, pending a run, or abandoned, can now be cancelled from both the UI and REST API, with support for both Celery and Kubernetes jobs. You will now be able to see a Job Cancel button on the job result list and detail views (for jobs not in a terminal state.)
The "Cancel Job" action will perform slightly different actions depending on the current state of the job, all of which end up with the Job Result in a terminal state. For technical details refer to the documentation for Job cancel.
Job Cancel requires the extras.run_job permission; additionally, non-staff users with this permission may cancel only jobs they submitted, while staff users with this permission may cancel any user's jobs.
Homepage Stickiness
The homepage now saves the location and collapsing of each panel. You can re-arrange them to your liking, and see it the same on any browser. Additionally, when resizing your browser window, the panels will be automatically and sanely rearranged to four, three, two, or one column as appropriate.
Search Enhancements
The header search bar gains two distinct capabilities. First, model-name typeahead: as you type an in: phrase, matching model names are suggested (e.g. typing in:dev suggests in:devices). Second, once you begin searching, live results appear as you type, showing up to the first 10 matches.
Object Metadata UI
ObjectMetadata records can now be created, edited, and deleted directly through the web UI (previously read-only). Metadata is added from the parent object's Metadata tab, which opens a pre-filled create form. The value input adapts to the selected MetadataType data type, and detail/list views render values appropriately for each type — clickable links for URLs, parsed HTML for Markdown, pretty-printed JSON for JSON, etc. The primary intent is still that metadata is managed by integrations (SSoTs, REST API), but users with the appropriate permissions can now manage individual records through the UI.
Other Additions
- Computed Fields can now optionally be rendered as Markdown.
- A reusable
copy_buttontemplate tag renders hover copy-to-clipboard buttons. - Added opt-in support for OpenTelemetry traces, metrics, and logs, disabled by default.
Changed
Cable Termination REST API
The Cable REST API serializer adds a single terminations field keyed by side (a/b) and then by 1-indexed connector number, mirroring the physical structure of the cable. This field is writable on POST and PATCH, and uncabled connectors on breakout cables are surfaced as explicit null slots. The legacy termination_a / termination_b (and *_type / *_id) fields remain for backward compatibility and refer to connector 1 on each side. The nested terminations field is omitted from CSV exports; use the CableToCableTermination endpoint for per-connector CSV detail.
Device Component Default Ordering
The default sort ordering of device-component models (Interface, Front Port, Rear Port, Module Bay, etc.) has been changed to group and sort records by their associated device_id (UUID) rather than by the associated device's name, as the prior behavior (requiring a join across database tables) performed poorly at high data scale. This change affects the default behavior of the following:
/dcim/interfaces/UI list view (and/dcim/front-ports/,/dcim/rear-ports/, etc.)/api/dcim/interfaces/REST API list view (and/api/dcim/front-ports/,/api/dcim/rear-ports/, etc.)- GraphQL query responses that invole listing any of these models
For the UI and REST API list views, if ordering by device name is desired, the prior behavior may be achieved by explicitly specifying a ?sort=device query parameter when requesting these views, but users are encouraged to be aware of the performance implications of doing so.
Device Component device Foreign Key
When device components (Interface, Front Port, Rear Port, Module Bay, etc.) belong to a Module, and that Module is installed in a Module Bay belonging to a Device, the device foreign key on each such device component is now automatically set to point to the Device in question. This is a behavior change from previous Nautobot versions, in which the device foreign key would remain as NULL/None for device components belonging to a Module even when that Module was installed into a Device's Module Bay. This change was made to improve the performance and self-consistency of Device component lookups, such as the Device.all_interfaces() method and Device.interfaces.all() queryset manager.
Device Module Bays Tree View
The Device detail view "Module Bays" tab now supports a hierarchical view with asynchronous loading of each nested module level as you expand it. Optionally, you can expand or collapse all levels to show or hide the entire nested structure.
Dependencies
As usual for Nautobot minor-version releases, 3.2.0 includes updates to many of Nautobot's dependencies, including in some cases major-version updates to these dependencies. These updates should generally be transparent to end users, but Apps relying on these dependencies may in turn require minor updates for compatibility.
v3.2.0 (2026-07-27)
Breaking Changes in v3.2.0
- GHSA-6jmc-h6f2-46j4 - It is no longer possible to pass an
app_nameargument to thesettings_or_configJinja template filter; this functionality is still supported in Django templates.
Security in v3.2.0
- GHSA-h8rv-c7c8-cvmx - Fixed a REST API information-disclosure issue where the
?depth=Nquery parameter exposed the full detail of related objects that the requesting user did not have permission to view. Related objects that the user cannot view are now rendered in a brief{id, object_type, url, display}form instead of full detail. Thedisplayvalue is included for parity with the UI, where a related object's display value is visible without requiring full view permission on that object. Additionally, the Notes REST API now enforcesviewpermission on the object a note is being assigned to on write (POST/PATCH), consistent with other object references. - GHSA-6jmc-h6f2-46j4 - Restricted the
settings_or_configtemplate filter and the templatesettingscontext to a non-sensitive allowlist of Django settings and Constance configuration values. - GHSA-6jmc-h6f2-46j4 - Removed the ability to pass an
app_nameargument to thesettings_or_configfilter to retrieve values from an app'sPLUGINS_CONFIGin Jinja templates (this is still supported in Django templates). - GHSA-mfwj-pjgx-22v2 - Fixed GraphQL not enforcing object-level permissions when traversing to related objects: filtering related objects (e.g.
locations { racks(...) }), traversing Relationship associations (e.g.rel_*fields), forward foreign-key traversal (e.g.racks { location }), reverse relations (reverse foreign keys such as*_setfields and reverse one-to-one relations such asmodule_bay { installed_module }), and other custom/property-backed related-object accessors (e.g. a Device'sall_interfaces, cable/connection peers, dynamic group memberships, assigned tags, and legacylocationaccessors onPrefix/VLAN) now respect the requesting user's view permissions on the related model (GHSA-mfwj-pjgx-22v2). - GHSA-mfwj-pjgx-22v2 - Changed GraphQL schema to represent all forward foreign-key fields as nullable (even if not actually nullable as database fields), since a related object the user is not permitted to view is now returned as
null. - GHSA-p99c-c9qx-34fw - Fixed a Jinja2 template sandbox escape that allowed users to reach the database cursor via the Django ORM and execute arbitrary SQL.
- GHSA-p99c-c9qx-34fw - Fixed a Jinja2 template sandbox escape that allowed users to reach arbitrary models via the Django application registry.
- GHSA-qr7c-g3j2-hw5q - Added enforcement of the
runpermission on the Job Hook dispatch path, fixing a privilege escalation in which a user without the run permission could execute a Job by triggering a Job Hook. - #9293 - Updated development NPM dependency
postcssto^8.5.22to mitigate GHSA-r28c-9q8g-f849. - #9294 - Updated dependency
gitpythonto>=3.1.55,<3.2to mitigate GHSA-94p4-4cq8-9g67. - #9294 - Updated documentation dependency
mkdocs-materialto~9.7.7to mitigate GHSA-xvg9-69gf-fjrf.
Added in v3.2.0
- GHSA-mfwj-pjgx-22v2 - Added GraphQL type-definition helper methods
permission_safe_resolverandpermission_safe_attribute_resolver. Apps defining custom GraphQL type classes may need to use these methods in some cases to prevent object-permission enforcement gaps similar to GHSA-mfwj-pjgx-22v2. - #9281 - Added a loading spinner to HTMX-driven buttons while their request is in flight.
Deprecated in v3.2.0
- #9285 - Deprecated
check_and_call_git_repository_functionin favor ofgit_repository_sync_view.
Fixed in v3.2.0
- #9174 - Fixed the OpenAPI schema for the Cable
terminationsfield. - #9252 - Fixed the action button showing a caret when
addis present but neitherimportnorexportis. - #9285 - Fixed GitRepository sync REST API endpoint not applying object-level permission restrictions (
restrict()) when retrieving the repository. - #9295 - Fixed
containsfilter on IP Address Ranges matching ranges of the wrong IP family due to raw byte comparison without IP version scoping. - #9299 - Fixed an
IndentationErrorin the generatednautobot_config.pycaused by an uncommentedEMAIL_SSL_KEYFILEline in the configuration template. - #9300 - Fixed
JobConsoleEntrymodel incorrectly being flagged as supporting object-metadata and data-compliance features.
Dependencies in v3.2.0
- #9294 - Updated dependency
kubernetesto>=36.0.3,<37. - #9294 - Updated dependency
packagingto>=23.2. - #9294 - Updated dependency
regexto>=2026.7.19.
Documentation in v3.2.0
- GHSA-h8rv-c7c8-cvmx - Added documentation describing the behavior of the REST API with respect to object permissions.
- GHSA-qr7c-g3j2-hw5q - Added documentation clarifying that a Job Hook is only dispatched when the user responsible for the triggering change has permission to run the target Job.
- #9285 - Clarified that managing a Git repository (create, change, sync) grants arbitrary code execution on the worker and that syncing repository-provided Jobs does not require the Job run permission.
- #9291 - Fixed a minor documentation issue with the 3.2 release notes.
- #9303 - Update
notices.mdrelease notes.
Housekeeping in v3.2.0
- #9285 - Refactored GitRepository sync/dry-run logic so that both the UI and REST API views share a common validation helper (
get_git_repository_for_sync()) and enqueue jobs exclusively through theGitRepository.sync()model method, instead of calling theenqueue_*datasource functions directly. - #9293 - Updated development NPM dependencies
eslintand@eslint/jsto^9.39.5. - #9293 - Updated development NPM dependency
autoprefixerto^10.5.4. - #9294 - Updated development dependency
coverageto~7.15.2. - #9294 - Updated development dependency
fakerto^40.35.0. - #9294 - Updated documentation dependency
mkdocstringsto~1.0.6. - #9294 - Updated development dependency
pymarkdownlntto~0.9.39.
Contributors
- @bernalvinicius
- @bennybates
- @bryanculver
- @collin-wicker
- @Defiantearth
- @Dhanus3133
- @dsoftait
- @gertzakis
- @glennmatthews
- @gsnider2195
- @itdependsnetworks
- @jakobsen-lrz
- @jerej
- @joewesch
- @loganknecht
- @nathanhitchcock
- @netcanon
- @nopg
- @Nor-arc
- @norbert-mieczkowski-codilime
- @pastoreerrante
- @Poleron402
- @renovate
- @sirisha-bode
- @smk4664
- @Sridevi-NTC
- @sudheervarma-network
New Contributors
- @netcanon made their first contribution in #9144
- @collin-wicker made their first contribution in #9211
- @Poleron402 made their first contribution in #9206
- @jerej made their first contribution in #9155
- @jakobsen-lrz made their first contribution in #8971
- @bernalvinicius made their first contribution in #9121
- @bennybates made their first contribution in #9299
Full Changelog: v3.1.8...v3.2.0