ipfabric_netbox 3.2.4__py3-none-any.whl → 3.2.4b2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ipfabric_netbox might be problematic. Click here for more details.

Files changed (38) hide show
  1. ipfabric_netbox/__init__.py +1 -1
  2. ipfabric_netbox/api/nested_serializers.py +0 -20
  3. ipfabric_netbox/api/serializers.py +7 -13
  4. ipfabric_netbox/api/urls.py +2 -2
  5. ipfabric_netbox/api/views.py +5 -5
  6. ipfabric_netbox/data/transform_map.json +21 -35
  7. ipfabric_netbox/filtersets.py +15 -13
  8. ipfabric_netbox/forms.py +14 -42
  9. ipfabric_netbox/jobs.py +12 -7
  10. ipfabric_netbox/migrations/0001_initial.py +1 -1
  11. ipfabric_netbox/migrations/0001_initial_squashed_0013_switch_to_branching_plugin.py +503 -0
  12. ipfabric_netbox/migrations/0007_prepare_custom_fields.py +3 -3
  13. ipfabric_netbox/migrations/0011_update_part_number_DCIM_inventory_item_template.py +57 -0
  14. ipfabric_netbox/migrations/0012_remove_status_field.py +18 -0
  15. ipfabric_netbox/migrations/0013_switch_to_branching_plugin.py +270 -0
  16. ipfabric_netbox/models.py +120 -91
  17. ipfabric_netbox/navigation.py +1 -1
  18. ipfabric_netbox/signals.py +9 -2
  19. ipfabric_netbox/tables.py +40 -46
  20. ipfabric_netbox/templates/ipfabric_netbox/{ipfabricbranch.html → ipfabricingestion.html} +14 -9
  21. ipfabric_netbox/templates/ipfabric_netbox/ipfabricsource.html +3 -3
  22. ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html +3 -3
  23. ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync_list.html +71 -0
  24. ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html +0 -4
  25. ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_all.html +10 -0
  26. ipfabric_netbox/templates/ipfabric_netbox/partials/{branch_progress.html → ingestion_progress.html} +1 -1
  27. ipfabric_netbox/templates/ipfabric_netbox/partials/sync_last_ingestion.html +1 -0
  28. ipfabric_netbox/urls.py +13 -3
  29. ipfabric_netbox/utilities/ipfutils.py +52 -19
  30. ipfabric_netbox/views.py +162 -155
  31. {ipfabric_netbox-3.2.4.dist-info → ipfabric_netbox-3.2.4b2.dist-info}/METADATA +12 -4
  32. {ipfabric_netbox-3.2.4.dist-info → ipfabric_netbox-3.2.4b2.dist-info}/RECORD +34 -31
  33. {ipfabric_netbox-3.2.4.dist-info → ipfabric_netbox-3.2.4b2.dist-info}/WHEEL +1 -1
  34. ipfabric_netbox/templates/ipfabric_netbox/inc/sync_delete.html +0 -19
  35. ipfabric_netbox/templates/ipfabric_netbox/partials/branch_all.html +0 -10
  36. ipfabric_netbox/templates/ipfabric_netbox/partials/sync_last_branch.html +0 -1
  37. ipfabric_netbox/templates/ipfabric_netbox/sync_list.html +0 -126
  38. /ipfabric_netbox/templates/ipfabric_netbox/partials/{branch_status.html → ingestion_status.html} +0 -0
ipfabric_netbox/tables.py CHANGED
@@ -1,12 +1,13 @@
1
1
  import django_tables2 as tables
2
+ from django.utils.html import format_html
2
3
  from django.utils.translation import gettext_lazy as _
3
4
  from django_tables2 import Column
4
- from extras.models import StagedChange
5
5
  from netbox.tables import columns
6
6
  from netbox.tables import NetBoxTable
7
+ from netbox_branching.models import ChangeDiff
7
8
 
8
- from .models import IPFabricBranch
9
9
  from .models import IPFabricData
10
+ from .models import IPFabricIngestion
10
11
  from .models import IPFabricRelationshipField
11
12
  from .models import IPFabricSnapshot
12
13
  from .models import IPFabricSource
@@ -17,7 +18,7 @@ from .models import IPFabricTransformMap
17
18
 
18
19
  DIFF_BUTTON = """
19
20
  <a href="#"
20
- hx-get="{% url 'plugins:ipfabric_netbox:ipfabricbranch_change_diff' pk=record.branch.pk change_pk=record.pk %}"
21
+ hx-get="{% url 'plugins:ipfabric_netbox:ipfabricingestion_change_diff' pk=record.branch.pk change_pk=record.pk %}"
21
22
  hx-target="#htmx-modal-content"
22
23
  data-bs-toggle="modal"
23
24
  data-bs-target="#htmx-modal"
@@ -64,23 +65,21 @@ class IPFabricTransformMapTable(NetBoxTable):
64
65
 
65
66
  class Meta(NetBoxTable.Meta):
66
67
  model = IPFabricTransformMap
67
- fields = ("name", "source_model", "target_model", "status")
68
- default_columns = ("name", "source_model", "target_model", "status")
68
+ fields = ("name", "source_model", "target_model")
69
+ default_columns = ("name", "source_model", "target_model")
69
70
 
70
71
 
71
- class BranchTable(NetBoxTable):
72
+ class IPFabricIngestionTable(NetBoxTable):
72
73
  name = tables.Column(linkify=True)
73
74
  sync = tables.Column(verbose_name="IP Fabric Sync", linkify=True)
75
+ branch = tables.Column(linkify=True)
74
76
  changes = tables.Column(accessor="staged_changes", verbose_name="Number of Changes")
75
77
  actions = columns.ActionsColumn(actions=("delete",))
76
78
 
77
- def render_changes(self, value):
78
- return value.count()
79
-
80
79
  class Meta(NetBoxTable.Meta):
81
- model = IPFabricBranch
82
- fields = ("name", "sync", "description", "user", "changes")
83
- default_columns = ("name", "sync", "description", "user", "changes")
80
+ model = IPFabricIngestion
81
+ fields = ("name", "sync", "branch", "description", "user", "changes")
82
+ default_columns = ("name", "sync", "branch", "description", "user", "changes")
84
83
 
85
84
 
86
85
  class IPFabricSnapshotTable(NetBoxTable):
@@ -142,51 +141,46 @@ class SyncTable(NetBoxTable):
142
141
  default_columns = ("id", "status", "snapshot_name")
143
142
 
144
143
 
145
- class StagedChangesTable(NetBoxTable):
146
- # There is no view for single StagedChange, remove the link in ID
144
+ class IPFabricIngestionChangesTable(NetBoxTable):
145
+ # There is no view for single change, remove the link in ID
147
146
  id = tables.Column(verbose_name=_("ID"))
148
147
  pk = None
149
148
  object_type = tables.Column(
150
149
  accessor="object_type.model", verbose_name="Object Type"
151
150
  )
152
- name = tables.Column(accessor="object_type.name", verbose_name="Name")
151
+ object = tables.Column(verbose_name="Object")
153
152
  actions = columns.TemplateColumn(template_code=DIFF_BUTTON)
154
153
 
155
- def render_name(self, value, record):
156
- models_with_names = [
157
- "site",
158
- "manufacturer",
159
- "device role",
160
- "device",
161
- "interface",
162
- "platform",
163
- "inventory item",
164
- "VLAN",
165
- ]
166
- if record.data:
167
- if value in models_with_names:
168
- name = record.data["name"]
169
- elif value == "device type":
170
- name = record.data["model"]
171
- elif value == "IP address":
172
- name = record.data["address"]
173
- elif value == "tagged item":
174
- name = f"Tagging object ({record.data['object_id']})"
175
- elif value == "prefix":
176
- name = f"{record.data['prefix']} ({record.data['vrf']})"
177
- elif value == "MAC address":
178
- name = record.data["mac_address"]
179
- else:
180
- name = record.data
154
+ def render_object(self, value, record):
155
+ model_templates = {
156
+ "Device": lambda v: v.name,
157
+ "DeviceRole": lambda v: v.name,
158
+ "DeviceType": lambda v: v.model,
159
+ "IPAddress": lambda v: v.address,
160
+ "Interface": lambda v: f"{v.name} (Device {v.device.name})",
161
+ "InventoryItem": lambda v: f"{v.name} (Device {v.device.name})",
162
+ "MACAddress": lambda v: v.mac_address,
163
+ "Manufacturer": lambda v: v.name,
164
+ "Platform": lambda v: v.name,
165
+ "Prefix": lambda v: f"{v.prefix} (VRF {v.vrf})",
166
+ "Site": lambda v: v.name,
167
+ "VirtualChassis": lambda v: v.name,
168
+ "VLAN": lambda v: f"{v.name} (VID {v.vid})",
169
+ "VRF": lambda v: v.name,
170
+ }
171
+ if value and (class_name := value.__class__.__name__) in model_templates:
172
+ field_value = model_templates[class_name](value)
173
+ if url := value.get_absolute_url():
174
+ return format_html("<a href={}>{}</a>", url, field_value)
181
175
  else:
182
- name = record.object.__str__()
183
- return name
176
+ field_value = record.object_repr
177
+ return field_value
184
178
 
185
179
  class Meta(NetBoxTable.Meta):
186
- model = StagedChange
180
+ model = ChangeDiff
187
181
  name = "staged_changes"
188
- fields = ("name", "action", "object_type", "actions")
189
- default_columns = ("name", "action", "object_type", "actions")
182
+ fields = ("object", "action", "object_type", "actions")
183
+ default_columns = ("object", "action", "object_type", "actions")
190
184
 
191
185
 
192
186
  class DeviceIPFTable(tables.Table):
@@ -17,7 +17,7 @@
17
17
  {% if perms.core.sync_datasource %}
18
18
  {% if object.sync.ready_for_sync %}
19
19
  <a href="#"
20
- hx-get="{% url 'plugins:ipfabric_netbox:ipfabricbranch_merge' pk=object.pk %}"
20
+ hx-get="{% url 'plugins:ipfabric_netbox:ipfabricingestion_merge' pk=object.pk %}"
21
21
  hx-target="#htmx-modal-content"
22
22
  class="btn btn-success"
23
23
  data-bs-toggle="modal"
@@ -41,7 +41,7 @@
41
41
  <div class="row mb-3">
42
42
  <div class="col col-md-6">
43
43
  <div class="card">
44
- <h5 class="card-header">Branch Information</h5>
44
+ <h5 class="card-header">Ingestion Information</h5>
45
45
  <div class="card-body">
46
46
  <table class="table table-hover attr-table">
47
47
  <tr>
@@ -57,13 +57,18 @@
57
57
  <td><a href="{% url 'plugins:ipfabric_netbox:ipfabricsync' pk=object.sync.pk %}">{{ object.sync.name}}</a>
58
58
  </td>
59
59
  </tr>
60
+ <tr>
61
+ <th scope="row">Branch Object</th>
62
+ <td>{% if object.branch %}<a href="{% url 'plugins:netbox_branching:branch' pk=object.branch.pk %}">{{ object.branch.name}}</a>{% else %}{{ ""|placeholder }}{% endif %}
63
+ </td>
64
+ </tr>
60
65
  <tr>
61
66
  <th scope="row">Job Object</th>
62
67
  <td><a href="{% url 'core:job' pk=object.job.pk %}">{{ object.job }}</a></td>
63
68
  </tr>
64
69
  <tr>
65
- <th scope="row">Sync Status</th>
66
- <td><div id="sync_status" hx-swap-oob="true">{% include 'ipfabric_netbox/partials/branch_status.html' with object=object %}</div></td>
70
+ <th scope="row">Ingestion Status</th>
71
+ <td><div id="ingestion_status" hx-swap-oob="true">{% include 'ipfabric_netbox/partials/ingestion_status.html' with object=object %}</div></td>
67
72
  <!-- <td >{% badge object.sync.get_status_display bg_color=object.sync.get_status_color %}</td> -->
68
73
  </div>
69
74
  </tr>
@@ -120,8 +125,8 @@
120
125
  </table>
121
126
  </div>
122
127
  </div>
123
- <div id="sync_progress">
124
- {% include 'ipfabric_netbox/partials/branch_progress.html' %}
128
+ <div id="ingestion_progress">
129
+ {% include 'ipfabric_netbox/partials/ingestion_progress.html' %}
125
130
  </div>
126
131
  <!-- {% include 'inc/panels/related_objects.html' %} -->
127
132
  {% include 'inc/panels/custom_fields.html' %}
@@ -129,10 +134,10 @@
129
134
  </div>
130
135
  </div>
131
136
  <div class="row mb-3">
132
- <div class="col col-md-12" {% if not object.job.completed %} hx-get="{% url 'plugins:ipfabric_netbox:ipfabricbranch_logs' pk=object.pk %}?type=info"
133
- hx-trigger="every 5s" hx-target="#sync_logs" hx-select="#sync_logs" hx-select-oob="#sync_status:innerHTML"
137
+ <div class="col col-md-12" {% if not object.job.completed %} hx-get="{% url 'plugins:ipfabric_netbox:ipfabricingestion_logs' pk=object.pk %}?type=info"
138
+ hx-trigger="every 5s" hx-target="#ingestion_logs" hx-select="#ingestion_logs" hx-select-oob="#ingestion_status:innerHTML"
134
139
  hx-swap="innerHTML" {% endif %}>
135
- <div id="sync_logs">{% include 'ipfabric_netbox/partials/job_logs.html' with job=object.job %}</div>
140
+ <div id="ingestion_logs">{% include 'ipfabric_netbox/partials/job_logs.html' with job=object.job %}</div>
136
141
  </div>
137
142
  {% endblock content %}
138
143
 
@@ -102,9 +102,9 @@
102
102
  {% plugin_right_page object %}
103
103
  </div>
104
104
  {% if job %}
105
- <div class="col col-md-12" {% if not object.job.completed %} hx-trigger="every 5s" hx-target="#sync_logs"
106
- hx-select="#sync_logs" hx-select-oob="#sync_status:innerHTML" hx-swap="innerHTML" {% endif %}>
107
- <div id="sync_logs">{% include 'ipfabric_netbox/partials/job_logs.html' with job=job %}</div>
105
+ <div class="col col-md-12" {% if not object.job.completed %} hx-trigger="every 5s" hx-target="#ingestion_logs"
106
+ hx-select="#ingestion_logs" hx-select-oob="#ingestion_status:innerHTML" hx-swap="innerHTML" {% endif %}>
107
+ <div id="ingestionlogs">{% include 'ipfabric_netbox/partials/job_logs.html' with job=job %}</div>
108
108
  </div>
109
109
  {% endif %}
110
110
  </div>
@@ -5,7 +5,7 @@
5
5
  {% load render_table from django_tables2 %}
6
6
 
7
7
  {% block extra_controls %}
8
- {% if perms.ipfabric_netbox.sync_ingest %}
8
+ {% if perms.ipfabric_netbox.start_sync %}
9
9
  {% if object.ready_for_sync %}
10
10
  <form action="{% url 'plugins:ipfabric_netbox:ipfabricsync_sync' pk=object.pk %}" method="post">
11
11
  {% csrf_token %}
@@ -51,8 +51,8 @@
51
51
  <td>{{ object.last_synced|placeholder }}</td>
52
52
  </tr>
53
53
  <tr>
54
- <th scope="row">Latest branch</th>
55
- <td><div {% if object.status == 'queued' or object.status == 'syncing' %} hx-get="{% url 'plugins:ipfabric_netbox:ipfabricsync' pk=object.pk %}" hx-trigger="every 1s"{% endif %}>{% include 'ipfabric_netbox/partials/sync_last_branch.html' %}</div></td>
54
+ <th scope="row">Latest ingestion</th>
55
+ <td><div {% if object.status == 'queued' or object.status == 'syncing' %} hx-get="{% url 'plugins:ipfabric_netbox:ipfabricsync' pk=object.pk %}" hx-trigger="every 1s"{% endif %}>{% include 'ipfabric_netbox/partials/sync_last_ingestion.html' %}</div></td>
56
56
  </tr>
57
57
  <tr>
58
58
  <th scope="row">Schedule</th>
@@ -0,0 +1,71 @@
1
+ {% extends 'base/layout.html' %}
2
+ {% load buttons %}
3
+ {% load helpers %}
4
+ {% load perms %}
5
+
6
+ {% block title %}Ingestion{% endblock %}
7
+
8
+ {% block tabs %}
9
+ <ul class="nav nav-tabs px-3">
10
+ <li class="nav-item" role="presentation">
11
+ <a class="nav-link active" role="tab">Ingestion</a>
12
+ </li>
13
+ </ul>
14
+ {% endblock tabs %}
15
+
16
+ {% block controls %}
17
+ <div class="controls">
18
+ <div class="control-group">
19
+ {% block extra_controls %}{% endblock %}
20
+ {% add_button model %}
21
+ </div>
22
+ </div>
23
+ {% endblock controls %}
24
+
25
+ {% block content %}
26
+ <div class="tab-content">
27
+ {% for sync in syncs %}
28
+ <div class="card">
29
+ <h5 class="card-header d-flex justify-content-between" id="module{{ module.pk }}">
30
+ <div>
31
+ <i class="mdi mdi-cloud-sync"></i><i class="mdi mdi-sync"></i> <a href="{% url 'plugins:ipfabric_netbox:ipfabricsync' pk=sync.pk %}">{{ sync.name}}</a>
32
+ </div>
33
+ {% if perms.ipfabric_netbox.delete_ipfabricsync %}
34
+ <a href="{% url 'plugins:ipfabric_netbox:ipfabricsync_delete' pk=sync.pk %}" class="btn btn-danger btn-sm">
35
+ <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
36
+ </a>
37
+ {% endif %}
38
+
39
+ </h5>
40
+ <div class="card-body">
41
+ {% include 'inc/sync_warning.html' with object=module %}
42
+ <table class="table table-hover table-headings reports">
43
+ <thead>
44
+ <tr>
45
+ <th width="200">Source</th>
46
+ <th width="400">Snapshot</th>
47
+ <th>Status</th>
48
+ <th>Last Run</th>
49
+ </tr>
50
+ </thead>
51
+ <tbody>
52
+ <tr>
53
+ <td><a href="{% url 'plugins:ipfabric_netbox:ipfabricsource' pk=sync.snapshot_data.source.pk %}">{{ sync.snapshot_data.source.name }}</a></td>
54
+ <td><a href="{% url 'plugins:ipfabric_netbox:ipfabricsnapshot' pk=sync.snapshot_data.pk %}">{{ sync.snapshot_data.name}}</a></td>
55
+ <td>{% badge sync.get_status_display last_job.get_status_color %}</td>
56
+ <td>{{sync.last_synced}}</td>
57
+ </tr>
58
+ </tbody>
59
+ </table>
60
+ </div>
61
+ </div>
62
+ {% empty %}
63
+ <div class="alert alert-info" role="alert">
64
+ <h4 class="alert-heading">Sync Jobs Settings Found</h4>
65
+ {% if perms.extras.add_reportmodule %}
66
+ Get started by <a href="{% url 'plugins:ipfabric_netbox:ipfabricsync_add' %}">creating a sync</a> from an IP Fabric source.
67
+ {% endif %}
68
+ </div>
69
+ {% endfor %}
70
+ </div>
71
+ {% endblock content %}
@@ -23,10 +23,6 @@
23
23
  <th scope="row">Target Model</th>
24
24
  <td>{{ object.target_model }}</td>
25
25
  </tr>
26
- <tr>
27
- <th scope="row">Status</th>
28
- <td>{{ object.status }}</td>
29
- </tr>
30
26
  </tr>
31
27
  </table>
32
28
  </div>
@@ -0,0 +1,10 @@
1
+
2
+ <div id="ingestion_status" hx-swap-oob="true">
3
+ {% include 'ipfabric_netbox/partials/ingestion_status.html' with object=object %}
4
+ </div>
5
+ <div id="ingestion_progress" hx-swap-oob="true">
6
+ {% include 'ipfabric_netbox/partials/ingestion_progress.html' %}
7
+ </div>
8
+ <div id="ingestion_logs">
9
+ {% include 'ipfabric_netbox/partials/job_logs.html' with job_results=job_results job=job %}
10
+ </div>
@@ -12,7 +12,7 @@
12
12
  {% endfor %}
13
13
  </table>
14
14
  {% else %}
15
- Sync progress pending...<br>
15
+ Ingestion progress pending...<br>
16
16
  <small class="text-muted">Last updated {% now "H:i:s" %}</small>
17
17
  {% endif %}
18
18
  </div>
@@ -0,0 +1 @@
1
+ {% if last_ingestion %}<a href="{% url 'plugins:ipfabric_netbox:ipfabricingestion' pk=last_ingestion.pk %}">{{ last_ingestion.name }}{% else %}{{ ""|placeholder }}{% endif %}</a>
ipfabric_netbox/urls.py CHANGED
@@ -63,10 +63,20 @@ urlpatterns = (
63
63
  path("sync/", views.IPFabricSyncListView.as_view(), name="ipfabricsync_list"),
64
64
  path("sync/add/", views.IPFabricSyncEditView.as_view(), name="ipfabricsync_add"),
65
65
  path("sync/<int:pk>/", include(get_model_urls("ipfabric_netbox", "ipfabricsync"))),
66
- # Branch
67
- path("branch/", views.IPFabricBranchListView.as_view(), name="ipfabricbranch_list"),
68
66
  path(
69
- "branch/<int:pk>/", include(get_model_urls("ipfabric_netbox", "ipfabricbranch"))
67
+ "sync/<int:pk>/delete/",
68
+ views.IPFabricSyncDeleteView.as_view(),
69
+ name="ipfabricsync_delete",
70
+ ),
71
+ # Ingestion
72
+ path(
73
+ "ingestion/",
74
+ views.IPFabricIngestionListView.as_view(),
75
+ name="ipfabricingestion_list",
76
+ ),
77
+ path(
78
+ "ingestion/<int:pk>/",
79
+ include(get_model_urls("ipfabric_netbox", "ipfabricingestion")),
70
80
  ),
71
81
  # Transform Map
72
82
  path(
@@ -28,7 +28,7 @@ from .nbutils import order_devices
28
28
  from .nbutils import order_members
29
29
 
30
30
  if TYPE_CHECKING:
31
- from ..models import IPFabricBranch
31
+ from ..models import IPFabricIngestion
32
32
 
33
33
  logger = logging.getLogger("ipfabric_netbox.utilities.ipf_utils")
34
34
 
@@ -128,10 +128,16 @@ class IPFabric(object):
128
128
 
129
129
  class IPFabricSyncRunner(object):
130
130
  def __init__(
131
- self, transform_map, sync=None, client: IPFabric = None, settings: dict = None
131
+ self,
132
+ transform_map,
133
+ sync=None,
134
+ client: IPFabric = None,
135
+ ingestion=None,
136
+ settings: dict = None,
132
137
  ) -> None:
133
138
  self.client = client
134
139
  self.settings = settings
140
+ self.ingestion = ingestion
135
141
  self.transform_map = transform_map
136
142
  self.sync = sync
137
143
  if hasattr(self.sync, "logger"):
@@ -153,17 +159,17 @@ class IPFabricSyncRunner(object):
153
159
  if isinstance(err, SearchError):
154
160
  if self.settings.get(err.model):
155
161
  self.logger.log_failure(
156
- f"Aborting syncing {err.model} instance due to above error, please check your transform maps and/or existing data.",
162
+ f"Aborting syncing `{err.model}` instance due to above error, please check your transform maps and/or existing data.",
157
163
  obj=self.sync,
158
164
  )
159
165
  else:
160
166
  self.logger.log_failure(
161
- f"Syncing {err.model} is disabled in settings, but hit above error trying to find the correct item. Please check your transform maps and/or existing data.",
167
+ f"Syncing `{err.model}` is disabled in settings, but hit above error trying to find the correct item. Please check your transform maps and/or existing data.",
162
168
  obj=self.sync,
163
169
  )
164
170
  else:
165
171
  self.logger.log_failure(
166
- f"Syncing failed with: {err}. See above error for more details.",
172
+ f"Syncing failed with: `{err}`. See above error for more details.",
167
173
  obj=self.sync,
168
174
  )
169
175
  # Make sure the whole sync is failed when we encounter error
@@ -172,6 +178,12 @@ class IPFabricSyncRunner(object):
172
178
 
173
179
  return wrapper
174
180
 
181
+ def get_db_connection_name(self) -> str:
182
+ connection_name = None
183
+ if self.ingestion:
184
+ connection_name = self.ingestion.branch.connection_name
185
+ return connection_name
186
+
175
187
  def get_model_or_update(self, app, model, data):
176
188
  transform_map = self.transform_map.objects.filter(
177
189
  target_model__app_label=app, target_model__model=model
@@ -198,16 +210,18 @@ class IPFabricSyncRunner(object):
198
210
 
199
211
  object = None
200
212
  try:
213
+ connection_name = self.get_db_connection_name()
201
214
  if model_settings:
202
215
  logger.info(f"Creating {model}")
203
216
  object = transform_map.update_or_create_instance(
204
217
  context=context,
205
218
  tags=self.sync.tags.all(),
219
+ connection_name=connection_name,
206
220
  )
207
221
  else:
208
222
  logger.info(f"Getting {model}")
209
223
  context.pop("defaults", None)
210
- object = queryset.get(**context)
224
+ object = queryset.using(connection_name).get(**context)
211
225
  except queryset.model.DoesNotExist as err:
212
226
  self.logger.log_failure(
213
227
  f"<b>`{model}` with these keys not found: `{context}`.</b><br/>Original data: `{data}`.",
@@ -474,6 +488,16 @@ class IPFabricSyncRunner(object):
474
488
  else:
475
489
  managed_ips[ip["sn"]][int_name].append(ip)
476
490
 
491
+ for vlan in data["vlan"][:]:
492
+ # Remove VLANs with ID 0, minimum VLAN ID in NetBox is 1
493
+ if vlan.get("vlanId") == 0:
494
+ data["vlan"].remove(vlan)
495
+
496
+ for item in data["inventoryitem"][:]:
497
+ # Remove items with empty serial number
498
+ if item.get("sn") in [None, "None"]:
499
+ data["inventoryitem"].remove(item)
500
+
477
501
  for model, item_count in [
478
502
  ("site", len(data.get("site", []))),
479
503
  ("device", len(devices)),
@@ -513,7 +537,7 @@ class IPFabricSyncRunner(object):
513
537
  app_label: str,
514
538
  model: str,
515
539
  cf: bool = False,
516
- branch: "IPFabricBranch" = None,
540
+ ingestion: "IPFabricIngestion" = None,
517
541
  ) -> None:
518
542
  """Sync list of items to NetBox."""
519
543
  if not self.settings.get(model):
@@ -530,17 +554,18 @@ class IPFabricSyncRunner(object):
530
554
  self.logger.increment_statistics(model=model)
531
555
 
532
556
  if cf:
557
+ synced_object.snapshot()
533
558
  synced_object.custom_field_data[
534
559
  "ipfabric_source"
535
560
  ] = self.sync.snapshot_data.source.pk
536
- if branch:
537
- synced_object.custom_field_data["ipfabric_branch"] = branch.pk
561
+ if ingestion:
562
+ synced_object.custom_field_data["ipfabric_ingestion"] = ingestion.pk
538
563
  synced_object.save()
539
564
 
540
565
  @handle_errors
541
566
  def sync_devices(
542
567
  self,
543
- branch,
568
+ ingestion,
544
569
  devices,
545
570
  interface_dict,
546
571
  managed_ips,
@@ -578,11 +603,12 @@ class IPFabricSyncRunner(object):
578
603
  continue
579
604
 
580
605
  if self.settings.get("device"):
606
+ device_object.snapshot()
581
607
  device_object.custom_field_data[
582
608
  "ipfabric_source"
583
609
  ] = self.sync.snapshot_data.source.pk
584
- if branch:
585
- device_object.custom_field_data["ipfabric_branch"] = branch.pk
610
+ if ingestion:
611
+ device_object.custom_field_data["ipfabric_ingestion"] = ingestion.pk
586
612
  device_object.save()
587
613
 
588
614
  self.logger.increment_statistics(model="device")
@@ -622,20 +648,26 @@ class IPFabricSyncRunner(object):
622
648
  return
623
649
  self.logger.increment_statistics(model="ipaddress")
624
650
 
651
+ connection_name = self.get_db_connection_name()
652
+
625
653
  try:
626
654
  # Removing other IP is done in .signals.clear_other_primary_ip
627
655
  # But do it here too so the change is shown in StagedChange diff
628
- other_device = Device.objects.get(primary_ip4=ip_address_obj)
656
+ other_device = Device.objects.using(connection_name).get(
657
+ primary_ip4=ip_address_obj
658
+ )
629
659
  if other_device and device_object != other_device:
660
+ other_device.snapshot()
630
661
  other_device.primary_ip4 = None
631
- other_device.save()
662
+ other_device.save(using=connection_name)
632
663
  except ObjectDoesNotExist:
633
664
  pass
634
665
 
635
666
  if login_ip == primary_ip:
636
667
  try:
668
+ device_object.snapshot()
637
669
  device_object.primary_ip4 = ip_address_obj
638
- device_object.save()
670
+ device_object.save(using=connection_name)
639
671
  except ValueError as err:
640
672
  self.logger.log_failure(
641
673
  f"Error assigning primary IP to device: {err}", obj=self.sync
@@ -659,8 +691,9 @@ class IPFabricSyncRunner(object):
659
691
  "dcim", "macaddress", macaddress_data
660
692
  )
661
693
  try:
694
+ interface_object.snapshot()
662
695
  interface_object.primary_mac_address = macaddress_object
663
- interface_object.save()
696
+ interface_object.save(using=self.get_db_connection_name())
664
697
  except ValueError as err:
665
698
  self.logger.log_failure(
666
699
  f"Error assigning MAC Address to interface: {err}", obj=self.sync
@@ -697,7 +730,7 @@ class IPFabricSyncRunner(object):
697
730
 
698
731
  return interface_object
699
732
 
700
- def collect_and_sync(self, branch=None) -> None:
733
+ def collect_and_sync(self, ingestion=None) -> None:
701
734
  self.logger.log_info("Starting data sync.", obj=self.sync)
702
735
  (
703
736
  sites,
@@ -711,10 +744,10 @@ class IPFabricSyncRunner(object):
711
744
  ) = self.collect_data()
712
745
 
713
746
  self.sync_items(
714
- app_label="dcim", model="site", items=sites, cf=True, branch=branch
747
+ app_label="dcim", model="site", items=sites, cf=True, ingestion=ingestion
715
748
  )
716
749
  self.sync_devices(
717
- branch,
750
+ ingestion,
718
751
  devices,
719
752
  interface_dict,
720
753
  managed_ips,