added files

This commit is contained in:
Hedde van der Heide
2021-11-24 09:33:32 +01:00
commit 63ebea9c8f
38 changed files with 1319 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
**/netbox-docker
**/*.pyc
**/__pycache__
**/.idea
**/.vscode
View File
+11
View File
@@ -0,0 +1,11 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
[dev-packages]
[requires]
python_version = "3.9"
View File
BIN
View File
Binary file not shown.
+11
View File
@@ -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
+21
View File
@@ -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
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
netbox_slm
+17
View File
@@ -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
+14
View File
@@ -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,)
View File
+26
View File
@@ -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",
)
+11
View File
@@ -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
+32
View File
@@ -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
+49
View File
@@ -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
+35
View File
@@ -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")
+120
View File
@@ -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 = []
View File
+50
View File
@@ -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,
},
),
]
+46
View File
@@ -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})
+45
View File
@@ -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"],
),
)
),
)
View File
+70
View File
@@ -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",
)
View File
@@ -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 #}
<nav class="breadcrumb-container px-3" aria-label="breadcrumb">
<ol class="breadcrumb">
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url object|plugin_viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a></li>
{% endblock %}
</ol>
</nav>
{{ block.super }}
{% endblock %}
{% block title %}{{ object }}{% endblock %}
{% block subtitle %}
<div class="object-subtitle">
<span>Created {{ object.created|annotated_date }}</span>
<span class="separator">&middot;</span>
<span>Updated <span title="{{ object.last_updated }}">{{ object.last_updated|timesince }}</span> ago</span>
</div>
{% endblock %}
{% block controls %}
{# Clone/Edit/Delete Buttons #}
<div class="controls">
<div class="control-group">
{% 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 %}
</div>
<div class="control-group">
{% custom_links object %}
</div>
</div>
{% endblock controls %}
{% block tabs %}
<ul class="nav nav-tabs px-3">
{% block tab_items %}
<li class="nav-item" role="presentation">
<a class="nav-link{% if not active_tab %} active{% endif %}" href="{{ object.get_absolute_url }}">{{ object|meta:"verbose_name"|bettertitle }}</a>
</li>
{% endblock tab_items %}
{% if perms.extras.view_journalentry %}
{% with journal_viewname=object|plugin_viewname:'journal' %}
{% url journal_viewname pk=object.pk as journal_url %}
{% if journal_url %}
<li role="presentation" class="nav-item">
<a href="{{ journal_url }}" class="nav-link{% if active_tab == 'journal'%} active{% endif %}">
Journal {% badge object.journal_entries.count %}
</a>
</li>
{% endif %}
{% endwith %}
{% endif %}
{% if perms.extras.view_objectchange %}
{% with changelog_viewname=object|plugin_viewname:'changelog' %}
{% url changelog_viewname pk=object.pk as changelog_url %}
{% if changelog_url %}
<li role="presentation" class="nav-item">
<a href="{{ changelog_url }}" class="nav-link{% if active_tab == 'changelog'%} active{% endif %}">Change Log</a>
</li>
{% endif %}
{% endwith %}
{% endif %}
</ul>
{% endblock tabs %}
{% block content-wrapper %}
<div class="tab-content">
{% block content %}{% endblock %}
</div>
{% endblock %}
@@ -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 %}
<div class="controls">
<div class="control-group">
{% 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 %}
</div>
</div>
{% endblock controls %}
{% block tabs %}
<ul class="nav nav-tabs px-3">
{% block tab_items %}
<li class="nav-item" role="presentation">
<button class="nav-link active" id="object-list-tab" data-bs-toggle="tab" data-bs-target="#object-list" type="button" role="tab" aria-controls="edit-form" aria-selected="true">
{% block title %}{{ content_type.model_class|meta:"verbose_name_plural"|bettertitle }}{% endblock %}
{% badge table.page.paginator.count %}
</button>
</li>
{% if filter_form %}
<li class="nav-item" role="presentation">
<button class="nav-link" id="filters-form-tab" data-bs-toggle="tab" data-bs-target="#filters-form" type="button" role="tab" aria-controls="object-list" aria-selected="false">
Filters
{% if filter_form %}{% badge filter_form.changed_data|length bg_class="primary" %}{% endif %}
</button>
</li>
{% endif %}
{% endblock tab_items %}
</ul>
{% endblock tabs %}
{% block content-wrapper %}
<div class="tab-content">
{# Object list #}
<div class="tab-pane show active" id="object-list" role="tabpanel" aria-labelledby="object-list-tab">
{# 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" %}
<div id="select-all-box" class="d-none card noprint">
<form method="post" class="form col-md-12">
{% csrf_token %}
<div class="card-body">
<div class="float-end">
{% if bulk_edit_url and permissions.change %}
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled>
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit All
</button>
{% endif %}
{% if bulk_delete_url and permissions.delete %}
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled>
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete All
</button>
{% endif %}
</div>
<div class="form-check">
<input type="checkbox" id="select-all" name="_all" class="form-check-input" />
<label for="select-all" class="form-check-label">
Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> matching query
</label>
</div>
</div>
</form>
</div>
{% endwith %}
{% endif %}
{# Object table controls #}
{% include 'inc/table_controls.html' with table_modal="ObjectTable_config" %}
<form method="post" class="form form-horizontal">
{% csrf_token %}
<input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
{# Object table #}
<div class="card">
<div class="card-body">
<div class="table-responsive">
{% render_table table 'inc/table.html' %}
</div>
</div>
</div>
{# 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" %}
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% block bulk_buttons %}{% endblock %}
{% if bulk_edit_url and permissions.change %}
<button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
</button>
{% endif %}
{% if bulk_delete_url and permissions.delete %}
<button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete Selected
</button>
{% endif %}
</div>
</div>
{% endwith %}
{% endif %}
</form>
{# Paginator #}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
</div>
{# Filter form #}
{% if filter_form %}
<div class="tab-pane show" id="filters-form" role="tabpanel" aria-labelledby="filters-form-tab">
{% include 'inc/filter_list.html' %}
</div>
{% endif %}
</div>
{# Table config form #}
{% table_config_form table table_name="ObjectTable" %}
{% endblock content-wrapper %}
@@ -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 %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<div class="card-header">
Software Product
</div>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Versions</th>
<td>
{% for version in object.softwareproduct_versions.all %}
<a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}">
<span class="badge" style="color: #ffffff; background-color: #9e9e9e">{{ version }}</span>
</a>
{% endfor %}
</td>
</tr>
</table>
</div>
</div>
{% include 'inc/custom_fields_panel.html' %}
</div>
<div class="col col-md-6">
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">Versions</h5>
<div class="card-body">
<table class="table table-hover attr-table table-striped">
<thead>
<tr>
<th scope="col">NAME</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% for version in versions %}
<tr>
<td>{{ version.name|truncatechars:32 }}</td>
<td class="noprint text-end text-nowrap">
{% if request.user|can_change:object %}
{% plugin_edit_button version %}
{% endif %}
{% if request.user|can_delete:object %}
{% plugin_delete_button version %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -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 %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<div class="card-header">
Software Product
</div>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Installations</th>
<td>
<!-- {% for version in object.softwareproduct_versions.all %}-->
<!-- <a href="{% url 'plugins:netbox_slm:softwareproductversion' pk=version.pk %}">-->
<!-- <span class="badge" style="color: #ffffff; background-color: #9e9e9e">{{ version }}</span>-->
<!-- </a>-->
<!-- {% endfor %}-->
</td>
</tr>
</table>
</div>
</div>
{% include 'inc/custom_fields_panel.html' %}
</div>
<div class="col col-md-6">
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %}
</div>
</div>
<!--<div class="row">-->
<!-- <div class="col col-md-12">-->
<!-- <div class="card">-->
<!-- <h5 class="card-header">Records</h5>-->
<!-- <div class="card-body">-->
<!-- <table class="table table-hover attr-table table-striped">-->
<!-- <thead>-->
<!-- <tr>-->
<!-- <th scope="col">TYPE</th>-->
<!-- <th scope="col">NAME</th>-->
<!-- <th scope="col">VALUE</th>-->
<!-- <th scope="col">TTL</th>-->
<!-- <th scope="col">Actions</th>-->
<!-- </tr>-->
<!-- </thead>-->
<!-- <tbody>-->
<!-- {% for record in records %}-->
<!-- <tr>-->
<!-- <td>{{ record.type }}</td>-->
<!-- <td>{{ record.name|truncatechars:32 }}</td>-->
<!-- <td>{{ record.value|truncatechars:64 }}</td>-->
<!-- <td>{{ record.ttl }}</td>-->
<!-- <td class="noprint text-end text-nowrap">-->
<!-- {% if request.user|can_change:object %}-->
<!-- {% plugin_edit_button record %}-->
<!-- {% endif %}-->
<!-- {% if request.user|can_delete:object %}-->
<!-- {% plugin_delete_button record %}-->
<!-- {% endif %}-->
<!-- </td>-->
<!-- </tr>-->
<!-- {% endfor %}-->
<!-- </tbody>-->
<!-- </table>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!--</div>-->
{% endblock %}
@@ -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 %}
<a href="{% url 'plugins:netbox_slm:record_add' %}?zone={{ object.id }}" class="btn btn-sm btn-primary" role="button">
<i class="mdi mdi-plus-thick"></i> Add Record
</a>
{% endcomment %}
{{ block.super }}
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<div class="card-header">
Zone
</div>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Versions</th>
<td>
<ul>
<!-- {% for nameserver in object.nameservers.all %}-->
<!-- <li>-->
<!-- <a href="{% url 'plugins:netbox_slm:nameserver' pk=nameserver.pk %}">-->
<!-- {{ nameserver }}-->
<!-- </a>-->
<!-- </li>-->
<!-- {% endfor %}-->
</ul>
</td>
</tr>
</table>
</div>
</div>
{% include 'inc/custom_fields_panel.html' %}
</div>
<div class="col col-md-6">
{% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='plugins:netbox_slm:softwareproduct_list' %}
</div>
</div>
<!--<div class="row">-->
<!-- <div class="col col-md-12">-->
<!-- <div class="card">-->
<!-- <h5 class="card-header">Records</h5>-->
<!-- <div class="card-body">-->
<!-- <table class="table table-hover attr-table table-striped">-->
<!-- <thead>-->
<!-- <tr>-->
<!-- <th scope="col">TYPE</th>-->
<!-- <th scope="col">NAME</th>-->
<!-- <th scope="col">VALUE</th>-->
<!-- <th scope="col">TTL</th>-->
<!-- <th scope="col">Actions</th>-->
<!-- </tr>-->
<!-- </thead>-->
<!-- <tbody>-->
<!-- {% for record in records %}-->
<!-- <tr>-->
<!-- <td>{{ record.type }}</td>-->
<!-- <td>{{ record.name|truncatechars:32 }}</td>-->
<!-- <td>{{ record.value|truncatechars:64 }}</td>-->
<!-- <td>{{ record.ttl }}</td>-->
<!-- <td class="noprint text-end text-nowrap">-->
<!-- {% if request.user|can_change:object %}-->
<!-- {% plugin_edit_button record %}-->
<!-- {% endif %}-->
<!-- {% if request.user|can_delete:object %}-->
<!-- {% plugin_delete_button record %}-->
<!-- {% endif %}-->
<!-- </td>-->
<!-- </tr>-->
<!-- {% endfor %}-->
<!-- </tbody>-->
<!-- </table>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!--</div>-->
{% endblock %}
@@ -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,
}
+38
View File
@@ -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/<int:pk>/", views.SoftwareProductView.as_view(), name="softwareproduct"),
path("software-products/<int:pk>/delete/", views.SoftwareProductDeleteView.as_view(), name="softwareproduct_delete"),
path("software-products/<int:pk>/edit/", views.SoftwareProductEditView.as_view(), name="softwareproduct_edit"),
path(
"software-products/<int:pk>/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/<int:pk>/", views.SoftwareProductVersionView.as_view(), name="softwareproductversion"),
path("versions/<int:pk>/delete/", views.SoftwareProductVersionDeleteView.as_view(), name="softwareproductversion_delete"),
path("versions/<int:pk>/edit/", views.SoftwareProductVersionEditView.as_view(), name="softwareproductversion_edit"),
path(
"versions/<int:pk>/changelog/",
ObjectChangeLogView.as_view(),
name="softwareproductversion_changelog",
kwargs={"model": SoftwareProductVersion},
),
]
+113
View File
@@ -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
+14
View File
@@ -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,
)
+11
View File
@@ -0,0 +1,11 @@
git clone -b release https://github.com/netbox-community/netbox-docker.git
cd netbox-docker
tee docker-compose.override.yml <<EOF
version: '3.4'
services:
netbox:
ports:
- 8000:8080
EOF
docker-compose pull
docker-compose up