commit 63ebea9c8faa45abcbff312b3ce181f6d4536aad Author: Hedde van der Heide Date: Wed Nov 24 09:33:32 2021 +0100 added files diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a16580f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +**/netbox-docker +**/*.pyc +**/__pycache__ +**/.idea +**/.vscode \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..71e4f7c --- /dev/null +++ b/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] + +[requires] +python_version = "3.9" diff --git a/src/README b/src/README new file mode 100644 index 0000000..e69de29 diff --git a/src/dist/netbox-slm-0.1.tar.gz b/src/dist/netbox-slm-0.1.tar.gz new file mode 100644 index 0000000..f0b269c Binary files /dev/null and b/src/dist/netbox-slm-0.1.tar.gz differ diff --git a/src/netbox_slm.egg-info/PKG-INFO b/src/netbox_slm.egg-info/PKG-INFO new file mode 100644 index 0000000..0a713c5 --- /dev/null +++ b/src/netbox_slm.egg-info/PKG-INFO @@ -0,0 +1,11 @@ +Metadata-Version: 2.1 +Name: netbox-slm +Version: 0.1 +Summary: Software Lifecycle Management Netbox Plugin +Home-page: https://github.com/ICTU/netbox-slm +Author: Hedde van der Heide +License: Apache 2.0 +Platform: UNKNOWN + +UNKNOWN + diff --git a/src/netbox_slm.egg-info/SOURCES.txt b/src/netbox_slm.egg-info/SOURCES.txt new file mode 100644 index 0000000..d156510 --- /dev/null +++ b/src/netbox_slm.egg-info/SOURCES.txt @@ -0,0 +1,21 @@ +README +setup.py +netbox_slm/__init__.py +netbox_slm/admin.py +netbox_slm/filtersets.py +netbox_slm/forms.py +netbox_slm/middleware.py +netbox_slm/models.py +netbox_slm/navigation.py +netbox_slm/signals.py +netbox_slm/tables.py +netbox_slm/template_content.py +netbox_slm/urls.py +netbox_slm/views.py +netbox_slm.egg-info/PKG-INFO +netbox_slm.egg-info/SOURCES.txt +netbox_slm.egg-info/dependency_links.txt +netbox_slm.egg-info/not-zip-safe +netbox_slm.egg-info/top_level.txt +netbox_slm/migrations/0001_initial.py +netbox_slm/migrations/__init__.py \ No newline at end of file diff --git a/src/netbox_slm.egg-info/dependency_links.txt b/src/netbox_slm.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/netbox_slm.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/netbox_slm.egg-info/not-zip-safe b/src/netbox_slm.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/netbox_slm.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/src/netbox_slm.egg-info/top_level.txt b/src/netbox_slm.egg-info/top_level.txt new file mode 100644 index 0000000..4bbd9b4 --- /dev/null +++ b/src/netbox_slm.egg-info/top_level.txt @@ -0,0 +1 @@ +netbox_slm diff --git a/src/netbox_slm/__init__.py b/src/netbox_slm/__init__.py new file mode 100644 index 0000000..11d1e52 --- /dev/null +++ b/src/netbox_slm/__init__.py @@ -0,0 +1,17 @@ +from extras.plugins import PluginConfig + + +class SLMConfig(PluginConfig): + name = 'netbox_slm' + verbose_name = 'Software Lifecycle Management' + description = 'Software Lifecycle Management' + version = '0.1' + author = 'Hedde van der Heide' + author_email = 'hedde.vanderheide@ictu.nl' + base_url = 'slm' + required_settings = [] + default_settings = { + 'version_info': False + } + +config = SLMConfig \ No newline at end of file diff --git a/src/netbox_slm/admin.py b/src/netbox_slm/admin.py new file mode 100644 index 0000000..1959cdc --- /dev/null +++ b/src/netbox_slm/admin.py @@ -0,0 +1,14 @@ +from django.contrib import admin +from netbox_slm.models import SoftwareProduct, SoftwareProductVersion + + +class SoftwareProductVersionInline(admin.TabularInline): + model = SoftwareProductVersion + fields = ('name',) + extra = 0 + + +@admin.register(SoftwareProduct) +class SoftwareProductAdmin(admin.ModelAdmin): + list_display = ('name',) + inlines = (SoftwareProductVersionInline,) diff --git a/src/netbox_slm/api/__init__.py b/src/netbox_slm/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/netbox_slm/api/serializers.py b/src/netbox_slm/api/serializers.py new file mode 100644 index 0000000..6300a6e --- /dev/null +++ b/src/netbox_slm/api/serializers.py @@ -0,0 +1,26 @@ +from rest_framework import serializers + +from netbox.api.serializers import PrimaryModelSerializer +# from netbox_slm.api.nested_serializers import ( +# NestedRecordSerializer, +# NestedNameServerSerializer, +# ) +from netbox_slm.models import SoftwareProduct + + +class SoftwareProductSerializer(PrimaryModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name="plugins-api:netbox_slm-api:softwareproduct-detail" + ) + + class Meta: + model = SoftwareProduct + fields = ( + "id", + "url", + "name", + "tags", + "custom_field_data", + "created", + "last_updated", + ) diff --git a/src/netbox_slm/api/urls.py b/src/netbox_slm/api/urls.py new file mode 100644 index 0000000..df53e80 --- /dev/null +++ b/src/netbox_slm/api/urls.py @@ -0,0 +1,11 @@ +from netbox.api import OrderedDefaultRouter +from netbox_slm.api.views import ( + NetboxSLMRootView, + SoftwareProductViewSet +) + +router = OrderedDefaultRouter() +router.APIRootView = NetboxSLMRootView + +router.register("softwareproducts", SoftwareProductViewSet) +urlpatterns = router.urls \ No newline at end of file diff --git a/src/netbox_slm/api/views.py b/src/netbox_slm/api/views.py new file mode 100644 index 0000000..c75c245 --- /dev/null +++ b/src/netbox_slm/api/views.py @@ -0,0 +1,32 @@ +from rest_framework import serializers +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.routers import APIRootView + +from extras.api.views import CustomFieldModelViewSet +from netbox_slm.api.serializers import SoftwareProductSerializer +from netbox_slm.filters import SoftwareProductFilter +from netbox_slm.models import SoftwareProduct + + +class NetboxSLMRootView(APIRootView): + """ + NetboxSLM API root view + """ + + def get_view_name(self): + return "NetboxSLM" + + +class SoftwareProductViewSet(CustomFieldModelViewSet): + queryset = SoftwareProduct.objects.all() + serializer_class = SoftwareProductSerializer + filterset_class = SoftwareProductFilter + + # @action(detail=True, methods=["get"]) + # def records(self, request, pk=None): + # records = Record.objects.filter(zone=pk) + # serializer = RecordSerializer(records, many=True, context={"request": request}) + # return Response(serializer.data) + +# for reference: https://github.com/auroraresearchlab/netbox-dns/blob/main/netbox_dns/api/views.py \ No newline at end of file diff --git a/src/netbox_slm/fields.py b/src/netbox_slm/fields.py new file mode 100644 index 0000000..c30de29 --- /dev/null +++ b/src/netbox_slm/fields.py @@ -0,0 +1,49 @@ +from django.forms import BoundField +from django.urls import reverse + +from utilities.forms import DynamicModelMultipleChoiceField + + +class CustomDynamicModelMultipleChoiceField(DynamicModelMultipleChoiceField): + def get_bound_field(self, form, field_name): + bound_field = BoundField(form, self, field_name) + + # Set initial value based on prescribed child fields (if not already set) + if not self.initial and self.initial_params: + filter_kwargs = {} + for kwarg, child_field in self.initial_params.items(): + value = form.initial.get(child_field.lstrip("$")) + if value: + filter_kwargs[kwarg] = value + if filter_kwargs: + self.initial = self.queryset.filter(**filter_kwargs).first() + + # Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options + # will be populated on-demand via the APISelect widget. + data = bound_field.value() + if data: + field_name = getattr(self, "to_field_name") or "pk" + filter = self.filter(field_name=field_name) + try: + self.queryset = filter.filter(self.queryset, data) + except (TypeError, ValueError): + # Catch any error caused by invalid initial data passed from the user + self.queryset = self.queryset.none() + else: + self.queryset = self.queryset.none() + + # Set the data URL on the APISelect widget (if not already set) + widget = bound_field.field.widget + if not widget.attrs.get("data-url"): + app_label = self.queryset.model._meta.app_label + model_name = self.queryset.model._meta.model_name + + # + # Custom url for work with plugin api + # + data_url = reverse( + "plugins-api:{}-api:{}-list".format(app_label, model_name) + ) + widget.attrs["data-url"] = data_url + + return bound_field diff --git a/src/netbox_slm/filters.py b/src/netbox_slm/filters.py new file mode 100644 index 0000000..e82c0a1 --- /dev/null +++ b/src/netbox_slm/filters.py @@ -0,0 +1,35 @@ +import django_filters +from django.db.models import Q +from extras.filters import TagFilter +from netbox.filtersets import PrimaryModelFilterSet +from netbox_slm.models import SoftwareProduct, SoftwareProductVersion + + +class SoftwareProductFilter(PrimaryModelFilterSet): + """Filter capabilities for SoftwareProduct instances.""" + + q = django_filters.CharFilter( + method="search", + label="Search", + ) + name = django_filters.CharFilter( + lookup_expr="icontains", + ) + # tag = TagFilter() + + class Meta: + model = SoftwareProduct + fields = ("name",) # "tag") + + def search(self, queryset, name, value): + """Perform the filtered search.""" + if not value.strip(): + return queryset + qs_filter = Q(name__icontains=value) # | Q(status__icontains=value) + return queryset.filter(qs_filter) + + +class SoftwareProductVersionFilter(SoftwareProductFilter): + class Meta: + model = SoftwareProductVersion + fields = ("name",) # "tag") diff --git a/src/netbox_slm/forms.py b/src/netbox_slm/forms.py new file mode 100644 index 0000000..1d127b4 --- /dev/null +++ b/src/netbox_slm/forms.py @@ -0,0 +1,120 @@ +from django import forms +from extras.forms import ( + CustomFieldModelForm, + CustomFieldModelCSVForm, + AddRemoveTagsForm, + CustomFieldModelBulkEditForm, + CustomFieldModelFilterForm, +) +from dcim.models import Manufacturer +from netbox_slm.models import SoftwareProduct, SoftwareProductVersion +from utilities.forms import ( + BootstrapMixin, DynamicModelChoiceField, +) + + +from netbox_slm.fields import CustomDynamicModelMultipleChoiceField + + +class SoftwareProductForm(BootstrapMixin, CustomFieldModelForm): + """Form for creating a new SoftwareProduct object.""" + + manufacturer = DynamicModelChoiceField( + queryset=Manufacturer.objects.all(), + required=False, + # initial_params={ + # 'device_types': 'device_type' + # } + ) + + # tags = DynamicModelMultipleChoiceField( + # queryset=Tag.objects.all(), + # required=False, + # ) + + class Meta: + model = SoftwareProduct + fields = ("name", "manufacturer",) # "tags") + + +class SoftwareProductFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + """Form for filtering SoftwareProduct instances.""" + + model = SoftwareProduct + + q = forms.CharField(required=False, label="Search") + + name = forms.CharField( + required=False, + label="Name", + ) + + # tag = TagFilterField(SoftwareProduct) + + +class SoftwareProductCSVForm(CustomFieldModelCSVForm): + class Meta: + model = SoftwareProduct + fields = ("name",) + + +class SoftwareProductBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=SoftwareProduct.objects.all(), + widget=forms.MultipleHiddenInput(), + ) + + class Meta: + nullable_fields = [] + + +class SoftwareProductVersionForm(BootstrapMixin, CustomFieldModelForm): + """Form for creating a new SoftwareProductVersion object.""" + + software_product = CustomDynamicModelMultipleChoiceField( + queryset=SoftwareProduct.objects.all(), + required=False, + # initial_params={ + # 'device_types': 'device_type' + # } + ) + + # tags = DynamicModelMultipleChoiceField( + # queryset=Tag.objects.all(), + # required=False, + # ) + + class Meta: + model = SoftwareProductVersion + fields = ("name", "software_product",) # "tags") + + +class SoftwareProductVersionFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + """Form for filtering SoftwareProductVersion instances.""" + + model = SoftwareProductVersion + + q = forms.CharField(required=False, label="Search") + + name = forms.CharField( + required=False, + label="Name", + ) + + # tag = TagFilterField(SoftwareProduct) + + +class SoftwareProductVersionCSVForm(CustomFieldModelCSVForm): + class Meta: + model = SoftwareProductVersion + fields = ("name",) + + +class SoftwareProductVersionBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=SoftwareProduct.objects.all(), + widget=forms.MultipleHiddenInput(), + ) + + class Meta: + nullable_fields = [] diff --git a/src/netbox_slm/middleware.py b/src/netbox_slm/middleware.py new file mode 100644 index 0000000..e69de29 diff --git a/src/netbox_slm/migrations/0001_initial.py b/src/netbox_slm/migrations/0001_initial.py new file mode 100644 index 0000000..0cfe290 --- /dev/null +++ b/src/netbox_slm/migrations/0001_initial.py @@ -0,0 +1,50 @@ +# Generated by Django 3.2.8 on 2021-11-04 15:38 + +import django.core.serializers.json +from django.db import migrations, models +import django.db.models.deletion +import taggit.managers + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('dcim', '0133_port_colors'), + ('extras', '0062_clear_secrets_changelog'), + ] + + operations = [ + migrations.CreateModel( + name='SoftwareProduct', + fields=[ + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=128)), + ('description', models.CharField(max_length=255)), + ('manufacturer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='software_products', to='dcim.manufacturer')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SoftwareProductVersion', + fields=[ + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('software_product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='softwareproduct_versions', to='netbox_slm.softwareproduct')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/src/netbox_slm/migrations/__init__.py b/src/netbox_slm/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/netbox_slm/models.py b/src/netbox_slm/models.py new file mode 100644 index 0000000..5a2857c --- /dev/null +++ b/src/netbox_slm/models.py @@ -0,0 +1,46 @@ +from django.db import models +from django.urls import reverse +from extras.utils import extras_features +from netbox.models import PrimaryModel +from utilities.querysets import RestrictedQuerySet + + +# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') +class SoftwareProduct(PrimaryModel): + """ + A SoftwareProduct represents ... + """ + name = models.CharField(max_length=128) + description = models.CharField(max_length=255) + + manufacturer = models.ForeignKey( + to='dcim.Manufacturer', + on_delete=models.PROTECT, + related_name='software_products' + ) + + objects = RestrictedQuerySet.as_manager() + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse("plugins:netbox_slm:softwareproduct", kwargs={"pk": self.pk}) + + +# @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') +class SoftwareProductVersion(PrimaryModel): + software_product = models.ForeignKey( + to='netbox_slm.SoftwareProduct', + on_delete=models.PROTECT, + related_name='softwareproduct_versions' + ) + name = models.CharField(max_length=64) + + objects = RestrictedQuerySet.as_manager() + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse("plugins:netbox_slm:softwareproductversion", kwargs={"pk": self.pk}) diff --git a/src/netbox_slm/navigation.py b/src/netbox_slm/navigation.py new file mode 100644 index 0000000..e461fbd --- /dev/null +++ b/src/netbox_slm/navigation.py @@ -0,0 +1,45 @@ +from extras.plugins import PluginMenuButton, PluginMenuItem +from utilities.choices import ButtonColorChoices + +menu_items = ( + PluginMenuItem( + link='plugins:netbox_slm:softwareproduct_list', + link_text='Software Products', + buttons=( + PluginMenuButton( + "plugins:netbox_slm:softwareproduct_add", + "Add", + "mdi mdi-plus-thick", + ButtonColorChoices.GREEN, + permissions=["netbox_slm.add_softwareproduct"], + ), + PluginMenuButton( + "plugins:netbox_slm:softwareproduct_import", + "Import", + "mdi mdi-upload", + ButtonColorChoices.CYAN, + permissions=["netbox_slm.add_softwareproduct"], + ), + ) + ), + PluginMenuItem( + link='plugins:netbox_slm:softwareproductversion_list', + link_text='Versions', + buttons=( + PluginMenuButton( + "plugins:netbox_slm:softwareproductversion_add", + "Add", + "mdi mdi-plus-thick", + ButtonColorChoices.GREEN, + permissions=["netbox_slm.add_softwareproductversion"], + ), + PluginMenuButton( + "plugins:netbox_slm:softwareproductversion_import", + "Import", + "mdi mdi-upload", + ButtonColorChoices.CYAN, + permissions=["netbox_slm.add_softwareproductversion"], + ), + ) + ), +) diff --git a/src/netbox_slm/signals.py b/src/netbox_slm/signals.py new file mode 100644 index 0000000..e69de29 diff --git a/src/netbox_slm/tables.py b/src/netbox_slm/tables.py new file mode 100644 index 0000000..3b50c86 --- /dev/null +++ b/src/netbox_slm/tables.py @@ -0,0 +1,70 @@ +import django_tables2 as tables +from django_tables2.utils import Accessor +from netbox_slm.models import SoftwareProduct, SoftwareProductVersion +from utilities.tables import BaseTable, ChoiceFieldColumn, ToggleColumn + + +class SoftwareProductTable(BaseTable): + """Table for displaying SoftwareProduct objects.""" + + pk = ToggleColumn() + name = tables.LinkColumn() + manufacturer = tables.Column( + accessor=Accessor('manufacturer'), + linkify=True + ) + + # tags = TagColumn( + # url_name="plugins:netbox_dns:zone_list", + # ) + + class Meta(BaseTable.Meta): + model = SoftwareProduct + fields = ( + "pk", + "name", + "manufacturer", + # "tags", + ) + default_columns = ( + "pk", + "name", + "manufacturer", + # "tags", + ) + + +class SoftwareProductVersionTable(BaseTable): + """Table for displaying SoftwareProductVersion objects.""" + + pk = ToggleColumn() + name = tables.LinkColumn() + software_product = tables.Column( + accessor=Accessor('software_product'), + linkify=True + ) + manufacturer = tables.Column( + accessor=Accessor('software_product__manufacturer'), + linkify=True + ) + + # tags = TagColumn( + # url_name="plugins:netbox_dns:zone_list", + # ) + + class Meta(BaseTable.Meta): + model = SoftwareProductVersion + fields = ( + "pk", + "name", + "software_product", + "manufacturer", + # "tags", + ) + default_columns = ( + "pk", + "name", + "software_product", + "manufacturer", + # "tags", + ) diff --git a/src/netbox_slm/template_content.py b/src/netbox_slm/template_content.py new file mode 100644 index 0000000..e69de29 diff --git a/src/netbox_slm/templates/netbox_slm/object.html b/src/netbox_slm/templates/netbox_slm/object.html new file mode 100644 index 0000000..2f123bc --- /dev/null +++ b/src/netbox_slm/templates/netbox_slm/object.html @@ -0,0 +1,93 @@ +{% extends 'base/layout.html' %} +{% load buttons %} +{% load custom_links %} +{% load helpers %} +{% load perms %} +{% load plugins %} +{% load view_helpers %} + +{% block header %} + {# Breadcrumbs #} + + {{ block.super }} +{% endblock %} + +{% block title %}{{ object }}{% endblock %} + +{% block subtitle %} +
+ Created {{ object.created|annotated_date }} + · + Updated {{ object.last_updated|timesince }} ago +
+{% endblock %} + +{% block controls %} + {# Clone/Edit/Delete Buttons #} +
+
+ {% plugin_buttons object %} + + {# Extra buttons #} + {% block extra_controls %}{% endblock %} + + {% if object.clone_fields and request.user|can_add:object %} + {% plugin_clone_button object %} + {% endif %} + {% if request.user|can_change:object %} + {% plugin_edit_button object %} + {% endif %} + {% if request.user|can_delete:object %} + {% plugin_delete_button object %} + {% endif %} + +
+
+ {% custom_links object %} +
+
+{% endblock controls %} + +{% block tabs %} + +{% endblock tabs %} + +{% block content-wrapper %} +
+ {% block content %}{% endblock %} +
+{% endblock %} \ No newline at end of file diff --git a/src/netbox_slm/templates/netbox_slm/object_list.html b/src/netbox_slm/templates/netbox_slm/object_list.html new file mode 100644 index 0000000..405dc87 --- /dev/null +++ b/src/netbox_slm/templates/netbox_slm/object_list.html @@ -0,0 +1,141 @@ +{% extends 'base/layout.html' %} +{% load buttons %} +{% load helpers %} +{% load render_table from django_tables2 %} +{% load static %} +{% load view_helpers %} + +{% block controls %} +
+
+ {% block extra_controls %}{% endblock %} + {% if permissions.add and 'add' in action_buttons %} + {% add_button content_type.model_class|plugin_viewname:"add" %} + {% endif %} + {% if permissions.add and 'import' in action_buttons %} + {% import_button content_type.model_class|plugin_viewname:"import" %} + {% endif %} + {% if 'export' in action_buttons %} + {% export_button content_type %} + {% endif %} +
+
+{% endblock controls %} + +{% block tabs %} + +{% endblock tabs %} + +{% block content-wrapper %} +
+ + {# Object list #} +
+ + {# Applied filters #} + {% if filter_form %} + {% applied_filters filter_form request.GET %} + {% endif %} + + {# "Select all" form #} + {% if table.paginator.num_pages > 1 %} + {% with bulk_edit_url=content_type.model_class|validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|validated_viewname:"bulk_delete" %} +
+
+ {% csrf_token %} +
+
+ {% if bulk_edit_url and permissions.change %} + + {% endif %} + {% if bulk_delete_url and permissions.delete %} + + {% endif %} +
+
+ + +
+
+
+
+ {% endwith %} + {% endif %} + + {# Object table controls #} + {% include 'inc/table_controls.html' with table_modal="ObjectTable_config" %} + +
+ {% csrf_token %} + + + {# Object table #} +
+
+
+ {% render_table table 'inc/table.html' %} +
+
+
+ + {# Form buttons #} + {% if permissions.change or permissions.delete %} + {% with bulk_edit_url=content_type.model_class|plugin_validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|plugin_validated_viewname:"bulk_delete" %} +
+
+ {% block bulk_buttons %}{% endblock %} + {% if bulk_edit_url and permissions.change %} + + {% endif %} + {% if bulk_delete_url and permissions.delete %} + + {% endif %} +
+
+ {% endwith %} + {% endif %} + +
+ + {# Paginator #} + {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} +
+ + {# Filter form #} + {% if filter_form %} +
+ {% include 'inc/filter_list.html' %} +
+ {% endif %} +
+ + {# Table config form #} + {% table_config_form table table_name="ObjectTable" %} +{% endblock content-wrapper %} \ No newline at end of file diff --git a/src/netbox_slm/templates/netbox_slm/softwareproduct.html b/src/netbox_slm/templates/netbox_slm/softwareproduct.html new file mode 100644 index 0000000..6202247 --- /dev/null +++ b/src/netbox_slm/templates/netbox_slm/softwareproduct.html @@ -0,0 +1,76 @@ +{% extends 'netbox_slm/object.html' %} +{% load helpers %} +{% load plugins %} +{% load render_table from django_tables2 %} +{% load view_helpers %} +{% load perms %} + +{% block extra_controls %} +{{ block.super }} +{% endblock %} + +{% block content %} +
+
+
+
+ Software Product +
+
+ + + + + + + + + +
Name{{ object.name }}
Versions + {% for version in object.softwareproduct_versions.all %} + + {{ version }} + + {% endfor %} +
+
+
+ {% include 'inc/custom_fields_panel.html' %} +
+
+ {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %} +
+
+
+
+
+
Versions
+
+ + + + + + + + + {% for version in versions %} + + + + + {% endfor %} + +
NAMEActions
{{ version.name|truncatechars:32 }} + {% if request.user|can_change:object %} + {% plugin_edit_button version %} + {% endif %} + {% if request.user|can_delete:object %} + {% plugin_delete_button version %} + {% endif %} +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/netbox_slm/templates/netbox_slm/softwareproductversion.html b/src/netbox_slm/templates/netbox_slm/softwareproductversion.html new file mode 100644 index 0000000..6853b61 --- /dev/null +++ b/src/netbox_slm/templates/netbox_slm/softwareproductversion.html @@ -0,0 +1,82 @@ +{% extends 'netbox_slm/object.html' %} +{% load helpers %} +{% load plugins %} +{% load render_table from django_tables2 %} +{% load view_helpers %} +{% load perms %} + +{% block extra_controls %} +{{ block.super }} +{% endblock %} + +{% block content %} +
+
+
+
+ Software Product +
+
+ + + + + + + + + +
Name{{ object.name }}
Installations + + + + + +
+
+
+ {% include 'inc/custom_fields_panel.html' %} +
+
+ {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/src/netbox_slm/templates/netbox_slm/version.html b/src/netbox_slm/templates/netbox_slm/version.html new file mode 100644 index 0000000..7257dd8 --- /dev/null +++ b/src/netbox_slm/templates/netbox_slm/version.html @@ -0,0 +1,91 @@ +{% extends 'netbox_slm/object.html' %} +{% load helpers %} +{% load plugins %} +{% load render_table from django_tables2 %} +{% load view_helpers %} +{% load perms %} + +{% block extra_controls %} +{% comment %} + + Add Record + +{% endcomment %} +{{ block.super }} +{% endblock %} + +{% block content %} +
+
+
+
+ Zone +
+
+ + + + + + + + + +
Name{{ object.name }}
Versions +
    + + + + + + + +
+
+
+
+ {% include 'inc/custom_fields_panel.html' %} +
+
+ {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/src/netbox_slm/templatetags/__init__.py b/src/netbox_slm/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/netbox_slm/templatetags/view_helpers.py b/src/netbox_slm/templatetags/view_helpers.py new file mode 100644 index 0000000..358469a --- /dev/null +++ b/src/netbox_slm/templatetags/view_helpers.py @@ -0,0 +1,94 @@ +from django import template +from django.urls import NoReverseMatch, reverse +from django import template + +from utilities.templatetags.buttons import _get_viewname +from utilities.utils import prepare_cloned_fields + +register = template.Library() + + +@register.filter() +def plugin_validated_viewname(model, action): + """Return the view name for : + * the given model and action if valid + * or the given model and action if valid inside a plugin + * or None if invalid. + """ + core_viewname = f"{model._meta.app_label}:{model._meta.model_name}_{action}" + plugin_viewname = ( + f"plugins:{model._meta.app_label}:{model._meta.model_name}_{action}" + ) + try: + # Validate and return the view name. We don't return the actual URL yet because many of the templates + # are written to pass a name to {% url %}. + reverse(core_viewname) + return core_viewname + except NoReverseMatch: + try: + reverse(plugin_viewname) + return plugin_viewname + except NoReverseMatch: + return None + + +@register.filter() +def plugin_viewname(model, action): + """ + Return the view name for the given model and action. Does not perform any validation. + """ + return f"plugins:{model._meta.app_label}:{model._meta.model_name}_{action}" + + +# +# Instance buttons +# + + +@register.inclusion_tag("buttons/clone.html") +def plugin_clone_button(instance): + viewname = _get_viewname(instance, "add") + url = reverse(f"plugins:{viewname}") + + # Populate cloned field values + param_string = prepare_cloned_fields(instance) + if param_string: + url = f"{url}?{param_string}" + + return { + "url": url, + } + + +@register.inclusion_tag("buttons/edit.html") +def plugin_edit_button(instance, **kwargs): + viewname = _get_viewname(instance, "edit") + url = reverse(f"plugins:{viewname}", kwargs={"pk": instance.pk, **kwargs}) + + return { + "url": url, + } + + +@register.inclusion_tag("buttons/delete.html") +def plugin_delete_button(instance, **kwargs): + viewname = _get_viewname(instance, "delete") + url = reverse(f"plugins:{viewname}", kwargs={"pk": instance.pk, **kwargs}) + + return { + "url": url, + } + + +# +# List buttons +# + + +@register.inclusion_tag("buttons/add.html") +def plugin_add_button(url): + url = reverse(f"plugins:{url}") + + return { + "add_url": url, + } diff --git a/src/netbox_slm/urls.py b/src/netbox_slm/urls.py new file mode 100644 index 0000000..85374ea --- /dev/null +++ b/src/netbox_slm/urls.py @@ -0,0 +1,38 @@ +from django.urls import path +from extras.views import ObjectChangeLogView +from netbox_slm import views +from netbox_slm.models import SoftwareProduct, SoftwareProductVersion + +urlpatterns = [ + # Software Products + path("software-products/", views.SoftwareProductListView.as_view(), name='softwareproduct_list'), + path("software-products/add/", views.SoftwareProductEditView.as_view(), name='softwareproduct_add'), + path("software-products/import/", views.SoftwareProductBulkImportView.as_view(), name="softwareproduct_import"), + path("software-products/edit/", views.SoftwareProductBulkEditView.as_view(), name="softwareproduct_bulk_edit"), + path("software-products/delete/", views.SoftwareProductBulkDeleteView.as_view(), name="softwareproduct_bulk_delete"), + path("software-products//", views.SoftwareProductView.as_view(), name="softwareproduct"), + path("software-products//delete/", views.SoftwareProductDeleteView.as_view(), name="softwareproduct_delete"), + path("software-products//edit/", views.SoftwareProductEditView.as_view(), name="softwareproduct_edit"), + path( + "software-products//changelog/", + ObjectChangeLogView.as_view(), + name="softwareproduct_changelog", + kwargs={"model": SoftwareProduct}, + ), + + # Software Product Versions + path("versions/", views.SoftwareProductVersionListView.as_view(), name='softwareproductversion_list'), + path("versions/add/", views.SoftwareProductVersionEditView.as_view(), name='softwareproductversion_add'), + path("versions/import/", views.SoftwareProductVersionBulkImportView.as_view(), name="softwareproductversion_import"), + path("versions/edit/", views.SoftwareProductVersionBulkEditView.as_view(), name="softwareproductversion_bulk_edit"), + path("versions/delete/", views.SoftwareProductVersionBulkDeleteView.as_view(), name="softwareproductversion_bulk_delete"), + path("versions//", views.SoftwareProductVersionView.as_view(), name="softwareproductversion"), + path("versions//delete/", views.SoftwareProductVersionDeleteView.as_view(), name="softwareproductversion_delete"), + path("versions//edit/", views.SoftwareProductVersionEditView.as_view(), name="softwareproductversion_edit"), + path( + "versions//changelog/", + ObjectChangeLogView.as_view(), + name="softwareproductversion_changelog", + kwargs={"model": SoftwareProductVersion}, + ), +] diff --git a/src/netbox_slm/views.py b/src/netbox_slm/views.py new file mode 100644 index 0000000..9173f6e --- /dev/null +++ b/src/netbox_slm/views.py @@ -0,0 +1,113 @@ +from netbox.views import generic +from netbox_slm.filters import SoftwareProductFilter, SoftwareProductVersionFilter +from netbox_slm.forms import ( + SoftwareProductForm, SoftwareProductFilterForm, SoftwareProductCSVForm, SoftwareProductBulkEditForm, + SoftwareProductVersionForm, SoftwareProductVersionFilterForm, SoftwareProductVersionCSVForm, + SoftwareProductVersionBulkEditForm +) +from netbox_slm.models import ( + SoftwareProduct, SoftwareProductVersion +) +from netbox_slm.tables import SoftwareProductTable, SoftwareProductVersionTable + + +class SoftwareProductListView(generic.ObjectListView): + """View for listing all existing SoftwareProducts.""" + + queryset = SoftwareProduct.objects.all() + filterset = SoftwareProductFilter + filterset_form = SoftwareProductFilterForm + table = SoftwareProductTable + template_name = "netbox_slm/object_list.html" + + +class SoftwareProductView(generic.ObjectView): + """Display SoftwareProduct details""" + + queryset = SoftwareProduct.objects.all() + + def get_extra_context(self, request, instance): + versions = instance.softwareproduct_versions.all() + return {"versions": versions} + + +class SoftwareProductEditView(generic.ObjectEditView): + """View for editing and creating a SoftwareProduct instance.""" + + queryset = SoftwareProduct.objects.all() + model_form = SoftwareProductForm + + +class SoftwareProductDeleteView(generic.ObjectDeleteView): + """View for deleting a SoftwareProduct instance""" + + queryset = SoftwareProduct.objects.all() + + +class SoftwareProductBulkImportView(generic.BulkImportView): + queryset = SoftwareProduct.objects.all() + model_form = SoftwareProductCSVForm + table = SoftwareProductTable + + +class SoftwareProductBulkEditView(generic.BulkEditView): + queryset = SoftwareProduct.objects.all() + filterset = SoftwareProductFilter + table = SoftwareProductTable + form = SoftwareProductBulkEditForm + + +class SoftwareProductBulkDeleteView(generic.BulkDeleteView): + queryset = SoftwareProduct.objects.all() + table = SoftwareProductTable + + +class SoftwareProductVersionListView(generic.ObjectListView): + """View for listing all existing SoftwareProductVersions.""" + + queryset = SoftwareProductVersion.objects.all() + filterset = SoftwareProductVersionFilter + filterset_form = SoftwareProductVersionFilterForm + table = SoftwareProductVersionTable + template_name = "netbox_slm/object_list.html" + + +class SoftwareProductVersionView(generic.ObjectView): + """Display SoftwareProductVersion details""" + + queryset = SoftwareProductVersion.objects.all() + + # def get_extra_context(self, request, instance): + # records = instance.record_set.all() + # return {"records": records} + + +class SoftwareProductVersionEditView(generic.ObjectEditView): + """View for editing and creating a SoftwareProductVersion instance.""" + + queryset = SoftwareProductVersion.objects.all() + model_form = SoftwareProductVersionForm + + +class SoftwareProductVersionDeleteView(generic.ObjectDeleteView): + """View for deleting a SoftwareProductVersion instance""" + + queryset = SoftwareProductVersion.objects.all() + + +class SoftwareProductVersionBulkImportView(generic.BulkImportView): + queryset = SoftwareProductVersion.objects.all() + model_form = SoftwareProductVersionCSVForm + table = SoftwareProductVersionTable + + +class SoftwareProductVersionBulkEditView(generic.BulkEditView): + queryset = SoftwareProductVersion.objects.all() + filterset = SoftwareProductVersionFilter + table = SoftwareProductVersionTable + form = SoftwareProductVersionBulkEditForm + + +class SoftwareProductVersionBulkDeleteView(generic.BulkDeleteView): + queryset = SoftwareProductVersion.objects.all() + table = SoftwareProductVersionTable diff --git a/src/setup.py b/src/setup.py new file mode 100644 index 0000000..242a1c5 --- /dev/null +++ b/src/setup.py @@ -0,0 +1,14 @@ +from setuptools import find_packages, setup + +setup( + name='netbox-slm', + version='0.1', + description='Software Lifecycle Management Netbox Plugin', + url='https://github.com/ICTU/netbox-slm', + author='Hedde van der Heide', + license='Apache 2.0', + install_requires=[], + packages=find_packages(), + include_package_data=True, + zip_safe=False, +) \ No newline at end of file diff --git a/start-netbox.sh b/start-netbox.sh new file mode 100755 index 0000000..f04fbb8 --- /dev/null +++ b/start-netbox.sh @@ -0,0 +1,11 @@ +git clone -b release https://github.com/netbox-community/netbox-docker.git +cd netbox-docker +tee docker-compose.override.yml <