Skip to content

Nautobot NetBox Importer API Package

nautobot_netbox_importer.diffsync

DiffSync adapter and model implementation for nautobot-netbox-importer.

adapters

DiffSync adapters for nautobot-netbox-importer.

NautobotDiffSync

Bases: N2NDiffSync

DiffSync adapter integrating with the Nautobot database.

Source code in nautobot_netbox_importer/diffsync/adapters/nautobot.py
class NautobotDiffSync(N2NDiffSync):
    """DiffSync adapter integrating with the Nautobot database."""

    logger = structlog.get_logger()

    def __init__(self, *args, bypass_data_validation=False, **kwargs):
        """Initialization of a NautobotDiffSync adapater instance."""
        super().__init__(*args, **kwargs)
        self.bypass_data_validation = bypass_data_validation

    def load_model(self, diffsync_model, record):  # pylint: disable=too-many-branches
        """Instantiate the given DiffSync model class from the given Django record."""
        data = {}

        # Iterate over all model fields on the Django record
        for field in record._meta.get_fields(include_hidden=True):
            if any(isinstance(field, ignored_class) for ignored_class in IGNORED_FIELD_CLASSES):
                continue

            # Get the value of this field from Django
            try:
                value = field.value_from_object(record)
            except AttributeError as exc:
                self.logger.error(f"Unable to get value_from_object for {record} {field}: {exc}")
                continue

            if field.name not in diffsync_model.fk_associations():
                # Field is a simple data type (str, int, bool) and can be used as-is with no modifications
                data[field.name] = value
                continue

            # If we got here, the field is some sort of foreign-key reference(s).
            if not value:
                # It's a null or empty list reference though, so we don't need to do anything special with it.
                data[field.name] = value
                continue

            # What's the name of the model that this is a reference to?
            target_name = diffsync_model.fk_associations()[field.name]

            if target_name == "status":
                data[field.name] = {"slug": self.status.nautobot_model().objects.get(pk=value).slug}
                continue

            # Special case: for generic foreign keys, the target_name is actually the name of
            # another field on this record that describes the content-type of this foreign key id.
            # We flag this by starting the target_name string with a '*', as if this were C or something.
            if target_name.startswith("*"):
                target_content_type_field = target_name[1:]
                target_content_type = getattr(record, target_content_type_field)
                target_name = target_content_type.model

            try:
                # Get the DiffSync model class that we know by the given target_name
                target_class = getattr(self, target_name)
            except AttributeError:
                self.logger.error("Unknown/unrecognized class name!", name=target_name)
                data[field.name] = None
                continue

            if isinstance(value, list):
                # This field is a one-to-many or many-to-many field, a list of object references.
                if issubclass(target_class, NautobotBaseModel):
                    # Replace each object reference with its appropriate primary key value
                    data[field.name] = [foreign_record.pk for foreign_record in value]
                else:
                    # Since the PKs of these built-in Django models may differ between NetBox and Nautobot,
                    # e.g., ContentTypes, replace each reference with the natural key (not PK) of the referenced model.
                    data[field.name] = [
                        self.get_by_pk(target_name, foreign_record.pk).get_identifiers() for foreign_record in value
                    ]
            elif isinstance(value, UUID):
                # Standard Nautobot UUID foreign-key reference, no transformation needed.
                data[field.name] = value
            elif isinstance(value, int):
                # Reference to a built-in model by its integer primary key.
                # Since this may not be the same value between NetBox and Nautobot (e.g., ContentType references)
                # replace the PK with the natural keys of the referenced model.
                data[field.name] = self.get_by_pk(target_name, value).get_identifiers()
            else:
                self.logger.error(f"Invalid PK value {value}")
                data[field.name] = None

        data["pk"] = record.pk
        return self.make_model(diffsync_model, data)

    def load(self):
        """Load all available and relevant data from Nautobot in the appropriate sequence."""
        self.logger.info("Loading data from Nautobot into DiffSync...")
        for modelname in ("contenttype", "permission", "status", *self.top_level):
            diffsync_model = getattr(self, modelname)
            if diffsync_model.nautobot_model().objects.exists():
                for instance in ProgressBar(
                    diffsync_model.nautobot_model().objects.all(),
                    total=diffsync_model.nautobot_model().objects.count(),
                    desc=f"{modelname:<25}",  # len("consoleserverporttemplate")
                    verbosity=self.verbosity,
                ):
                    self.load_model(diffsync_model, instance)

        self.logger.info("Data loading from Nautobot complete.")

    def restore_required_custom_fields(self, source: DiffSync):
        """Post-synchronization cleanup function to restore any 'required=True' custom field records."""
        self.logger.debug("Restoring the 'required=True' flag on any such custom fields")
        for source_customfield in source.get_all(source.customfield):
            if source_customfield.actual_required:
                # Update both the local DiffSync record (so that on the second-pass resync we again reset required=False)
                # and the Nautobot record (so that the end state is correct)
                self.get(self.customfield, source_customfield.get_unique_id()).update({"required": True})

    def sync_complete(self, source: DiffSync, *args, **kwargs):
        """Callback invoked after completing a sync operation in which changes occurred."""
        # During the sync, we intentionally marked all custom fields as "required=False"
        # so that we could sync records that predated the creation of said custom fields.
        # Now that we've updated all records that might contain custom field data,
        # only now can we re-mark any "required" custom fields as such.
        self.restore_required_custom_fields(source)

        return super().sync_complete(source, *args, **kwargs)
__init__(args, bypass_data_validation=False, kwargs)

Initialization of a NautobotDiffSync adapater instance.

Source code in nautobot_netbox_importer/diffsync/adapters/nautobot.py
def __init__(self, *args, bypass_data_validation=False, **kwargs):
    """Initialization of a NautobotDiffSync adapater instance."""
    super().__init__(*args, **kwargs)
    self.bypass_data_validation = bypass_data_validation
load()

Load all available and relevant data from Nautobot in the appropriate sequence.

Source code in nautobot_netbox_importer/diffsync/adapters/nautobot.py
def load(self):
    """Load all available and relevant data from Nautobot in the appropriate sequence."""
    self.logger.info("Loading data from Nautobot into DiffSync...")
    for modelname in ("contenttype", "permission", "status", *self.top_level):
        diffsync_model = getattr(self, modelname)
        if diffsync_model.nautobot_model().objects.exists():
            for instance in ProgressBar(
                diffsync_model.nautobot_model().objects.all(),
                total=diffsync_model.nautobot_model().objects.count(),
                desc=f"{modelname:<25}",  # len("consoleserverporttemplate")
                verbosity=self.verbosity,
            ):
                self.load_model(diffsync_model, instance)

    self.logger.info("Data loading from Nautobot complete.")
load_model(diffsync_model, record)

Instantiate the given DiffSync model class from the given Django record.

Source code in nautobot_netbox_importer/diffsync/adapters/nautobot.py
def load_model(self, diffsync_model, record):  # pylint: disable=too-many-branches
    """Instantiate the given DiffSync model class from the given Django record."""
    data = {}

    # Iterate over all model fields on the Django record
    for field in record._meta.get_fields(include_hidden=True):
        if any(isinstance(field, ignored_class) for ignored_class in IGNORED_FIELD_CLASSES):
            continue

        # Get the value of this field from Django
        try:
            value = field.value_from_object(record)
        except AttributeError as exc:
            self.logger.error(f"Unable to get value_from_object for {record} {field}: {exc}")
            continue

        if field.name not in diffsync_model.fk_associations():
            # Field is a simple data type (str, int, bool) and can be used as-is with no modifications
            data[field.name] = value
            continue

        # If we got here, the field is some sort of foreign-key reference(s).
        if not value:
            # It's a null or empty list reference though, so we don't need to do anything special with it.
            data[field.name] = value
            continue

        # What's the name of the model that this is a reference to?
        target_name = diffsync_model.fk_associations()[field.name]

        if target_name == "status":
            data[field.name] = {"slug": self.status.nautobot_model().objects.get(pk=value).slug}
            continue

        # Special case: for generic foreign keys, the target_name is actually the name of
        # another field on this record that describes the content-type of this foreign key id.
        # We flag this by starting the target_name string with a '*', as if this were C or something.
        if target_name.startswith("*"):
            target_content_type_field = target_name[1:]
            target_content_type = getattr(record, target_content_type_field)
            target_name = target_content_type.model

        try:
            # Get the DiffSync model class that we know by the given target_name
            target_class = getattr(self, target_name)
        except AttributeError:
            self.logger.error("Unknown/unrecognized class name!", name=target_name)
            data[field.name] = None
            continue

        if isinstance(value, list):
            # This field is a one-to-many or many-to-many field, a list of object references.
            if issubclass(target_class, NautobotBaseModel):
                # Replace each object reference with its appropriate primary key value
                data[field.name] = [foreign_record.pk for foreign_record in value]
            else:
                # Since the PKs of these built-in Django models may differ between NetBox and Nautobot,
                # e.g., ContentTypes, replace each reference with the natural key (not PK) of the referenced model.
                data[field.name] = [
                    self.get_by_pk(target_name, foreign_record.pk).get_identifiers() for foreign_record in value
                ]
        elif isinstance(value, UUID):
            # Standard Nautobot UUID foreign-key reference, no transformation needed.
            data[field.name] = value
        elif isinstance(value, int):
            # Reference to a built-in model by its integer primary key.
            # Since this may not be the same value between NetBox and Nautobot (e.g., ContentType references)
            # replace the PK with the natural keys of the referenced model.
            data[field.name] = self.get_by_pk(target_name, value).get_identifiers()
        else:
            self.logger.error(f"Invalid PK value {value}")
            data[field.name] = None

    data["pk"] = record.pk
    return self.make_model(diffsync_model, data)
restore_required_custom_fields(source)

Post-synchronization cleanup function to restore any 'required=True' custom field records.

Source code in nautobot_netbox_importer/diffsync/adapters/nautobot.py
def restore_required_custom_fields(self, source: DiffSync):
    """Post-synchronization cleanup function to restore any 'required=True' custom field records."""
    self.logger.debug("Restoring the 'required=True' flag on any such custom fields")
    for source_customfield in source.get_all(source.customfield):
        if source_customfield.actual_required:
            # Update both the local DiffSync record (so that on the second-pass resync we again reset required=False)
            # and the Nautobot record (so that the end state is correct)
            self.get(self.customfield, source_customfield.get_unique_id()).update({"required": True})
sync_complete(source, args, kwargs)

Callback invoked after completing a sync operation in which changes occurred.

Source code in nautobot_netbox_importer/diffsync/adapters/nautobot.py
def sync_complete(self, source: DiffSync, *args, **kwargs):
    """Callback invoked after completing a sync operation in which changes occurred."""
    # During the sync, we intentionally marked all custom fields as "required=False"
    # so that we could sync records that predated the creation of said custom fields.
    # Now that we've updated all records that might contain custom field data,
    # only now can we re-mark any "required" custom fields as such.
    self.restore_required_custom_fields(source)

    return super().sync_complete(source, *args, **kwargs)

abstract

Abstract base DiffSync adapter class for code shared by NetBox and Nautobot adapters.

N2NDiffSync

Bases: DiffSync

Generic DiffSync adapter base class for working with NetBox/Nautobot data models.

Source code in nautobot_netbox_importer/diffsync/adapters/abstract.py
class N2NDiffSync(DiffSync):
    """Generic DiffSync adapter base class for working with NetBox/Nautobot data models."""

    _data_by_pk: MutableMapping[str, MutableMapping[Union[UUID, int], DiffSyncModel]]

    logger = structlog.get_logger()

    #
    # Add references to all baseline shared models between NetBox and Nautobot
    #

    contenttype = n2nmodels.ContentType

    # Users and auth
    group = n2nmodels.Group
    objectpermission = n2nmodels.ObjectPermission
    permission = n2nmodels.Permission
    token = n2nmodels.Token
    user = n2nmodels.User

    # Circuits
    circuit = n2nmodels.Circuit
    circuittermination = n2nmodels.CircuitTermination
    circuittype = n2nmodels.CircuitType
    provider = n2nmodels.Provider
    providernetwork = n2nmodels.ProviderNetwork

    # DCIM
    cable = n2nmodels.Cable
    consoleport = n2nmodels.ConsolePort
    consoleporttemplate = n2nmodels.ConsolePortTemplate
    consoleserverport = n2nmodels.ConsoleServerPort
    consoleserverporttemplate = n2nmodels.ConsoleServerPortTemplate
    device = n2nmodels.Device
    devicebay = n2nmodels.DeviceBay
    devicebaytemplate = n2nmodels.DeviceBayTemplate
    devicerole = n2nmodels.DeviceRole
    devicetype = n2nmodels.DeviceType
    frontport = n2nmodels.FrontPort
    frontporttemplate = n2nmodels.FrontPortTemplate
    interface = n2nmodels.Interface
    interfacetemplate = n2nmodels.InterfaceTemplate
    inventoryitem = n2nmodels.InventoryItem
    manufacturer = n2nmodels.Manufacturer
    platform = n2nmodels.Platform
    powerfeed = n2nmodels.PowerFeed
    poweroutlet = n2nmodels.PowerOutlet
    poweroutlettemplate = n2nmodels.PowerOutletTemplate
    powerpanel = n2nmodels.PowerPanel
    powerport = n2nmodels.PowerPort
    powerporttemplate = n2nmodels.PowerPortTemplate
    rack = n2nmodels.Rack
    rackgroup = n2nmodels.RackGroup
    rackreservation = n2nmodels.RackReservation
    rackrole = n2nmodels.RackRole
    rearport = n2nmodels.RearPort
    rearporttemplate = n2nmodels.RearPortTemplate
    region = n2nmodels.Region
    site = n2nmodels.Site
    virtualchassis = n2nmodels.VirtualChassis

    # Extras
    configcontext = n2nmodels.ConfigContext
    customfield = n2nmodels.CustomField
    customfieldchoice = n2nmodels.CustomFieldChoice
    customlink = n2nmodels.CustomLink
    exporttemplate = n2nmodels.ExportTemplate
    imageattachment = n2nmodels.ImageAttachment
    jobresult = n2nmodels.JobResult
    note = n2nmodels.Note
    status = n2nmodels.Status
    tag = n2nmodels.Tag
    taggeditem = n2nmodels.TaggedItem
    webhook = n2nmodels.Webhook

    # IPAM
    aggregate = n2nmodels.Aggregate
    ipaddress = n2nmodels.IPAddress
    prefix = n2nmodels.Prefix
    rir = n2nmodels.RIR
    role = n2nmodels.Role
    routetarget = n2nmodels.RouteTarget
    service = n2nmodels.Service
    vlan = n2nmodels.VLAN
    vlangroup = n2nmodels.VLANGroup
    vrf = n2nmodels.VRF

    # Tenancy
    tenantgroup = n2nmodels.TenantGroup
    tenant = n2nmodels.Tenant

    # Virtualization
    clustertype = n2nmodels.ClusterType
    clustergroup = n2nmodels.ClusterGroup
    cluster = n2nmodels.Cluster
    virtualmachine = n2nmodels.VirtualMachine
    vminterface = n2nmodels.VMInterface

    #
    # DiffSync allows implementors to describe data hierarchically, in which case "top_level" would only
    # contain the models that exist at the root of this data hierarchy.
    # However, NetBox/Nautobot data models do not generally fit cleanly into such a hierarchy;
    # for example, not all Tenants belong to a parent TenantGroup, not all Sites belong to a parent Region.
    # Therefore, all of these models need to be treated by DiffSync as "top-level" models.
    #
    # There are a small number of models that *do* fit into this paradigm (such as Manufacturer -> DeviceType)
    # but they are so few in number that it was simpler to just remain consistent with all other models,
    # rather than adding hierarchy in just these few special cases.
    #
    # The specific order of models below is constructed empirically, but basically attempts to place all models
    # in sequence so that if model A has a hard dependency on a reference to model B, model B gets processed first.
    #
    # Note: with the latest changes in design for this plugin (using deterministic UUIDs in Nautobot to allow
    # direct mapping of NetBox PKs to Nautobot PKs), this order is now far less critical than it was previously.
    #

    top_level = (
        # "contenttype", Not synced, as these are hard-coded in NetBox/Nautobot
        # "permission", Not synced, as these are superseded by "objectpermission"
        "group",
        "user",  # Includes NetBox "userconfig" model as well
        "objectpermission",
        "token",
        "customfield",
        "customfieldchoice",
        # "status", Not synced, as these are hard-coded in NetBox and autogenerated in Nautobot
        # Need Tenant and TenantGroup before we can populate Sites
        "tenantgroup",
        "tenant",  # Not all Tenants belong to a TenantGroup
        "region",
        "site",  # Not all Sites belong to a Region
        "manufacturer",
        "devicetype",
        "devicerole",
        "platform",
        "clustertype",
        "clustergroup",
        "cluster",
        "provider",
        "providernetwork",
        "circuittype",
        "circuit",
        "circuittermination",
        "rackgroup",
        "rackrole",
        "rack",
        "rackreservation",
        "powerpanel",
        "powerfeed",
        "routetarget",
        "vrf",
        "rir",
        "aggregate",
        "role",
        "vlangroup",
        "vlan",
        "prefix",
        # Lots of pre-requisites for constructing a Device!
        "device",
        # Create device component templates **after** creating Devices,
        # as otherwise the created Devices will all use the fully-populated templates,
        # and we want to ensure that the Devices have only the components we have identified!
        "consoleporttemplate",
        "consoleserverporttemplate",
        "powerporttemplate",
        "poweroutlettemplate",
        "rearporttemplate",
        "frontporttemplate",
        "interfacetemplate",
        "devicebaytemplate",
        # All device components require a parent Device
        "devicebay",
        "inventoryitem",
        "virtualchassis",
        "virtualmachine",
        "consoleport",
        "consoleserverport",
        "powerport",
        "poweroutlet",
        "rearport",
        "frontport",
        "interface",
        "vminterface",
        # Reference loop:
        #   Device/VirtualMachine -> IPAddress (primary_ip4/primary_ip6)
        #   IPAddress -> Interface/VMInterface (assigned_object)
        #   Interface/VMInterface -> Device/VirtualMachine (device)
        # Interface comes after Device because it MUST have a Device to be created;
        # IPAddress comes after Interface because we use the assigned_object as part of the IP's unique ID.
        "ipaddress",
        "cable",
        "service",
        # The below have no particular upward dependencies and could be processed much earlier,
        # but from a logistical standpoint they "feel" like they make sense to handle last.
        "tag",
        "configcontext",
        "customlink",
        "exporttemplate",
        "webhook",
        "taggeditem",
        "imageattachment",
        "jobresult",
        # Notes will have unknown dependencies
        "note",
    )

    def __init__(self, *args, verbosity: int = 0, **kwargs):
        """Initialize this container, including its PK-indexed alternate data store."""
        super().__init__(*args, **kwargs)
        self.verbosity = verbosity
        self._data_by_pk = defaultdict(dict)
        self._sync_summary = None

    def sync_summary(self):
        """Get the summary of the last sync, if any."""
        return self._sync_summary

    def add(self, obj: DiffSyncModel):
        """Add a DiffSync model to the store, as well as registering it by PK for fast later retrieval."""
        # Store it by PK *before* we attempt to add it to the parent datastore,
        # in case we have duplicate objects with the same unique_id but different PKs.
        modelname = obj.get_type()
        if obj.pk in self._data_by_pk[modelname]:
            raise ObjectAlreadyExists(
                f"Object {modelname} with pk {obj.pk} already loaded",
                self.get_by_pk(modelname, obj.pk),
            )
        self._data_by_pk[modelname][obj.pk] = obj
        super().add(obj)

    def get_fk_identifiers(self, source_object, target_class, pk):
        """Helper to load_record: given a class and a PK, get the identifiers of the given instance."""
        if isinstance(pk, int):
            pk = netbox_pk_to_nautobot_pk(target_class.get_type(), pk)
        target_record = self.get_by_pk(target_class, pk)
        if not target_record:
            self.logger.debug(
                "Unresolved forward reference, will require later fixup",
                source_class=source_object.get_type(),
                target_class=target_class.get_type(),
                pk=pk,
            )
            return pk
        return target_record.get_identifiers()

    def get_by_pk(self, obj, pk):
        """Retrieve a previously loaded object by its primary key."""
        if isinstance(obj, str):
            modelname = obj
        else:
            modelname = obj.get_type()
        if pk not in self._data_by_pk[modelname]:
            raise ObjectNotFound(f"PK {pk} not found in stored {modelname} instances")
        return self._data_by_pk[modelname].get(pk)

    def make_model(self, diffsync_model, data):
        """Instantiate and add the given diffsync_model."""
        try:
            instance = diffsync_model(**data, diffsync=self)
        except ValidationError as exc:
            self.logger.error(
                "Invalid data according to internal data model",
                comment="This may be an issue with your source data or may reflect a bug in this plugin.",
                action="load",
                exception=str(exc),
                model=diffsync_model.get_type(),
                model_data=data,
            )
            return None
        try:
            self.add(instance)
        except ObjectAlreadyExists:
            existing_instance = self.get(diffsync_model, instance.get_unique_id())
            self.logger.warning(
                "Apparent duplicate object encountered?",
                comment="This may be an issue with your source data or may reflect a bug in this plugin.",
                duplicate_id=instance.get_identifiers(),
                model=diffsync_model.get_type(),
                pk_1=existing_instance.pk,
                pk_2=instance.pk,
            )
        return instance

    def sync_from(  # pylint: disable=too-many-arguments
        self,
        source: DiffSync,
        diff_class: Type[Diff] = Diff,
        flags: DiffSyncFlags = DiffSyncFlags.NONE,
        callback: Optional[Callable[[Text, int, int], None]] = None,
        diff: Optional[Diff] = None,
    ):
        """Synchronize data from the given source DiffSync object into the current DiffSync object."""
        self._sync_summary = None
        return super().sync_from(source, diff_class=diff_class, flags=flags, callback=callback, diff=diff)

    def sync_complete(
        self,
        source: DiffSync,
        diff: Diff,
        flags: DiffSyncFlags = DiffSyncFlags.NONE,
        logger: structlog.BoundLogger = None,
    ):
        """Callback invoked after completing a sync operation in which changes occurred."""
        self._sync_summary = diff.summary()
        self.logger.info("Summary of changes", summary=self._sync_summary)
        return super().sync_complete(source, diff, flags=flags, logger=logger)
__init__(args, verbosity=0, kwargs)

Initialize this container, including its PK-indexed alternate data store.

Source code in nautobot_netbox_importer/diffsync/adapters/abstract.py
def __init__(self, *args, verbosity: int = 0, **kwargs):
    """Initialize this container, including its PK-indexed alternate data store."""
    super().__init__(*args, **kwargs)
    self.verbosity = verbosity
    self._data_by_pk = defaultdict(dict)
    self._sync_summary = None
add(obj)

Add a DiffSync model to the store, as well as registering it by PK for fast later retrieval.

Source code in nautobot_netbox_importer/diffsync/adapters/abstract.py
def add(self, obj: DiffSyncModel):
    """Add a DiffSync model to the store, as well as registering it by PK for fast later retrieval."""
    # Store it by PK *before* we attempt to add it to the parent datastore,
    # in case we have duplicate objects with the same unique_id but different PKs.
    modelname = obj.get_type()
    if obj.pk in self._data_by_pk[modelname]:
        raise ObjectAlreadyExists(
            f"Object {modelname} with pk {obj.pk} already loaded",
            self.get_by_pk(modelname, obj.pk),
        )
    self._data_by_pk[modelname][obj.pk] = obj
    super().add(obj)
get_by_pk(obj, pk)

Retrieve a previously loaded object by its primary key.

Source code in nautobot_netbox_importer/diffsync/adapters/abstract.py
def get_by_pk(self, obj, pk):
    """Retrieve a previously loaded object by its primary key."""
    if isinstance(obj, str):
        modelname = obj
    else:
        modelname = obj.get_type()
    if pk not in self._data_by_pk[modelname]:
        raise ObjectNotFound(f"PK {pk} not found in stored {modelname} instances")
    return self._data_by_pk[modelname].get(pk)
get_fk_identifiers(source_object, target_class, pk)

Helper to load_record: given a class and a PK, get the identifiers of the given instance.

Source code in nautobot_netbox_importer/diffsync/adapters/abstract.py
def get_fk_identifiers(self, source_object, target_class, pk):
    """Helper to load_record: given a class and a PK, get the identifiers of the given instance."""
    if isinstance(pk, int):
        pk = netbox_pk_to_nautobot_pk(target_class.get_type(), pk)
    target_record = self.get_by_pk(target_class, pk)
    if not target_record:
        self.logger.debug(
            "Unresolved forward reference, will require later fixup",
            source_class=source_object.get_type(),
            target_class=target_class.get_type(),
            pk=pk,
        )
        return pk
    return target_record.get_identifiers()
make_model(diffsync_model, data)

Instantiate and add the given diffsync_model.

Source code in nautobot_netbox_importer/diffsync/adapters/abstract.py
def make_model(self, diffsync_model, data):
    """Instantiate and add the given diffsync_model."""
    try:
        instance = diffsync_model(**data, diffsync=self)
    except ValidationError as exc:
        self.logger.error(
            "Invalid data according to internal data model",
            comment="This may be an issue with your source data or may reflect a bug in this plugin.",
            action="load",
            exception=str(exc),
            model=diffsync_model.get_type(),
            model_data=data,
        )
        return None
    try:
        self.add(instance)
    except ObjectAlreadyExists:
        existing_instance = self.get(diffsync_model, instance.get_unique_id())
        self.logger.warning(
            "Apparent duplicate object encountered?",
            comment="This may be an issue with your source data or may reflect a bug in this plugin.",
            duplicate_id=instance.get_identifiers(),
            model=diffsync_model.get_type(),
            pk_1=existing_instance.pk,
            pk_2=instance.pk,
        )
    return instance
sync_complete(source, diff, flags=DiffSyncFlags.NONE, logger=None)

Callback invoked after completing a sync operation in which changes occurred.

Source code in nautobot_netbox_importer/diffsync/adapters/abstract.py
def sync_complete(
    self,
    source: DiffSync,
    diff: Diff,
    flags: DiffSyncFlags = DiffSyncFlags.NONE,
    logger: structlog.BoundLogger = None,
):
    """Callback invoked after completing a sync operation in which changes occurred."""
    self._sync_summary = diff.summary()
    self.logger.info("Summary of changes", summary=self._sync_summary)
    return super().sync_complete(source, diff, flags=flags, logger=logger)
sync_from(source, diff_class=Diff, flags=DiffSyncFlags.NONE, callback=None, diff=None)

Synchronize data from the given source DiffSync object into the current DiffSync object.

Source code in nautobot_netbox_importer/diffsync/adapters/abstract.py
def sync_from(  # pylint: disable=too-many-arguments
    self,
    source: DiffSync,
    diff_class: Type[Diff] = Diff,
    flags: DiffSyncFlags = DiffSyncFlags.NONE,
    callback: Optional[Callable[[Text, int, int], None]] = None,
    diff: Optional[Diff] = None,
):
    """Synchronize data from the given source DiffSync object into the current DiffSync object."""
    self._sync_summary = None
    return super().sync_from(source, diff_class=diff_class, flags=flags, callback=callback, diff=diff)
sync_summary()

Get the summary of the last sync, if any.

Source code in nautobot_netbox_importer/diffsync/adapters/abstract.py
def sync_summary(self):
    """Get the summary of the last sync, if any."""
    return self._sync_summary

nautobot

DiffSync adapter for Nautobot database.

IGNORED_FIELD_CLASSES = (GenericRel, GenericForeignKey, models.ManyToManyRel, models.ManyToOneRel) module-attribute

Field types that will appear in record._meta.get_fields() but can be generally ignored.

The *Rel models are reverse-lookup relations and are not "real" fields on the model.

We handle GenericForeignKeys by managing their component content_type and id fields separately.

NautobotDiffSync

Bases: N2NDiffSync

DiffSync adapter integrating with the Nautobot database.

Source code in nautobot_netbox_importer/diffsync/adapters/nautobot.py
class NautobotDiffSync(N2NDiffSync):
    """DiffSync adapter integrating with the Nautobot database."""

    logger = structlog.get_logger()

    def __init__(self, *args, bypass_data_validation=False, **kwargs):
        """Initialization of a NautobotDiffSync adapater instance."""
        super().__init__(*args, **kwargs)
        self.bypass_data_validation = bypass_data_validation

    def load_model(self, diffsync_model, record):  # pylint: disable=too-many-branches
        """Instantiate the given DiffSync model class from the given Django record."""
        data = {}

        # Iterate over all model fields on the Django record
        for field in record._meta.get_fields(include_hidden=True):
            if any(isinstance(field, ignored_class) for ignored_class in IGNORED_FIELD_CLASSES):
                continue

            # Get the value of this field from Django
            try:
                value = field.value_from_object(record)
            except AttributeError as exc:
                self.logger.error(f"Unable to get value_from_object for {record} {field}: {exc}")
                continue

            if field.name not in diffsync_model.fk_associations():
                # Field is a simple data type (str, int, bool) and can be used as-is with no modifications
                data[field.name] = value
                continue

            # If we got here, the field is some sort of foreign-key reference(s).
            if not value:
                # It's a null or empty list reference though, so we don't need to do anything special with it.
                data[field.name] = value
                continue

            # What's the name of the model that this is a reference to?
            target_name = diffsync_model.fk_associations()[field.name]

            if target_name == "status":
                data[field.name] = {"slug": self.status.nautobot_model().objects.get(pk=value).slug}
                continue

            # Special case: for generic foreign keys, the target_name is actually the name of
            # another field on this record that describes the content-type of this foreign key id.
            # We flag this by starting the target_name string with a '*', as if this were C or something.
            if target_name.startswith("*"):
                target_content_type_field = target_name[1:]
                target_content_type = getattr(record, target_content_type_field)
                target_name = target_content_type.model

            try:
                # Get the DiffSync model class that we know by the given target_name
                target_class = getattr(self, target_name)
            except AttributeError:
                self.logger.error("Unknown/unrecognized class name!", name=target_name)
                data[field.name] = None
                continue

            if isinstance(value, list):
                # This field is a one-to-many or many-to-many field, a list of object references.
                if issubclass(target_class, NautobotBaseModel):
                    # Replace each object reference with its appropriate primary key value
                    data[field.name] = [foreign_record.pk for foreign_record in value]
                else:
                    # Since the PKs of these built-in Django models may differ between NetBox and Nautobot,
                    # e.g., ContentTypes, replace each reference with the natural key (not PK) of the referenced model.
                    data[field.name] = [
                        self.get_by_pk(target_name, foreign_record.pk).get_identifiers() for foreign_record in value
                    ]
            elif isinstance(value, UUID):
                # Standard Nautobot UUID foreign-key reference, no transformation needed.
                data[field.name] = value
            elif isinstance(value, int):
                # Reference to a built-in model by its integer primary key.
                # Since this may not be the same value between NetBox and Nautobot (e.g., ContentType references)
                # replace the PK with the natural keys of the referenced model.
                data[field.name] = self.get_by_pk(target_name, value).get_identifiers()
            else:
                self.logger.error(f"Invalid PK value {value}")
                data[field.name] = None

        data["pk"] = record.pk
        return self.make_model(diffsync_model, data)

    def load(self):
        """Load all available and relevant data from Nautobot in the appropriate sequence."""
        self.logger.info("Loading data from Nautobot into DiffSync...")
        for modelname in ("contenttype", "permission", "status", *self.top_level):
            diffsync_model = getattr(self, modelname)
            if diffsync_model.nautobot_model().objects.exists():
                for instance in ProgressBar(
                    diffsync_model.nautobot_model().objects.all(),
                    total=diffsync_model.nautobot_model().objects.count(),
                    desc=f"{modelname:<25}",  # len("consoleserverporttemplate")
                    verbosity=self.verbosity,
                ):
                    self.load_model(diffsync_model, instance)

        self.logger.info("Data loading from Nautobot complete.")

    def restore_required_custom_fields(self, source: DiffSync):
        """Post-synchronization cleanup function to restore any 'required=True' custom field records."""
        self.logger.debug("Restoring the 'required=True' flag on any such custom fields")
        for source_customfield in source.get_all(source.customfield):
            if source_customfield.actual_required:
                # Update both the local DiffSync record (so that on the second-pass resync we again reset required=False)
                # and the Nautobot record (so that the end state is correct)
                self.get(self.customfield, source_customfield.get_unique_id()).update({"required": True})

    def sync_complete(self, source: DiffSync, *args, **kwargs):
        """Callback invoked after completing a sync operation in which changes occurred."""
        # During the sync, we intentionally marked all custom fields as "required=False"
        # so that we could sync records that predated the creation of said custom fields.
        # Now that we've updated all records that might contain custom field data,
        # only now can we re-mark any "required" custom fields as such.
        self.restore_required_custom_fields(source)

        return super().sync_complete(source, *args, **kwargs)
__init__(args, bypass_data_validation=False, kwargs)

Initialization of a NautobotDiffSync adapater instance.

Source code in nautobot_netbox_importer/diffsync/adapters/nautobot.py
def __init__(self, *args, bypass_data_validation=False, **kwargs):
    """Initialization of a NautobotDiffSync adapater instance."""
    super().__init__(*args, **kwargs)
    self.bypass_data_validation = bypass_data_validation
load()

Load all available and relevant data from Nautobot in the appropriate sequence.

Source code in nautobot_netbox_importer/diffsync/adapters/nautobot.py
def load(self):
    """Load all available and relevant data from Nautobot in the appropriate sequence."""
    self.logger.info("Loading data from Nautobot into DiffSync...")
    for modelname in ("contenttype", "permission", "status", *self.top_level):
        diffsync_model = getattr(self, modelname)
        if diffsync_model.nautobot_model().objects.exists():
            for instance in ProgressBar(
                diffsync_model.nautobot_model().objects.all(),
                total=diffsync_model.nautobot_model().objects.count(),
                desc=f"{modelname:<25}",  # len("consoleserverporttemplate")
                verbosity=self.verbosity,
            ):
                self.load_model(diffsync_model, instance)

    self.logger.info("Data loading from Nautobot complete.")
load_model(diffsync_model, record)

Instantiate the given DiffSync model class from the given Django record.

Source code in nautobot_netbox_importer/diffsync/adapters/nautobot.py
def load_model(self, diffsync_model, record):  # pylint: disable=too-many-branches
    """Instantiate the given DiffSync model class from the given Django record."""
    data = {}

    # Iterate over all model fields on the Django record
    for field in record._meta.get_fields(include_hidden=True):
        if any(isinstance(field, ignored_class) for ignored_class in IGNORED_FIELD_CLASSES):
            continue

        # Get the value of this field from Django
        try:
            value = field.value_from_object(record)
        except AttributeError as exc:
            self.logger.error(f"Unable to get value_from_object for {record} {field}: {exc}")
            continue

        if field.name not in diffsync_model.fk_associations():
            # Field is a simple data type (str, int, bool) and can be used as-is with no modifications
            data[field.name] = value
            continue

        # If we got here, the field is some sort of foreign-key reference(s).
        if not value:
            # It's a null or empty list reference though, so we don't need to do anything special with it.
            data[field.name] = value
            continue

        # What's the name of the model that this is a reference to?
        target_name = diffsync_model.fk_associations()[field.name]

        if target_name == "status":
            data[field.name] = {"slug": self.status.nautobot_model().objects.get(pk=value).slug}
            continue

        # Special case: for generic foreign keys, the target_name is actually the name of
        # another field on this record that describes the content-type of this foreign key id.
        # We flag this by starting the target_name string with a '*', as if this were C or something.
        if target_name.startswith("*"):
            target_content_type_field = target_name[1:]
            target_content_type = getattr(record, target_content_type_field)
            target_name = target_content_type.model

        try:
            # Get the DiffSync model class that we know by the given target_name
            target_class = getattr(self, target_name)
        except AttributeError:
            self.logger.error("Unknown/unrecognized class name!", name=target_name)
            data[field.name] = None
            continue

        if isinstance(value, list):
            # This field is a one-to-many or many-to-many field, a list of object references.
            if issubclass(target_class, NautobotBaseModel):
                # Replace each object reference with its appropriate primary key value
                data[field.name] = [foreign_record.pk for foreign_record in value]
            else:
                # Since the PKs of these built-in Django models may differ between NetBox and Nautobot,
                # e.g., ContentTypes, replace each reference with the natural key (not PK) of the referenced model.
                data[field.name] = [
                    self.get_by_pk(target_name, foreign_record.pk).get_identifiers() for foreign_record in value
                ]
        elif isinstance(value, UUID):
            # Standard Nautobot UUID foreign-key reference, no transformation needed.
            data[field.name] = value
        elif isinstance(value, int):
            # Reference to a built-in model by its integer primary key.
            # Since this may not be the same value between NetBox and Nautobot (e.g., ContentType references)
            # replace the PK with the natural keys of the referenced model.
            data[field.name] = self.get_by_pk(target_name, value).get_identifiers()
        else:
            self.logger.error(f"Invalid PK value {value}")
            data[field.name] = None

    data["pk"] = record.pk
    return self.make_model(diffsync_model, data)
restore_required_custom_fields(source)

Post-synchronization cleanup function to restore any 'required=True' custom field records.

Source code in nautobot_netbox_importer/diffsync/adapters/nautobot.py
def restore_required_custom_fields(self, source: DiffSync):
    """Post-synchronization cleanup function to restore any 'required=True' custom field records."""
    self.logger.debug("Restoring the 'required=True' flag on any such custom fields")
    for source_customfield in source.get_all(source.customfield):
        if source_customfield.actual_required:
            # Update both the local DiffSync record (so that on the second-pass resync we again reset required=False)
            # and the Nautobot record (so that the end state is correct)
            self.get(self.customfield, source_customfield.get_unique_id()).update({"required": True})
sync_complete(source, args, kwargs)

Callback invoked after completing a sync operation in which changes occurred.

Source code in nautobot_netbox_importer/diffsync/adapters/nautobot.py
def sync_complete(self, source: DiffSync, *args, **kwargs):
    """Callback invoked after completing a sync operation in which changes occurred."""
    # During the sync, we intentionally marked all custom fields as "required=False"
    # so that we could sync records that predated the creation of said custom fields.
    # Now that we've updated all records that might contain custom field data,
    # only now can we re-mark any "required" custom fields as such.
    self.restore_required_custom_fields(source)

    return super().sync_complete(source, *args, **kwargs)

netbox

DiffSync adapters for NetBox data dumps.

NetBox210DiffSync

Bases: N2NDiffSync

DiffSync adapter for working with data from NetBox 2.10.x.

Source code in nautobot_netbox_importer/diffsync/adapters/netbox.py
class NetBox210DiffSync(N2NDiffSync):
    """DiffSync adapter for working with data from NetBox 2.10.x."""

    logger = structlog.get_logger()

    _unsupported_fields = {}

    def __init__(self, *args, source_data=None, **kwargs):
        """Store the provided source_data for use when load() is called later."""
        self.source_data = source_data
        super().__init__(*args, **kwargs)

    @property
    def unsupported_fields(self):
        """Public interface for accessing class attr `_unsupported_fields`."""
        return self.__class__._unsupported_fields  # pylint: disable=protected-access

    @staticmethod
    def _get_ignored_fields(netbox_data: Dict, nautobot_instance: NautobotBaseModel) -> Set[str]:
        """
        Get fields from NetBox JSON that were not handled by the importer.

        This only counts fields that have values.

        Args:
            netbox_data: The NetBox data for a particular database entry.
            nautobot_instance: The Nautobot DiffSync instance created for `netbox_data`.

        Returns:
            set: The NetBox field names ignored by the importer.
        """
        # Get fields passed from NetBox that have values and ignore internal fields
        netbox_fields = {key for key, value in netbox_data.items() if value and not key.startswith("_")}
        # Get fields set on the model instance
        instance_fields = nautobot_instance.__fields_set__
        # Account for aliases when gettig a diff of fields instantiated on the model
        field_aliases = {field.alias for field in nautobot_instance.__fields__.values() if field.alias != field.name}
        return netbox_fields - instance_fields - field_aliases - nautobot_instance.ignored_fields

    def _log_ignored_fields_details(
        self,
        netbox_data: Dict,
        nautobot_instance: NautobotBaseModel,
        model_name: str,
        ignored_fields: Set[str],
    ) -> None:
        """
        Log a debug message for NetBox fields ignored by the importer.

        This will log for every instance of fields that were ignored by the
        importer, so if there are a 100 instances of a model with an ignored
        field, then 100 log entries will be generated. In order to prevent the
        logs from generating too much noise, this is only logged as a debug.

        Args:
            netbox_data: The NetBox data for a particular database entry.
            nautobot_instance: The Nautobot DiffSync instance created for `netbox_data`.
            model_name: The DiffSync modelname for the NetBox entry.
            ignored_fields: The field names in `netbox_data` that were ignored.
        """
        ignored_fields_with_values = (f"{field}={netbox_data[field]}" for field in ignored_fields)
        ignored_fields_data_str = ", ".join(ignored_fields_with_values)
        self.logger.debug(
            "NetBox field not defined for DiffSync Model",
            comment=(
                f"The following fields were defined in NetBox for {model_name} - {str(nautobot_instance)}, "
                f"but they will be ignored by the Nautobot import: {ignored_fields_data_str}"
            ),
            pk=nautobot_instance.pk,
        )

    def _log_ignored_fields_info(
        self,
        model_name: str,
        ignored_fields: Set[str],
    ) -> None:
        """
        Log a warning message for NetBox fields ignored by the importer.

        This will log a warning for each unique field that is ignored by the
        importer, so if there are 100 instances of a model with an ignored field,
        then only 1 entry will be logged. This is used to inform users that the
        field is not supported by the importer, but not flood the logs.

        Args:
            netbox_data: The NetBox data for a particular database entry.
            nautobot_instance: The Nautobot DiffSync instance created for `netbox_data`.
            model_name: The DiffSync modelname for the NetBox entry.
            ignored_fields: The field names in `netbox_data` that were ignored.
        """
        log_message = (
            f"The following fields are defined in NetBox for {model_name}, "
            "but are not supported by this importer: {}"
        )
        # first time instance has ignored fields
        if model_name not in self.unsupported_fields:
            ignored_fields_str = ", ".join(ignored_fields)
            self.logger.warning(log_message.format(ignored_fields_str))
            self.unsupported_fields[model_name] = ignored_fields
        # subsequent instances might have newly ignored fields
        else:
            unlogged_fields = ignored_fields - self.unsupported_fields[model_name]
            if unlogged_fields:
                unlogged_ignored_fields_str = ", ".join(unlogged_fields)
                self.logger.warning(log_message.format(unlogged_ignored_fields_str))
                self.unsupported_fields[model_name].update(unlogged_fields)

    def _log_ignored_fields(
        self,
        netbox_data: Dict,
        nautobot_instance: NautobotBaseModel,
    ) -> None:
        """
        Convenience method for handling logging of ignored fields.

        Args:
            netbox_data: The NetBox data for a particular database entry.
            nautobot_instance: The Nautobot DiffSync instance created for `netbox_data`.
        """
        ignored_fields = self._get_ignored_fields(netbox_data, nautobot_instance)
        if ignored_fields:
            model_name = nautobot_instance._modelname  # pylint: disable=protected-access
            self._log_ignored_fields_details(netbox_data, nautobot_instance, model_name, ignored_fields)
            self._log_ignored_fields_info(model_name, ignored_fields)

    def load_record(self, diffsync_model, record):  # pylint: disable=too-many-branches,too-many-statements
        """Instantiate the given model class from the given record."""
        data = record["fields"].copy()
        data["pk"] = record["pk"]

        # Fixup fields that are actually foreign-key (FK) associations by replacing
        # their FK ids with the DiffSync model unique-id fields.
        for key, target_name in diffsync_model.fk_associations().items():
            if key not in data or not data[key]:
                # Null reference, no processing required.
                continue

            if target_name == "status":
                # Special case as Status is a hard-coded field in NetBox, not a model reference
                # Construct an appropriately-formatted mock natural key and use that instead
                # TODO: we could also do this with a custom validator on the StatusRef model; might be better?
                data[key] = {"slug": data[key]}
                continue

            # In the case of generic foreign keys, we have to actually check a different field
            # on the DiffSync model to determine the model type that this foreign key is referring to.
            # By convention, we label such fields with a '*', as if this were a C pointer.
            if target_name.startswith("*"):
                target_content_type_field = target_name[1:]
                target_content_type_pk = record["fields"][target_content_type_field]
                if not isinstance(target_content_type_pk, int):
                    self.logger.error(f"Invalid content-type PK value {target_content_type_pk}")
                    data[key] = None
                    continue
                target_content_type_record = self.get_by_pk(self.contenttype, target_content_type_pk)
                target_name = target_content_type_record.model

            # Identify the DiffSyncModel class that this FK is pointing to
            try:
                target_class = getattr(self, target_name)
            except AttributeError:
                self.logger.warning("Unknown/unrecognized class name!", name=target_name)
                data[key] = None
                continue

            if isinstance(data[key], list):
                # This field is a one-to-many or many-to-many field, a list of foreign key references.
                if issubclass(target_class, NautobotBaseModel):
                    # Replace each NetBox integer FK with the corresponding deterministic Nautobot UUID FK.
                    data[key] = [netbox_pk_to_nautobot_pk(target_name, pk) for pk in data[key]]
                else:
                    # It's a base Django model such as ContentType or Group.
                    # Since we can't easily control its PK in Nautobot, use its natural key instead.
                    #
                    # Special case: there are ContentTypes in NetBox that don't exist in Nautobot,
                    # skip over references to them.
                    references = [self.get_by_pk(target_name, pk) for pk in data[key]]
                    references = filter(lambda entry: not entry.model_flags & DiffSyncModelFlags.IGNORE, references)
                    data[key] = [entry.get_identifiers() for entry in references]
            elif isinstance(data[key], int):
                # Standard NetBox integer foreign-key reference
                if issubclass(target_class, NautobotBaseModel):
                    # Replace the NetBox integer FK with the corresponding deterministic Nautobot UUID FK.
                    data[key] = netbox_pk_to_nautobot_pk(target_name, data[key])
                else:
                    # It's a base Django model such as ContentType or Group.
                    # Since we can't easily control its PK in Nautobot, use its natural key instead
                    reference = self.get_by_pk(target_name, data[key])
                    if reference.model_flags & DiffSyncModelFlags.IGNORE:
                        data[key] = None
                    else:
                        data[key] = reference.get_identifiers()
            else:
                self.logger.error(f"Invalid PK value {data[key]}")
                data[key] = None

        if diffsync_model == self.user:
            # NetBox has separate User and UserConfig models, but in Nautobot they're combined.
            # Load the corresponding UserConfig into the User record for completeness.
            self.logger.debug("Looking for UserConfig corresponding to User", username=data["username"])
            for other_record in self.source_data:
                if other_record["model"] == "users.userconfig" and other_record["fields"]["user"] == record["pk"]:
                    data["config_data"] = other_record["fields"]["data"]
                    break
            else:
                self.logger.warning("No UserConfig found for User", username=data["username"], pk=record["pk"])
                data["config_data"] = {}
        elif diffsync_model == self.customfield:
            # Because marking a custom field as "required" doesn't automatically assign a value to pre-existing records,
            # we never want to enforce 'required=True' at import time as there may be otherwise valid records that predate
            # the creation of this field. Store it on a private field instead and we'll fix it up at the end.
            data["actual_required"] = data["required"]
            data["required"] = False

            if data["type"] == "select":
                # NetBox stores the choices for a "select" CustomField (NetBox has no "multiselect" CustomFields)
                # locally within the CustomField model, whereas Nautobot has a separate CustomFieldChoices model.
                # So we need to split the choices out into separate DiffSync instances.
                # Since "choices" is an ArrayField, we have to parse it from the JSON string
                # see also models.abstract.ArrayField
                for choice in json.loads(data["choices"]):
                    self.make_model(
                        self.customfieldchoice,
                        {
                            "pk": uuid4(),
                            "field": netbox_pk_to_nautobot_pk("customfield", record["pk"]),
                            "value": choice,
                        },
                    )
                del data["choices"]
        elif diffsync_model == self.virtualmachine:
            # NetBox stores the vCPU value as DecimalField, Nautobot has PositiveSmallIntegerField,
            # so we need to cast here
            if data["vcpus"] is not None:
                data["vcpus"] = int(float(data["vcpus"]))

        instance = self.make_model(diffsync_model, data)
        self._log_ignored_fields(data, instance)
        return instance

    def load(self):
        """Load records from the provided source_data into DiffSync."""
        self.logger.info("Loading imported NetBox source data into DiffSync...")
        for modelname in ("contenttype", "permission", *self.top_level):
            diffsync_model = getattr(self, modelname)
            content_type_label = diffsync_model.nautobot_model()._meta.label_lower
            # Handle a NetBox vs Nautobot discrepancy - the Nautobot target model is 'users.user',
            # but the NetBox data export will have user records under the label 'auth.user'.
            if content_type_label == "users.user":
                content_type_label = "auth.user"
            records = [record for record in self.source_data if record["model"] == content_type_label]
            if records:
                for record in ProgressBar(
                    records,
                    desc=f"{modelname:<25}",  # len("consoleserverporttemplate")
                    verbosity=self.verbosity,
                ):
                    self.load_record(diffsync_model, record)

        self.logger.info("Data loading from NetBox source data complete.")
        # Discard the source data to free up memory
        self.source_data = None
unsupported_fields property

Public interface for accessing class attr _unsupported_fields.

__init__(args, source_data=None, kwargs)

Store the provided source_data for use when load() is called later.

Source code in nautobot_netbox_importer/diffsync/adapters/netbox.py
def __init__(self, *args, source_data=None, **kwargs):
    """Store the provided source_data for use when load() is called later."""
    self.source_data = source_data
    super().__init__(*args, **kwargs)
load()

Load records from the provided source_data into DiffSync.

Source code in nautobot_netbox_importer/diffsync/adapters/netbox.py
def load(self):
    """Load records from the provided source_data into DiffSync."""
    self.logger.info("Loading imported NetBox source data into DiffSync...")
    for modelname in ("contenttype", "permission", *self.top_level):
        diffsync_model = getattr(self, modelname)
        content_type_label = diffsync_model.nautobot_model()._meta.label_lower
        # Handle a NetBox vs Nautobot discrepancy - the Nautobot target model is 'users.user',
        # but the NetBox data export will have user records under the label 'auth.user'.
        if content_type_label == "users.user":
            content_type_label = "auth.user"
        records = [record for record in self.source_data if record["model"] == content_type_label]
        if records:
            for record in ProgressBar(
                records,
                desc=f"{modelname:<25}",  # len("consoleserverporttemplate")
                verbosity=self.verbosity,
            ):
                self.load_record(diffsync_model, record)

    self.logger.info("Data loading from NetBox source data complete.")
    # Discard the source data to free up memory
    self.source_data = None
load_record(diffsync_model, record)

Instantiate the given model class from the given record.

Source code in nautobot_netbox_importer/diffsync/adapters/netbox.py
def load_record(self, diffsync_model, record):  # pylint: disable=too-many-branches,too-many-statements
    """Instantiate the given model class from the given record."""
    data = record["fields"].copy()
    data["pk"] = record["pk"]

    # Fixup fields that are actually foreign-key (FK) associations by replacing
    # their FK ids with the DiffSync model unique-id fields.
    for key, target_name in diffsync_model.fk_associations().items():
        if key not in data or not data[key]:
            # Null reference, no processing required.
            continue

        if target_name == "status":
            # Special case as Status is a hard-coded field in NetBox, not a model reference
            # Construct an appropriately-formatted mock natural key and use that instead
            # TODO: we could also do this with a custom validator on the StatusRef model; might be better?
            data[key] = {"slug": data[key]}
            continue

        # In the case of generic foreign keys, we have to actually check a different field
        # on the DiffSync model to determine the model type that this foreign key is referring to.
        # By convention, we label such fields with a '*', as if this were a C pointer.
        if target_name.startswith("*"):
            target_content_type_field = target_name[1:]
            target_content_type_pk = record["fields"][target_content_type_field]
            if not isinstance(target_content_type_pk, int):
                self.logger.error(f"Invalid content-type PK value {target_content_type_pk}")
                data[key] = None
                continue
            target_content_type_record = self.get_by_pk(self.contenttype, target_content_type_pk)
            target_name = target_content_type_record.model

        # Identify the DiffSyncModel class that this FK is pointing to
        try:
            target_class = getattr(self, target_name)
        except AttributeError:
            self.logger.warning("Unknown/unrecognized class name!", name=target_name)
            data[key] = None
            continue

        if isinstance(data[key], list):
            # This field is a one-to-many or many-to-many field, a list of foreign key references.
            if issubclass(target_class, NautobotBaseModel):
                # Replace each NetBox integer FK with the corresponding deterministic Nautobot UUID FK.
                data[key] = [netbox_pk_to_nautobot_pk(target_name, pk) for pk in data[key]]
            else:
                # It's a base Django model such as ContentType or Group.
                # Since we can't easily control its PK in Nautobot, use its natural key instead.
                #
                # Special case: there are ContentTypes in NetBox that don't exist in Nautobot,
                # skip over references to them.
                references = [self.get_by_pk(target_name, pk) for pk in data[key]]
                references = filter(lambda entry: not entry.model_flags & DiffSyncModelFlags.IGNORE, references)
                data[key] = [entry.get_identifiers() for entry in references]
        elif isinstance(data[key], int):
            # Standard NetBox integer foreign-key reference
            if issubclass(target_class, NautobotBaseModel):
                # Replace the NetBox integer FK with the corresponding deterministic Nautobot UUID FK.
                data[key] = netbox_pk_to_nautobot_pk(target_name, data[key])
            else:
                # It's a base Django model such as ContentType or Group.
                # Since we can't easily control its PK in Nautobot, use its natural key instead
                reference = self.get_by_pk(target_name, data[key])
                if reference.model_flags & DiffSyncModelFlags.IGNORE:
                    data[key] = None
                else:
                    data[key] = reference.get_identifiers()
        else:
            self.logger.error(f"Invalid PK value {data[key]}")
            data[key] = None

    if diffsync_model == self.user:
        # NetBox has separate User and UserConfig models, but in Nautobot they're combined.
        # Load the corresponding UserConfig into the User record for completeness.
        self.logger.debug("Looking for UserConfig corresponding to User", username=data["username"])
        for other_record in self.source_data:
            if other_record["model"] == "users.userconfig" and other_record["fields"]["user"] == record["pk"]:
                data["config_data"] = other_record["fields"]["data"]
                break
        else:
            self.logger.warning("No UserConfig found for User", username=data["username"], pk=record["pk"])
            data["config_data"] = {}
    elif diffsync_model == self.customfield:
        # Because marking a custom field as "required" doesn't automatically assign a value to pre-existing records,
        # we never want to enforce 'required=True' at import time as there may be otherwise valid records that predate
        # the creation of this field. Store it on a private field instead and we'll fix it up at the end.
        data["actual_required"] = data["required"]
        data["required"] = False

        if data["type"] == "select":
            # NetBox stores the choices for a "select" CustomField (NetBox has no "multiselect" CustomFields)
            # locally within the CustomField model, whereas Nautobot has a separate CustomFieldChoices model.
            # So we need to split the choices out into separate DiffSync instances.
            # Since "choices" is an ArrayField, we have to parse it from the JSON string
            # see also models.abstract.ArrayField
            for choice in json.loads(data["choices"]):
                self.make_model(
                    self.customfieldchoice,
                    {
                        "pk": uuid4(),
                        "field": netbox_pk_to_nautobot_pk("customfield", record["pk"]),
                        "value": choice,
                    },
                )
            del data["choices"]
    elif diffsync_model == self.virtualmachine:
        # NetBox stores the vCPU value as DecimalField, Nautobot has PositiveSmallIntegerField,
        # so we need to cast here
        if data["vcpus"] is not None:
            data["vcpus"] = int(float(data["vcpus"]))

    instance = self.make_model(diffsync_model, data)
    self._log_ignored_fields(data, instance)
    return instance

models

DiffSync model class definitions for nautobot-netbox-importer.

Note that in most cases the same model classes are used for both NetBox imports and Nautobot exports. Because this plugin is meant only for NetBox-to-Nautobot migration, the create/update/delete methods on these classes are for populating data into Nautobot only, never the reverse.

Aggregate

Bases: PrimaryModel

An aggregate exists at the root level of the IP address space hierarchy.

Source code in nautobot_netbox_importer/diffsync/models/ipam.py
class Aggregate(PrimaryModel):
    """An aggregate exists at the root level of the IP address space hierarchy."""

    _modelname = "aggregate"
    _attributes = (*PrimaryModel._attributes, "prefix", "rir", "tenant", "date_added", "description")
    _nautobot_model = ipam.Aggregate

    prefix: netaddr.IPNetwork
    rir: RIRRef
    tenant: Optional[TenantRef]
    date_added: Optional[date]
    description: str

    def __init__(self, *args, **kwargs):
        """Clean up prefix to an IPNetwork before initializing as normal."""
        if "prefix" in kwargs:
            # NetBox import
            if isinstance(kwargs["prefix"], str):
                kwargs["prefix"] = netaddr.IPNetwork(kwargs["prefix"])
        else:
            # Nautobot import
            kwargs["prefix"] = network_from_components(kwargs["network"], kwargs["prefix_length"])
            del kwargs["network"]
            del kwargs["broadcast"]
            del kwargs["prefix_length"]

        super().__init__(*args, **kwargs)
__init__(args, kwargs)

Clean up prefix to an IPNetwork before initializing as normal.

Source code in nautobot_netbox_importer/diffsync/models/ipam.py
def __init__(self, *args, **kwargs):
    """Clean up prefix to an IPNetwork before initializing as normal."""
    if "prefix" in kwargs:
        # NetBox import
        if isinstance(kwargs["prefix"], str):
            kwargs["prefix"] = netaddr.IPNetwork(kwargs["prefix"])
    else:
        # Nautobot import
        kwargs["prefix"] = network_from_components(kwargs["network"], kwargs["prefix_length"])
        del kwargs["network"]
        del kwargs["broadcast"]
        del kwargs["prefix_length"]

    super().__init__(*args, **kwargs)

Cable

Bases: StatusModelMixin, PrimaryModel

A physical connection between two endpoints.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class Cable(StatusModelMixin, PrimaryModel):
    """A physical connection between two endpoints."""

    _modelname = "cable"
    _attributes = (
        *PrimaryModel._attributes,
        *StatusModelMixin._attributes,
        "termination_a_type",
        "termination_a_id",
        "termination_b_type",
        "termination_b_id",
        "type",
        "label",
        "color",
        "length",
        "length_unit",
    )
    _nautobot_model = dcim.Cable

    termination_a_type: ContentTypeRef
    _termination_a_id = foreign_key_field("*termination_a_type")
    termination_a_id: _termination_a_id
    termination_b_type: ContentTypeRef
    _termination_b_id = foreign_key_field("*termination_b_type")
    termination_b_id: _termination_b_id
    type: str
    label: str
    color: str
    length: Optional[int]
    length_unit: str

    _type_choices = set(dcim_choices.CableTypeChoices.values())

    @root_validator
    def invalid_type_to_other(cls, values):  # pylint: disable=no-self-argument,no-self-use
        """
        Default invalid `type` fields to use `other` type.

        The `type` field uses a ChoiceSet to limit valid choices. This uses Pydantic's
        root_validator to clean up the `type` data before loading it into a Model
        instance. All invalid types will be changed to "other."
        """
        cable_type = values["type"]
        if cable_type not in cls._type_choices:
            values["type"] = "other"
            term_a_id = values["termination_a_id"]
            term_b_id = values["termination_b_id"]
            logger.warning(
                f"Encountered a NetBox {cls._modelname}.type that is not valid in this version of Nautobot, will convert it",
                termination_a_id=term_a_id,
                termination_b_id=term_b_id,
                netbox_type=cable_type,
                nautobot_type="other",
            )
        return values
invalid_type_to_other(values)

Default invalid type fields to use other type.

The type field uses a ChoiceSet to limit valid choices. This uses Pydantic's root_validator to clean up the type data before loading it into a Model instance. All invalid types will be changed to "other."

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
@root_validator
def invalid_type_to_other(cls, values):  # pylint: disable=no-self-argument,no-self-use
    """
    Default invalid `type` fields to use `other` type.

    The `type` field uses a ChoiceSet to limit valid choices. This uses Pydantic's
    root_validator to clean up the `type` data before loading it into a Model
    instance. All invalid types will be changed to "other."
    """
    cable_type = values["type"]
    if cable_type not in cls._type_choices:
        values["type"] = "other"
        term_a_id = values["termination_a_id"]
        term_b_id = values["termination_b_id"]
        logger.warning(
            f"Encountered a NetBox {cls._modelname}.type that is not valid in this version of Nautobot, will convert it",
            termination_a_id=term_a_id,
            termination_b_id=term_b_id,
            netbox_type=cable_type,
            nautobot_type="other",
        )
    return values

Circuit

Bases: StatusModelMixin, PrimaryModel

A communications circuit connects two points.

Source code in nautobot_netbox_importer/diffsync/models/circuits.py
class Circuit(StatusModelMixin, PrimaryModel):
    """A communications circuit connects two points."""

    _modelname = "circuit"
    _attributes = (
        *PrimaryModel._attributes,
        *StatusModelMixin._attributes,
        "provider",
        "cid",
        "status",
        "type",
        "tenant",
        "install_date",
        "commit_rate",
        "description",
        "comments",
    )
    _nautobot_model = circuits.Circuit

    provider: ProviderRef
    cid: str
    type: CircuitTypeRef
    tenant: Optional[TenantRef]
    install_date: Optional[date]
    commit_rate: Optional[int]
    description: str
    comments: str

    @validator("install_date", pre=True)
    def check_install_date(cls, value):  # pylint: disable=no-self-argument,no-self-use
        """Pre-cleaning: in JSON dump from Django, date string is formatted differently than Pydantic expects."""
        if isinstance(value, str) and value.endswith("T00:00:00Z"):
            value = value.replace("T00:00:00Z", "")
        return value
check_install_date(value)

Pre-cleaning: in JSON dump from Django, date string is formatted differently than Pydantic expects.

Source code in nautobot_netbox_importer/diffsync/models/circuits.py
@validator("install_date", pre=True)
def check_install_date(cls, value):  # pylint: disable=no-self-argument,no-self-use
    """Pre-cleaning: in JSON dump from Django, date string is formatted differently than Pydantic expects."""
    if isinstance(value, str) and value.endswith("T00:00:00Z"):
        value = value.replace("T00:00:00Z", "")
    return value

CircuitTermination

Bases: CableTerminationMixin, NautobotBaseModel

An endpoint of a Circuit.

Source code in nautobot_netbox_importer/diffsync/models/circuits.py
class CircuitTermination(CableTerminationMixin, NautobotBaseModel):
    """An endpoint of a Circuit."""

    _modelname = "circuittermination"
    _attributes = (
        *CableTerminationMixin._attributes,
        "circuit",
        "term_side",
        "site",
        "port_speed",
        "upstream_speed",
        "xconnect_id",
        "pp_info",
        "description",
    )
    _nautobot_model = circuits.CircuitTermination

    circuit: CircuitRef
    term_side: str
    site: SiteRef
    port_speed: Optional[int]
    upstream_speed: Optional[int]
    xconnect_id: str
    pp_info: str
    description: str

CircuitType

Bases: OrganizationalModel

Circuits can be organized by their functional role.

Source code in nautobot_netbox_importer/diffsync/models/circuits.py
class CircuitType(OrganizationalModel):
    """Circuits can be organized by their functional role."""

    _modelname = "circuittype"
    _attributes = (*OrganizationalModel._attributes, "name", "slug", "description")
    _nautobot_model = circuits.CircuitType

    name: str
    slug: str
    description: str

Cluster

Bases: PrimaryModel

A cluster of VirtualMachines, optionally associated with one or more Devices.

Source code in nautobot_netbox_importer/diffsync/models/virtualization.py
class Cluster(PrimaryModel):
    """A cluster of VirtualMachines, optionally associated with one or more Devices."""

    _modelname = "cluster"
    _attributes = (*PrimaryModel._attributes, "name", "type", "group", "tenant", "site", "comments")
    _nautobot_model = virtualization.Cluster

    name: str
    type: ClusterTypeRef
    group: Optional[ClusterGroupRef]
    tenant: Optional[TenantRef]
    site: Optional[SiteRef]
    comments: str

ClusterGroup

Bases: OrganizationalModel

An organizational group of Clusters.

Source code in nautobot_netbox_importer/diffsync/models/virtualization.py
class ClusterGroup(OrganizationalModel):
    """An organizational group of Clusters."""

    _modelname = "clustergroup"
    _attributes = (*OrganizationalModel._attributes, "name", "slug", "description")
    _nautobot_model = virtualization.ClusterGroup

    name: str
    slug: str
    description: str

ClusterType

Bases: OrganizationalModel

A type of Cluster.

Source code in nautobot_netbox_importer/diffsync/models/virtualization.py
class ClusterType(OrganizationalModel):
    """A type of Cluster."""

    _modelname = "clustertype"
    _attributes = (*OrganizationalModel._attributes, "name", "slug", "description")
    _nautobot_model = virtualization.ClusterType

    name: str
    slug: str
    description: str

ConfigContext

Bases: ChangeLoggedModelMixin, NautobotBaseModel

A set of arbitrary data available to Devices and VirtualMachines.

Source code in nautobot_netbox_importer/diffsync/models/extras.py
class ConfigContext(ChangeLoggedModelMixin, NautobotBaseModel):
    """A set of arbitrary data available to Devices and VirtualMachines."""

    _modelname = "configcontext"
    _attributes = (
        *ChangeLoggedModelMixin._attributes,
        "name",
        "weight",
        "description",
        "is_active",
        "regions",
        "sites",
        "roles",
        "platforms",
        "cluster_groups",
        "clusters",
        "tenant_groups",
        "tenants",
        "tags",
        "data",
    )
    _nautobot_model = extras.ConfigContext

    name: str

    weight: int
    description: str
    is_active: bool
    regions: List[RegionRef] = []
    sites: List[SiteRef] = []
    roles: List[DeviceRoleRef] = []
    platforms: List[PlatformRef] = []
    cluster_groups: List[ClusterGroupRef] = []
    clusters: List[ClusterRef] = []
    tenant_groups: List[TenantGroupRef] = []
    tenants: List[TenantRef] = []
    tags: List[TagRef] = []
    data: dict

ConsolePort

Bases: CableTerminationMixin, ComponentModel

A physical console port within a Device.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class ConsolePort(CableTerminationMixin, ComponentModel):
    """A physical console port within a Device."""

    _modelname = "consoleport"
    _attributes = (*ComponentModel._attributes, *CableTerminationMixin._attributes, "type")
    _nautobot_model = dcim.ConsolePort

    type: str

    _type_choices = set(dcim_choices.ConsolePortTypeChoices.values())

ConsolePortTemplate

Bases: ComponentTemplateModel

A template for a ConsolePort.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class ConsolePortTemplate(ComponentTemplateModel):
    """A template for a ConsolePort."""

    _modelname = "consoleporttemplate"
    _attributes = (*ComponentTemplateModel._attributes, "type")
    _nautobot_model = dcim.ConsolePortTemplate

    type: str

    _type_choices = set(dcim_choices.ConsolePortTypeChoices.values())

ConsoleServerPort

Bases: CableTerminationMixin, ComponentModel

A physical port that provides access to console ports.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class ConsoleServerPort(CableTerminationMixin, ComponentModel):
    """A physical port that provides access to console ports."""

    _modelname = "consoleserverport"
    _attributes = (*ComponentModel._attributes, *CableTerminationMixin._attributes, "type")
    _nautobot_model = dcim.ConsoleServerPort

    type: str

    _type_choices = set(dcim_choices.ConsolePortTypeChoices.values())

ConsoleServerPortTemplate

Bases: ComponentTemplateModel

A template for a ConsoleServerPort.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class ConsoleServerPortTemplate(ComponentTemplateModel):
    """A template for a ConsoleServerPort."""

    _modelname = "consoleserverporttemplate"
    _attributes = (*ComponentTemplateModel._attributes, "type")
    _nautobot_model = dcim.ConsoleServerPortTemplate

    type: str

    _type_choices = set(dcim_choices.ConsolePortTypeChoices.values())

ContentType

Bases: DjangoBaseModel

A reference to a model type, in the form (, ).

Source code in nautobot_netbox_importer/diffsync/models/contenttypes.py
class ContentType(DjangoBaseModel):
    """A reference to a model type, in the form (<app_label>, <modelname>)."""

    _modelname = "contenttype"
    _identifiers = (
        "app_label",
        "model",
    )
    _attributes = ("pk",)
    _nautobot_model = models.ContentType

    app_label: str
    model: str

    def __init__(self, *args, app_label=None, model=None, **kwargs):
        """Map NetBox 'auth.user' content type to Nautobot 'users.user' content type."""
        if app_label == "auth" and model == "user":
            app_label = "users"
        super().__init__(*args, app_label=app_label, model=model, **kwargs)
__init__(args, app_label=None, model=None, kwargs)

Map NetBox 'auth.user' content type to Nautobot 'users.user' content type.

Source code in nautobot_netbox_importer/diffsync/models/contenttypes.py
def __init__(self, *args, app_label=None, model=None, **kwargs):
    """Map NetBox 'auth.user' content type to Nautobot 'users.user' content type."""
    if app_label == "auth" and model == "user":
        app_label = "users"
    super().__init__(*args, app_label=app_label, model=model, **kwargs)

CustomField

Bases: NautobotBaseModel

Custom field defined on a model(s).

Source code in nautobot_netbox_importer/diffsync/models/extras.py
class CustomField(NautobotBaseModel):
    """Custom field defined on a model(s)."""

    _modelname = "customfield"
    _attributes = (
        "name",
        "content_types",
        "type",
        "label",
        "description",
        "required",
        "filter_logic",
        "default",
        "weight",
        "validation_minimum",
        "validation_maximum",
        "validation_regex",
    )
    _nautobot_model = extras.CustomField

    name: str

    content_types: List[ContentTypeRef] = []
    type: str
    label: str
    description: str
    required: bool
    filter_logic: str
    default: Optional[Any]  # any JSON value
    weight: int
    validation_minimum: Optional[int]
    validation_maximum: Optional[int]
    validation_regex: str

    # Because marking a custom field as "required" doesn't automatically assign a value to pre-existing records,
    # we never want, when adding custom fields from NetBox, to flag fields as required=True.
    # Instead we store it in "actual_required" and fix it up only afterwards.
    actual_required: Optional[bool]

    _ignored_fields = {"choices"} | NautobotBaseModel._ignored_fields

    @classmethod
    def special_clean(cls, diffsync, ids, attrs):
        """Special-case handling for the "default" attribute."""
        if attrs.get("default") and attrs["type"] in ("select", "multiselect"):
            # There's a bit of a chicken-and-egg problem here in that we have to create a CustomField
            # before we can create any CustomFieldChoice records that reference it, but the "default"
            # attribute on the CustomField is only valid if it references an existing CustomFieldChoice.
            # So what we have to do is skip over the "default" field if it references a nonexistent CustomFieldChoice.
            default = attrs.get("default")
            try:
                diffsync.get("customfieldchoice", {"field": {"name": attrs["name"]}, "value": default})
            except ObjectNotFound:
                logger.debug(
                    "CustomFieldChoice not yet present to set as 'default' for CustomField, will fixup later",
                    field=attrs["name"],
                    default=default,
                )
                del attrs["default"]
special_clean(diffsync, ids, attrs) classmethod

Special-case handling for the "default" attribute.

Source code in nautobot_netbox_importer/diffsync/models/extras.py
@classmethod
def special_clean(cls, diffsync, ids, attrs):
    """Special-case handling for the "default" attribute."""
    if attrs.get("default") and attrs["type"] in ("select", "multiselect"):
        # There's a bit of a chicken-and-egg problem here in that we have to create a CustomField
        # before we can create any CustomFieldChoice records that reference it, but the "default"
        # attribute on the CustomField is only valid if it references an existing CustomFieldChoice.
        # So what we have to do is skip over the "default" field if it references a nonexistent CustomFieldChoice.
        default = attrs.get("default")
        try:
            diffsync.get("customfieldchoice", {"field": {"name": attrs["name"]}, "value": default})
        except ObjectNotFound:
            logger.debug(
                "CustomFieldChoice not yet present to set as 'default' for CustomField, will fixup later",
                field=attrs["name"],
                default=default,
            )
            del attrs["default"]

CustomFieldChoice

Bases: NautobotBaseModel

One of the valid options for a CustomField of type "select" or "multiselect".

Source code in nautobot_netbox_importer/diffsync/models/extras.py
class CustomFieldChoice(NautobotBaseModel):
    """One of the valid options for a CustomField of type "select" or "multiselect"."""

    _modelname = "customfieldchoice"
    # Since these only exist in Nautobot and not in NetBox, we can't match them between the two systems by PK.
    _identifiers = ("field", "value")
    _attributes = ("weight",)
    _nautobot_model = extras.CustomFieldChoice

    field: CustomFieldRef
    value: str
    weight: int = 100

Bases: ChangeLoggedModelMixin, NautobotBaseModel

A custom link to an external representation of a Nautobot object.

Source code in nautobot_netbox_importer/diffsync/models/extras.py
class CustomLink(ChangeLoggedModelMixin, NautobotBaseModel):
    """A custom link to an external representation of a Nautobot object."""

    _modelname = "customlink"
    _attributes = (
        "name",
        "content_type",
        "text",
        "target_url",
        "weight",
        "group_name",
        "button_class",
        "new_window",
        *ChangeLoggedModelMixin._attributes,
    )
    _nautobot_model = extras.CustomLink

    name: str

    content_type: ContentTypeRef
    text: str
    # Field name is "url" in NetBox, "target_url" in Nautobot
    target_url: str = Field(alias="url")
    weight: int
    group_name: str
    button_class: str
    new_window: bool

    class Config:
        """Pydantic configuration of the CustomLink class."""

        # Allow both "url" and "target_url" as property setters
        allow_population_by_field_name = True
Config

Pydantic configuration of the CustomLink class.

Source code in nautobot_netbox_importer/diffsync/models/extras.py
class Config:
    """Pydantic configuration of the CustomLink class."""

    # Allow both "url" and "target_url" as property setters
    allow_population_by_field_name = True

Device

Bases: ConfigContextModelMixin, StatusModelMixin, PrimaryModel

A Device represents a piece of physical hardware mounted within a Rack.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class Device(ConfigContextModelMixin, StatusModelMixin, PrimaryModel):
    """A Device represents a piece of physical hardware mounted within a Rack."""

    _modelname = "device"
    _attributes = (
        *PrimaryModel._attributes,
        *ConfigContextModelMixin._attributes,
        *StatusModelMixin._attributes,
        "site",
        "tenant",
        "name",
        "rack",
        "position",
        "face",
        "vc_position",
        "vc_priority",
        "device_type",
        "device_role",
        "platform",
        "serial",
        "asset_tag",
        "cluster",
        "virtual_chassis",
        "primary_ip4",
        "primary_ip6",
        "comments",
    )
    _nautobot_model = dcim.Device

    site: Optional[SiteRef]
    tenant: Optional[TenantRef]
    name: Optional[str]
    rack: Optional[RackRef]
    position: Optional[int]
    face: str  # may not be None but may be empty
    vc_position: Optional[int]
    vc_priority: Optional[int]
    device_type: DeviceTypeRef
    device_role: DeviceRoleRef
    platform: Optional[PlatformRef]
    serial: str  # may not be None but may be empty
    asset_tag: Optional[str]
    cluster: Optional[ClusterRef]

    virtual_chassis: Optional[VirtualChassisRef]
    primary_ip4: Optional[IPAddressRef]
    primary_ip6: Optional[IPAddressRef]
    comments: str

DeviceBay

Bases: ComponentModel

An empty space within a Device which can house a child Device.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class DeviceBay(ComponentModel):
    """An empty space within a Device which can house a child Device."""

    _modelname = "devicebay"
    _attributes = (*ComponentModel._attributes, "installed_device")
    _nautobot_model = dcim.DeviceBay

    installed_device: Optional[DeviceRef]

DeviceBayTemplate

Bases: ComponentTemplateModel

A template for a DeviceBay.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class DeviceBayTemplate(ComponentTemplateModel):
    """A template for a DeviceBay."""

    _modelname = "devicebaytemplate"
    _nautobot_model = dcim.DeviceBayTemplate

DeviceRole

Bases: OrganizationalModel

Devices are organized by functional role.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class DeviceRole(OrganizationalModel):
    """Devices are organized by functional role."""

    _modelname = "devicerole"
    _attributes = (*OrganizationalModel._attributes, "name", "slug", "color", "vm_role", "description")
    _nautobot_model = dcim.DeviceRole

    name: str
    slug: str
    color: str
    vm_role: bool
    description: str

DeviceType

Bases: PrimaryModel

A DeviceType represents a particular make and model of device.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class DeviceType(PrimaryModel):
    """A DeviceType represents a particular make and model of device."""

    _modelname = "devicetype"
    _attributes = (
        *PrimaryModel._attributes,
        "manufacturer",
        "model",
        "slug",
        "part_number",
        "u_height",
        "is_full_depth",
        "subdevice_role",
        "front_image",
        "rear_image",
        "comments",
    )
    _nautobot_model = dcim.DeviceType

    manufacturer: ManufacturerRef
    model: str
    slug: str
    part_number: str
    u_height: int
    is_full_depth: bool
    subdevice_role: str
    front_image: str
    rear_image: str
    comments: str

    @validator("front_image", pre=True)
    def front_imagefieldfile_to_str(cls, value):  # pylint: disable=no-self-argument,no-self-use
        """Convert ImageFieldFile objects to strings."""
        if hasattr(value, "name"):
            value = value.name
        return value

    @validator("rear_image", pre=True)
    def rear_imagefieldfile_to_str(cls, value):  # pylint: disable=no-self-argument,no-self-use
        """Convert ImageFieldFile objects to strings."""
        if hasattr(value, "name"):
            value = value.name
        return value
front_imagefieldfile_to_str(value)

Convert ImageFieldFile objects to strings.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
@validator("front_image", pre=True)
def front_imagefieldfile_to_str(cls, value):  # pylint: disable=no-self-argument,no-self-use
    """Convert ImageFieldFile objects to strings."""
    if hasattr(value, "name"):
        value = value.name
    return value
rear_imagefieldfile_to_str(value)

Convert ImageFieldFile objects to strings.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
@validator("rear_image", pre=True)
def rear_imagefieldfile_to_str(cls, value):  # pylint: disable=no-self-argument,no-self-use
    """Convert ImageFieldFile objects to strings."""
    if hasattr(value, "name"):
        value = value.name
    return value

ExportTemplate

Bases: ChangeLoggedModelMixin, NautobotBaseModel

A Jinja2 template for exporting records as text.

Source code in nautobot_netbox_importer/diffsync/models/extras.py
class ExportTemplate(ChangeLoggedModelMixin, NautobotBaseModel):
    """A Jinja2 template for exporting records as text."""

    _modelname = "exporttemplate"
    _attributes = (
        "name",
        "content_type",
        "description",
        "template_code",
        "mime_type",
        "file_extension",
        *ChangeLoggedModelMixin._attributes,
    )
    _nautobot_model = extras.ExportTemplate

    name: str

    content_type: ContentTypeRef
    description: str
    template_code: str
    mime_type: str
    file_extension: str

FrontPort

Bases: CableTerminationMixin, ComponentModel

A pass-through port on the front of a Device.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class FrontPort(CableTerminationMixin, ComponentModel):
    """A pass-through port on the front of a Device."""

    _modelname = "frontport"
    _attributes = (
        *ComponentModel._attributes,
        *CableTerminationMixin._attributes,
        "type",
        "rear_port",
        "rear_port_position",
    )
    _nautobot_model = dcim.FrontPort

    type: str
    rear_port: RearPortRef
    rear_port_position: int

    _type_choices = set(dcim_choices.PortTypeChoices.values())

FrontPortTemplate

Bases: ComponentTemplateModel

A template for a FrontPort.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class FrontPortTemplate(ComponentTemplateModel):
    """A template for a FrontPort."""

    _modelname = "frontporttemplate"
    _attributes = (*ComponentTemplateModel._attributes, "type", "rear_port", "rear_port_position")
    _nautobot_model = dcim.FrontPortTemplate

    type: str
    rear_port: RearPortTemplateRef
    rear_port_position: int

    _type_choices = set(dcim_choices.PortTypeChoices.values())

Group

Bases: DjangoBaseModel

Definition of a user group.

Source code in nautobot_netbox_importer/diffsync/models/auth.py
class Group(DjangoBaseModel):
    """Definition of a user group."""

    _modelname = "group"
    _identifiers = ("name",)
    _attributes = ("permissions",)
    _nautobot_model = auth.Group

    name: str
    permissions: List[PermissionRef] = []

IPAddress

Bases: StatusModelMixin, PrimaryModel

An individual IPv4 or IPv6 address.

Source code in nautobot_netbox_importer/diffsync/models/ipam.py
class IPAddress(StatusModelMixin, PrimaryModel):
    """An individual IPv4 or IPv6 address."""

    _modelname = "ipaddress"
    _attributes = (
        *PrimaryModel._attributes,
        *StatusModelMixin._attributes,
        "address",
        "vrf",
        "tenant",
        "assigned_object_type",
        "assigned_object_id",
        "role",
        "nat_inside",
        "dns_name",
        "description",
    )
    _nautobot_model = ipam.IPAddress

    address: netaddr.IPNetwork  # not IPAddress
    vrf: Optional[VRFRef]
    tenant: Optional[TenantRef]
    assigned_object_type: Optional[ContentTypeRef]
    _assigned_object_id = foreign_key_field("*assigned_object_type")
    assigned_object_id: Optional[_assigned_object_id]
    role: str
    nat_inside: Optional[IPAddressRef]
    dns_name: str
    description: str

    def __init__(self, *args, **kwargs):
        """Clean up address to an IPNetwork before initializing as normal."""
        if "address" in kwargs:
            # Import from NetBox
            if isinstance(kwargs["address"], str):
                kwargs["address"] = netaddr.IPNetwork(kwargs["address"])
        else:
            # Import from Nautobot
            kwargs["address"] = network_from_components(kwargs["host"], kwargs["prefix_length"])
            del kwargs["host"]
            del kwargs["broadcast"]
            del kwargs["prefix_length"]
        super().__init__(*args, **kwargs)
__init__(args, kwargs)

Clean up address to an IPNetwork before initializing as normal.

Source code in nautobot_netbox_importer/diffsync/models/ipam.py
def __init__(self, *args, **kwargs):
    """Clean up address to an IPNetwork before initializing as normal."""
    if "address" in kwargs:
        # Import from NetBox
        if isinstance(kwargs["address"], str):
            kwargs["address"] = netaddr.IPNetwork(kwargs["address"])
    else:
        # Import from Nautobot
        kwargs["address"] = network_from_components(kwargs["host"], kwargs["prefix_length"])
        del kwargs["host"]
        del kwargs["broadcast"]
        del kwargs["prefix_length"]
    super().__init__(*args, **kwargs)

ImageAttachment

Bases: NautobotBaseModel

An uploaded image which is associated with an object.

Source code in nautobot_netbox_importer/diffsync/models/extras.py
class ImageAttachment(NautobotBaseModel):
    """An uploaded image which is associated with an object."""

    _modelname = "imageattachment"
    _attributes = ("content_type", "object_id", "image", "image_height", "image_width", "name", "created")
    _nautobot_model = extras.ImageAttachment

    content_type: ContentTypeRef
    _object_id = foreign_key_field("*content_type")
    object_id: _object_id
    image: str
    image_height: int
    image_width: int
    name: str
    created: datetime

    @validator("image", pre=True)
    def imagefieldfile_to_str(cls, value):  # pylint: disable=no-self-argument,no-self-use
        """Convert ImageFieldFile objects to strings."""
        if hasattr(value, "name"):
            value = value.name
        return value
imagefieldfile_to_str(value)

Convert ImageFieldFile objects to strings.

Source code in nautobot_netbox_importer/diffsync/models/extras.py
@validator("image", pre=True)
def imagefieldfile_to_str(cls, value):  # pylint: disable=no-self-argument,no-self-use
    """Convert ImageFieldFile objects to strings."""
    if hasattr(value, "name"):
        value = value.name
    return value

Interface

Bases: BaseInterfaceMixin, CableTerminationMixin, ComponentModel

A network interface within a Device.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class Interface(BaseInterfaceMixin, CableTerminationMixin, ComponentModel):
    """A network interface within a Device."""

    _modelname = "interface"
    _attributes = (
        *ComponentModel._attributes,
        *CableTerminationMixin._attributes,
        *BaseInterfaceMixin._attributes,
        "lag",
        "type",
        "mgmt_only",
        "untagged_vlan",
        "tagged_vlans",
    )
    _nautobot_model = dcim.Interface

    lag: Optional[InterfaceRef]
    type: str
    mgmt_only: bool
    untagged_vlan: Optional[VLANRef]
    tagged_vlans: List[VLANRef] = []

    _type_choices = set(dcim_choices.InterfaceTypeChoices.values())

InterfaceTemplate

Bases: ComponentTemplateModel

A template for a physical data interface.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class InterfaceTemplate(ComponentTemplateModel):
    """A template for a physical data interface."""

    _modelname = "interfacetemplate"
    _attributes = (*ComponentTemplateModel._attributes, "type", "mgmt_only")
    _nautobot_model = dcim.InterfaceTemplate

    type: str
    mgmt_only: bool

    _type_choices = set(dcim_choices.InterfaceTypeChoices.values())

InventoryItem

Bases: MPTTModelMixin, ComponentModel

A serialized piece of hardware within a Device.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class InventoryItem(MPTTModelMixin, ComponentModel):
    """A serialized piece of hardware within a Device."""

    _modelname = "inventoryitem"
    _attributes = (
        *ComponentModel._attributes,
        "device",
        "parent",
        "name",
        "manufacturer",
        "part_id",
        "serial",
        "asset_tag",
        "discovered",
    )
    _nautobot_model = dcim.InventoryItem

    parent: Optional[InventoryItemRef]
    manufacturer: Optional[ManufacturerRef]
    part_id: str
    serial: str
    asset_tag: Optional[str]
    discovered: bool

JobResult

Bases: NautobotBaseModel

Results of running a Job / Script / Report.

Source code in nautobot_netbox_importer/diffsync/models/extras.py
class JobResult(NautobotBaseModel):
    """Results of running a Job / Script / Report."""

    _modelname = "jobresult"
    _attributes = ("job_id", "name", "obj_type", "completed", "user", "status", "data")
    _nautobot_model = extras.JobResult

    job_id: UUID

    name: str
    obj_type: ContentTypeRef
    completed: Optional[datetime]
    user: Optional[UserRef]
    status: str  # not a StatusRef!
    data: Optional[JobResultData]

    created: Optional[datetime]  # Not synced

Manufacturer

Bases: OrganizationalModel

A Manufacturer represents a company which produces hardware devices.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class Manufacturer(OrganizationalModel):
    """A Manufacturer represents a company which produces hardware devices."""

    _modelname = "manufacturer"
    _attributes = (*OrganizationalModel._attributes, "name", "slug", "description")
    _nautobot_model = dcim.Manufacturer

    name: str
    slug: str
    description: str

Note

Bases: ChangeLoggedModelMixin, NautobotBaseModel

Representation of NetBox JournalEntry to Nautobot Note.

NetBox fields ignored: kind Nautobot fields not supported by NetBox: user_name, slug

Source code in nautobot_netbox_importer/diffsync/models/extras.py
class Note(ChangeLoggedModelMixin, NautobotBaseModel):
    """
    Representation of NetBox JournalEntry to Nautobot Note.

    NetBox fields ignored: kind
    Nautobot fields not supported by NetBox: user_name, slug
    """

    _modelname = "note"
    _attributes = (
        *ChangeLoggedModelMixin._attributes,
        "assigned_object_type",
        "assigned_object_id",
        "user",
        "note",
    )
    _nautobot_model = extras.Note

    assigned_object_type: ContentTypeRef
    _assigned_object_id = foreign_key_field("*assigned_object_type")
    assigned_object_id: _assigned_object_id
    # NetBox uses `created_by` where Nautobot uses `user`
    user: UserRef = Field(alias="created_by")
    # NetBox uses `comments` where Nautobot uses `note`
    note: str = Field(alias="comments")

    class Config:
        """Pydantic configuration of the Note class."""

        # Allow both "url" and "target_url" as property setters
        allow_population_by_field_name = True
Config

Pydantic configuration of the Note class.

Source code in nautobot_netbox_importer/diffsync/models/extras.py
class Config:
    """Pydantic configuration of the Note class."""

    # Allow both "url" and "target_url" as property setters
    allow_population_by_field_name = True

ObjectPermission

Bases: NautobotBaseModel

A mapping of view, add, change, and/or delete permissions for users and/or groups.

Source code in nautobot_netbox_importer/diffsync/models/users.py
class ObjectPermission(NautobotBaseModel):
    """A mapping of view, add, change, and/or delete permissions for users and/or groups."""

    _modelname = "objectpermission"
    _attributes = ("name", "object_types", "groups", "users", "actions", "constraints", "description", "enabled")
    _nautobot_model = users.ObjectPermission

    name: str

    object_types: List[ContentTypeRef] = []
    groups: List[GroupRef] = []
    users: List[UserRef] = []
    actions: ArrayField
    constraints: Optional[Union[dict, list]]
    description: str
    enabled: bool

Permission

Bases: DjangoBaseModel

Definition of a permissions rule.

Source code in nautobot_netbox_importer/diffsync/models/auth.py
class Permission(DjangoBaseModel):
    """Definition of a permissions rule."""

    _modelname = "permission"
    _identifiers = ("content_type", "codename")
    _attributes = ("name",)
    _nautobot_model = auth.Permission

    content_type: ContentTypeRef
    codename: str

    name: str

    def __init__(self, *args, **kwargs):
        """Set the IGNORE flag on permissions that refer to content-types that do not exist in Nautobot."""
        super().__init__(*args, **kwargs)
        if (
            self.content_type["app_label"]
            not in [
                "auth",
                "circuits",
                "contenttypes",
                "dcim",
                "extras",
                "ipam",
                "references",
                "taggit",
                "tenancy",
                "users",
                "virtualization",
            ]
            or self.content_type["model"] not in self.diffsync.top_level
        ):
            structlog.get_logger().debug(
                "Flagging permission for extraneous content-type as ignorable",
                diffsync=self.diffsync,
                app_label=self.content_type["app_label"],
                model=self.content_type["model"],
            )
            self.model_flags |= DiffSyncModelFlags.IGNORE
__init__(args, kwargs)

Set the IGNORE flag on permissions that refer to content-types that do not exist in Nautobot.

Source code in nautobot_netbox_importer/diffsync/models/auth.py
def __init__(self, *args, **kwargs):
    """Set the IGNORE flag on permissions that refer to content-types that do not exist in Nautobot."""
    super().__init__(*args, **kwargs)
    if (
        self.content_type["app_label"]
        not in [
            "auth",
            "circuits",
            "contenttypes",
            "dcim",
            "extras",
            "ipam",
            "references",
            "taggit",
            "tenancy",
            "users",
            "virtualization",
        ]
        or self.content_type["model"] not in self.diffsync.top_level
    ):
        structlog.get_logger().debug(
            "Flagging permission for extraneous content-type as ignorable",
            diffsync=self.diffsync,
            app_label=self.content_type["app_label"],
            model=self.content_type["model"],
        )
        self.model_flags |= DiffSyncModelFlags.IGNORE

Platform

Bases: OrganizationalModel

Platform refers to the software or firmware running on a device.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class Platform(OrganizationalModel):
    """Platform refers to the software or firmware running on a device."""

    _modelname = "platform"
    _attributes = (
        *OrganizationalModel._attributes,
        "name",
        "slug",
        "manufacturer",
        "napalm_driver",
        "napalm_args",
        "description",
    )
    _nautobot_model = dcim.Platform

    name: str
    slug: str
    manufacturer: Optional[ManufacturerRef]
    napalm_driver: str
    napalm_args: Optional[dict]
    description: str

PowerFeed

Bases: CableTerminationMixin, StatusModelMixin, PrimaryModel

An electrical circuit delivered from a PowerPanel.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class PowerFeed(CableTerminationMixin, StatusModelMixin, PrimaryModel):
    """An electrical circuit delivered from a PowerPanel."""

    _modelname = "powerfeed"
    _attributes = (
        *PrimaryModel._attributes,
        *CableTerminationMixin._attributes,
        *StatusModelMixin._attributes,
        "power_panel",
        "name",
        "rack",
        "type",
        "supply",
        "phase",
        "voltage",
        "amperage",
        "max_utilization",
        "available_power",
        "comments",
    )
    _nautobot_model = dcim.PowerFeed

    power_panel: PowerPanelRef
    name: str

    rack: Optional[RackRef]
    type: str
    supply: str
    phase: str
    voltage: int
    amperage: int
    max_utilization: int
    available_power: int
    comments: str

PowerOutlet

Bases: CableTerminationMixin, ComponentModel

A physical power outlet (output) within a Device.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class PowerOutlet(CableTerminationMixin, ComponentModel):
    """A physical power outlet (output) within a Device."""

    _modelname = "poweroutlet"
    _attributes = (*ComponentModel._attributes, *CableTerminationMixin._attributes, "type", "power_port", "feed_leg")
    _nautobot_model = dcim.PowerOutlet

    type: str
    power_port: Optional[PowerPortRef]
    feed_leg: str

    _type_choices = set(dcim_choices.PowerOutletTypeChoices.values())

PowerOutletTemplate

Bases: ComponentTemplateModel

A template for a PowerOutlet.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class PowerOutletTemplate(ComponentTemplateModel):
    """A template for a PowerOutlet."""

    _modelname = "poweroutlettemplate"
    _attributes = (*ComponentTemplateModel._attributes, "type", "power_port", "feed_leg")
    _nautobot_model = dcim.PowerOutletTemplate

    type: str
    power_port: Optional[PowerPortTemplateRef]
    feed_leg: str

    _type_choices = set(dcim_choices.PowerOutletTypeChoices.values())

PowerPanel

Bases: PrimaryModel

A distribution point for electrical power.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class PowerPanel(PrimaryModel):
    """A distribution point for electrical power."""

    _modelname = "powerpanel"
    _attributes = (*PrimaryModel._attributes, "site", "name", "rack_group")
    _nautobot_model = dcim.PowerPanel

    site: SiteRef
    name: str
    rack_group: Optional[RackGroupRef]

PowerPort

Bases: CableTerminationMixin, ComponentModel

A physical power supply (input) port within a Device.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class PowerPort(CableTerminationMixin, ComponentModel):
    """A physical power supply (input) port within a Device."""

    _modelname = "powerport"
    _attributes = (
        *ComponentModel._attributes,
        *CableTerminationMixin._attributes,
        "type",
        "maximum_draw",
        "allocated_draw",
    )
    _nautobot_model = dcim.PowerPort

    type: str
    maximum_draw: Optional[int]
    allocated_draw: Optional[int]

    _type_choices = set(dcim_choices.PowerPortTypeChoices.values())

PowerPortTemplate

Bases: ComponentTemplateModel

A template for a PowerPort.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class PowerPortTemplate(ComponentTemplateModel):
    """A template for a PowerPort."""

    _modelname = "powerporttemplate"
    _attributes = (*ComponentTemplateModel._attributes, "type", "maximum_draw", "allocated_draw")
    _nautobot_model = dcim.PowerPortTemplate

    type: str
    maximum_draw: Optional[int]
    allocated_draw: Optional[int]

    _type_choices = set(dcim_choices.PowerPortTypeChoices.values())

Prefix

Bases: StatusModelMixin, PrimaryModel

An IPv4 or IPv4 network, including mask.

Source code in nautobot_netbox_importer/diffsync/models/ipam.py
class Prefix(StatusModelMixin, PrimaryModel):
    """An IPv4 or IPv4 network, including mask."""

    _modelname = "prefix"
    _attributes = (
        *PrimaryModel._attributes,
        *StatusModelMixin._attributes,
        "prefix",
        "vrf",
        "site",
        "tenant",
        "vlan",
        "role",
        "is_pool",
        "description",
    )
    _nautobot_model = ipam.Prefix

    prefix: netaddr.IPNetwork
    vrf: Optional[VRFRef]
    site: Optional[SiteRef]
    tenant: Optional[TenantRef]
    vlan: Optional[VLANRef]
    role: Optional[RoleRef]
    is_pool: bool
    description: str

    def __init__(self, *args, **kwargs):
        """Clean up prefix to an IPNetwork before initializing as normal."""
        if "prefix" in kwargs:
            # NetBox import
            if isinstance(kwargs["prefix"], str):
                kwargs["prefix"] = netaddr.IPNetwork(kwargs["prefix"])
        else:
            # Nautobot import
            kwargs["prefix"] = network_from_components(kwargs["network"], kwargs["prefix_length"])
            del kwargs["network"]
            del kwargs["broadcast"]
            del kwargs["prefix_length"]
        super().__init__(*args, **kwargs)
__init__(args, kwargs)

Clean up prefix to an IPNetwork before initializing as normal.

Source code in nautobot_netbox_importer/diffsync/models/ipam.py
def __init__(self, *args, **kwargs):
    """Clean up prefix to an IPNetwork before initializing as normal."""
    if "prefix" in kwargs:
        # NetBox import
        if isinstance(kwargs["prefix"], str):
            kwargs["prefix"] = netaddr.IPNetwork(kwargs["prefix"])
    else:
        # Nautobot import
        kwargs["prefix"] = network_from_components(kwargs["network"], kwargs["prefix_length"])
        del kwargs["network"]
        del kwargs["broadcast"]
        del kwargs["prefix_length"]
    super().__init__(*args, **kwargs)

Provider

Bases: PrimaryModel

Each Circuit belongs to a Provider.

Source code in nautobot_netbox_importer/diffsync/models/circuits.py
class Provider(PrimaryModel):
    """Each Circuit belongs to a Provider."""

    _modelname = "provider"
    _attributes = (
        *PrimaryModel._attributes,
        "name",
        "slug",
        "asn",
        "account",
        "portal_url",
        "noc_contact",
        "admin_contact",
        "comments",
    )
    _nautobot_model = circuits.Provider

    name: str
    slug: str
    asn: Optional[int]
    account: str
    portal_url: str
    noc_contact: str
    admin_contact: str
    comments: str

ProviderNetwork

Bases: PrimaryModel

Service Provider Network Model.

Source code in nautobot_netbox_importer/diffsync/models/circuits.py
class ProviderNetwork(PrimaryModel):
    """Service Provider Network Model."""

    _modelname = "providernetwork"
    _attributes = (
        *PrimaryModel._attributes,
        "name",
        "slug",
        "provider",
        "description",
        "comments",
    )
    _nautobot_model = circuits.ProviderNetwork

    name: str
    slug: str
    provider: ProviderRef
    description: Optional[str]
    comments: Optional[str]

RIR

Bases: OrganizationalModel

A Regional Internet Registry (RIR).

Source code in nautobot_netbox_importer/diffsync/models/ipam.py
class RIR(OrganizationalModel):
    """A Regional Internet Registry (RIR)."""

    _modelname = "rir"
    _attributes = (*OrganizationalModel._attributes, "name", "slug", "is_private", "description")
    _nautobot_model = ipam.RIR

    name: str
    slug: str
    is_private: bool
    description: str

Rack

Bases: StatusModelMixin, PrimaryModel

Devices are housed within Racks.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class Rack(StatusModelMixin, PrimaryModel):
    """Devices are housed within Racks."""

    _modelname = "rack"
    _attributes = (
        *PrimaryModel._attributes,
        *StatusModelMixin._attributes,
        "group",
        "name",
        "facility_id",
        "site",
        "tenant",
        "role",
        "serial",
        "asset_tag",
        "type",
        "width",
        "u_height",
        "desc_units",
        "outer_width",
        "outer_depth",
        "outer_unit",
        "comments",
    )
    _nautobot_model = dcim.Rack

    group: Optional[RackGroupRef]
    name: str

    facility_id: Optional[str]
    site: SiteRef
    tenant: Optional[TenantRef]
    role: Optional[RackRoleRef]
    serial: str
    asset_tag: Optional[str]
    type: str
    width: int
    u_height: int
    desc_units: bool
    outer_width: Optional[int]
    outer_depth: Optional[int]
    outer_unit: str
    comments: str

    _type_choices = set(dcim_choices.RackTypeChoices.values())

    @root_validator
    def invalid_type_to_other(cls, values):  # pylint: disable=no-self-argument,no-self-use
        """
        Default invalid `type` fields to use `other` type.

        The `type` field uses a ChoiceSet to limit valid choices. This uses Pydantic's
        root_validator to clean up the `type` data before loading it into a Model
        instance. All invalid types will be changed to "other."
        """
        rack_type = values["type"]
        if rack_type not in cls._type_choices:
            values["type"] = "other"
            site = values["site"]
            tenant = values.get("tenant")
            name = values["name"]
            logger.warning(
                f"Encountered a NetBox {cls._modelname}.type that is not valid in this version of Nautobot, will convert it",
                site=site,
                tenant=tenant,
                name=name,
                netbox_type=rack_type,
                nautobot_type="other",
            )
        return values
invalid_type_to_other(values)

Default invalid type fields to use other type.

The type field uses a ChoiceSet to limit valid choices. This uses Pydantic's root_validator to clean up the type data before loading it into a Model instance. All invalid types will be changed to "other."

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
@root_validator
def invalid_type_to_other(cls, values):  # pylint: disable=no-self-argument,no-self-use
    """
    Default invalid `type` fields to use `other` type.

    The `type` field uses a ChoiceSet to limit valid choices. This uses Pydantic's
    root_validator to clean up the `type` data before loading it into a Model
    instance. All invalid types will be changed to "other."
    """
    rack_type = values["type"]
    if rack_type not in cls._type_choices:
        values["type"] = "other"
        site = values["site"]
        tenant = values.get("tenant")
        name = values["name"]
        logger.warning(
            f"Encountered a NetBox {cls._modelname}.type that is not valid in this version of Nautobot, will convert it",
            site=site,
            tenant=tenant,
            name=name,
            netbox_type=rack_type,
            nautobot_type="other",
        )
    return values

RackGroup

Bases: MPTTModelMixin, OrganizationalModel

Racks can be grouped as subsets within a Site.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class RackGroup(MPTTModelMixin, OrganizationalModel):
    """Racks can be grouped as subsets within a Site."""

    _modelname = "rackgroup"
    _attributes = (*OrganizationalModel._attributes, "site", "name", "slug", "parent", "description")
    # Not all Racks belong to a RackGroup, so we don't treat Racks as a child.
    _nautobot_model = dcim.RackGroup

    site: SiteRef
    name: str
    slug: str
    parent: Optional[RackGroupRef]
    description: str

RackReservation

Bases: PrimaryModel

One or more reserved units within a Rack.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class RackReservation(PrimaryModel):
    """One or more reserved units within a Rack."""

    _modelname = "rackreservation"
    _attributes = (*PrimaryModel._attributes, "rack", "units", "tenant", "user", "description")
    _nautobot_model = dcim.RackReservation

    rack: RackRef
    units: ArrayField
    tenant: Optional[TenantRef]
    user: UserRef
    description: str

RackRole

Bases: OrganizationalModel

Racks can be organized by functional role.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class RackRole(OrganizationalModel):
    """Racks can be organized by functional role."""

    _modelname = "rackrole"
    _attributes = (*OrganizationalModel._attributes, "name", "slug", "color", "description")
    _nautobot_model = dcim.RackRole

    name: str
    slug: str
    color: str
    description: str

RearPort

Bases: CableTerminationMixin, ComponentModel

A pass-through port on the rear of a Device.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class RearPort(CableTerminationMixin, ComponentModel):
    """A pass-through port on the rear of a Device."""

    _modelname = "rearport"
    _attributes = (
        *ComponentModel._attributes,
        *CableTerminationMixin._attributes,
        "type",
        "positions",
    )
    _nautobot_model = dcim.RearPort

    type: str
    positions: int

    _type_choices = set(dcim_choices.PortTypeChoices.values())

RearPortTemplate

Bases: ComponentTemplateModel

A template for a RearPort.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class RearPortTemplate(ComponentTemplateModel):
    """A template for a RearPort."""

    _modelname = "rearporttemplate"
    _attributes = (*ComponentTemplateModel._attributes, "type", "positions")
    _nautobot_model = dcim.RearPortTemplate

    type: str
    positions: int

    _type_choices = set(dcim_choices.PortTypeChoices.values())

Region

Bases: MPTTModelMixin, OrganizationalModel

Sites can be grouped within geographic Regions.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class Region(MPTTModelMixin, OrganizationalModel):
    """Sites can be grouped within geographic Regions."""

    _modelname = "region"
    _attributes = (*OrganizationalModel._attributes, "name", "parent", "slug", "description")
    # Not all Sites belong to a Region, so we don't treat Sites as a child.
    _nautobot_model = dcim.Region

    name: str
    parent: Optional[RegionRef]
    slug: str
    description: str

Role

Bases: OrganizationalModel

The functional role of a Prefix or VLAN.

Source code in nautobot_netbox_importer/diffsync/models/ipam.py
class Role(OrganizationalModel):
    """The functional role of a Prefix or VLAN."""

    _modelname = "role"  # ambiguous much?
    _attributes = (*OrganizationalModel._attributes, "name", "slug", "weight", "description")
    _nautobot_model = ipam.Role

    name: str
    slug: str
    weight: int
    description: str

RouteTarget

Bases: PrimaryModel

A BGP extended community.

Source code in nautobot_netbox_importer/diffsync/models/ipam.py
class RouteTarget(PrimaryModel):
    """A BGP extended community."""

    _modelname = "routetarget"
    _attributes = (*PrimaryModel._attributes, "name", "description", "tenant")
    _nautobot_model = ipam.RouteTarget

    name: str
    description: str
    tenant: Optional[TenantRef]

Service

Bases: PrimaryModel

A layer-four service such as HTTP or SSH.

Source code in nautobot_netbox_importer/diffsync/models/ipam.py
class Service(PrimaryModel):
    """A layer-four service such as HTTP or SSH."""

    _modelname = "service"
    _attributes = (
        *PrimaryModel._attributes,
        "device",
        "virtual_machine",
        "protocol",
        "ports",
        "name",
        "ipaddresses",
        "description",
    )
    _nautobot_model = ipam.Service

    device: Optional[DeviceRef]
    virtual_machine: Optional[VirtualMachineRef]
    protocol: str
    ports: ArrayField

    name: str
    ipaddresses: List[IPAddressRef]
    description: str

Site

Bases: StatusModelMixin, PrimaryModel

A Site represents a geographic location within a network.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class Site(StatusModelMixin, PrimaryModel):
    """A Site represents a geographic location within a network."""

    _modelname = "site"
    _attributes = (
        *PrimaryModel._attributes,
        *StatusModelMixin._attributes,
        "name",
        "slug",
        "region",
        "tenant",
        "facility",
        "asn",
        "time_zone",
        "description",
        "physical_address",
        "shipping_address",
        "latitude",
        "longitude",
        "contact_name",
        "contact_phone",
        "contact_email",
        "comments",
    )
    _nautobot_model = dcim.Site

    name: str
    slug: str
    region: Optional[RegionRef]
    tenant: Optional[TenantRef]
    facility: str
    asn: Optional[int]
    time_zone: Optional[str]
    description: str
    physical_address: str
    shipping_address: str
    latitude: Optional[float]
    longitude: Optional[float]
    contact_name: str
    contact_phone: str
    contact_email: str
    comments: str

    _name: Optional[str]
    images: Any

    def __init__(self, *args, **kwargs):
        """Explicitly convert time_zone to a string if needed."""
        if "time_zone" in kwargs and kwargs["time_zone"]:
            kwargs["time_zone"] = str(kwargs["time_zone"])
        super().__init__(*args, **kwargs)
__init__(args, kwargs)

Explicitly convert time_zone to a string if needed.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
def __init__(self, *args, **kwargs):
    """Explicitly convert time_zone to a string if needed."""
    if "time_zone" in kwargs and kwargs["time_zone"]:
        kwargs["time_zone"] = str(kwargs["time_zone"])
    super().__init__(*args, **kwargs)

Status

Bases: ChangeLoggedModelMixin, NautobotBaseModel

Representation of a status value.

Source code in nautobot_netbox_importer/diffsync/models/extras.py
class Status(ChangeLoggedModelMixin, NautobotBaseModel):
    """Representation of a status value."""

    _modelname = "status"
    _attributes = ("slug", "name", "color", "description", *ChangeLoggedModelMixin._attributes)  # TODO content_types?
    _nautobot_model = extras.Status

    slug: str
    name: str
    color: str
    description: str

    content_types: List = []

Tag

Bases: ChangeLoggedModelMixin, CustomFieldModelMixin, NautobotBaseModel

A tag that can be associated with various objects.

Source code in nautobot_netbox_importer/diffsync/models/extras.py
class Tag(ChangeLoggedModelMixin, CustomFieldModelMixin, NautobotBaseModel):
    """A tag that can be associated with various objects."""

    _modelname = "tag"
    _attributes = (
        *CustomFieldModelMixin._attributes,
        "name",
        "slug",
        "color",
        "description",
        *ChangeLoggedModelMixin._attributes,
    )
    _nautobot_model = extras.Tag

    name: str

    slug: str
    color: str
    description: str

TaggedItem

Bases: NautobotBaseModel

Mapping between a record and a Tag.

Source code in nautobot_netbox_importer/diffsync/models/extras.py
class TaggedItem(NautobotBaseModel):
    """Mapping between a record and a Tag."""

    _modelname = "taggeditem"
    _attributes = ("content_type", "object_id", "tag")
    _nautobot_model = extras.TaggedItem

    content_type: ContentTypeRef
    _object_id = foreign_key_field("*content_type")
    object_id: _object_id
    tag: TagRef

Tenant

Bases: PrimaryModel

A Tenant represents an organization served by the application owner.

Source code in nautobot_netbox_importer/diffsync/models/tenancy.py
class Tenant(PrimaryModel):
    """A Tenant represents an organization served by the application owner."""

    _modelname = "tenant"
    _attributes = (*PrimaryModel._attributes, "name", "slug", "group", "description", "comments")
    _nautobot_model = tenancy.Tenant

    name: str
    slug: str
    group: Optional[TenantGroupRef]
    description: str
    comments: str

TenantGroup

Bases: MPTTModelMixin, OrganizationalModel

An arbitrary collection of Tenants.

Source code in nautobot_netbox_importer/diffsync/models/tenancy.py
class TenantGroup(MPTTModelMixin, OrganizationalModel):
    """An arbitrary collection of Tenants."""

    _modelname = "tenantgroup"
    _attributes = (*OrganizationalModel._attributes, "name", "slug", "parent", "description")
    # Not all Tenants belong to a TenantGroup, so we don't treat them as children
    _nautobot_model = tenancy.TenantGroup

    name: str
    slug: str
    parent: Optional[TenantGroupRef]
    description: str

Token

Bases: NautobotBaseModel

An API token used for user authentication.

Source code in nautobot_netbox_importer/diffsync/models/users.py
class Token(NautobotBaseModel):
    """An API token used for user authentication."""

    _modelname = "token"
    _attributes = ("key", "user", "expires", "write_enabled", "description")
    _nautobot_model = users.Token

    key: str

    user: UserRef
    expires: Optional[datetime]
    write_enabled: bool
    description: str

User

Bases: NautobotBaseModel

A user account, for authentication and authorization purposes.

Note that in NetBox this is actually two separate models - Django's built-in User class, and a custom UserConfig class - while in Nautobot it is a single custom User class model.

Source code in nautobot_netbox_importer/diffsync/models/users.py
class User(NautobotBaseModel):
    """A user account, for authentication and authorization purposes.

    Note that in NetBox this is actually two separate models - Django's built-in User class, and
    a custom UserConfig class - while in Nautobot it is a single custom User class model.
    """

    _modelname = "user"
    _attributes = (
        "username",
        "first_name",
        "last_name",
        "email",
        "password",
        "is_staff",
        "is_active",
        "is_superuser",
        "date_joined",
        "groups",
        "user_permissions",
        "config_data",
    )
    _nautobot_model = get_user_model()

    username: str
    first_name: str
    last_name: str
    email: str
    password: str
    groups: List[GroupRef] = []
    user_permissions: List[PermissionRef] = []
    is_staff: bool
    is_active: bool
    is_superuser: bool
    date_joined: datetime
    config_data: dict

    last_login: Optional[datetime]

VLAN

Bases: StatusModelMixin, PrimaryModel

A distinct layer two forwarding domain.

Source code in nautobot_netbox_importer/diffsync/models/ipam.py
class VLAN(StatusModelMixin, PrimaryModel):
    """A distinct layer two forwarding domain."""

    _modelname = "vlan"
    _attributes = (
        *PrimaryModel._attributes,
        *StatusModelMixin._attributes,
        "group",
        "vid",
        "name",
        "site",
        "tenant",
        "status",
        "role",
        "description",
    )
    _nautobot_model = ipam.VLAN

    name: str
    vid: int
    group: Optional[VLANGroupRef]
    site: Optional[SiteRef]
    tenant: Optional[TenantRef]
    role: Optional[RoleRef]
    description: str

VLANGroup

Bases: OrganizationalModel

An arbitrary collection of VLANs.

Source code in nautobot_netbox_importer/diffsync/models/ipam.py
class VLANGroup(OrganizationalModel):
    """An arbitrary collection of VLANs."""

    _modelname = "vlangroup"
    _attributes = (*OrganizationalModel._attributes, "site", "name", "slug", "description")
    _nautobot_model = ipam.VLANGroup

    name: str
    slug: str
    site: Optional[SiteRef]
    description: str

VMInterface

Bases: CustomFieldModelMixin, BaseInterfaceMixin, NautobotBaseModel

An interface on a VirtualMachine.

Source code in nautobot_netbox_importer/diffsync/models/virtualization.py
class VMInterface(CustomFieldModelMixin, BaseInterfaceMixin, NautobotBaseModel):
    """An interface on a VirtualMachine."""

    _modelname = "vminterface"
    _attributes = (
        *CustomFieldModelMixin._attributes,
        *BaseInterfaceMixin._attributes,
        "virtual_machine",
        "name",
        "description",
        "untagged_vlan",
        "tagged_vlans",
    )
    _nautobot_model = virtualization.VMInterface

    virtual_machine: VirtualMachineRef
    name: str
    description: str
    untagged_vlan: Optional[VLANRef]
    tagged_vlans: List[VLANRef] = []

VRF

Bases: PrimaryModel

A virtual routing and forwarding (VRF) table.

Source code in nautobot_netbox_importer/diffsync/models/ipam.py
class VRF(PrimaryModel):
    """A virtual routing and forwarding (VRF) table."""

    _modelname = "vrf"
    _attributes = (
        *PrimaryModel._attributes,
        "name",
        "rd",
        "tenant",
        "enforce_unique",
        "description",
        "import_targets",
        "export_targets",
    )
    _nautobot_model = ipam.VRF

    name: str
    rd: Optional[str]
    tenant: Optional[TenantRef]

    enforce_unique: bool
    description: str
    import_targets: List[RouteTargetRef] = []
    export_targets: List[RouteTargetRef] = []

VirtualChassis

Bases: PrimaryModel

A collection of Devices which operate with a shared control plane.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
class VirtualChassis(PrimaryModel):
    """A collection of Devices which operate with a shared control plane."""

    _modelname = "virtualchassis"
    _attributes = (*PrimaryModel._attributes, "master", "name", "domain")
    _nautobot_model = dcim.VirtualChassis

    master: Optional[DeviceRef]
    name: str
    domain: str

    @classmethod
    def create(cls, diffsync: DiffSync, ids: Mapping, attrs: Mapping) -> Optional[NautobotBaseModel]:
        """Create an instance of this model, both in Nautobot and in DiffSync.

        There is an odd behavior (bug?) in Nautobot 1.0.0 wherein when creating a VirtualChassis with
        a predefined "master" Device, it changes the master device's position to 1 regardless of what
        it was previously configured to. This will cause us problems later when we attempt to associate
        member devices with the VirtualChassis if there's a member that's supposed to be using position 1.
        So, we take it upon ourselves to overrule Nautobot and put the master back into the position it's
        supposed to be in.
        """
        diffsync_record = super().create(diffsync, ids, attrs)
        if diffsync_record is not None and diffsync_record.master is not None:  # pylint: disable=no-member
            nautobot_record = cls.nautobot_model().objects.get(**ids)
            nautobot_master = nautobot_record.master
            diffsync_master = diffsync.get("device", str(nautobot_master.pk))
            if nautobot_master.vc_position != diffsync_master.vc_position:
                logger.debug(
                    "Fixing up master device vc_position",
                    virtual_chassis=diffsync_record,
                    incorrect_position=nautobot_master.vc_position,
                    correct_position=diffsync_master.vc_position,
                )
                nautobot_master.vc_position = diffsync_master.vc_position
                nautobot_master.save()

        return diffsync_record
create(diffsync, ids, attrs) classmethod

Create an instance of this model, both in Nautobot and in DiffSync.

There is an odd behavior (bug?) in Nautobot 1.0.0 wherein when creating a VirtualChassis with a predefined "master" Device, it changes the master device's position to 1 regardless of what it was previously configured to. This will cause us problems later when we attempt to associate member devices with the VirtualChassis if there's a member that's supposed to be using position 1. So, we take it upon ourselves to overrule Nautobot and put the master back into the position it's supposed to be in.

Source code in nautobot_netbox_importer/diffsync/models/dcim.py
@classmethod
def create(cls, diffsync: DiffSync, ids: Mapping, attrs: Mapping) -> Optional[NautobotBaseModel]:
    """Create an instance of this model, both in Nautobot and in DiffSync.

    There is an odd behavior (bug?) in Nautobot 1.0.0 wherein when creating a VirtualChassis with
    a predefined "master" Device, it changes the master device's position to 1 regardless of what
    it was previously configured to. This will cause us problems later when we attempt to associate
    member devices with the VirtualChassis if there's a member that's supposed to be using position 1.
    So, we take it upon ourselves to overrule Nautobot and put the master back into the position it's
    supposed to be in.
    """
    diffsync_record = super().create(diffsync, ids, attrs)
    if diffsync_record is not None and diffsync_record.master is not None:  # pylint: disable=no-member
        nautobot_record = cls.nautobot_model().objects.get(**ids)
        nautobot_master = nautobot_record.master
        diffsync_master = diffsync.get("device", str(nautobot_master.pk))
        if nautobot_master.vc_position != diffsync_master.vc_position:
            logger.debug(
                "Fixing up master device vc_position",
                virtual_chassis=diffsync_record,