netboxlabs-netbox-custom-objects 0.1.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.
Files changed (35) hide show
  1. netbox_custom_objects/__init__.py +42 -0
  2. netbox_custom_objects/api/__init__.py +0 -0
  3. netbox_custom_objects/api/serializers.py +244 -0
  4. netbox_custom_objects/api/urls.py +83 -0
  5. netbox_custom_objects/api/views.py +49 -0
  6. netbox_custom_objects/choices.py +20 -0
  7. netbox_custom_objects/constants.py +8 -0
  8. netbox_custom_objects/field_types.py +796 -0
  9. netbox_custom_objects/fields.py +86 -0
  10. netbox_custom_objects/filtersets.py +21 -0
  11. netbox_custom_objects/forms.py +144 -0
  12. netbox_custom_objects/migrations/0001_initial.py +199 -0
  13. netbox_custom_objects/migrations/__init__.py +0 -0
  14. netbox_custom_objects/models.py +1056 -0
  15. netbox_custom_objects/navigation.py +70 -0
  16. netbox_custom_objects/tables.py +183 -0
  17. netbox_custom_objects/template_content.py +82 -0
  18. netbox_custom_objects/templates/buttons/custom_objects_delete.html +4 -0
  19. netbox_custom_objects/templates/netbox_custom_objects/custom_object_list.html +30 -0
  20. netbox_custom_objects/templates/netbox_custom_objects/customobject.html +134 -0
  21. netbox_custom_objects/templates/netbox_custom_objects/customobject_edit.html +12 -0
  22. netbox_custom_objects/templates/netbox_custom_objects/customobjecttype.html +157 -0
  23. netbox_custom_objects/templates/netbox_custom_objects/field_delete.html +30 -0
  24. netbox_custom_objects/templates/netbox_custom_objects/htmx/delete_form.html +30 -0
  25. netbox_custom_objects/templatetags/__init__.py +0 -0
  26. netbox_custom_objects/templatetags/custom_object_buttons.py +246 -0
  27. netbox_custom_objects/templatetags/custom_object_utils.py +49 -0
  28. netbox_custom_objects/urls.py +63 -0
  29. netbox_custom_objects/utilities.py +85 -0
  30. netbox_custom_objects/views.py +453 -0
  31. netboxlabs_netbox_custom_objects-0.1.0.dist-info/METADATA +61 -0
  32. netboxlabs_netbox_custom_objects-0.1.0.dist-info/RECORD +35 -0
  33. netboxlabs_netbox_custom_objects-0.1.0.dist-info/WHEEL +5 -0
  34. netboxlabs_netbox_custom_objects-0.1.0.dist-info/licenses/LICENSE.md +85 -0
  35. netboxlabs_netbox_custom_objects-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,42 @@
1
+ from netbox.plugins import PluginConfig
2
+
3
+
4
+ # Plugin Configuration
5
+ class CustomObjectsPluginConfig(PluginConfig):
6
+ name = "netbox_custom_objects"
7
+ verbose_name = "Custom Objects"
8
+ description = "A plugin to manage custom objects in NetBox"
9
+ version = "0.1.0"
10
+ base_url = "custom-objects"
11
+ min_version = "4.2.0"
12
+ default_settings = {}
13
+ required_settings = []
14
+ template_extensions = "template_content.template_extensions"
15
+
16
+ # def get_model(self, model_name, require_ready=True):
17
+ # if require_ready:
18
+ # self.apps.check_models_ready()
19
+ # else:
20
+ # self.apps.check_apps_ready()
21
+ #
22
+ # if model_name.lower() in self.models:
23
+ # return self.models[model_name.lower()]
24
+ #
25
+ # from .models import CustomObjectType
26
+ # if "table" not in model_name.lower() or "model" not in model_name.lower():
27
+ # raise LookupError(
28
+ # "App '%s' doesn't have a '%s' model." % (self.label, model_name)
29
+ # )
30
+ #
31
+ # custom_object_type_id = int(model_name.replace("table", "").replace("model", ""))
32
+ #
33
+ # try:
34
+ # obj = CustomObjectType.objects.get(pk=custom_object_type_id)
35
+ # except CustomObjectType.DoesNotExist:
36
+ # raise LookupError(
37
+ # "App '%s' doesn't have a '%s' model." % (self.label, model_name)
38
+ # )
39
+ # return obj.get_model()
40
+
41
+
42
+ config = CustomObjectsPluginConfig
File without changes
@@ -0,0 +1,244 @@
1
+ from django.contrib.contenttypes.models import ContentType
2
+ from extras.choices import CustomFieldTypeChoices
3
+ from netbox.api.serializers import NetBoxModelSerializer
4
+ from rest_framework import serializers
5
+ from rest_framework.exceptions import ValidationError
6
+ from rest_framework.reverse import reverse
7
+
8
+ from netbox_custom_objects import field_types
9
+ from netbox_custom_objects.models import CustomObject, CustomObjectType, CustomObjectTypeField
10
+
11
+ __all__ = (
12
+ "CustomObjectTypeSerializer",
13
+ "CustomObjectSerializer",
14
+ )
15
+
16
+
17
+ class ContentTypeSerializer(NetBoxModelSerializer):
18
+ class Meta:
19
+ model = ContentType
20
+ fields = (
21
+ "id",
22
+ "app_label",
23
+ "model",
24
+ )
25
+
26
+
27
+ class CustomObjectTypeFieldSerializer(NetBoxModelSerializer):
28
+ url = serializers.HyperlinkedIdentityField(
29
+ view_name="plugins-api:netbox_custom_objects-api:customobjecttypefield-detail"
30
+ )
31
+ app_label = serializers.CharField(required=False)
32
+ model = serializers.CharField(required=False)
33
+
34
+ class Meta:
35
+ model = CustomObjectTypeField
36
+ fields = (
37
+ # 'id', 'url', 'name', 'label', 'custom_object_type', 'field_type', 'content_type', 'many', 'options',
38
+ "id",
39
+ "name",
40
+ "label",
41
+ "custom_object_type",
42
+ "type",
43
+ "primary",
44
+ "default",
45
+ "choice_set",
46
+ "validation_regex",
47
+ "validation_minimum",
48
+ "validation_maximum",
49
+ "related_object_type",
50
+ "app_label",
51
+ "model",
52
+ )
53
+
54
+ def validate(self, attrs):
55
+ app_label = attrs.pop("app_label", None)
56
+ model = attrs.pop("model", None)
57
+ if attrs["type"] in [
58
+ CustomFieldTypeChoices.TYPE_OBJECT,
59
+ CustomFieldTypeChoices.TYPE_MULTIOBJECT,
60
+ ]:
61
+ try:
62
+ attrs["related_object_type"] = ContentType.objects.get(
63
+ app_label=app_label, model=model
64
+ )
65
+ except ContentType.DoesNotExist:
66
+ raise ValidationError(
67
+ "Must provide valid app_label and model for object field type."
68
+ )
69
+ if attrs["type"] in [
70
+ CustomFieldTypeChoices.TYPE_SELECT,
71
+ CustomFieldTypeChoices.TYPE_MULTISELECT,
72
+ ]:
73
+ if not attrs.get("choice_set", None):
74
+ raise ValidationError(
75
+ "Must provide choice_set with valid PK for select field type."
76
+ )
77
+ return super().validate(attrs)
78
+
79
+ def create(self, validated_data):
80
+ """
81
+ Record the user who created the Custom Object as its owner.
82
+ """
83
+ return super().create(validated_data)
84
+
85
+ def get_related_object_type(self, obj):
86
+ if obj.related_object_type:
87
+ return dict(
88
+ id=obj.related_object_type.id,
89
+ app_label=obj.related_object_type.app_label,
90
+ model=obj.related_object_type.model,
91
+ )
92
+
93
+
94
+ class CustomObjectTypeSerializer(NetBoxModelSerializer):
95
+ url = serializers.HyperlinkedIdentityField(
96
+ view_name="plugins-api:netbox_custom_objects-api:customobjecttype-detail"
97
+ )
98
+ fields = CustomObjectTypeFieldSerializer(
99
+ nested=True,
100
+ read_only=True,
101
+ many=True,
102
+ )
103
+
104
+ class Meta:
105
+ model = CustomObjectType
106
+ fields = [
107
+ "id",
108
+ "url",
109
+ "name",
110
+ "description",
111
+ "tags",
112
+ "created",
113
+ "last_updated",
114
+ "fields",
115
+ ]
116
+ brief_fields = ("id", "url", "name", "description")
117
+
118
+ def create(self, validated_data):
119
+ return super().create(validated_data)
120
+
121
+
122
+ # TODO: Remove or reduce to a stub (not needed as all custom object serializers are generated via get_serializer_class)
123
+ class CustomObjectSerializer(NetBoxModelSerializer):
124
+ relation_fields = None
125
+
126
+ url = serializers.SerializerMethodField()
127
+ field_data = serializers.SerializerMethodField()
128
+ custom_object_type = CustomObjectTypeSerializer(nested=True)
129
+
130
+ class Meta:
131
+ model = CustomObject
132
+ fields = [
133
+ "id",
134
+ "url",
135
+ "name",
136
+ "display",
137
+ "custom_object_type",
138
+ "tags",
139
+ "created",
140
+ "last_updated",
141
+ "data",
142
+ "field_data",
143
+ ]
144
+ brief_fields = (
145
+ "id",
146
+ "url",
147
+ "name",
148
+ "custom_object_type",
149
+ )
150
+
151
+ def get_display(self, obj):
152
+ return f"{obj.custom_object_type}: {obj.name}"
153
+
154
+ def validate(self, attrs):
155
+ return super().validate(attrs)
156
+
157
+ def update_relation_fields(self, instance):
158
+ # TODO: Implement this
159
+ pass
160
+
161
+ def create(self, validated_data):
162
+ model = validated_data["custom_object_type"].get_model()
163
+ instance = model.objects.create(**validated_data)
164
+
165
+ return instance
166
+
167
+ def update(self, instance, validated_data):
168
+ instance = super().update(instance, validated_data)
169
+ # self.update_relation_fields(instance)
170
+ return instance
171
+
172
+ def get_url(self, obj):
173
+ """
174
+ Given an object, return the URL that hyperlinks to the object.
175
+
176
+ May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
177
+ attributes are not configured to correctly match the URL conf.
178
+ """
179
+ # Unsaved objects will not yet have a valid URL.
180
+ if hasattr(obj, "pk") and obj.pk in (None, ""):
181
+ return None
182
+
183
+ view_name = "plugins-api:netbox_custom_objects-api:customobject-detail"
184
+ lookup_value = getattr(obj, "pk")
185
+ kwargs = {
186
+ "pk": lookup_value,
187
+ "custom_object_type": obj.custom_object_type.name.lower(),
188
+ }
189
+ request = self.context["request"]
190
+ format = self.context.get("format")
191
+ return reverse(view_name, kwargs=kwargs, request=request, format=format)
192
+
193
+ def get_field_data(self, obj):
194
+ result = {}
195
+ return result
196
+
197
+
198
+ def get_serializer_class(model):
199
+ model_fields = model.custom_object_type.fields.all()
200
+ meta = type(
201
+ "Meta",
202
+ (),
203
+ {
204
+ "model": model,
205
+ "fields": "__all__",
206
+ },
207
+ )
208
+
209
+ def get_url(self, obj):
210
+ # Unsaved objects will not yet have a valid URL.
211
+ if hasattr(obj, "pk") and obj.pk in (None, ""):
212
+ return None
213
+
214
+ view_name = "plugins-api:netbox_custom_objects-api:customobject-detail"
215
+ lookup_value = getattr(obj, "pk")
216
+ kwargs = {
217
+ "pk": lookup_value,
218
+ "custom_object_type": obj.custom_object_type.name.lower(),
219
+ }
220
+ request = self.context["request"]
221
+ format = self.context.get("format")
222
+ return reverse(view_name, kwargs=kwargs, request=request, format=format)
223
+
224
+ attrs = {
225
+ "Meta": meta,
226
+ "__module__": "database.serializers",
227
+ "url": serializers.SerializerMethodField(),
228
+ "get_url": get_url,
229
+ }
230
+
231
+ for field in model_fields:
232
+ field_type = field_types.FIELD_TYPE_CLASS[field.type]()
233
+ try:
234
+ attrs[field.name] = field_type.get_serializer_field(field)
235
+ except NotImplementedError:
236
+ print(f"serializer: {field.name} field is not implemented; using a default serializer field")
237
+
238
+ serializer = type(
239
+ f"{model._meta.object_name}Serializer",
240
+ (serializers.ModelSerializer,),
241
+ attrs,
242
+ )
243
+
244
+ return serializer
@@ -0,0 +1,83 @@
1
+ from copy import deepcopy
2
+ from django.urls import include, path, NoReverseMatch
3
+ from rest_framework.response import Response
4
+ from rest_framework.reverse import reverse
5
+ from rest_framework.views import APIView
6
+ from netbox.api.routers import NetBoxRouter
7
+ from netbox_custom_objects.models import CustomObjectType
8
+
9
+ from . import views
10
+
11
+ custom_object_list = views.CustomObjectViewSet.as_view(
12
+ {"get": "list", "post": "create"}
13
+ )
14
+ custom_object_detail = views.CustomObjectViewSet.as_view(
15
+ {"get": "retrieve", "put": "update", "patch": "partial_update", "delete": "destroy"}
16
+ )
17
+
18
+
19
+ class CustomObjectsAPIRootView(APIView):
20
+ """
21
+ This is the root of the NetBox Custom Objects plugin API. Custom Object Types defined at application startup
22
+ are listed by lowercased name; e.g. `/api/plugins/custom-objects/cat/`.
23
+ """
24
+ def get_view_name(self):
25
+ return "Custom Objects API Root"
26
+
27
+ _ignore_model_permissions = True
28
+ schema = None # exclude from schema
29
+ api_root_dict = None
30
+
31
+ # This logic is copied from stock DRF APIRootView
32
+ def get(self, request, *args, **kwargs):
33
+ # Return a plain {"name": "hyperlink"} response.
34
+ ret = {}
35
+ namespace = request.resolver_match.namespace
36
+ for key, url_name in self.api_root_dict.items():
37
+ if namespace:
38
+ url_name = namespace + ':' + url_name
39
+ try:
40
+ ret[key] = reverse(
41
+ url_name,
42
+ args=args,
43
+ kwargs=kwargs,
44
+ request=request,
45
+ format=kwargs.get('format')
46
+ )
47
+ except NoReverseMatch:
48
+ # Don't bail out if eg. no list routes exist, only detail routes.
49
+ continue
50
+
51
+ # Extra logic to populate roots for custom object type lists
52
+ for custom_object_type in CustomObjectType.objects.all():
53
+ local_kwargs = deepcopy(kwargs)
54
+ cot_name = custom_object_type.name.lower()
55
+ url_name = 'customobject-list'
56
+ local_kwargs['custom_object_type'] = cot_name
57
+ if namespace:
58
+ url_name = namespace + ':' + url_name
59
+ ret[cot_name] = reverse(
60
+ url_name,
61
+ args=args,
62
+ kwargs=local_kwargs,
63
+ request=request,
64
+ format=local_kwargs.get('format')
65
+ )
66
+
67
+ return Response(ret)
68
+
69
+
70
+ router = NetBoxRouter()
71
+ router.APIRootView = CustomObjectsAPIRootView
72
+ router.register("custom-object-types", views.CustomObjectTypeViewSet)
73
+ router.register("custom-object-type-fields", views.CustomObjectTypeFieldViewSet)
74
+
75
+ urlpatterns = [
76
+ path("", include(router.urls)),
77
+ path("<str:custom_object_type>/", custom_object_list, name="customobject-list"),
78
+ path(
79
+ "<str:custom_object_type>/<int:pk>/",
80
+ custom_object_detail,
81
+ name="customobject-detail",
82
+ ),
83
+ ]
@@ -0,0 +1,49 @@
1
+ from django.http import Http404
2
+ from rest_framework.routers import APIRootView
3
+ from rest_framework.viewsets import ModelViewSet
4
+
5
+ from netbox_custom_objects.models import CustomObject, CustomObjectType, CustomObjectTypeField
6
+
7
+ from . import serializers
8
+
9
+
10
+ class RootView(APIRootView):
11
+ def get_view_name(self):
12
+ return "CustomObjects"
13
+
14
+
15
+ class CustomObjectTypeViewSet(ModelViewSet):
16
+ queryset = CustomObjectType.objects.all()
17
+ serializer_class = serializers.CustomObjectTypeSerializer
18
+
19
+
20
+ class CustomObjectViewSet(ModelViewSet):
21
+ queryset = CustomObject.objects.all()
22
+ serializer_class = serializers.CustomObjectSerializer
23
+ model = None
24
+
25
+ def get_view_name(self):
26
+ if self.model:
27
+ return self.model.custom_object_type.name
28
+ return super().get_view_name()
29
+
30
+ def get_serializer_class(self):
31
+ return serializers.get_serializer_class(self.model)
32
+
33
+ def get_queryset(self):
34
+ try:
35
+ custom_object_type = CustomObjectType.objects.get(
36
+ name__iexact=self.kwargs["custom_object_type"]
37
+ )
38
+ except CustomObjectType.DoesNotExist:
39
+ raise Http404
40
+ self.model = custom_object_type.get_model()
41
+ return self.model.objects.all()
42
+
43
+ def list(self, request, *args, **kwargs):
44
+ return super().list(request, *args, **kwargs)
45
+
46
+
47
+ class CustomObjectTypeFieldViewSet(ModelViewSet):
48
+ queryset = CustomObjectTypeField.objects.all()
49
+ serializer_class = serializers.CustomObjectTypeFieldSerializer
@@ -0,0 +1,20 @@
1
+ from django.utils.translation import gettext_lazy as _
2
+ from utilities.choices import ChoiceSet
3
+
4
+
5
+ class MappingFieldTypeChoices(ChoiceSet):
6
+ CHAR = "char"
7
+ INTEGER = "integer"
8
+ BOOLEAN = "boolean"
9
+ DATE = "date"
10
+ DATETIME = "datetime"
11
+ OBJECT = "object"
12
+
13
+ CHOICES = (
14
+ (CHAR, _("String"), "cyan"),
15
+ (INTEGER, _("Integer"), "orange"),
16
+ (BOOLEAN, _("Boolean"), "green"),
17
+ (DATE, _("Date"), "red"),
18
+ (DATETIME, _("DateTime"), "blue"),
19
+ (OBJECT, _("Object"), "orange"),
20
+ )
@@ -0,0 +1,8 @@
1
+ # Models which do not support change logging, but whose database tables
2
+ # must be replicated for each branch to ensure proper functionality
3
+ INCLUDE_MODELS = (
4
+ "dcim.cablepath",
5
+ "extras.cachedvalue",
6
+ )
7
+
8
+ APP_LABEL = "netbox_custom_objects"