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/forms.py CHANGED
@@ -1,16 +1,15 @@
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
- from django.conf import settings
6
6
  from django.contrib.contenttypes.models import ContentType
7
7
  from django.core.exceptions import ValidationError
8
8
  from django.utils import timezone
9
9
  from django.utils.translation import gettext_lazy as _
10
- 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 = {
@@ -482,7 +476,7 @@ class IPFabricSyncForm(NetBoxModelForm):
482
476
  required=False,
483
477
  min_value=1,
484
478
  label=_("Recurs every"),
485
- widget=NumberWithOptions(options=DurationChoices),
479
+ widget=NumberWithOptions(options=JobIntervalChoices),
486
480
  help_text=_("Interval at which this sync is re-run (in minutes)"),
487
481
  )
488
482
  auto_merge = forms.BooleanField(
@@ -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,224 @@
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
+ def create_transform_map(apps: "apps", schema_editor: "BaseDatabaseSchemaEditor"):
45
+ """Create transform map structure for MAC Address (new in NetBox v4.2).
46
+ Keeps current template for MAC Address if it exists.
47
+
48
+ Also fixes interface.duplex not being populated."""
49
+
50
+ with contextlib.suppress(
51
+ apps.get_model("ipfabric_netbox", "IPFabricTransformMap").DoesNotExist
52
+ ):
53
+ # The map will already be present if we're using newer transform map.
54
+ # This means we don't need to run the migration since it got created with 0008_prepare_transform_maps migration
55
+ get_current_transform_map(apps, "macaddress")
56
+ return
57
+
58
+ # region - MAC Address
59
+ current_mac_field = get_current_map_field(apps, "interface")
60
+ transform_map_data = get_transform_map()
61
+
62
+ mac_address_transform_map = None
63
+ # Get MAC Address transform map defined in transform_map.json
64
+ for i, transform_map in enumerate(transform_map_data[:]):
65
+ if transform_map["data"]["name"] == "MAC Address Transform Map":
66
+ mac_address_transform_map = [transform_map]
67
+ # Replace the template only the current field is there
68
+ if current_mac_field:
69
+ for field_map in mac_address_transform_map[0]["field_maps"]:
70
+ if field_map["source_field"] == "mac":
71
+ field_map["template"] = current_mac_field.template
72
+
73
+ build_transform_maps(mac_address_transform_map)
74
+ current_mac_field.delete()
75
+ # endregion
76
+
77
+ # region - duplex
78
+ # Create duplex transform field for Interface. Ignore if it already exists
79
+ IPFabricTransformField = apps.get_model("ipfabric_netbox", "IPFabricTransformField")
80
+ field_data = {
81
+ "source_field": "duplex",
82
+ "target_field": "duplex",
83
+ "transform_map": get_current_transform_map(apps, "interface"),
84
+ }
85
+ try:
86
+ IPFabricTransformField.objects.get(**field_data)
87
+ except IPFabricTransformField.DoesNotExist:
88
+ IPFabricTransformField(**field_data).save()
89
+ # endregion
90
+
91
+ # region - Prefix site
92
+ # Prefix.site has changed to Prefix.scope relation (to allow site groups etc.)
93
+ prefix_transform_map = get_current_transform_map(
94
+ apps, source_model="prefix", app_label="ipam", target_model="prefix"
95
+ )
96
+ current_site_relationship = prefix_transform_map.relationship_maps.get(
97
+ target_field="site"
98
+ )
99
+ IPFabricTransformField(
100
+ source_field="siteName",
101
+ target_field="scope_id",
102
+ coalesce=True,
103
+ template="{% if object.siteName is defined %}"
104
+ + current_site_relationship.template
105
+ + "{% else %}None{% endif %}",
106
+ transform_map=prefix_transform_map,
107
+ ).save()
108
+ current_site_relationship.template = '{% if object.siteName is defined %}{{ contenttypes.ContentType.objects.get(app_label="dcim", model="site").pk }}{% else %}None{% endif %}'
109
+ current_site_relationship.source_model = apps.get_model(
110
+ "contenttypes", "ContentType"
111
+ ).objects.get(app_label="contenttypes", model="contenttype")
112
+ current_site_relationship.target_field = "scope_type"
113
+ current_site_relationship.save()
114
+ # endregion
115
+
116
+
117
+ def delete_transform_map(apps: "apps", schema_editor: "BaseDatabaseSchemaEditor"):
118
+ current_mac_field = get_current_map_field(apps, "macaddress")
119
+
120
+ # region - MAC Address
121
+ apps.get_model("ipfabric_netbox", "IPFabricTransformField")(
122
+ coalesce=False,
123
+ source_field="mac",
124
+ target_field="mac_address",
125
+ template=current_mac_field.template,
126
+ transform_map=get_current_transform_map(apps, "interface"),
127
+ ).save()
128
+
129
+ # Use CASCADE to delete the transform fields and relationships
130
+ current_mac_field.transform_map.delete()
131
+ # endregion
132
+
133
+ # region - duplex
134
+ # Delete new duplex field
135
+ apps.get_model("ipfabric_netbox", "IPFabricTransformField").objects.get(
136
+ source_field="duplex",
137
+ target_field="duplex",
138
+ transform_map=get_current_transform_map(apps, "interface"),
139
+ ).delete()
140
+ # endregion
141
+
142
+ # region - Prefix site
143
+ prefix_transform_map = get_current_transform_map(
144
+ apps, source_model="prefix", app_label="ipam", target_model="prefix"
145
+ )
146
+ current_site_relationship = prefix_transform_map.relationship_maps.get(
147
+ target_field="scope_type"
148
+ )
149
+ current_site_field = prefix_transform_map.field_maps.get(source_field="siteName")
150
+ match = re.search(r"is defined %}(.*){% else %}None", current_site_field.template)
151
+ current_site_field.delete()
152
+ if match:
153
+ current_site_relationship.template = match.group(1)
154
+ else:
155
+ current_site_relationship.template = "{% set SLUG = object.siteName | slugify %}{{ dcim.Site.objects.filter(slug=SLUG).first().pk }}"
156
+ current_site_relationship.source_model = apps.get_model(
157
+ "contenttypes", "ContentType"
158
+ ).objects.get(app_label="dcim", model="site")
159
+ current_site_relationship.target_field = "site"
160
+ current_site_relationship.save()
161
+ # endregion
162
+
163
+
164
+ def set_macaddress_sync_param(apps: "apps", schema_editor: "BaseDatabaseSchemaEditor"):
165
+ IPFabricSync = apps.get_model("ipfabric_netbox", "IPFabricSync")
166
+ for sync in IPFabricSync.objects.all():
167
+ if sync.parameters.get("interface"):
168
+ sync.parameters["macaddress"] = True
169
+ else:
170
+ sync.parameters["macaddress"] = False
171
+ sync.save()
172
+
173
+
174
+ def remove_macaddress_sync_param(
175
+ apps: "apps", schema_editor: "BaseDatabaseSchemaEditor"
176
+ ):
177
+ IPFabricSync = apps.get_model("ipfabric_netbox", "IPFabricSync")
178
+ for sync in IPFabricSync.objects.all():
179
+ sync.parameters.pop("macaddress", None)
180
+ sync.save()
181
+
182
+
183
+ class Migration(migrations.Migration):
184
+ dependencies = [
185
+ ("contenttypes", "0002_remove_content_type_name"),
186
+ ("ipfabric_netbox", "0008_prepare_transform_maps"),
187
+ ]
188
+
189
+ operations = [
190
+ migrations.AlterField(
191
+ model_name="ipfabrictransformmap",
192
+ name="target_model",
193
+ field=models.ForeignKey(
194
+ limit_choices_to=models.Q(
195
+ models.Q(
196
+ models.Q(("app_label", "dcim"), ("model", "site")),
197
+ models.Q(("app_label", "dcim"), ("model", "manufacturer")),
198
+ models.Q(("app_label", "dcim"), ("model", "platform")),
199
+ models.Q(("app_label", "dcim"), ("model", "devicerole")),
200
+ models.Q(("app_label", "dcim"), ("model", "devicetype")),
201
+ models.Q(("app_label", "dcim"), ("model", "device")),
202
+ models.Q(("app_label", "dcim"), ("model", "virtualchassis")),
203
+ models.Q(("app_label", "dcim"), ("model", "interface")),
204
+ models.Q(("app_label", "dcim"), ("model", "macaddress")),
205
+ models.Q(("app_label", "ipam"), ("model", "vlan")),
206
+ models.Q(("app_label", "ipam"), ("model", "vrf")),
207
+ models.Q(("app_label", "ipam"), ("model", "prefix")),
208
+ models.Q(("app_label", "ipam"), ("model", "ipaddress")),
209
+ models.Q(
210
+ ("app_label", "contenttypes"), ("model", "contenttype")
211
+ ),
212
+ models.Q(("app_label", "tenancy"), ("model", "tenant")),
213
+ models.Q(("app_label", "dcim"), ("model", "inventoryitem")),
214
+ _connector="OR",
215
+ )
216
+ ),
217
+ on_delete=django.db.models.deletion.PROTECT,
218
+ related_name="+",
219
+ to="contenttypes.contenttype",
220
+ ),
221
+ ),
222
+ migrations.RunPython(set_macaddress_sync_param, remove_macaddress_sync_param),
223
+ migrations.RunPython(create_transform_map, delete_transform_map),
224
+ ]
@@ -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
+ ]