ipfabric_netbox 3.1.2__py3-none-any.whl → 3.2.0__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/forms.py CHANGED
@@ -2,7 +2,6 @@ import copy
2
2
 
3
3
  from core.choices import DataSourceStatusChoices
4
4
  from django import forms
5
- from django.conf import settings
6
5
  from django.contrib.contenttypes.models import ContentType
7
6
  from django.core.exceptions import ValidationError
8
7
  from django.utils import timezone
@@ -10,7 +9,7 @@ from django.utils.translation import gettext_lazy as _
10
9
  from extras.choices import DurationChoices
11
10
  from netbox.forms import NetBoxModelFilterSetForm
12
11
  from netbox.forms import NetBoxModelForm
13
- from packaging import version
12
+ from netbox.forms.mixins import SavedFiltersMixin
14
13
  from utilities.datetime import local_now
15
14
  from utilities.forms import add_blank_choice
16
15
  from utilities.forms import FilterForm
@@ -34,12 +33,6 @@ from .models import IPFabricSync
34
33
  from .models import IPFabricTransformField
35
34
  from .models import IPFabricTransformMap
36
35
 
37
- NETBOX_CURRENT_VERSION = version.parse(settings.VERSION)
38
-
39
- if NETBOX_CURRENT_VERSION >= version.parse("3.7.0"):
40
- from netbox.forms.mixins import SavedFiltersMixin
41
- else:
42
- from extras.forms.mixins import SavedFiltersMixin
43
36
 
44
37
  exclude_fields = [
45
38
  "id",
@@ -83,6 +76,7 @@ dcim_parameters = {
83
76
  required=False, label=_("Virtual Chassis"), initial=True
84
77
  ),
85
78
  "interface": forms.BooleanField(required=False, label=_("Interfaces")),
79
+ "macaddress": forms.BooleanField(required=False, label=_("MAC Addresses")),
86
80
  "inventoryitem": forms.BooleanField(required=False, label=_("Part Numbers")),
87
81
  }
88
82
  ipam_parameters = {
@@ -0,0 +1,102 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from django.db import migrations
4
+ from extras.choices import CustomFieldTypeChoices
5
+ from extras.choices import CustomFieldUIEditableChoices
6
+ from extras.choices import CustomFieldUIVisibleChoices
7
+ from extras.choices import CustomLinkButtonClassChoices
8
+
9
+
10
+ if TYPE_CHECKING:
11
+ from django.apps import apps
12
+ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
13
+
14
+
15
+ def create_custom_field(
16
+ apps: "apps",
17
+ field_name: str,
18
+ label: str,
19
+ models: list,
20
+ object_type=None,
21
+ cf_type: str | None = "type_text",
22
+ ):
23
+ """Create a single custom field and link it to required models."""
24
+ ObjectType = apps.get_model("core", "ObjectType")
25
+
26
+ defaults = {
27
+ "label": label,
28
+ "related_object_type": ObjectType.objects.get_for_model(object_type)
29
+ if object_type
30
+ else None,
31
+ "ui_visible": getattr(CustomFieldUIVisibleChoices, "ALWAYS"),
32
+ "ui_editable": getattr(CustomFieldUIEditableChoices, "NO"),
33
+ }
34
+
35
+ custom_field, _ = apps.get_model("extras", "CustomField").objects.update_or_create(
36
+ type=getattr(CustomFieldTypeChoices, cf_type.upper()),
37
+ name=field_name,
38
+ defaults=defaults,
39
+ )
40
+
41
+ for model in models:
42
+ custom_field.object_types.add(ObjectType.objects.get_for_model(model))
43
+
44
+
45
+ def prepare_custom_fields(apps: "apps", schema_editor: "BaseDatabaseSchemaEditor"):
46
+ """Forward migration to prepare ipfabric_netbox custom fields and links."""
47
+ Device = apps.get_model("dcim", "Device")
48
+ Site = apps.get_model("dcim", "Site")
49
+
50
+ create_custom_field(
51
+ apps,
52
+ "ipfabric_source",
53
+ "IP Fabric Source",
54
+ [Device, Site],
55
+ cf_type="type_object",
56
+ object_type=apps.get_model("ipfabric_netbox", "IPFabricSource"),
57
+ )
58
+ create_custom_field(
59
+ apps,
60
+ "ipfabric_branch",
61
+ "IP Fabric Last Sync",
62
+ [Device, Site],
63
+ cf_type="type_object",
64
+ object_type=apps.get_model("ipfabric_netbox", "IPFabricBranch"),
65
+ )
66
+ cl, _ = apps.get_model("extras", "CustomLink").objects.update_or_create(
67
+ defaults={
68
+ "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 %}",
69
+ "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%}',
70
+ "new_window": True,
71
+ "button_class": CustomLinkButtonClassChoices.BLUE,
72
+ },
73
+ name="ipfabric",
74
+ )
75
+ cl.object_types.add(
76
+ apps.get_model("core", "ObjectType").objects.get_for_model(Device)
77
+ )
78
+
79
+
80
+ def cleanup_custom_fields(apps: "apps", schema_editor: "BaseDatabaseSchemaEditor"):
81
+ """Reverse migration to prepare ipfabric_netbox custom fields and links."""
82
+ for custom_field_name in ["ipfabric_source", "ipfabric_branch"]:
83
+ custom_field = apps.get_model("extras", "CustomField").objects.get(
84
+ name=custom_field_name
85
+ )
86
+ for model in custom_field.object_types.all()[:]:
87
+ custom_field.object_types.remove(model)
88
+ custom_field.delete()
89
+
90
+
91
+ class Migration(migrations.Migration):
92
+ dependencies = [
93
+ ("dcim", "0191_module_bay_rebuild"),
94
+ ("extras", "0121_customfield_related_object_filter"),
95
+ (
96
+ "ipfabric_netbox",
97
+ "0006_alter_ipfabrictransformmap_target_model",
98
+ ),
99
+ ]
100
+ operations = [
101
+ migrations.RunPython(prepare_custom_fields, cleanup_custom_fields),
102
+ ]
@@ -0,0 +1,42 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from django.db import migrations
4
+
5
+ from ipfabric_netbox.utilities.transform_map import build_transform_maps
6
+ from ipfabric_netbox.utilities.transform_map import get_transform_map
7
+
8
+
9
+ if TYPE_CHECKING:
10
+ from django.apps import apps
11
+ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
12
+
13
+
14
+ def prepare_transform_maps(apps: "apps", schema_editor: "BaseDatabaseSchemaEditor"):
15
+ """Create transform maps if they do not exist yet.
16
+ They used to be created during plugin.ready() so they might be present on older DBs.
17
+ """
18
+ if apps.get_model("ipfabric_netbox", "IPFabricTransformMap").objects.count() == 0:
19
+ build_transform_maps(data=get_transform_map())
20
+
21
+
22
+ def cleanup_transform_maps(apps: "apps", schema_editor: "BaseDatabaseSchemaEditor"):
23
+ """Delete all transform maps."""
24
+ # IPFabricTransformField and IPFabricRelationshipField are deleted by CASCADE
25
+ apps.get_model("ipfabric_netbox", "IPFabricTransformMap").objects.all().delete()
26
+
27
+
28
+ class Migration(migrations.Migration):
29
+ # Depend on all models that are used in transform maps
30
+ dependencies = [
31
+ ("core", "0012_job_object_type_optional"),
32
+ ("dcim", "0191_module_bay_rebuild"),
33
+ ("extras", "0121_customfield_related_object_filter"),
34
+ ("ipam", "0070_vlangroup_vlan_id_ranges"),
35
+ (
36
+ "ipfabric_netbox",
37
+ "0007_prepare_custom_fields",
38
+ ),
39
+ ]
40
+ operations = [
41
+ migrations.RunPython(prepare_transform_maps, cleanup_transform_maps),
42
+ ]
@@ -0,0 +1,227 @@
1
+ import contextlib
2
+ import re
3
+ from typing import TYPE_CHECKING
4
+
5
+ import django.db.models.deletion
6
+ from django.db import migrations
7
+ from django.db import models
8
+
9
+ from ipfabric_netbox.utilities.transform_map import build_transform_maps
10
+ from ipfabric_netbox.utilities.transform_map import get_transform_map
11
+
12
+
13
+ if TYPE_CHECKING:
14
+ from django.apps import apps
15
+ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
16
+ from ipfabric_netbox.models import IPFabricTransformField, IPFabricTransformMap
17
+
18
+
19
+ def get_current_transform_map(
20
+ apps: "apps",
21
+ target_model: str,
22
+ app_label: str = "dcim",
23
+ source_model: str = "interface",
24
+ ) -> "IPFabricTransformMap":
25
+ return apps.get_model("ipfabric_netbox", "IPFabricTransformMap").objects.get(
26
+ source_model=source_model,
27
+ target_model=apps.get_model("contenttypes", "ContentType").objects.get(
28
+ app_label=app_label,
29
+ model=target_model,
30
+ ),
31
+ )
32
+
33
+
34
+ def get_current_map_field(apps: "apps", target_model: str) -> "IPFabricTransformField":
35
+ """Finds the current TransformMapField for MAC address so we keep the template.
36
+ We need to do this because some customer might have altered it."""
37
+ return apps.get_model("ipfabric_netbox", "IPFabricTransformField").objects.get(
38
+ source_field="mac",
39
+ target_field="mac_address",
40
+ transform_map=get_current_transform_map(apps, target_model),
41
+ )
42
+
43
+
44
+ # TODO: Add MAC Address to existing IPFabricSync instances if Interface is allowed
45
+
46
+
47
+ def create_transform_map(apps: "apps", schema_editor: "BaseDatabaseSchemaEditor"):
48
+ """Create transform map structure for MAC Address (new in NetBox v4.2).
49
+ Keeps current template for MAC Address if it exists.
50
+
51
+ Also fixes interface.duplex not being populated."""
52
+
53
+ with contextlib.suppress(
54
+ apps.get_model("ipfabric_netbox", "IPFabricTransformMap").DoesNotExist
55
+ ):
56
+ # The map will already be present if we're using newer transform map.
57
+ # This means we don't need to run the migration since it got created with 0008_prepare_transform_maps migration
58
+ get_current_transform_map(apps, "macaddress")
59
+ return
60
+
61
+ # region - MAC Address
62
+ current_mac_field = get_current_map_field(apps, "interface")
63
+ transform_map_data = get_transform_map()
64
+
65
+ mac_address_transform_map = None
66
+ # Get MAC Address transform map defined in transform_map.json
67
+ for i, transform_map in enumerate(transform_map_data[:]):
68
+ if transform_map["data"]["name"] == "MAC Address Transform Map":
69
+ mac_address_transform_map = [transform_map]
70
+ # Replace the template only the current field is there
71
+ if current_mac_field:
72
+ for field_map in mac_address_transform_map[0]["field_maps"]:
73
+ if field_map["source_field"] == "mac":
74
+ field_map["template"] = current_mac_field.template
75
+
76
+ build_transform_maps(mac_address_transform_map)
77
+ current_mac_field.delete()
78
+ # endregion
79
+
80
+ # region - duplex
81
+ # Create duplex transform field for Interface. Ignore if it already exists
82
+ IPFabricTransformField = apps.get_model("ipfabric_netbox", "IPFabricTransformField")
83
+ field_data = {
84
+ "source_field": "duplex",
85
+ "target_field": "duplex",
86
+ "transform_map": get_current_transform_map(apps, "interface"),
87
+ }
88
+ try:
89
+ IPFabricTransformField.objects.get(**field_data)
90
+ except IPFabricTransformField.DoesNotExist:
91
+ IPFabricTransformField(**field_data).save()
92
+ # endregion
93
+
94
+ # region - Prefix site
95
+ # Prefix.site has changed to Prefix.scope relation (to allow site groups etc.)
96
+ prefix_transform_map = get_current_transform_map(
97
+ apps, source_model="prefix", app_label="ipam", target_model="prefix"
98
+ )
99
+ current_site_relationship = prefix_transform_map.relationship_maps.get(
100
+ target_field="site"
101
+ )
102
+ IPFabricTransformField(
103
+ source_field="siteName",
104
+ target_field="scope_id",
105
+ coalesce=True,
106
+ template="{% if object.siteName is defined %}"
107
+ + current_site_relationship.template
108
+ + "{% else %}None{% endif %}",
109
+ transform_map=prefix_transform_map,
110
+ ).save()
111
+ current_site_relationship.template = '{% if object.siteName is defined %}{{ contenttypes.ContentType.objects.get(app_label="dcim", model="site").pk }}{% else %}None{% endif %}'
112
+ current_site_relationship.source_model = apps.get_model(
113
+ "contenttypes", "ContentType"
114
+ ).objects.get(app_label="contenttypes", model="contenttype")
115
+ current_site_relationship.target_field = "scope_type"
116
+ current_site_relationship.save()
117
+ # endregion
118
+
119
+
120
+ def delete_transform_map(apps: "apps", schema_editor: "BaseDatabaseSchemaEditor"):
121
+ current_mac_field = get_current_map_field(apps, "macaddress")
122
+
123
+ # region - MAC Address
124
+ apps.get_model("ipfabric_netbox", "IPFabricTransformField")(
125
+ coalesce=False,
126
+ source_field="mac",
127
+ target_field="mac_address",
128
+ template=current_mac_field.template,
129
+ transform_map=get_current_transform_map(apps, "interface"),
130
+ ).save()
131
+
132
+ # Use CASCADE to delete the transform fields and relationships
133
+ current_mac_field.transform_map.delete()
134
+ # endregion
135
+
136
+ # region - duplex
137
+ # Delete new duplex field
138
+ apps.get_model("ipfabric_netbox", "IPFabricTransformField").objects.get(
139
+ source_field="duplex",
140
+ target_field="duplex",
141
+ transform_map=get_current_transform_map(apps, "interface"),
142
+ ).delete()
143
+ # endregion
144
+
145
+ # region - Prefix site
146
+ prefix_transform_map = get_current_transform_map(
147
+ apps, source_model="prefix", app_label="ipam", target_model="prefix"
148
+ )
149
+ current_site_relationship = prefix_transform_map.relationship_maps.get(
150
+ target_field="scope_type"
151
+ )
152
+ current_site_field = prefix_transform_map.field_maps.get(source_field="siteName")
153
+ match = re.search(r"is defined %}(.*){% else %}None", current_site_field.template)
154
+ current_site_field.delete()
155
+ if match:
156
+ current_site_relationship.template = match.group(1)
157
+ else:
158
+ current_site_relationship.template = "{% set SLUG = object.siteName | slugify %}{{ dcim.Site.objects.filter(slug=SLUG).first().pk }}"
159
+ current_site_relationship.source_model = apps.get_model(
160
+ "contenttypes", "ContentType"
161
+ ).objects.get(app_label="dcim", model="site")
162
+ current_site_relationship.target_field = "site"
163
+ current_site_relationship.save()
164
+ # endregion
165
+
166
+
167
+ def set_macaddress_sync_param(apps: "apps", schema_editor: "BaseDatabaseSchemaEditor"):
168
+ IPFabricSync = apps.get_model("ipfabric_netbox", "IPFabricSync")
169
+ for sync in IPFabricSync.objects.all():
170
+ if sync.parameters.get("interface"):
171
+ sync.parameters["macaddress"] = True
172
+ else:
173
+ sync.parameters["macaddress"] = False
174
+ sync.save()
175
+
176
+
177
+ def remove_macaddress_sync_param(
178
+ apps: "apps", schema_editor: "BaseDatabaseSchemaEditor"
179
+ ):
180
+ IPFabricSync = apps.get_model("ipfabric_netbox", "IPFabricSync")
181
+ for sync in IPFabricSync.objects.all():
182
+ sync.parameters.pop("macaddress", None)
183
+ sync.save()
184
+
185
+
186
+ class Migration(migrations.Migration):
187
+ dependencies = [
188
+ ("contenttypes", "0002_remove_content_type_name"),
189
+ ("ipfabric_netbox", "0008_prepare_transform_maps"),
190
+ ]
191
+
192
+ operations = [
193
+ migrations.AlterField(
194
+ model_name="ipfabrictransformmap",
195
+ name="target_model",
196
+ field=models.ForeignKey(
197
+ limit_choices_to=models.Q(
198
+ models.Q(
199
+ models.Q(("app_label", "dcim"), ("model", "site")),
200
+ models.Q(("app_label", "dcim"), ("model", "manufacturer")),
201
+ models.Q(("app_label", "dcim"), ("model", "platform")),
202
+ models.Q(("app_label", "dcim"), ("model", "devicerole")),
203
+ models.Q(("app_label", "dcim"), ("model", "devicetype")),
204
+ models.Q(("app_label", "dcim"), ("model", "device")),
205
+ models.Q(("app_label", "dcim"), ("model", "virtualchassis")),
206
+ models.Q(("app_label", "dcim"), ("model", "interface")),
207
+ models.Q(("app_label", "dcim"), ("model", "macaddress")),
208
+ models.Q(("app_label", "ipam"), ("model", "vlan")),
209
+ models.Q(("app_label", "ipam"), ("model", "vrf")),
210
+ models.Q(("app_label", "ipam"), ("model", "prefix")),
211
+ models.Q(("app_label", "ipam"), ("model", "ipaddress")),
212
+ models.Q(
213
+ ("app_label", "contenttypes"), ("model", "contenttype")
214
+ ),
215
+ models.Q(("app_label", "tenancy"), ("model", "tenant")),
216
+ models.Q(("app_label", "dcim"), ("model", "inventoryitem")),
217
+ _connector="OR",
218
+ )
219
+ ),
220
+ on_delete=django.db.models.deletion.PROTECT,
221
+ related_name="+",
222
+ to="contenttypes.contenttype",
223
+ ),
224
+ ),
225
+ migrations.RunPython(set_macaddress_sync_param, remove_macaddress_sync_param),
226
+ migrations.RunPython(create_transform_map, delete_transform_map),
227
+ ]
ipfabric_netbox/models.py CHANGED
@@ -71,6 +71,7 @@ IPFabricSupportedSyncModels = Q(
71
71
  | Q(app_label="dcim", model="device")
72
72
  | Q(app_label="dcim", model="virtualchassis")
73
73
  | Q(app_label="dcim", model="interface")
74
+ | Q(app_label="dcim", model="macaddress")
74
75
  | Q(app_label="ipam", model="vlan")
75
76
  | Q(app_label="ipam", model="vrf")
76
77
  | Q(app_label="ipam", model="prefix")
@@ -199,7 +200,7 @@ class IPFabricTransformMap(NetBoxModel):
199
200
  if instance:
200
201
  apply_tags(instance, tags)
201
202
  except Exception as e:
202
- error_message = f"""Failed to create instance:<br/>
203
+ error_message = f"""Failed to create instance of `{str(self.target_model)}`:<br/>
203
204
  message: `{e}`<br/>
204
205
  raw data: `{data}`<br/>
205
206
  context: `{context}`<br/>
@@ -597,6 +598,14 @@ class IPFabricSync(IPFabricClient, JobsMixin, TagsMixin, ChangeLoggedModel):
597
598
  # TODO: Add docs url
598
599
  return ""
599
600
 
601
+ @property
602
+ def logger(self):
603
+ return getattr(self, "_logger", SyncLogging(job=self.pk))
604
+
605
+ @logger.setter
606
+ def logger(self, value):
607
+ self._logger = value
608
+
600
609
  def get_absolute_url(self):
601
610
  return reverse("plugins:ipfabric_netbox:ipfabricsync", args=[self.pk])
602
611
 
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: