Repository: nautobot/nautobot

Issue:
Cable paths likely to become corrupted after dumpdata/loaddata, such as postgres/mysql migration
### Environment
* Nautobot version (Docker tag too if applicable): 1.4.2
* Python version: any
* Database platform, version: postgres, mysql
* Middleware(s): n/a

### Steps to Reproduce
1. Have a Postgres database with connected cables and traced cable paths
2. Export data via `nautobot-server dumpdata ...` [as described in our docs](https://nautobot.readthedocs.io/en/stable/installation/migrating-from-postgresql/#export-data-from-postgresql)
3. Import exported data into MySQL as described in the docs
4. View the interfaces of a device that has cables with traces

<!-- What did you expect to happen? -->
### Expected Behavior

Cable traces to be available and correct

<!-- What happened instead? -->
### Observed Behavior

The obvious symptom is a KeyError being thrown from `CablePath.get_path()`, however that specific error is going to be tracked and fixed as a separate GitHub issue (adding error handling in `get_path()` to address the case where a cable path is corrupt or invalid. I'm filing this issue to focus on the *cause* of the path corruption, which is a result of two details interacting:

1. In our dumpdata/loaddata, we do not include `contenttypes.ContentType`, expecting that Django will handle creation of contenttypes records in the target system instead. As a result, content-type PKs are *not* guaranteed to be the same between the two databases
2. The `CablePath.path` field stores denormalized content-type PKs, using the style `["<content-type PK>:<CableTermination PK>", "..."]`, for example `['2:7b33b9c2-2f4b-4f2d-8732-befa4d90776e', '48:bfbf506a-d4cf-4f37-96a0-3f7e2cd38c00', '62:bb12a028-5a83-481e-971d-2a5fb6396fa9', '2:6aae157c-2d61-4c52-a5ab-2376851d7fbf']` Since these are strings, and not actual foreign-keys at the database level, they are not validated and are not converted to natural-keys (e.g. ["dcim", "interface"]) when the data is dumped.

Hence, post-import, the CablePath may contain invalid content-types and/or invalid combinations of content-type PK and instance UUID.

Offhand, the idea that comes to mind as a possible fix is to migrate things to use natural-keys for the content-type in the `path` database field, i.e. instead of `'2:7b33b9c2-2f4b-4f2d-8732-befa4d90776e'`, store the path node as `'dcim.interface:7b33b9c2-2f4b-4f2d-8732-befa4d90776e'`. (Once we have natural keys for the CableTermination models as well, we can take this the rest of the way as something like `dcim.interface:device-1,ethernet0`...)


Discussion:
The workaround is to run `nautobot-server trace_paths --force` to force regeneration of all CablePaths after the import.
Proper fix may be blocked until the natural key portion of #1574, but in the meantime an improvement to documentation should be done for this scenario.
@lampwins

Would it be acceptable to just recommend never exporting cable path data (`dumpdata --exclude dcim.cablepath`) and that `nautobot-server trace_paths --force` should always be run after import? Instead of overhauling how this cache is stored.
@bryanculver makes sense to me.
> @bryanculver makes sense to me.

:shipit: 
Drat. We can't just `--exclude dcim.cablepath` because `Interface._path` is a cached foreign key reference, so we get an error on import of the dump:

```python
Traceback (most recent call last):
  File "/usr/local/bin/nautobot-server", line 6, in <module>
    sys.exit(main())
  File "/source/nautobot/core/cli.py", line 61, in main
    initializer=_configure_settings,  # Called after defaults
  File "/source/nautobot/core/runner/runner.py", line 263, in run_app
    management.execute_from_command_line([runner_name, command] + command_args)
  File "/usr/local/lib/python3.7/site-packages/django/core/management/__init__.py", line 419, in execute_from_command_line
    utility.execute()
  File "/usr/local/lib/python3.7/site-packages/django/core/management/__init__.py", line 413, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/usr/local/lib/python3.7/site-packages/django/core/management/base.py", line 354, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/usr/local/lib/python3.7/site-packages/django/core/management/base.py", line 398, in execute
    output = self.handle(*args, **options)
  File "/usr/local/lib/python3.7/site-packages/django/core/management/commands/loaddata.py", line 78, in handle
    self.loaddata(fixture_labels)
  File "/usr/local/lib/python3.7/site-packages/django/core/management/commands/loaddata.py", line 131, in loaddata
    connection.check_constraints(table_names=table_names)
  File "/usr/local/lib/python3.7/site-packages/django/db/backends/mysql/base.py", line 333, in check_constraints
    bad_row[1], referenced_table_name, referenced_column_name,
django.db.utils.IntegrityError: Problem installing fixtures: The row in table 'dcim_interface' with primary key '3a81f4782a424eeb9e061024b92e6ca9' has an invalid foreign key: dcim_interface._path_id contains a value '16fc53369dd04f4a9522ed1f4cf5de71' that does not have a corresponding value in dcim_cablepath.id.
```

Maybe the user needs to do a  `CablePath.objects.all().delete()` before doing `dumpdata`?

Resolving patch:
diff --git a/nautobot/core/signals.py b/nautobot/core/signals.py
--- a/nautobot/core/signals.py
+++ b/nautobot/core/signals.py
@@ -1,4 +1,6 @@
 """Custom signals and handlers for the core Nautobot application."""
+from functools import wraps
+import inspect
 import logging
 
 from django.contrib.auth.signals import user_logged_in, user_logged_out
@@ -33,3 +35,24 @@ def user_logged_out_signal(sender, request, user, **kwargs):
     """Generate a log message when a user logs out from the web ui"""
     logger = logging.getLogger("nautobot.auth.logout")
     logger.info(f"User {user} has logged out")
+
+
+def disable_for_loaddata(signal_handler):
+    """
+    Return early from the given signal handler if triggered during a `nautobot-server loaddata` call.
+
+    Necessary because for whatever reason, Django's `m2m_changed` signal lacks a `raw` flag and so there's no easy way
+    to tell whether any given m2m_changed signal handler is being called from loaddata otherwise.
+
+    Copied shamelessly from https://code.djangoproject.com/ticket/8399#comment:7
+    """
+
+    @wraps(signal_handler)
+    def wrapper(*args, **kwargs):
+        """Return early if loaddata is part of the stack."""
+        for fr in inspect.stack():
+            if inspect.getmodulename(fr[1]) == "loaddata":
+                return
+        signal_handler(*args, **kwargs)
+
+    return wrapper
diff --git a/nautobot/dcim/signals.py b/nautobot/dcim/signals.py
--- a/nautobot/dcim/signals.py
+++ b/nautobot/dcim/signals.py
@@ -6,6 +6,7 @@
 from django.db import transaction
 from django.dispatch import receiver
 
+from nautobot.core.signals import disable_for_loaddata
 from .models import (
     Cable,
     CablePath,
@@ -286,6 +287,7 @@ def nullify_connected_endpoints(instance, **kwargs):
 
 
 @receiver(m2m_changed, sender=Interface.tagged_vlans.through)
+@disable_for_loaddata
 def prevent_adding_tagged_vlans_with_incorrect_mode_or_site(sender, instance, action, **kwargs):
     if action != "pre_add":
         return


Test patch: