ipfabric_netbox 4.3.2b1__py3-none-any.whl → 4.3.2b3__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.
@@ -6,7 +6,7 @@ class NetboxIPFabricConfig(PluginConfig):
6
6
  name = "ipfabric_netbox"
7
7
  verbose_name = "NetBox IP Fabric SoT Plugin"
8
8
  description = "Sync IP Fabric into NetBox"
9
- version = "4.3.2b1"
9
+ version = "4.3.2b3"
10
10
  base_url = "ipfabric"
11
11
  min_version = "4.4.0"
12
12
 
@@ -41,8 +41,8 @@
41
41
  "model": "vrf"
42
42
  },
43
43
  "target_field": "vrf",
44
- "coalesce": false,
45
- "template": "{% if object.vrf is defined and object.vrf | string not in [\"\", \"system\", \"0\"] %}{{ ipam.VRF.objects.filter(name=object.vrf).first().pk }}{% else %}None{% endif %}"
44
+ "coalesce": true,
45
+ "template": "{% if object.vrf is defined and object.vrf | string not in [\"\", \"system\", \"0\", \"global\"] %}{{ ipam.VRF.objects.filter(name=object.vrf).first().pk }}{% else %}None{% endif %}"
46
46
  }
47
47
  ]
48
48
  },
@@ -169,7 +169,7 @@
169
169
  "source_field": "hostname",
170
170
  "target_field": "vc_position",
171
171
  "coalesce": false,
172
- "template": "{% if object.virtual_chassis is defined %}{{ object.virtual_chassis.member }}{% else %}None{% endif %}"
172
+ "template": "{% if object.virtual_chassis %}{{ object.virtual_chassis.member }}{% else %}None{% endif %}"
173
173
  }
174
174
  ],
175
175
  "relationship_maps": [
@@ -180,7 +180,7 @@
180
180
  },
181
181
  "target_field": "virtual_chassis",
182
182
  "coalesce": false,
183
- "template": "{% if object.virtual_chassis is defined %}{{ dcim.VirtualChassis.objects.filter(name=object.virtual_chassis.master).first().pk }}{% endif %}"
183
+ "template": "{% if object.virtual_chassis %}{{ dcim.VirtualChassis.objects.filter(name=object.virtual_chassis.master).first().pk }}{% endif %}"
184
184
  },
185
185
  {
186
186
  "source_model": {
@@ -339,7 +339,7 @@
339
339
  "source_field": "primaryIp",
340
340
  "target_field": "mgmt_only",
341
341
  "coalesce": false,
342
- "template": "{% if object.primaryIp == object.loginIp %}True{% else %}False{% endif %}"
342
+ "template": "{% if object.primaryIp and object.primaryIp == object.loginIp %}True{% else %}False{% endif %}"
343
343
  },
344
344
  {
345
345
  "source_field": "speedValue",
@@ -386,10 +386,10 @@
386
386
  "template": "{% if object.mac %}{{ object.mac | mac_to_format(frmt=\"MAC_COLON_TWO\") | upper }}{% else %}{{ \"00:00:00:00:00:01\" | mac_to_format(frmt=\"MAC_COLON_TWO\") | upper }}{% endif %}"
387
387
  },
388
388
  {
389
- "source_field": "id",
389
+ "source_field": "sn",
390
390
  "target_field": "assigned_object_id",
391
391
  "coalesce": true,
392
- "template": ""
392
+ "template": "{% if object.nameOriginal %}{{ dcim.Interface.objects.filter(device__serial=object.sn, name=object.nameOriginal).first().pk }}{% else %}{{ dcim.Interface.objects.filter(device__serial=object.sn, name=object.intName).first().pk }}{% endif %}"
393
393
  }
394
394
  ],
395
395
  "relationship_maps": [
@@ -432,7 +432,7 @@
432
432
  {
433
433
  "source_field": "name",
434
434
  "target_field": "name",
435
- "coalesce": false,
435
+ "coalesce": true,
436
436
  "template": "{% if object.name is not none %}{{ object.name | string | truncate(64, True) }}{% elif object.dscr is not none %}{{ object.dscr | string | truncate(64, True) }}{% else %}Default Name{% endif %}"
437
437
  }
438
438
  ],
@@ -443,7 +443,7 @@
443
443
  "model": "device"
444
444
  },
445
445
  "target_field": "device",
446
- "coalesce": false,
446
+ "coalesce": true,
447
447
  "template": "{{ dcim.Device.objects.get(serial=object.deviceSn).pk }}"
448
448
  },
449
449
  {
@@ -474,7 +474,7 @@
474
474
  "source_field": "vlanName",
475
475
  "target_field": "name",
476
476
  "coalesce": false,
477
- "template": "{% if object.vlanName is defined and object.vlanName | lower != \"none\" %}{{ object.vlanName | string | truncate(64, True) }}{% else %}\"\"{% endif %}"
477
+ "template": "{% if object.vlanName is defined and object.vlanName | lower not in [\"none\", \"\"] %}{{ object.vlanName | string | truncate(64, True) }}{% else %}\"\"{% endif %}"
478
478
  },
479
479
  {
480
480
  "source_field": "dscr",
@@ -563,7 +563,7 @@
563
563
  },
564
564
  "target_field": "vrf",
565
565
  "coalesce": true,
566
- "template": "{% if object.vrf is defined and object.vrf | string not in [\"\", \"system\", \"0\"] %}{{ ipam.VRF.objects.filter(name=object.vrf).first().pk }}{% else %}None{% endif %}"
566
+ "template": "{% if object.vrf is defined and object.vrf | string not in [\"\", \"system\", \"0\", \"global\"] %}{{ ipam.VRF.objects.filter(name=object.vrf).first().pk }}{% else %}None{% endif %}"
567
567
  },
568
568
  {
569
569
  "source_model": {
@@ -1,11 +1,5 @@
1
- from typing import TYPE_CHECKING
2
-
3
1
  from core.exceptions import SyncError
4
2
 
5
- if TYPE_CHECKING:
6
- from .models import IPFabricIngestionIssue
7
- from .models import IPFabricIngestion
8
-
9
3
 
10
4
  class IngestionIssue(Exception):
11
5
  """
@@ -47,35 +41,6 @@ class IPAddressDuplicateError(IngestionIssue, SyncError):
47
41
  return f"IP address {self.data.get('address')} already exists in {self.model} with coalesce_fields={self.coalesce_fields}."
48
42
 
49
43
 
50
- def create_or_get_sync_issue(
51
- exception: Exception,
52
- ingestion: "IPFabricIngestion",
53
- message: str = None,
54
- model: str = None,
55
- context: dict = None,
56
- data: dict = None,
57
- ) -> (bool, "IPFabricIngestionIssue"):
58
- """
59
- Helper function to handle sync errors and create IPFabricIngestionIssue if needed.
60
- """
61
- context = context or {}
62
-
63
- # TODO: This is to prevent circular import issues, clean it up later.
64
- from .models import IPFabricIngestionIssue
65
-
66
- if not hasattr(exception, "issue_id") or not exception.issue_id:
67
- issue = IPFabricIngestionIssue.objects.create(
68
- ingestion=ingestion,
69
- exception=exception.__class__.__name__,
70
- message=message or getattr(exception, "message", str(exception)),
71
- model=model,
72
- coalesce_fields={k: v for k, v in context.items() if k not in ["defaults"]},
73
- defaults=context.get("defaults", dict()),
74
- raw_data=data or dict(),
75
- )
76
- if hasattr(exception, "issue_id"):
77
- exception.issue_id = issue.id
78
- return True, issue
79
- else:
80
- issue = IPFabricIngestionIssue.objects.get(id=exception.issue_id)
81
- return False, issue
44
+ class IPAddressPrimaryRemovalError(IngestionIssue, SyncError):
45
+ def __str__(self):
46
+ return "Error removing primary IP from other device."
@@ -0,0 +1,133 @@
1
+ from django.db import migrations
2
+
3
+ from ipfabric_netbox.utilities.transform_map import do_change
4
+ from ipfabric_netbox.utilities.transform_map import FieldRecord
5
+ from ipfabric_netbox.utilities.transform_map import RelationshipRecord
6
+ from ipfabric_netbox.utilities.transform_map import TransformMapRecord
7
+
8
+
9
+ CHANGES = (
10
+ TransformMapRecord(
11
+ source_model="ipaddress",
12
+ target_model="ipam.ipaddress",
13
+ relationships=(
14
+ RelationshipRecord(
15
+ source_model="ipam.vrf",
16
+ target_field="vrf",
17
+ coalesce=True,
18
+ old_template='{% if object.vrf is defined and object.vrf | string not in ["", "system", "0"] %}{{ ipam.VRF.objects.filter(name=object.vrf).first().pk }}{% else %}None{% endif %}',
19
+ new_template='{% if object.vrf is defined and object.vrf | string not in ["", "system", "0", "global"] %}{{ ipam.VRF.objects.filter(name=object.vrf).first().pk }}{% else %}None{% endif %}',
20
+ ),
21
+ ),
22
+ ),
23
+ TransformMapRecord(
24
+ source_model="device",
25
+ target_model="dcim.device",
26
+ fields=(
27
+ FieldRecord(
28
+ source_field="hostname",
29
+ target_field="vc_position",
30
+ old_template="{% if object.virtual_chassis is defined %}{{ object.virtual_chassis.member }}{% else %}None{% endif %}",
31
+ new_template="{% if object.virtual_chassis %}{{ object.virtual_chassis.member }}{% else %}None{% endif %}",
32
+ ),
33
+ ),
34
+ relationships=(
35
+ RelationshipRecord(
36
+ source_model="dcim.virtualchassis",
37
+ target_field="virtual_chassis",
38
+ old_template="{% if object.virtual_chassis is defined %}{{ dcim.VirtualChassis.objects.filter(name=object.virtual_chassis.master).first().pk }}{% endif %}",
39
+ new_template="{% if object.virtual_chassis %}{{ dcim.VirtualChassis.objects.filter(name=object.virtual_chassis.master).first().pk }}{% endif %}",
40
+ ),
41
+ ),
42
+ ),
43
+ TransformMapRecord(
44
+ source_model="interface",
45
+ target_model="dcim.interface",
46
+ fields=(
47
+ FieldRecord(
48
+ source_field="primaryIp",
49
+ target_field="mgmt_only",
50
+ old_template="{% if object.primaryIp == object.loginIp %}True{% else %}False{% endif %}",
51
+ new_template="{% if object.primaryIp and object.primaryIp == object.loginIp %}True{% else %}False{% endif %}",
52
+ ),
53
+ ),
54
+ ),
55
+ TransformMapRecord(
56
+ source_model="interface",
57
+ target_model="dcim.macaddress",
58
+ fields=(
59
+ FieldRecord(
60
+ source_field="id",
61
+ new_source_field="sn",
62
+ target_field="assigned_object_id",
63
+ old_template="",
64
+ new_template="{% if object.nameOriginal %}{{ dcim.Interface.objects.filter(device__serial=object.sn, name=object.nameOriginal).first().pk }}{% else %}{{ dcim.Interface.objects.filter(device__serial=object.sn, name=object.intName).first().pk }}{% endif %}",
65
+ ),
66
+ ),
67
+ ),
68
+ TransformMapRecord(
69
+ source_model="part_number",
70
+ target_model="dcim.inventoryitem",
71
+ fields=(
72
+ FieldRecord(
73
+ source_field="name",
74
+ target_field="name",
75
+ coalesce=True,
76
+ ),
77
+ ),
78
+ relationships=(
79
+ RelationshipRecord(
80
+ source_model="dcim.device",
81
+ target_field="device",
82
+ coalesce=True,
83
+ ),
84
+ ),
85
+ ),
86
+ TransformMapRecord(
87
+ source_model="vlan",
88
+ target_model="ipam.vlan",
89
+ fields=(
90
+ FieldRecord(
91
+ source_field="vlanName",
92
+ target_field="name",
93
+ old_template='{% if object.vlanName is defined and object.vlanName | lower != "none" %}{{ object.vlanName | string | truncate(64, True) }}{% else %}""{% endif %}',
94
+ new_template='{% if object.vlanName is defined and object.vlanName | lower not in ["none", ""] %}{{ object.vlanName | string | truncate(64, True) }}{% else %}""{% endif %}',
95
+ ),
96
+ ),
97
+ ),
98
+ TransformMapRecord(
99
+ source_model="prefix",
100
+ target_model="ipam.prefix",
101
+ relationships=(
102
+ RelationshipRecord(
103
+ source_model="ipam.vrf",
104
+ target_field="vrf",
105
+ old_template='{% if object.vrf is defined and object.vrf | string not in ["", "system", "0"] %}{{ ipam.VRF.objects.filter(name=object.vrf).first().pk }}{% else %}None{% endif %}',
106
+ new_template='{% if object.vrf is defined and object.vrf | string not in ["", "system", "0", "global"] %}{{ ipam.VRF.objects.filter(name=object.vrf).first().pk }}{% else %}None{% endif %}',
107
+ ),
108
+ ),
109
+ ),
110
+ )
111
+
112
+
113
+ def forward_change(apps, schema_editor):
114
+ """Replace old template with updated version."""
115
+ do_change(apps, schema_editor, changes=CHANGES, forward=True)
116
+
117
+
118
+ def revert_change(apps, schema_editor):
119
+ """Revert template back to the previous exact template."""
120
+ do_change(apps, schema_editor, changes=CHANGES, forward=False)
121
+
122
+
123
+ class Migration(migrations.Migration):
124
+ dependencies = [
125
+ ("ipfabric_netbox", "0020_clean_scheduled_jobs"),
126
+ ]
127
+
128
+ operations = [
129
+ migrations.RunPython(
130
+ forward_change,
131
+ revert_change,
132
+ ),
133
+ ]
ipfabric_netbox/models.py CHANGED
@@ -2,6 +2,7 @@ import ast
2
2
  import functools
3
3
  import json
4
4
  import logging
5
+ import re
5
6
  import traceback
6
7
  from copy import deepcopy
7
8
  from uuid import uuid4
@@ -12,10 +13,8 @@ from core.exceptions import SyncError
12
13
  from core.models import Job
13
14
  from core.models import ObjectType
14
15
  from core.signals import pre_sync
15
- from dcim.models import Device
16
+ from dcim.models import MACAddress
16
17
  from dcim.models import Site
17
- from dcim.models import VirtualChassis
18
- from dcim.signals import assign_virtualchassis_master
19
18
  from django.apps import apps
20
19
  from django.conf import settings
21
20
  from django.contrib.contenttypes.models import ContentType
@@ -50,7 +49,7 @@ from .choices import IPFabricSourceTypeChoices
50
49
  from .choices import IPFabricSyncStatusChoices
51
50
  from .choices import IPFabricTransformMapSourceModelChoices
52
51
  from .choices import required_transform_map_contenttypes
53
- from .signals import clear_other_primary_ip
52
+ from .signals import assign_primary_mac_address
54
53
  from .utilities.ipfutils import IPFabric
55
54
  from .utilities.ipfutils import IPFabricSyncRunner
56
55
  from .utilities.ipfutils import render_jinja2
@@ -229,6 +228,22 @@ class IPFabricTransformMap(NetBoxModel):
229
228
  rel_dict_coalesce[field.target_field] = related_object
230
229
  return rel_dict, rel_dict_coalesce
231
230
 
231
+ def strip_source_data(self, source_data: dict) -> dict:
232
+ """Strip data according to Transform Map mappings but without rendering templates."""
233
+ keys = set()
234
+ for field in self.field_maps.all():
235
+ keys.add(field.source_field)
236
+ if field.template:
237
+ keys.update(
238
+ re.findall(r"object\.([a-zA-Z_0-9]+)(?=.*)", field.template)
239
+ )
240
+ for field in self.relationship_maps.all():
241
+ if field.template:
242
+ keys.update(
243
+ re.findall(r"object\.([a-zA-Z_0-9]+)(?=.*)", field.template)
244
+ )
245
+ return {k: source_data[k] for k in keys}
246
+
232
247
  def get_context(self, source_data):
233
248
  new_data = deepcopy(source_data)
234
249
  relationship, coalesce_relationship = self.build_relationships(
@@ -241,7 +256,8 @@ class IPFabricTransformMap(NetBoxModel):
241
256
  context = self.render(new_data)
242
257
  return context
243
258
 
244
- def update_or_create_instance(self, context, tags=[], connection_name=None):
259
+ def update_or_create_instance(self, context, tags=None, connection_name=None):
260
+ tags = tags or []
245
261
  target_class = self.target_model.model_class()
246
262
  queryset = target_class.objects.using(connection_name)
247
263
 
@@ -272,8 +288,11 @@ class IPFabricTransformMap(NetBoxModel):
272
288
  if field.endswith("__isnull"):
273
289
  context[field[:-8]] = None
274
290
  del context[field]
275
- instance = queryset.create(**context, **defaults)
291
+ # Using queryset.create() creates the object even when it fails on clean()
292
+ # To to work around it, we do it in two steps to avoid saving it to DB before clean()
293
+ instance = queryset.model(**context, **defaults)
276
294
  instance.full_clean()
295
+ instance.save(using=connection_name)
277
296
 
278
297
  apply_tags(instance, tags, connection_name)
279
298
 
@@ -867,9 +886,17 @@ class IPFabricSync(IPFabricClient, JobsMixin, TagsMixin, ChangeLoggedModel):
867
886
  try:
868
887
  active_branch.set(branch)
869
888
  try:
870
- runner.collect_and_sync(
871
- ingestion=IPFabricIngestion.objects.get(pk=ingestion.pk)
872
- )
889
+ try:
890
+ signals.post_save.connect(
891
+ assign_primary_mac_address, sender=MACAddress
892
+ )
893
+ runner.collect_and_sync(
894
+ ingestion=IPFabricIngestion.objects.get(pk=ingestion.pk)
895
+ )
896
+ finally:
897
+ signals.post_save.disconnect(
898
+ assign_primary_mac_address, sender=MACAddress
899
+ )
873
900
  finally:
874
901
  active_branch.set(None)
875
902
  finally:
@@ -1002,15 +1029,7 @@ class IPFabricIngestion(JobsMixin, models.Model):
1002
1029
  # Begin Sync
1003
1030
  logger.debug(f"Merging {self.name}")
1004
1031
  try:
1005
- signals.pre_save.connect(clear_other_primary_ip, sender=Device)
1006
- signals.post_save.disconnect(
1007
- assign_virtualchassis_master, sender=VirtualChassis
1008
- )
1009
1032
  self.branch.merge(user=self.sync.user)
1010
- signals.post_save.connect(
1011
- assign_virtualchassis_master, sender=VirtualChassis
1012
- )
1013
- signals.pre_save.disconnect(clear_other_primary_ip, sender=Device)
1014
1033
  ipfabricsync.status = IPFabricSyncStatusChoices.COMPLETED
1015
1034
  except Exception as e:
1016
1035
  ipfabricsync.status = IPFabricSyncStatusChoices.FAILED
@@ -1,39 +1,27 @@
1
1
  import logging
2
2
 
3
- from dcim.models import Device
4
- from ipam.models import IPAddress
3
+ from dcim.models import Interface
4
+ from dcim.models import MACAddress
5
5
  from netbox_branching.contextvars import active_branch
6
6
 
7
7
  logger = logging.getLogger("ipfabric_netbox.utilities.ipf_utils")
8
8
 
9
9
 
10
- def clear_other_primary_ip(instance: Device, **kwargs) -> None:
11
- """
12
- When a new device is created with primary IP, make sure there is no other device with the same IP.
13
-
14
- This signal is used when merging stashed changes. It's needed because we cannot
15
- guarantee that removing primary IP from Device will happen before adding new one.
16
- """
10
+ def assign_primary_mac_address(instance: MACAddress, **kwargs) -> None:
17
11
  try:
18
- if not instance.primary_ip:
19
- # The device has no primary IP, nothing to do
12
+ if instance.assigned_object and instance.assigned_object.primary_mac_address:
13
+ # The Interface already has primary MAC, nothing to do
20
14
  return
21
- except IPAddress.DoesNotExist:
22
- # THe IP is not created yet, cannot be assigned
15
+ except Interface.DoesNotExist:
16
+ # The Interface is not created yet, cannot be assigned
23
17
  return
24
- try:
25
- connection_name = None
26
- if branch := active_branch.get():
27
- connection_name = branch.connection_name
28
- other_device = Device.objects.using(connection_name).get(
29
- primary_ip4=instance.primary_ip
30
- )
31
- if other_device and instance != other_device:
32
- other_device.snapshot()
33
- other_device.primary_ip4 = None
34
- other_device.save(using=connection_name)
35
- except Device.DoesNotExist:
36
- pass
18
+
19
+ connection_name = None
20
+ if branch := active_branch.get():
21
+ connection_name = branch.connection_name
22
+ instance.assigned_object.snapshot()
23
+ instance.assigned_object.primary_mac_address = instance
24
+ instance.assigned_object.save(using=connection_name)
37
25
 
38
26
 
39
27
  def remove_group_from_syncs(instance, **kwargs):
@@ -9,7 +9,7 @@
9
9
  <div class="row mb-3">
10
10
  <div class="col-6">
11
11
  <div class="card">
12
- <h5 class="card-header">%{ trans "Pre-Change Data" %}</h5>
12
+ <h5 class="card-header">{% trans "Pre-Change Data" %}</h5>
13
13
  <div class="card-body">
14
14
  {% if prechange_data %}
15
15
  <pre class="change-data">{% for k, v in prechange_data.items %}{% spaceless %}
@@ -17,14 +17,14 @@
17
17
  {% endspaceless %}{% endfor %}
18
18
  </pre>
19
19
  {% else %}
20
- <span class="text-muted">%{ trans "None" %}</span>
20
+ <span class="text-muted">{% trans "None" %}</span>
21
21
  {% endif %}
22
22
  </div>
23
23
  </div>
24
24
  </div>
25
25
  <div class="col-6">
26
26
  <div class="card">
27
- <h5 class="card-header">%{ trans "Post-Change Data" %}</h5>
27
+ <h5 class="card-header">{% trans "Post-Change Data" %}</h5>
28
28
  <div class="card-body">
29
29
  {% if postchange_data %}
30
30
  <pre class="change-data">{% for k, v in postchange_data.items %}{% spaceless %}
@@ -38,7 +38,7 @@
38
38
  <div class="col">
39
39
  <div class="card">
40
40
  <div class="card-header">
41
- <h5 class="d-inline">%{ trans "Difference" %}</h5>
41
+ <h5 class="d-inline">{% trans "Difference" %}</h5>
42
42
  <!-- <div class="btn-group btn-group-sm float-end noprint">
43
43
  <a {% if prev_change %}href="{% url 'extras:objectchange' pk=prev_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
44
44
  <i class="mdi mdi-chevron-left" aria-hidden="true"></i> Previous
@@ -52,11 +52,11 @@
52
52
  {% if diff_added == diff_removed %}
53
53
  <span class="text-muted" style="margin-left: 10px;">
54
54
  {% if object.action == 'create' %}
55
- %{ trans "Object Created" %}
55
+ {% trans "Object Created" %}
56
56
  {% elif object.action == 'delete' %}
57
- %{ trans "Object Deleted" %}
57
+ {% trans "Object Deleted" %}
58
58
  {% else %}
59
- %{ trans "No Changes" %}
59
+ {% trans "No Changes" %}
60
60
  {% endif %}
61
61
  </span>
62
62
  {% else %}
@@ -91,11 +91,11 @@
91
91
  </div>
92
92
  </div>
93
93
  {% plugin_left_page object %}
94
- </div>
95
- <div class="col col-md-6">
96
94
  <div id="ingestion_statistics" hx-swap-oob="true">
97
95
  {% include 'ipfabric_netbox/partials/ingestion_statistics.html' with object=object %}
98
96
  </div>
97
+ </div>
98
+ <div class="col col-md-6">
99
99
  <div id="ingestion_progress" hx-swap-oob="true">
100
100
  {% include 'ipfabric_netbox/partials/ingestion_progress.html' with job=object.job %}
101
101
  </div>
@@ -174,7 +174,10 @@ class IPFabricTransformMapModelTestCase(TestCase):
174
174
  }
175
175
 
176
176
  self.site = runner.sync_item(
177
- item=site_data, app_label="dcim", model="site", cf=sync.update_custom_fields
177
+ record=runner.create_new_data_record(
178
+ app="dcim", model="site", data=site_data
179
+ ),
180
+ cf=sync.update_custom_fields,
178
181
  )
179
182
 
180
183
  device_data = {
@@ -210,37 +213,38 @@ class IPFabricTransformMapModelTestCase(TestCase):
210
213
  "uptime": 7254180,
211
214
  "vendor": "cisco",
212
215
  "version": "15.2(4)M1",
216
+ "virtual_chassis": None,
213
217
  "slug": None,
214
218
  }
215
219
 
216
220
  self.mf_obj = runner.sync_item(
217
- item=device_data,
218
- app_label="dcim",
219
- model="manufacturer",
221
+ record=runner.create_new_data_record(
222
+ app="dcim", model="manufacturer", data=device_data
223
+ ),
220
224
  cf=sync.update_custom_fields,
221
225
  )
222
226
  self.dt_obj = runner.sync_item(
223
- item=device_data,
224
- app_label="dcim",
225
- model="devicetype",
227
+ record=runner.create_new_data_record(
228
+ app="dcim", model="devicetype", data=device_data
229
+ ),
226
230
  cf=sync.update_custom_fields,
227
231
  )
228
232
  self.platform = runner.sync_item(
229
- item=device_data,
230
- app_label="dcim",
231
- model="platform",
233
+ record=runner.create_new_data_record(
234
+ app="dcim", model="platform", data=device_data
235
+ ),
232
236
  cf=sync.update_custom_fields,
233
237
  )
234
238
  self.role = runner.sync_item(
235
- item=device_data,
236
- app_label="dcim",
237
- model="devicerole",
239
+ record=runner.create_new_data_record(
240
+ app="dcim", model="devicerole", data=device_data
241
+ ),
238
242
  cf=sync.update_custom_fields,
239
243
  )
240
244
  self.device_object = runner.sync_item(
241
- item=device_data,
242
- app_label="dcim",
243
- model="device",
245
+ record=runner.create_new_data_record(
246
+ app="dcim", model="device", data=device_data
247
+ ),
244
248
  cf=sync.update_custom_fields,
245
249
  )
246
250
 
@@ -401,6 +405,7 @@ class IPFabricTransformMapModelTestCase(TestCase):
401
405
  "uptime": 7254180,
402
406
  "vendor": "cisco",
403
407
  "version": "15.2(4)M1",
408
+ "virtual_chassis": None,
404
409
  "slug": None,
405
410
  }
406
411
 
@@ -415,9 +420,9 @@ class IPFabricTransformMapModelTestCase(TestCase):
415
420
  transform_field.template = "{{ object.hostname }} - test"
416
421
  transform_field.save()
417
422
  device_object = runner.sync_item(
418
- item=device_data,
419
- app_label="dcim",
420
- model="device",
423
+ record=runner.create_new_data_record(
424
+ app="dcim", model="device", data=device_data
425
+ ),
421
426
  cf=sync.update_custom_fields,
422
427
  )
423
428
  self.assertEqual(device_object.name, "L21PE152 - test")