artifacthub helm/nautobot/nautobot 3.2.0
v3.2.0 - 2026-07-27

5 hours ago

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:

  1. Update all job invocations to explicitly pass job_kwargs (e.g job_kwargs={})
  2. 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_id cache fields have been removed from CableTermination. The public peer accessors (get_cable_peer(), REST cable_peer / cable_peer_type, GraphQL cable_peer_*) are unchanged; get_cable_peer() now accepts an optional peer_connector argument 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 on Cable (cable.interfaces, cable.front_ports, etc.), the singular cable_termination reverse accessor on each termination, and PathEndpoint.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_button template 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_name argument to the settings_or_config Jinja 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=N query 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. The display value 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 enforces view permission 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_config template filter and the template settings context to a non-sensitive allowlist of Django settings and Constance configuration values.
  • GHSA-6jmc-h6f2-46j4 - Removed the ability to pass an app_name argument to the settings_or_config filter to retrieve values from an app's PLUGINS_CONFIG in 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 *_set fields and reverse one-to-one relations such as module_bay { installed_module }), and other custom/property-backed related-object accessors (e.g. a Device's all_interfaces, cable/connection peers, dynamic group memberships, assigned tags, and legacy location accessors on Prefix/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 run permission 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 postcss to ^8.5.22 to mitigate GHSA-r28c-9q8g-f849.
  • #9294 - Updated dependency gitpython to >=3.1.55,<3.2 to mitigate GHSA-94p4-4cq8-9g67.
  • #9294 - Updated documentation dependency mkdocs-material to ~9.7.7 to mitigate GHSA-xvg9-69gf-fjrf.

Added in v3.2.0

  • GHSA-mfwj-pjgx-22v2 - Added GraphQL type-definition helper methods permission_safe_resolver and permission_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_function in favor of git_repository_sync_view.

Fixed in v3.2.0

  • #9174 - Fixed the OpenAPI schema for the Cable terminations field.
  • #9252 - Fixed the action button showing a caret when add is present but neither import nor export is.
  • #9285 - Fixed GitRepository sync REST API endpoint not applying object-level permission restrictions (restrict()) when retrieving the repository.
  • #9295 - Fixed contains filter on IP Address Ranges matching ranges of the wrong IP family due to raw byte comparison without IP version scoping.
  • #9299 - Fixed an IndentationError in the generated nautobot_config.py caused by an uncommented EMAIL_SSL_KEYFILE line in the configuration template.
  • #9300 - Fixed JobConsoleEntry model incorrectly being flagged as supporting object-metadata and data-compliance features.

Dependencies in v3.2.0

  • #9294 - Updated dependency kubernetes to >=36.0.3,<37.
  • #9294 - Updated dependency packaging to >=23.2.
  • #9294 - Updated dependency regex to >=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.md release 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 the GitRepository.sync() model method, instead of calling the enqueue_* datasource functions directly.
  • #9293 - Updated development NPM dependencies eslint and @eslint/js to ^9.39.5.
  • #9293 - Updated development NPM dependency autoprefixer to ^10.5.4.
  • #9294 - Updated development dependency coverage to ~7.15.2.
  • #9294 - Updated development dependency faker to ^40.35.0.
  • #9294 - Updated documentation dependency mkdocstrings to ~1.0.6.
  • #9294 - Updated development dependency pymarkdownlnt to ~0.9.39.

Contributors

New Contributors

Full Changelog: v3.1.8...v3.2.0

Don't miss a new nautobot release

NewReleases is sending notifications on new releases.