Skip to content

Inline Many-to-Many Associations

Added in version 6.2.0

Many Nautobot models have many-to-many (M2M) relationships with other models. For example, a device can be associated with multiple VRFs, a location can have multiple prefixes, and a secrets group can contain multiple secrets.

Previously, managing these associations required using separate standalone modules (e.g., vrf_device_assignment, prefix_location, secrets_groups_association). Now you can manage them inline on the parent module using the M2M field options.

Supported Parent Modules and Fields

Parent Module M2M Field Child Object Key Description
device vrfs vrf VRF assignments
device clusters cluster Cluster assignments
virtual_machine vrfs vrf VRF assignments
virtual_device_context vrfs vrf VRF assignments
device_interface ip_addresses ip_address IP address to interface
vm_interface ip_addresses ip_address IP address to VM interface
location prefixes prefix Prefix to location
location vlans vlan VLAN to location
cloud_network prefixes prefix Cloud network prefix assignments
cloud_service cloud_networks cloud_network Cloud service network assignments
secrets_group secrets secret Secrets group associations
dynamic_group static_group_associations associated_object_type / associated_object_id Static group memberships
custom_field custom_field_choices value Custom field choices
metadata_type metadata_choices value Metadata type choices
provider provider_networks name Provider networks

M2M Field Structure

All M2M fields follow the same structure:

<m2m_field>:
  state: merge  # merge (default), replace, or delete
  objects:
    - <child_key>: "value"
    - <child_key>: "another value"

States

  • merge (default): Adds the specified associations without removing existing ones. Safe for incremental changes.
  • replace: Enforces exactly the listed associations. Any existing associations not in the list are removed.
  • delete: Removes only the specified associations. Other existing associations are left intact.

Basic Examples

Adding VRFs to a Device

- name: Create a device with VRF associations
  networktocode.nautobot.device:
    url: "{{ nautobot_url }}"
    token: "{{ nautobot_token }}"
    name: "my-router"
    device_type: "Cisco CSR1000v"
    role: "Router"
    location: "Main Site"
    status: "Active"
    vrfs:
      objects:
        - vrf: "Management VRF"
        - vrf: "Production VRF"
    state: present

Since no state is specified on the vrfs field, it defaults to merge -- the VRFs are added without affecting any other existing VRF associations.

Adding IP Addresses to an Interface

The child key accepts either a simple string or a dictionary for more specific lookups:

# Simple string -- looks up the IP address by address
- name: Associate IP addresses with an interface
  networktocode.nautobot.device_interface:
    url: "{{ nautobot_url }}"
    token: "{{ nautobot_token }}"
    device: "my-router"
    name: "GigabitEthernet0/0"
    ip_addresses:
      objects:
        - ip_address: "10.0.0.1/24"
    state: present

# Dictionary -- useful when disambiguation is needed (e.g., multiple namespaces)
- name: Associate IP address with namespace specified
  networktocode.nautobot.device_interface:
    url: "{{ nautobot_url }}"
    token: "{{ nautobot_token }}"
    device: "my-router"
    name: "GigabitEthernet0/0"
    ip_addresses:
      objects:
        - ip_address:
            address: "10.0.0.1/24"
            namespace: "Production"
    state: present

Adding Secrets to a Secrets Group

Some M2M associations have extra fields beyond just the child identifier. For secrets group associations, access_type and secret_type are part of the association itself:

- name: Create secrets group with secret associations
  networktocode.nautobot.secrets_group:
    url: "{{ nautobot_url }}"
    token: "{{ nautobot_token }}"
    name: "Device Credentials"
    secrets:
      objects:
        - secret: "admin-username"
          access_type: "SSH"
          secret_type: "username"
        - secret: "admin-password"
          access_type: "SSH"
          secret_type: "password"
    state: present

Managing Association State

Merge (Default)

Merge adds new associations without removing existing ones. Running the same task twice is idempotent.

# First run: adds VRF-A
- name: Add first VRF
  networktocode.nautobot.device:
    url: "{{ nautobot_url }}"
    token: "{{ nautobot_token }}"
    name: "my-router"
    vrfs:
      objects:
        - vrf: "VRF-A"
    state: present

# Second run: adds VRF-B, VRF-A is untouched
- name: Add second VRF
  networktocode.nautobot.device:
    url: "{{ nautobot_url }}"
    token: "{{ nautobot_token }}"
    name: "my-router"
    vrfs:
      objects:
        - vrf: "VRF-B"
    state: present
# Result: device has both VRF-A and VRF-B

Replace

Replace enforces exactly the listed set of associations. Any existing associations not in the list are removed.

# Device currently has VRF-A and VRF-B
- name: Replace all VRFs with only VRF-C
  networktocode.nautobot.device:
    url: "{{ nautobot_url }}"
    token: "{{ nautobot_token }}"
    name: "my-router"
    vrfs:
      state: replace
      objects:
        - vrf: "VRF-C"
    state: present
# Result: device has only VRF-C (VRF-A and VRF-B removed)

Delete

Delete removes only the specified associations. Other associations are left intact.

# Device currently has VRF-A and VRF-B
- name: Remove only VRF-B
  networktocode.nautobot.device:
    url: "{{ nautobot_url }}"
    token: "{{ nautobot_token }}"
    name: "my-router"
    vrfs:
      state: delete
      objects:
        - vrf: "VRF-B"
    state: present
# Result: device has only VRF-A

Diff Output

When M2M fields change, the diff output includes the before and after state as sorted lists of child object UUIDs:

{
    "diff": {
        "before": {
            "vrfs": ["<uuid-of-vrf-a>"]
        },
        "after": {
            "vrfs": ["<uuid-of-vrf-a>", "<uuid-of-vrf-b>"]
        }
    }
}

Contact and Team Associations

Added in version 6.3.0

Contacts and teams are associated with parent objects through Nautobot's contact-associations endpoint, which uses a generic foreign key (associated_object_type + associated_object_id) rather than a single FK column. The Ansible collection exposes them inline on the parent module via the contacts and teams fields, which follow the same state: merge|replace|delete semantics as the M2M fields described above.

Supported Parent Modules

contacts and teams are available on most parent modules that represent objects with a Nautobot content type. The exclusions are the contact and team modules themselves, plus any modules that don't have a Nautobot content type to associate against. If a module pulls in the contacts_and_teams doc fragment, the fields are available.

Field Structure

Each entry in contacts.objects requires the child contact, a role, and a status. teams.objects is the same but with team in place of contact. The role and status content types must include extras.contactassociation. The contact or team can be specified as a string (will be looked up by name, ignoring case) or a dictionary with the key name and the value of the exact name. Other available keys are email and phone.

contacts:
  state: merge  # merge (default), replace, or delete
  objects:
    - contact: "Jane Doe"
      role: "Admin"
      status: "Active"
    - contact:
        name: "John Smith"
      role: "Admin"
      status: "Active"
teams:
  state: merge
  objects:
    - team: "Network Operations"
      role: "On-Call"
      status: "Active"

Example: Associating a Contact and Team with a Device

- name: Associate a contact and a team with a device
  networktocode.nautobot.device:
    url: "{{ nautobot_url }}"
    token: "{{ nautobot_token }}"
    name: "my-router"
    contacts:
      objects:
        - contact: "Jane Doe"
          role: "Admin"
          status: "Active"
    teams:
      objects:
        - team: "Network Operations"
          role: "On-Call"
          status: "Active"
    state: present

State Semantics

The three states behave identically to the M2M fields, but each state is scoped to the field it appears on. A state: replace on contacts will not touch any team associations on the same parent, and vice versa.

# Device currently has contacts: Jane Doe (Admin), John Smith (User)
- name: Replace contacts with only John Smith as Admin
  networktocode.nautobot.device:
    url: "{{ nautobot_url }}"
    token: "{{ nautobot_token }}"
    name: "my-router"
    contacts:
      state: replace
      objects:
        - contact: "John Smith"
          role: "Admin"
          status: "Active"
    state: present
# Result: only John Smith (Admin) remains as a contact; existing team associations are unchanged.

Each association is keyed by (contact_or_team, role, status), so the same contact can appear multiple times with different roles or statuses and will be reconciled as distinct associations.

Diff Output

Contact and team changes appear under contacts and teams in the diff body, formatted the same way as M2M diffs (sorted lists of the contact / team UUIDs):

{
    "diff": {
        "before": {
            "contacts": ["<uuid-of-jane-doe>"]
        },
        "after": {
            "contacts": ["<uuid-of-jane-doe>", "<uuid-of-john-smith>"],
            "teams": ["<uuid-of-network-ops>"]
        }
    }
}

Notes

Notes are attached to parent objects through Nautobot's notes endpoint, which uses a generic foreign key (assigned_object_type + assigned_object_id) rather than a single FK column. The Ansible collection exposes them inline on the parent module via the notes field, which follows the same state: merge|replace|delete semantics as the M2M fields described above.

Field Structure

Each entry in notes.objects requires only the note text. A note is uniquely identified by its text for reconciliation purposes, so re-running with the same text is idempotent.

notes:
  objects:
    - note: This device was provisioned by Ansible.
    - note: Pending hardware refresh in Q3.

Example: Adding a Note to a Device

- name: Add a note to a device
  networktocode.nautobot.device:
    url: http://nautobot.local
    token: thisIsMyToken
    name: Test Device
    notes:
      objects:
        - note: This device was provisioned by Ansible.
    state: present

State Semantics

The three states behave identically to the M2M fields. merge (default) adds any notes whose text is not already present, replace enforces exactly the listed notes (removing any others on the object), and delete removes the listed notes by text.

# Device currently has the note "Old note"
- name: Replace all notes with a single new note
  networktocode.nautobot.device:
    url: http://nautobot.local
    token: thisIsMyToken
    name: Test Device
    notes:
      state: replace
      objects:
        - note: New note
    state: present
# Result: only "New note" remains on the device.

Diff Output

Note changes appear under notes in the diff body as sorted lists of the note text (rather than the opaque note UUID, so the diff is human-readable):

{
    "diff": {
        "before": {
            "notes": ["Old note"]
        },
        "after": {
            "notes": ["New note"]
        }
    }
}

Note

Keep in mind, the order of the notes in the diff may not be the order that the notes will appear in Nautobot. We must sort the notes by text here in order to properly handle idempotency.