ipfabric_netbox 3.1.3__py3-none-any.whl → 3.2.1__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.
- ipfabric_netbox/__init__.py +2 -31
- ipfabric_netbox/data/transform_map.json +625 -0
- ipfabric_netbox/exceptions.py +24 -0
- ipfabric_netbox/forms.py +4 -10
- ipfabric_netbox/migrations/0007_prepare_custom_fields.py +102 -0
- ipfabric_netbox/migrations/0008_prepare_transform_maps.py +42 -0
- ipfabric_netbox/migrations/0009_transformmap_changes_for_netbox_v4_2.py +224 -0
- ipfabric_netbox/migrations/0010_remove_uuid_from_get_or_create.py +95 -0
- ipfabric_netbox/models.py +67 -63
- ipfabric_netbox/signals.py +27 -66
- ipfabric_netbox/tables.py +4 -0
- ipfabric_netbox/tests/test_models.py +26 -975
- ipfabric_netbox/utilities/ipfutils.py +327 -200
- ipfabric_netbox/utilities/logging.py +12 -7
- ipfabric_netbox/utilities/nbutils.py +0 -26
- ipfabric_netbox/utilities/transform_map.py +37 -15
- ipfabric_netbox/views.py +3 -9
- {ipfabric_netbox-3.1.3.dist-info → ipfabric_netbox-3.2.1.dist-info}/METADATA +19 -11
- {ipfabric_netbox-3.1.3.dist-info → ipfabric_netbox-3.2.1.dist-info}/RECORD +20 -14
- {ipfabric_netbox-3.1.3.dist-info → ipfabric_netbox-3.2.1.dist-info}/WHEEL +1 -1
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
|
|
@@ -71,6 +74,7 @@ IPFabricSupportedSyncModels = Q(
|
|
|
71
74
|
| Q(app_label="dcim", model="device")
|
|
72
75
|
| Q(app_label="dcim", model="virtualchassis")
|
|
73
76
|
| Q(app_label="dcim", model="interface")
|
|
77
|
+
| Q(app_label="dcim", model="macaddress")
|
|
74
78
|
| Q(app_label="ipam", model="vlan")
|
|
75
79
|
| Q(app_label="ipam", model="vrf")
|
|
76
80
|
| Q(app_label="ipam", model="prefix")
|
|
@@ -138,85 +142,60 @@ class IPFabricTransformMap(NetBoxModel):
|
|
|
138
142
|
_context["contenttypes"]["ContentType"] = ContentType
|
|
139
143
|
return _context
|
|
140
144
|
|
|
141
|
-
def build_relationships(self,
|
|
145
|
+
def build_relationships(self, source_data):
|
|
142
146
|
relationship_maps = self.relationship_maps.all()
|
|
143
147
|
rel_dict = {}
|
|
144
148
|
rel_dict_coalesce = {}
|
|
145
149
|
|
|
146
150
|
for field in relationship_maps:
|
|
147
|
-
if field.template:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
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)
|
|
165
168
|
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
168
175
|
else:
|
|
169
176
|
rel_dict_coalesce[field.target_field] = related_object
|
|
170
|
-
elif uuid and self.relationship_store.get(uuid):
|
|
171
|
-
object = self.relationship_store[uuid].get(
|
|
172
|
-
field.source_model.model_class()
|
|
173
|
-
)
|
|
174
|
-
if object:
|
|
175
|
-
if not field.coalesce:
|
|
176
|
-
rel_dict[field.target_field] = object
|
|
177
|
-
else:
|
|
178
|
-
rel_dict_coalesce[field.target_field] = object
|
|
179
|
-
|
|
180
177
|
return rel_dict, rel_dict_coalesce
|
|
181
178
|
|
|
182
|
-
def
|
|
183
|
-
|
|
184
|
-
):
|
|
185
|
-
self.relationship_store = relationship_store
|
|
186
|
-
new_data = deepcopy(data)
|
|
179
|
+
def get_context(self, source_data):
|
|
180
|
+
new_data = deepcopy(source_data)
|
|
187
181
|
relationship, coalesce_relationship = self.build_relationships(
|
|
188
|
-
|
|
182
|
+
source_data=source_data
|
|
189
183
|
)
|
|
190
184
|
if relationship:
|
|
191
185
|
new_data["relationship"] = relationship
|
|
192
186
|
if coalesce_relationship:
|
|
193
187
|
new_data["relationship_coalesce"] = coalesce_relationship
|
|
194
188
|
context = self.render(new_data)
|
|
195
|
-
|
|
196
|
-
instance, _ = self.target_model.model_class().objects.update_or_create(
|
|
197
|
-
**context
|
|
198
|
-
)
|
|
199
|
-
if instance:
|
|
200
|
-
apply_tags(instance, tags)
|
|
201
|
-
except Exception as e:
|
|
202
|
-
error_message = f"""Failed to create instance:<br/>
|
|
203
|
-
message: `{e}`<br/>
|
|
204
|
-
raw data: `{data}`<br/>
|
|
205
|
-
context: `{context}`<br/>
|
|
206
|
-
""" # noqa E231 E222
|
|
207
|
-
logger.log_failure(error_message, obj=self)
|
|
208
|
-
logger.log_failure(
|
|
209
|
-
"Ensure that all transform map fields are present.", obj=self
|
|
210
|
-
)
|
|
211
|
-
raise SyncError("Unable to update_or_create_instance.")
|
|
189
|
+
return context
|
|
212
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)
|
|
213
197
|
return instance
|
|
214
198
|
|
|
215
|
-
def get_coalesce_fields(self, source_data):
|
|
216
|
-
data = self.render(source_data)
|
|
217
|
-
del data["defaults"]
|
|
218
|
-
return data
|
|
219
|
-
|
|
220
199
|
def render(self, source_data):
|
|
221
200
|
data = {"defaults": {}}
|
|
222
201
|
for field in self.field_maps.all():
|
|
@@ -250,7 +229,10 @@ class IPFabricTransformMap(NetBoxModel):
|
|
|
250
229
|
if not field.coalesce:
|
|
251
230
|
data["defaults"][field.target_field] = text
|
|
252
231
|
else:
|
|
253
|
-
|
|
232
|
+
if text is None:
|
|
233
|
+
data[field.target_field + "__isnull"] = True
|
|
234
|
+
else:
|
|
235
|
+
data[field.target_field] = text
|
|
254
236
|
|
|
255
237
|
if relationship := source_data.get("relationship"):
|
|
256
238
|
data["defaults"].update(relationship)
|
|
@@ -597,6 +579,14 @@ class IPFabricSync(IPFabricClient, JobsMixin, TagsMixin, ChangeLoggedModel):
|
|
|
597
579
|
# TODO: Add docs url
|
|
598
580
|
return ""
|
|
599
581
|
|
|
582
|
+
@property
|
|
583
|
+
def logger(self):
|
|
584
|
+
return getattr(self, "_logger", SyncLogging(job=self.pk))
|
|
585
|
+
|
|
586
|
+
@logger.setter
|
|
587
|
+
def logger(self, value):
|
|
588
|
+
self._logger = value
|
|
589
|
+
|
|
600
590
|
def get_absolute_url(self):
|
|
601
591
|
return reverse("plugins:ipfabric_netbox:ipfabricsync", args=[self.pk])
|
|
602
592
|
|
|
@@ -711,9 +701,11 @@ class IPFabricSync(IPFabricClient, JobsMixin, TagsMixin, ChangeLoggedModel):
|
|
|
711
701
|
)
|
|
712
702
|
|
|
713
703
|
with checkout(branch):
|
|
714
|
-
runner.
|
|
704
|
+
runner.collect_and_sync(branch=branch)
|
|
705
|
+
|
|
706
|
+
if self.status != DataSourceStatusChoices.FAILED:
|
|
707
|
+
self.status = DataSourceStatusChoices.COMPLETED
|
|
715
708
|
|
|
716
|
-
self.status = DataSourceStatusChoices.COMPLETED
|
|
717
709
|
except Exception as e:
|
|
718
710
|
self.status = DataSourceStatusChoices.FAILED
|
|
719
711
|
self.logger.log_failure(f"Branch Failed: `{e}`", obj=branch)
|
|
@@ -809,6 +801,8 @@ class IPFabricBranch(JobsMixin, Branch):
|
|
|
809
801
|
statistics = {}
|
|
810
802
|
if job_results:
|
|
811
803
|
for model, stats in job_results["statistics"].items():
|
|
804
|
+
if not stats["total"]:
|
|
805
|
+
continue
|
|
812
806
|
if stats["total"] > 0:
|
|
813
807
|
statistics[model] = stats["current"] / stats["total"] * 100
|
|
814
808
|
else:
|
|
@@ -820,7 +814,15 @@ class IPFabricBranch(JobsMixin, Branch):
|
|
|
820
814
|
with transaction.atomic():
|
|
821
815
|
for change in self.staged_changes.all():
|
|
822
816
|
logger.debug("Applying change: %s", change)
|
|
823
|
-
|
|
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
|
|
824
826
|
signals.pre_delete.disconnect(handle_deleted_object)
|
|
825
827
|
self.staged_changes.all().delete()
|
|
826
828
|
signals.pre_delete.connect(handle_deleted_object, sender=StagedChange)
|
|
@@ -840,6 +842,7 @@ class IPFabricBranch(JobsMixin, Branch):
|
|
|
840
842
|
# Begin Sync
|
|
841
843
|
logger.debug(f"Merging {self.name}")
|
|
842
844
|
try:
|
|
845
|
+
signals.pre_save.connect(clear_other_primary_ip, sender=Device)
|
|
843
846
|
signals.post_save.disconnect(
|
|
844
847
|
assign_virtualchassis_master, sender=VirtualChassis
|
|
845
848
|
)
|
|
@@ -847,6 +850,7 @@ class IPFabricBranch(JobsMixin, Branch):
|
|
|
847
850
|
signals.post_save.connect(
|
|
848
851
|
assign_virtualchassis_master, sender=VirtualChassis
|
|
849
852
|
)
|
|
853
|
+
signals.pre_save.disconnect(clear_other_primary_ip, sender=Device)
|
|
850
854
|
ipfabricsync.status = DataSourceStatusChoices.COMPLETED
|
|
851
855
|
except Exception as e:
|
|
852
856
|
ipfabricsync.status = DataSourceStatusChoices.FAILED
|
ipfabric_netbox/signals.py
CHANGED
|
@@ -1,68 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
import logging
|
|
2
2
|
|
|
3
|
-
from core.models import ObjectType
|
|
4
3
|
from dcim.models import Device
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"ui_editable": getattr(CustomFieldUIEditableChoices, "NO"),
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
custom_field, _ = CustomField.objects.update_or_create(
|
|
35
|
-
type=getattr(CustomFieldTypeChoices, cf_type.upper()),
|
|
36
|
-
name=field_name,
|
|
37
|
-
defaults=defaults,
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
for model in models:
|
|
41
|
-
custom_field.object_types.add(ObjectType.objects.get_for_model(model))
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def ipfabric_netbox_init():
|
|
45
|
-
create_custom_field(
|
|
46
|
-
"ipfabric_source",
|
|
47
|
-
"IP Fabric Source",
|
|
48
|
-
[Device, Site],
|
|
49
|
-
cf_type="type_object",
|
|
50
|
-
object_type=IPFabricSource,
|
|
51
|
-
)
|
|
52
|
-
create_custom_field(
|
|
53
|
-
"ipfabric_branch",
|
|
54
|
-
"IP Fabric Last Sync",
|
|
55
|
-
[Device, Site],
|
|
56
|
-
cf_type="type_object",
|
|
57
|
-
object_type=IPFabricBranch,
|
|
58
|
-
)
|
|
59
|
-
cl, _ = CustomLink.objects.update_or_create(
|
|
60
|
-
defaults={
|
|
61
|
-
"link_text": "{% if object.custom_field_data.ipfabric_source is defined %}{% set SOURCE_ID = object.custom_field_data.ipfabric_source %}{% if SOURCE_ID %}IP Fabric{% endif %}{% endif %}",
|
|
62
|
-
"link_url": '{% if object.custom_field_data.ipfabric_source is defined %}{% set SOURCE_ID = object.custom_field_data.ipfabric_source %}{% if SOURCE_ID %}{% set BASE_URL = object.custom_fields.filter(related_object_type__model="ipfabricsource").first().related_object_type.model_class().objects.get(pk=SOURCE_ID).url %}{{ BASE_URL }}/inventory/devices?options={"filters":{"sn": ["like","{{ object.serial }}"]}}{% endif %}{%endif%}',
|
|
63
|
-
"new_window": True,
|
|
64
|
-
"button_class": CustomLinkButtonClassChoices.BLUE,
|
|
65
|
-
},
|
|
66
|
-
name="ipfabric",
|
|
67
|
-
)
|
|
68
|
-
cl.object_types.add(ObjectType.objects.get_for_model(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
|
ipfabric_netbox/tables.py
CHANGED
|
@@ -143,6 +143,8 @@ class SyncTable(NetBoxTable):
|
|
|
143
143
|
|
|
144
144
|
|
|
145
145
|
class StagedChangesTable(NetBoxTable):
|
|
146
|
+
# There is no view for single StagedChange, remove the link in ID
|
|
147
|
+
id = tables.Column(verbose_name=_("ID"))
|
|
146
148
|
pk = None
|
|
147
149
|
object_type = tables.Column(
|
|
148
150
|
accessor="object_type.model", verbose_name="Object Type"
|
|
@@ -172,6 +174,8 @@ class StagedChangesTable(NetBoxTable):
|
|
|
172
174
|
name = f"Tagging object ({record.data['object_id']})"
|
|
173
175
|
elif value == "prefix":
|
|
174
176
|
name = f"{record.data['prefix']} ({record.data['vrf']})"
|
|
177
|
+
elif value == "MAC address":
|
|
178
|
+
name = record.data["mac_address"]
|
|
175
179
|
else:
|
|
176
180
|
name = record.data
|
|
177
181
|
else:
|