ipfabric_netbox 3.2.0__py3-none-any.whl → 3.2.2__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.

@@ -5,9 +5,9 @@ class NetboxIPFabricConfig(PluginConfig):
5
5
  name = "ipfabric_netbox"
6
6
  verbose_name = "NetBox IP Fabric SoT Plugin"
7
7
  description = "Sync IP Fabric into NetBox"
8
- version = "3.2.0"
8
+ version = "3.2.2"
9
9
  base_url = "ipfabric"
10
- min_version = "4.2"
10
+ min_version = "4.2.4"
11
11
 
12
12
 
13
13
  config = NetboxIPFabricConfig
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "target_field": "vrf",
45
45
  "coalesce": false,
46
- "template": "{{ ipam.VRF.objects.filter(name=object.vrf).first().pk }}"
46
+ "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 %}"
47
47
  }
48
48
  ]
49
49
  },
@@ -82,7 +82,7 @@
82
82
  },
83
83
  "target_field": "manufacturer",
84
84
  "coalesce": false,
85
- "template": null
85
+ "template": "{% set SLUG = object.vendor | slugify %}\n{{ dcim.Manufacturer.objects.get(slug=SLUG).pk }}"
86
86
  }
87
87
  ]
88
88
  },
@@ -194,7 +194,7 @@
194
194
  },
195
195
  "target_field": "platform",
196
196
  "coalesce": false,
197
- "template": null
197
+ "template": "{% set SLUG = object.vendor | slugify %}{% if object.family %}{% set SLUG = SLUG ~ \"_\" ~ object.family | slugify %}{% endif %}\n{{ dcim.Platform.objects.get(slug=SLUG).pk }}"
198
198
  },
199
199
  {
200
200
  "source_model": {
@@ -203,7 +203,7 @@
203
203
  },
204
204
  "target_field": "site",
205
205
  "coalesce": false,
206
- "template": null
206
+ "template": "{% if object.siteName is defined %}{% set SLUG = object.siteName | slugify %}\n{{ dcim.Site.objects.get(slug=SLUG).pk }}{% else %}None{% endif %}"
207
207
  },
208
208
  {
209
209
  "source_model": {
@@ -212,7 +212,7 @@
212
212
  },
213
213
  "target_field": "device_type",
214
214
  "coalesce": false,
215
- "template": null
215
+ "template": "{% if object.model != none %}{% set SLUG = object.model | string | slugify %}{% else %}{% set SLUG = object.vendor | slugify ~ \"-\" ~ object.family | slugify ~ \"-\" ~ object.platform %}{% endif %}{{ dcim.DeviceType.objects.get(slug=SLUG).pk }}"
216
216
  },
217
217
  {
218
218
  "source_model": {
@@ -221,7 +221,7 @@
221
221
  },
222
222
  "target_field": "role",
223
223
  "coalesce": false,
224
- "template": null
224
+ "template": "{% if object.devType is defined %}{% set SLUG = object.devType | slugify %}{{ dcim.DeviceRole.objects.get(slug=SLUG).pk }}{% endif %}"
225
225
  }
226
226
  ]
227
227
  },
@@ -295,7 +295,7 @@
295
295
  },
296
296
  "target_field": "manufacturer",
297
297
  "coalesce": false,
298
- "template": null
298
+ "template": "{% set SLUG = object.vendor | slugify %}\n{{ dcim.Manufacturer.objects.get(slug=SLUG).pk }}"
299
299
  }
300
300
  ]
301
301
  },
@@ -370,7 +370,7 @@
370
370
  },
371
371
  "target_field": "device",
372
372
  "coalesce": true,
373
- "template": null
373
+ "template": "{{ dcim.Device.objects.get(serial=object.sn).pk }}"
374
374
  }
375
375
  ]
376
376
  },
@@ -431,7 +431,7 @@
431
431
  "source_field": "pid",
432
432
  "target_field": "part_id",
433
433
  "coalesce": false,
434
- "template": ""
434
+ "template": "{% if object.pid and object.pid != \"None\" %}{{ object.pid }}{% else %}unknown{% endif %}"
435
435
  },
436
436
  {
437
437
  "source_field": "sn",
@@ -454,7 +454,7 @@
454
454
  },
455
455
  "target_field": "device",
456
456
  "coalesce": false,
457
- "template": "{{ dcim.Device.objects.filter(serial=object.deviceSn).first().pk }}"
457
+ "template": "{{ dcim.Device.objects.get(serial=object.deviceSn).pk }}"
458
458
  },
459
459
  {
460
460
  "source_model": {
@@ -463,7 +463,7 @@
463
463
  },
464
464
  "target_field": "manufacturer",
465
465
  "coalesce": false,
466
- "template": null
466
+ "template": "{% if object.vendor is defined %}{% set SLUG = object.vendor | slugify %}\n{{ dcim.Manufacturer.objects.get(slug=SLUG).pk }}{% endif %}"
467
467
  }
468
468
  ]
469
469
  },
@@ -485,7 +485,7 @@
485
485
  "source_field": "vlanName",
486
486
  "target_field": "name",
487
487
  "coalesce": false,
488
- "template": "{{ object.vlanName or \"\"}}"
488
+ "template": "{% if object.vlanName is defined and object.vlanName | lower != \"none\" %}{{ object.vlanName | string | truncate(64, True) }}{% else %}\"\"{% endif %}"
489
489
  },
490
490
  {
491
491
  "source_field": "dscr",
@@ -508,7 +508,7 @@
508
508
  },
509
509
  "target_field": "site",
510
510
  "coalesce": true,
511
- "template": null
511
+ "template": "{% if object.siteName is defined %}{% set SLUG = object.siteName | slugify %}\n{{ dcim.Site.objects.get(slug=SLUG).pk }}{% else %}None{% endif %}"
512
512
  }
513
513
  ]
514
514
  },
@@ -565,7 +565,7 @@
565
565
  "source_field": "siteName",
566
566
  "target_field": "scope_id",
567
567
  "coalesce": true,
568
- "template": "{% if object.siteName is defined %}{% set SLUG = object.siteName | slugify %}\n{{ dcim.Site.objects.filter(slug=SLUG).first().pk }}{% else %}None{% endif %}"
568
+ "template": "{% if object.siteName is defined %}{% set SLUG = object.siteName | slugify %}\n{{ dcim.Site.objects.get(slug=SLUG).pk }}{% else %}None{% endif %}"
569
569
  }
570
570
  ],
571
571
  "relationship_maps": [
@@ -576,7 +576,7 @@
576
576
  },
577
577
  "target_field": "vrf",
578
578
  "coalesce": true,
579
- "template": "{% if object.vrf %}{{ ipam.VRF.objects.filter(name=object.vrf).first().pk }}{% else %}None{% endif %}"
579
+ "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 %}"
580
580
  },
581
581
  {
582
582
  "source_model": {
@@ -618,7 +618,7 @@
618
618
  },
619
619
  "target_field": "master",
620
620
  "coalesce": false,
621
- "template": "{{ dcim.Device.objects.filter(serial=object.sn).first().pk }}"
621
+ "template": "{% set DEVICE = dcim.Device.objects.filter(serial=object.sn).first() %}{% if DEVICE %}{{ DEVICE.pk }}{% endif %}"
622
622
  }
623
623
  ]
624
624
  }
@@ -0,0 +1,24 @@
1
+ from core.exceptions import SyncError
2
+
3
+
4
+ class ErrorMixin(Exception):
5
+ model: str = ""
6
+ defaults: dict[str, str] = {}
7
+ coalesce_fields: dict[str, str] = {}
8
+
9
+ def __init__(self, model: str, context: dict, data: dict = None):
10
+ super().__init__()
11
+ self.model = model
12
+ self.data = data or {}
13
+ self.defaults = context.pop("defaults", {})
14
+ self.coalesce_fields = context
15
+
16
+
17
+ class SearchError(ErrorMixin, LookupError):
18
+ def __str__(self):
19
+ return f"{self.model} with these keys not found: {self.coalesce_fields}."
20
+
21
+
22
+ class SyncDataError(ErrorMixin, SyncError):
23
+ def __str__(self):
24
+ return f"Sync failed for {self.model}: coalesce_fields={self.coalesce_fields} defaults={self.defaults}."
ipfabric_netbox/forms.py CHANGED
@@ -1,12 +1,12 @@
1
1
  import copy
2
2
 
3
3
  from core.choices import DataSourceStatusChoices
4
+ from core.choices import JobIntervalChoices
4
5
  from django import forms
5
6
  from django.contrib.contenttypes.models import ContentType
6
7
  from django.core.exceptions import ValidationError
7
8
  from django.utils import timezone
8
9
  from django.utils.translation import gettext_lazy as _
9
- from extras.choices import DurationChoices
10
10
  from netbox.forms import NetBoxModelFilterSetForm
11
11
  from netbox.forms import NetBoxModelForm
12
12
  from netbox.forms.mixins import SavedFiltersMixin
@@ -476,7 +476,7 @@ class IPFabricSyncForm(NetBoxModelForm):
476
476
  required=False,
477
477
  min_value=1,
478
478
  label=_("Recurs every"),
479
- widget=NumberWithOptions(options=DurationChoices),
479
+ widget=NumberWithOptions(options=JobIntervalChoices),
480
480
  help_text=_("Interval at which this sync is re-run (in minutes)"),
481
481
  )
482
482
  auto_merge = forms.BooleanField(
@@ -41,9 +41,6 @@ def get_current_map_field(apps: "apps", target_model: str) -> "IPFabricTransform
41
41
  )
42
42
 
43
43
 
44
- # TODO: Add MAC Address to existing IPFabricSync instances if Interface is allowed
45
-
46
-
47
44
  def create_transform_map(apps: "apps", schema_editor: "BaseDatabaseSchemaEditor"):
48
45
  """Create transform map structure for MAC Address (new in NetBox v4.2).
49
46
  Keeps current template for MAC Address if it exists.
@@ -0,0 +1,95 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from django.db import migrations
4
+
5
+ from ipfabric_netbox.utilities.transform_map import get_transform_map
6
+
7
+ if TYPE_CHECKING:
8
+ from django.apps import apps
9
+ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
10
+
11
+
12
+ items_to_change = {
13
+ ("ip_address", "ipam", "ipaddress"): {"fields": [], "relationships": ["vrf"]},
14
+ ("device", "dcim", "platform"): {"fields": [], "relationships": ["manufacturer"]},
15
+ ("device", "dcim", "device"): {
16
+ "fields": [],
17
+ "relationships": ["platform", "site", "device_type", "role"],
18
+ },
19
+ ("device", "dcim", "devicetype"): {"fields": [], "relationships": ["manufacturer"]},
20
+ ("interface", "dcim", "interface"): {"fields": [], "relationships": ["device"]},
21
+ ("part_number", "dcim", "inventoryitem"): {
22
+ "fields": ["part_id"],
23
+ "relationships": ["device", "manufacturer"],
24
+ },
25
+ ("vlan", "ipam", "vlan"): {"fields": ["name"], "relationships": ["site"]},
26
+ ("prefix", "ipam", "prefix"): {"fields": ["scope_id"], "relationships": ["vrf"]},
27
+ ("virtualchassis", "dcim", "virtualchassis"): {
28
+ "fields": [],
29
+ "relationships": ["master"],
30
+ },
31
+ }
32
+
33
+
34
+ def add_templates(apps: "apps", schema_editor: "BaseDatabaseSchemaEditor"):
35
+ transform_map_data = get_transform_map()
36
+
37
+ for item in transform_map_data:
38
+ transform_map_id = (
39
+ item["data"]["source_model"],
40
+ item["data"]["target_model"]["app_label"],
41
+ item["data"]["target_model"]["model"],
42
+ )
43
+ if transform_map_id not in items_to_change:
44
+ continue
45
+ source_model, app_label, target_model = transform_map_id
46
+ transform_map = apps.get_model(
47
+ "ipfabric_netbox", "IPFabricTransformMap"
48
+ ).objects.get(
49
+ source_model=source_model,
50
+ target_model=apps.get_model("contenttypes", "ContentType").objects.get(
51
+ app_label=app_label,
52
+ model=target_model,
53
+ ),
54
+ )
55
+
56
+ change_data = items_to_change[transform_map_id]
57
+
58
+ for field_map in item["field_maps"]:
59
+ if field_map["target_field"] not in change_data["fields"]:
60
+ continue
61
+ field = apps.get_model(
62
+ "ipfabric_netbox", "IPFabricTransformField"
63
+ ).objects.get(
64
+ target_field=field_map["target_field"], transform_map=transform_map
65
+ )
66
+ field.template = field_map["template"]
67
+ field.save()
68
+
69
+ for relationship_map in item["relationship_maps"]:
70
+ if relationship_map["target_field"] not in change_data["relationships"]:
71
+ continue
72
+ relationship = apps.get_model(
73
+ "ipfabric_netbox", "IPFabricRelationshipField"
74
+ ).objects.get(
75
+ target_field=relationship_map["target_field"],
76
+ transform_map=transform_map,
77
+ )
78
+ relationship.template = relationship_map["template"]
79
+ relationship.save()
80
+
81
+
82
+ def return_templates(apps: "apps", schema_editor: "BaseDatabaseSchemaEditor"):
83
+ """It would be way too complex to revert this migration and there is no need to do so.
84
+ The original code works the same with or without the templates."""
85
+ pass
86
+
87
+
88
+ class Migration(migrations.Migration):
89
+ dependencies = [
90
+ ("ipfabric_netbox", "0009_transformmap_changes_for_netbox_v4_2"),
91
+ ]
92
+
93
+ operations = [
94
+ migrations.RunPython(add_templates, return_templates),
95
+ ]
ipfabric_netbox/models.py CHANGED
@@ -10,6 +10,7 @@ from core.exceptions import SyncError
10
10
  from core.models import Job
11
11
  from core.signals import handle_deleted_object
12
12
  from core.signals import pre_sync
13
+ from dcim.models import Device
13
14
  from dcim.models import VirtualChassis
14
15
  from dcim.signals import assign_virtualchassis_master
15
16
  from django.apps import apps
@@ -21,6 +22,7 @@ from django.db import models
21
22
  from django.db import transaction
22
23
  from django.db.models import Q
23
24
  from django.db.models import signals
25
+ from django.forms.models import model_to_dict
24
26
  from django.urls import reverse
25
27
  from django.utils import timezone
26
28
  from django.utils.module_loading import import_string
@@ -43,6 +45,7 @@ from .choices import IPFabricSnapshotStatusModelChoices
43
45
  from .choices import IPFabricSourceTypeChoices
44
46
  from .choices import IPFabricSyncTypeChoices
45
47
  from .choices import IPFabricTransformMapSourceModelChoices
48
+ from .signals import clear_other_primary_ip
46
49
  from .utilities.ipfutils import IPFabric
47
50
  from .utilities.ipfutils import IPFabricSyncRunner
48
51
  from .utilities.ipfutils import render_jinja2
@@ -139,85 +142,60 @@ class IPFabricTransformMap(NetBoxModel):
139
142
  _context["contenttypes"]["ContentType"] = ContentType
140
143
  return _context
141
144
 
142
- def build_relationships(self, uuid, source_data):
145
+ def build_relationships(self, source_data):
143
146
  relationship_maps = self.relationship_maps.all()
144
147
  rel_dict = {}
145
148
  rel_dict_coalesce = {}
146
149
 
147
150
  for field in relationship_maps:
148
- if field.template:
149
- context = {
150
- "object": source_data,
151
- }
152
- context.update(self.get_models())
153
- text = render_jinja2(field.template, context).strip()
154
- if text:
155
- try:
156
- pk = int(text)
157
- except ValueError:
158
- pk = text
159
-
160
- if isinstance(pk, int):
161
- related_object = field.source_model.model_class().objects.get(
162
- pk=pk
163
- )
164
- else:
165
- related_object = ast.literal_eval(pk)
151
+ if not field.template:
152
+ continue
153
+ context = {
154
+ "object": source_data,
155
+ }
156
+ context.update(self.get_models())
157
+ text = render_jinja2(field.template, context).strip()
158
+ if text:
159
+ try:
160
+ pk = int(text)
161
+ except ValueError:
162
+ pk = text
163
+
164
+ if isinstance(pk, int):
165
+ related_object = field.source_model.model_class().objects.get(pk=pk)
166
+ else:
167
+ related_object = ast.literal_eval(pk)
166
168
 
167
- if not field.coalesce:
168
- rel_dict[field.target_field] = related_object
169
+ if not field.coalesce:
170
+ rel_dict[field.target_field] = related_object
171
+ else:
172
+ if related_object is None:
173
+ # We are searching by this field, so we need to set it to None
174
+ rel_dict_coalesce[field.target_field + "__isnull"] = True
169
175
  else:
170
176
  rel_dict_coalesce[field.target_field] = related_object
171
- elif uuid and self.relationship_store.get(uuid):
172
- object = self.relationship_store[uuid].get(
173
- field.source_model.model_class()
174
- )
175
- if object:
176
- if not field.coalesce:
177
- rel_dict[field.target_field] = object
178
- else:
179
- rel_dict_coalesce[field.target_field] = object
180
-
181
177
  return rel_dict, rel_dict_coalesce
182
178
 
183
- def update_or_create_instance(
184
- self, data, tags=[], uuid=None, relationship_store={}, logger=None
185
- ):
186
- self.relationship_store = relationship_store
187
- new_data = deepcopy(data)
179
+ def get_context(self, source_data):
180
+ new_data = deepcopy(source_data)
188
181
  relationship, coalesce_relationship = self.build_relationships(
189
- uuid=uuid, source_data=data
182
+ source_data=source_data
190
183
  )
191
184
  if relationship:
192
185
  new_data["relationship"] = relationship
193
186
  if coalesce_relationship:
194
187
  new_data["relationship_coalesce"] = coalesce_relationship
195
188
  context = self.render(new_data)
196
- try:
197
- instance, _ = self.target_model.model_class().objects.update_or_create(
198
- **context
199
- )
200
- if instance:
201
- apply_tags(instance, tags)
202
- except Exception as e:
203
- error_message = f"""Failed to create instance of `{str(self.target_model)}`:<br/>
204
- message: `{e}`<br/>
205
- raw data: `{data}`<br/>
206
- context: `{context}`<br/>
207
- """ # noqa E231 E222
208
- logger.log_failure(error_message, obj=self)
209
- logger.log_failure(
210
- "Ensure that all transform map fields are present.", obj=self
211
- )
212
- raise SyncError("Unable to update_or_create_instance.")
189
+ return context
213
190
 
191
+ def update_or_create_instance(self, context, tags=[]):
192
+ instance, _ = self.target_model.model_class().objects.update_or_create(
193
+ **context
194
+ )
195
+ if instance:
196
+ apply_tags(instance, tags)
214
197
  return instance
215
198
 
216
- def get_coalesce_fields(self, source_data):
217
- data = self.render(source_data)
218
- del data["defaults"]
219
- return data
220
-
221
199
  def render(self, source_data):
222
200
  data = {"defaults": {}}
223
201
  for field in self.field_maps.all():
@@ -251,7 +229,10 @@ class IPFabricTransformMap(NetBoxModel):
251
229
  if not field.coalesce:
252
230
  data["defaults"][field.target_field] = text
253
231
  else:
254
- data[field.target_field] = text
232
+ if text is None:
233
+ data[field.target_field + "__isnull"] = True
234
+ else:
235
+ data[field.target_field] = text
255
236
 
256
237
  if relationship := source_data.get("relationship"):
257
238
  data["defaults"].update(relationship)
@@ -720,9 +701,11 @@ class IPFabricSync(IPFabricClient, JobsMixin, TagsMixin, ChangeLoggedModel):
720
701
  )
721
702
 
722
703
  with checkout(branch):
723
- runner.sync_devices(branch=branch)
704
+ runner.collect_and_sync(branch=branch)
705
+
706
+ if self.status != DataSourceStatusChoices.FAILED:
707
+ self.status = DataSourceStatusChoices.COMPLETED
724
708
 
725
- self.status = DataSourceStatusChoices.COMPLETED
726
709
  except Exception as e:
727
710
  self.status = DataSourceStatusChoices.FAILED
728
711
  self.logger.log_failure(f"Branch Failed: `{e}`", obj=branch)
@@ -818,6 +801,8 @@ class IPFabricBranch(JobsMixin, Branch):
818
801
  statistics = {}
819
802
  if job_results:
820
803
  for model, stats in job_results["statistics"].items():
804
+ if not stats["total"]:
805
+ continue
821
806
  if stats["total"] > 0:
822
807
  statistics[model] = stats["current"] / stats["total"] * 100
823
808
  else:
@@ -829,7 +814,15 @@ class IPFabricBranch(JobsMixin, Branch):
829
814
  with transaction.atomic():
830
815
  for change in self.staged_changes.all():
831
816
  logger.debug("Applying change: %s", change)
832
- change.apply()
817
+ try:
818
+ change.apply()
819
+ except Exception as err:
820
+ content_type = ContentType.objects.get(pk=change.object_type.pk)
821
+ data = model_to_dict(change)["data"]
822
+ logger.error(
823
+ f"Got error applying change ({content_type}: {data}): {err}"
824
+ )
825
+ raise
833
826
  signals.pre_delete.disconnect(handle_deleted_object)
834
827
  self.staged_changes.all().delete()
835
828
  signals.pre_delete.connect(handle_deleted_object, sender=StagedChange)
@@ -849,6 +842,7 @@ class IPFabricBranch(JobsMixin, Branch):
849
842
  # Begin Sync
850
843
  logger.debug(f"Merging {self.name}")
851
844
  try:
845
+ signals.pre_save.connect(clear_other_primary_ip, sender=Device)
852
846
  signals.post_save.disconnect(
853
847
  assign_virtualchassis_master, sender=VirtualChassis
854
848
  )
@@ -856,6 +850,7 @@ class IPFabricBranch(JobsMixin, Branch):
856
850
  signals.post_save.connect(
857
851
  assign_virtualchassis_master, sender=VirtualChassis
858
852
  )
853
+ signals.pre_save.disconnect(clear_other_primary_ip, sender=Device)
859
854
  ipfabricsync.status = DataSourceStatusChoices.COMPLETED
860
855
  except Exception as e:
861
856
  ipfabricsync.status = DataSourceStatusChoices.FAILED
@@ -0,0 +1,29 @@
1
+ import logging
2
+
3
+ from dcim.models import Device
4
+ from ipam.models import IPAddress
5
+
6
+ logger = logging.getLogger("ipfabric_netbox.utilities.ipf_utils")
7
+
8
+
9
+ def clear_other_primary_ip(instance: Device, **kwargs) -> None:
10
+ """
11
+ When a new device is created with primary IP, make sure there is no other device with the same IP.
12
+
13
+ This signal is used when merging stashed changes. It's needed because we cannot
14
+ guarantee that removing primary IP from Device will happen before adding new one.
15
+ """
16
+ try:
17
+ if not instance.primary_ip:
18
+ # The device has no primary IP, nothing to do
19
+ return
20
+ except IPAddress.DoesNotExist:
21
+ # THe IP is not created yet, cannot be assigned
22
+ return
23
+ try:
24
+ other_device = Device.objects.get(primary_ip4=instance.primary_ip)
25
+ if other_device and instance != other_device:
26
+ other_device.primary_ip4 = None
27
+ other_device.save()
28
+ except Device.DoesNotExist:
29
+ pass
@@ -1,6 +1,5 @@
1
1
  import copy
2
2
  import json
3
- import uuid
4
3
 
5
4
  from dcim.models import Device
6
5
  from django.contrib.contenttypes.models import ContentType
@@ -155,10 +154,9 @@ class IPFabricTransformMapModelTestCase(TestCase):
155
154
  },
156
155
  sync=sync,
157
156
  )
158
- device_uuid = str(uuid.uuid4())
159
157
 
160
158
  site_data = {
161
- "siteName": "Site 1",
159
+ "siteName": "MPLS",
162
160
  "devicesCount": 1,
163
161
  "usersCount": 2,
164
162
  "stpDCount": 0,
@@ -169,9 +167,7 @@ class IPFabricTransformMapModelTestCase(TestCase):
169
167
  "networksCount": 6,
170
168
  }
171
169
 
172
- self.site = runner.get_model_or_update(
173
- "dcim", "site", site_data, uuid=device_uuid
174
- )
170
+ self.site = runner.get_model_or_update("dcim", "site", site_data)
175
171
 
176
172
  device_data = {
177
173
  "id": "961251111",
@@ -209,24 +205,14 @@ class IPFabricTransformMapModelTestCase(TestCase):
209
205
  "slug": None,
210
206
  }
211
207
 
212
- self.mf_obj = runner.get_model_or_update(
213
- "dcim", "manufacturer", device_data, uuid=device_uuid
214
- )
215
- self.dt_obj = runner.get_model_or_update(
216
- "dcim", "devicetype", device_data, uuid=device_uuid
217
- )
208
+ self.mf_obj = runner.get_model_or_update("dcim", "manufacturer", device_data)
209
+ self.dt_obj = runner.get_model_or_update("dcim", "devicetype", device_data)
218
210
 
219
- self.platform = runner.get_model_or_update(
220
- "dcim", "platform", device_data, uuid=device_uuid
221
- )
211
+ self.platform = runner.get_model_or_update("dcim", "platform", device_data)
222
212
 
223
- self.role = runner.get_model_or_update(
224
- "dcim", "devicerole", device_data, uuid=device_uuid
225
- )
213
+ self.role = runner.get_model_or_update("dcim", "devicerole", device_data)
226
214
 
227
- self.device_object = runner.get_model_or_update(
228
- "dcim", "device", device_data, uuid=device_uuid
229
- )
215
+ self.device_object = runner.get_model_or_update("dcim", "device", device_data)
230
216
 
231
217
  def test_transform_map(self):
232
218
  site_transform_map = IPFabricTransformMap.objects.get(name="Site Transform Map")
@@ -318,7 +304,8 @@ class IPFabricTransformMapModelTestCase(TestCase):
318
304
  "routersCount": 0,
319
305
  "networksCount": 6,
320
306
  }
321
- object = site_transform_map.update_or_create_instance(data=data)
307
+ context = site_transform_map.get_context(data)
308
+ object = site_transform_map.update_or_create_instance(context)
322
309
  self.assertEqual(object.name, "Site 1")
323
310
  self.assertEqual(object.slug, "site-1")
324
311
 
@@ -353,7 +340,6 @@ class IPFabricTransformMapModelTestCase(TestCase):
353
340
  },
354
341
  sync=sync,
355
342
  )
356
- device_uuid = str(uuid.uuid4())
357
343
 
358
344
  device_data = {
359
345
  "id": "961251111",
@@ -401,7 +387,5 @@ class IPFabricTransformMapModelTestCase(TestCase):
401
387
  )
402
388
  transform_field.template = "{{ object.hostname }} - test"
403
389
  transform_field.save()
404
- device_object = runner.get_model_or_update(
405
- "dcim", "device", device_data, uuid=device_uuid
406
- )
390
+ device_object = runner.get_model_or_update("dcim", "device", device_data)
407
391
  self.assertEqual(device_object.name, "L21PE152 - test")