From 43f8feccfc98d6ac0e8e53d21bda1501bef199ee Mon Sep 17 00:00:00 2001 From: Vincent Simonin <vincent.simonin@ext.ec.europa.eu> Date: Thu, 7 Sep 2023 18:57:50 +0200 Subject: [PATCH 1/7] Add new plugin for certificate management --- .gitlab-ci.yml | 10 +- plugins/netbox-cert-plugin/README.md | 6 + .../netbox_cert_plugin/__init__.py | 19 +++ .../netbox_cert_plugin/api/__init__.py | 0 .../netbox_cert_plugin/api/serializers.py | 30 +++++ .../netbox_cert_plugin/api/urls.py | 11 ++ .../netbox_cert_plugin/api/views.py | 14 +++ .../netbox_cert_plugin/filtersets.py | 31 +++++ .../netbox_cert_plugin/forms.py | 86 +++++++++++++ .../migrations/0001_initial.py | 53 ++++++++ .../netbox_cert_plugin/migrations/__init__.py | 0 .../netbox_cert_plugin/models.py | 86 +++++++++++++ .../netbox_cert_plugin/navigation.py | 34 ++++++ .../netbox_cert_plugin/search.py | 16 +++ .../netbox_cert_plugin/tables.py | 34 ++++++ .../netbox_cert_plugin/certificate.html | 50 ++++++++ .../netbox_cert_plugin/urls.py | 24 ++++ .../netbox_cert_plugin/views.py | 48 ++++++++ plugins/netbox-cert-plugin/setup.py | 11 ++ plugins/netbox-cert-plugin/tests/README.md | 12 ++ plugins/netbox-cert-plugin/tests/__init__.py | 0 .../netbox-cert-plugin/tests/e2e/__init__.py | 0 .../tests/e2e/test_certificate_creation.py | 115 ++++++++++++++++++ .../tests/requirements.e2e.txt | 1 + 24 files changed, 687 insertions(+), 4 deletions(-) create mode 100644 plugins/netbox-cert-plugin/README.md create mode 100644 plugins/netbox-cert-plugin/netbox_cert_plugin/__init__.py create mode 100644 plugins/netbox-cert-plugin/netbox_cert_plugin/api/__init__.py create mode 100644 plugins/netbox-cert-plugin/netbox_cert_plugin/api/serializers.py create mode 100644 plugins/netbox-cert-plugin/netbox_cert_plugin/api/urls.py create mode 100644 plugins/netbox-cert-plugin/netbox_cert_plugin/api/views.py create mode 100644 plugins/netbox-cert-plugin/netbox_cert_plugin/filtersets.py create mode 100644 plugins/netbox-cert-plugin/netbox_cert_plugin/forms.py create mode 100644 plugins/netbox-cert-plugin/netbox_cert_plugin/migrations/0001_initial.py create mode 100644 plugins/netbox-cert-plugin/netbox_cert_plugin/migrations/__init__.py create mode 100644 plugins/netbox-cert-plugin/netbox_cert_plugin/models.py create mode 100644 plugins/netbox-cert-plugin/netbox_cert_plugin/navigation.py create mode 100644 plugins/netbox-cert-plugin/netbox_cert_plugin/search.py create mode 100644 plugins/netbox-cert-plugin/netbox_cert_plugin/tables.py create mode 100644 plugins/netbox-cert-plugin/netbox_cert_plugin/templates/netbox_cert_plugin/certificate.html create mode 100644 plugins/netbox-cert-plugin/netbox_cert_plugin/urls.py create mode 100644 plugins/netbox-cert-plugin/netbox_cert_plugin/views.py create mode 100644 plugins/netbox-cert-plugin/setup.py create mode 100644 plugins/netbox-cert-plugin/tests/README.md create mode 100644 plugins/netbox-cert-plugin/tests/__init__.py create mode 100644 plugins/netbox-cert-plugin/tests/e2e/__init__.py create mode 100644 plugins/netbox-cert-plugin/tests/e2e/test_certificate_creation.py create mode 100644 plugins/netbox-cert-plugin/tests/requirements.e2e.txt diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c3cb9ba..cc34aca 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -31,20 +31,22 @@ build-job: # This job runs in the build stage, which runs first. script: - ansible-playbook ansible/build.yml -run-test: +run-test-job: stage: test before_script: - chmod 600 $ANSIBLE_PRIVATE_KEY_FILE - env ANSIBLE_HOST_KEY_CHECKING=false ANSIBLE_PRIVATE_KEY_FILE="$ANSIBLE_PRIVATE_KEY_FILE" ansible-playbook -i "$TESTING_HOSTS" -u debian ansible/deploy_on_test.yml - - python3 -m venv "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/venv" - - source "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/venv/bin/activate" + - python3 -m venv "$CI_PROJECT_DIR/plugins/venv" + - source "$CI_PROJECT_DIR/plugins/venv/bin/activate" - pip install -r "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/tests/requirements.e2e.txt" + - pip install -r "$CI_PROJECT_DIR/plugins/netbox-cert-plugin/tests/requirements.e2e.txt" script: - env HOST="$HOST" PORT="$PORT" API_KEY="$API_KEY" python3 -m unittest discover -b "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/tests/" + - env HOST="$HOST" PORT="$PORT" API_KEY="$API_KEY" python3 -m unittest discover -b "$CI_PROJECT_DIR/plugins/netbox-cert-plugin/tests/" after_script: - env ANSIBLE_HOST_KEY_CHECKING=false ANSIBLE_PRIVATE_KEY_FILE="$ANSIBLE_PRIVATE_KEY_FILE" ansible-playbook -i "$TESTING_HOSTS" -u debian ansible/halt_test.yml - deactivate - - rm -rf "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/venv" + - rm -rf "$CI_PROJECT_DIR/plugins/venv" deliver-latest-job: stage: deliver diff --git a/plugins/netbox-cert-plugin/README.md b/plugins/netbox-cert-plugin/README.md new file mode 100644 index 0000000..af05d91 --- /dev/null +++ b/plugins/netbox-cert-plugin/README.md @@ -0,0 +1,6 @@ +# Certificate Plugin for Netbox + +A Netbox plugin for certificate management + +## Tests +see [tests directory](./tests/README.md) diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/__init__.py b/plugins/netbox-cert-plugin/netbox_cert_plugin/__init__.py new file mode 100644 index 0000000..8fe39a8 --- /dev/null +++ b/plugins/netbox-cert-plugin/netbox_cert_plugin/__init__.py @@ -0,0 +1,19 @@ +"""Netbox Plugin Configuration""" + +# pylint: disable=E0401 +from extras.plugins import PluginConfig + + +class NetBoxCertConfig(PluginConfig): + """Netbox Plugin Configuration class""" + + name = "netbox_cert_plugin" + verbose_name = "Netbox Certificate" + description = "A Netbox plugin to manage certificates" + version = "0.0.1" + author = "Vincent Simonin" + author_email = "vincent.simonin@ext.ec.europa.eu" + base_url = "cert" + +# pylint: disable=C0103 +config = NetBoxCertConfig diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/api/__init__.py b/plugins/netbox-cert-plugin/netbox_cert_plugin/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/api/serializers.py b/plugins/netbox-cert-plugin/netbox_cert_plugin/api/serializers.py new file mode 100644 index 0000000..a0d4163 --- /dev/null +++ b/plugins/netbox-cert-plugin/netbox_cert_plugin/api/serializers.py @@ -0,0 +1,30 @@ +"""API Serializer definitions""" + +from rest_framework import serializers +from netbox.api.serializers import NetBoxModelSerializer +from ..models import Certificate + + +class CertificateSerializer(NetBoxModelSerializer): + """Certificate Serializer class""" + + url = serializers.HyperlinkedIdentityField( + view_name="plugins-api:netbox_cert_plugin-api:certificate-detail" + ) + + class Meta: + model = Certificate + fields = ( + "id", + "url", + "cn", + "alt_name", + "ca", + "expiration_time", + "cert_created_at", + "cert_expired_at", + "custom_fields", + "created", + "last_updated", + "tags" + ) diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/api/urls.py b/plugins/netbox-cert-plugin/netbox_cert_plugin/api/urls.py new file mode 100644 index 0000000..ac9953f --- /dev/null +++ b/plugins/netbox-cert-plugin/netbox_cert_plugin/api/urls.py @@ -0,0 +1,11 @@ +"""API URLs definition""" + +from netbox.api.routers import NetBoxRouter +from . import views + +APP_NAME = 'netbox_cert_plugin' + +router = NetBoxRouter() +router.register('certificate', views.CertificateViewSet) + +urlpatterns = router.urls diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/api/views.py b/plugins/netbox-cert-plugin/netbox_cert_plugin/api/views.py new file mode 100644 index 0000000..9d494a1 --- /dev/null +++ b/plugins/netbox-cert-plugin/netbox_cert_plugin/api/views.py @@ -0,0 +1,14 @@ +"""API views definitions""" + +from netbox.api.viewsets import NetBoxModelViewSet +from .. import filtersets, models +from .serializers import CertificateSerializer + + +class CertificateViewSet(NetBoxModelViewSet): + """Mapping view set class""" + + queryset = models.Certificate.objects.all() + serializer_class = CertificateSerializer + filterset_class = filtersets.CertificateFilterSet + http_method_names = ["get", "post", "patch", "delete"] diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/filtersets.py b/plugins/netbox-cert-plugin/netbox_cert_plugin/filtersets.py new file mode 100644 index 0000000..51bf487 --- /dev/null +++ b/plugins/netbox-cert-plugin/netbox_cert_plugin/filtersets.py @@ -0,0 +1,31 @@ +"""Filtersets definitions""" + +from netbox.filtersets import NetBoxModelFilterSet +from django.db.models import Q +from .models import Certificate + + +class CertificateFilterSet(NetBoxModelFilterSet): + """Certificate filterset definition class""" + + class Meta: + model = Certificate + fields = ( + "id", + "cn", + "alt_name", + "ca", + "expiration_time", + "cert_created_at", + "cert_expired_at", + ) + + # pylint: disable=W0613 + def search(self, queryset, name, value): + """override""" + if not value.strip(): + return queryset + return queryset.filter( + Q(cn__icontains=value) + | Q(alt_name__icontains=value) + ) diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/forms.py b/plugins/netbox-cert-plugin/netbox_cert_plugin/forms.py new file mode 100644 index 0000000..736821f --- /dev/null +++ b/plugins/netbox-cert-plugin/netbox_cert_plugin/forms.py @@ -0,0 +1,86 @@ +"""Forms definitions""" + +from django import forms +from netbox.forms import ( + NetBoxModelForm, + NetBoxModelImportForm, + NetBoxModelFilterSetForm, +) +from utilities.forms.widgets import DatePicker +from utilities.forms.fields import TagFilterField +from .models import Certificate, ExpirationTimeChoices, CaChoices + + +class CertificateForm(NetBoxModelForm): + """Certificate form definition class""" + + class Meta: + model = Certificate + fields = ( + "cn", + "alt_name", + "ca", + "expiration_time", + "cert_created_at", + "cert_expired_at", + "tags", + ) + help_texts = {"CN": "Unique Common Name", "CA": "Valid Certificate Authority"} + labels = { + "cn": "CN", + "alt_name": "Alt name", + "ca": "CA", + "expiration_time": "Expiration time", + "cert_created_at": "Effective certificate's creation date", + "cert_expired_at": "Effective certificate's expiration date", + } + widgets = { + "cert_created_at": DatePicker(), + "cert_expired_at": DatePicker(), + } + + +class CertificateFilterForm(NetBoxModelFilterSetForm): + """Certificate filter form definition class""" + + model = Certificate + cn = forms.CharField( + label="Common Name", max_length=256, min_length=1, required=False + ) + alt_name = forms.CharField( + label="Alt name", max_length=256, min_length=1, required=False + ) + ca = forms.MultipleChoiceField( + label="Certificate Authority", choices=CaChoices, required=False + ) + expiration_time = forms.MultipleChoiceField( + label="Expiration time", choices=ExpirationTimeChoices, required=False + ) + cert_created_at = forms.DateField( + label="Effective certificate's creation date", required=False, widget=DatePicker + ) + cert_expired_at = forms.DateField( + label="Effective certificate's expirations date", + required=False, + widget=DatePicker, + ) + tag = TagFilterField(model) + + +class CertificateImportForm(NetBoxModelImportForm): + """Certificate importation form definition class""" + + class Meta: + model = Certificate + fields = ( + "cn", + "alt_name", + "ca", + "expiration_time", + "cert_created_at", + "cert_expired_at", + ) + labels = { + "ca": "Certificate Authority. Can be 'letsencrypt', 'comisign', 'globalsign'", + "expiration_time": "Expiration time needed. Can be '1m', '3m', '6m', '1y', '3y'", + } diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/migrations/0001_initial.py b/plugins/netbox-cert-plugin/netbox_cert_plugin/migrations/0001_initial.py new file mode 100644 index 0000000..9379126 --- /dev/null +++ b/plugins/netbox-cert-plugin/netbox_cert_plugin/migrations/0001_initial.py @@ -0,0 +1,53 @@ +"""Migration File""" +# pylint: disable=C0103 + +from django.db import migrations, models +import taggit.managers +import utilities.json + + +class Migration(migrations.Migration): + """Migration Class""" + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Certificate", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False + ), + ), + ("created", models.DateTimeField(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=utilities.json.CustomFieldJSONEncoder, + ), + ), + ("cn", models.CharField(blank=False, unique=True, max_length=256)), + ("alt_name", models.CharField(blank=True, null=True, max_length=256)), + ("ca", models.CharField(blank=False, max_length=32)), + ("expiration_time", models.CharField(blank=False, max_length=2)), + ("cert_created_at", models.DateField(blank=True, null=True)), + ("cert_expired_at", models.DateField(blank=True, null=True)), + ( + "tags", + taggit.managers.TaggableManager( + through="extras.TaggedItem", to="extras.Tag" + ), + ), + ], + options={ + "ordering": ("source"), + }, + ), + ] diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/migrations/__init__.py b/plugins/netbox-cert-plugin/netbox_cert_plugin/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/models.py b/plugins/netbox-cert-plugin/netbox_cert_plugin/models.py new file mode 100644 index 0000000..7812802 --- /dev/null +++ b/plugins/netbox-cert-plugin/netbox_cert_plugin/models.py @@ -0,0 +1,86 @@ +"""Models definitions""" + +from django.db import models +from django.urls import reverse +from netbox.models import NetBoxModel +from utilities.choices import ChoiceSet + + +class CaChoices(ChoiceSet): + """CA choices definition class""" + + key = "Certificate.ca" + + DEFAULT_VALUE = "letsencrypt" + + CHOICES = [ + ("letsencrypt", "Let's Encrypt", "yellow"), + ("comisign", "Comisign", "red"), + ("globalsign", "GlobalSign", "blue"), + ] + +class ExpirationTimeChoices(ChoiceSet): + """Expiration time choices definition class""" + + key = "Certificate.expiration_time" + + DEFAULT_VALUE = "1m" + + CHOICES = [ + ("1m", "1 month", "blue"), + ("3m", "3 months", "blue"), + ("6m", "6 months", "blue"), + ("1y", "1 year", "blue"), + ("3y", "3 year", "blue"), + ] + + +class Certificate(NetBoxModel): + """Certificate definition class""" + + cn = models.CharField( + max_length=256, + blank=False, + verbose_name="Common Name", + unique=True, + ) + alt_name = models.CharField( + max_length=256, + blank=True, + null=True, + verbose_name="Alt Name", + ) + ca = models.CharField( + max_length=32, + choices=CaChoices, + default=CaChoices.DEFAULT_VALUE, + blank=False, + verbose_name="Certificate Authority", + ) + expiration_time = models.CharField( + max_length=2, + choices=ExpirationTimeChoices, + default=CaChoices.DEFAULT_VALUE, + blank=False, + verbose_name="Expiration time needed", + ) + cert_created_at = models.DateField( + blank=True, + null=True, + verbose_name="Effective certificate's creation date" + ) + cert_expired_at = models.DateField( + blank=True, + null=True, + verbose_name="Effective certificate's expiration date" + ) + + class Meta: + ordering = ["cn"] + + def __str__(self): + return f"{self.cn}" + + def get_absolute_url(self): + """override""" + return reverse("plugins:netbox_cert_plugin:certificate", args=[self.pk]) diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/navigation.py b/plugins/netbox-cert-plugin/netbox_cert_plugin/navigation.py new file mode 100644 index 0000000..a6cf54e --- /dev/null +++ b/plugins/netbox-cert-plugin/netbox_cert_plugin/navigation.py @@ -0,0 +1,34 @@ +"""Navigation Menu definitions""" + +from extras.plugins import PluginMenuButton, PluginMenuItem, PluginMenu +from utilities.choices import ButtonColorChoices + +cert_buttons = [ + PluginMenuButton( + link="plugins:netbox_cert_plugin:certificate_add", + title="Add", + icon_class="mdi mdi-plus-thick", + color=ButtonColorChoices.GREEN, + ), + PluginMenuButton( + link="plugins:netbox_cert_plugin:certificate_import", + title="Import", + icon_class="mdi mdi-upload", + color=ButtonColorChoices.CYAN, + ), +] + +certItem = [ + PluginMenuItem( + link="plugins:netbox_cert_plugin:certificate_list", + link_text="Certificates", + buttons=cert_buttons, + permissions=["netbox_cert_plugin.view_certificate"], + ), +] + +menu = PluginMenu( + label="Certificates", + groups=(("CERTIFICATE", certItem),), + icon_class="mdi mdi-certificate", +) diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/search.py b/plugins/netbox-cert-plugin/netbox_cert_plugin/search.py new file mode 100644 index 0000000..603713e --- /dev/null +++ b/plugins/netbox-cert-plugin/netbox_cert_plugin/search.py @@ -0,0 +1,16 @@ +"""Search definitions""" + +from netbox.search import SearchIndex, register_search +from .models import Certificate + + +@register_search +class CertificateIndex(SearchIndex): + """Certificate search definition class""" + + model = Certificate + fields = ( + ('cn', 256), + ('alt_name', 256), + ('ca', 32), + ) diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/tables.py b/plugins/netbox-cert-plugin/netbox_cert_plugin/tables.py new file mode 100644 index 0000000..fc7d98b --- /dev/null +++ b/plugins/netbox-cert-plugin/netbox_cert_plugin/tables.py @@ -0,0 +1,34 @@ +"""Tables definitions""" + +import django_tables2 as tables +from netbox.tables import NetBoxTable, columns +from .models import Certificate + + +class CertificateTable(NetBoxTable): + """Certificate Table definition class""" + + cn = tables.Column(linkify=True) + tags = columns.TagColumn() + + class Meta(NetBoxTable.Meta): + model = Certificate + fields = ( + "pk", + "id", + "cn", + "alt_name", + "ca", + "expiration_time", + "cert_created_at", + "cert_expired_at", + "tags" + ) + default_columns = ( + "cn", + "alt_name", + "ca", + "expiration_time", + "cert_created_at", + "cert_expired_at", + ) diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/templates/netbox_cert_plugin/certificate.html b/plugins/netbox-cert-plugin/netbox_cert_plugin/templates/netbox_cert_plugin/certificate.html new file mode 100644 index 0000000..7334a0b --- /dev/null +++ b/plugins/netbox-cert-plugin/netbox_cert_plugin/templates/netbox_cert_plugin/certificate.html @@ -0,0 +1,50 @@ +{% extends 'generic/object.html' %} + +{% block content %} +<div class="row mb-3"> + <div class="col col-md-6"> + <div class="card"> + <h5 class="card-header">CERTIFICATE</h5> + <div class="card-body"> + <table class="table table-hover attr-table"> + <tr> + <th scope="row">Common Name</th> + <td>{{ object.cn }}</td> + </tr> + <tr> + <th scope="row">Certificate Authority</th> + <td>{{ object.get_ca_display }}</td> + </tr> + <tr> + <th scope="row">Effective certificate's expiration date</th> + <td>{{ object.cert_expired_at|placeholder }}</td> + </tr> + </table> + </div> + </div> + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' %} + </div> + <div class="col col-md-6"> + <div class="card"> + <h5 class="card-header">Details</h5> + <div class="card-body"> + <table class="table table-hover attr-table"> + <tr> + <th scope="row">Alt name</th> + <td>{{ object.alt_name|placeholder }}</td> + </tr> + <tr> + <th scope="row">Expiration time</th> + <td>{{ object.get_expiration_time_display|placeholder }}</td> + </tr> + <tr> + <th scope="row">Effective certificate's creation date</th> + <td>{{ object.cert_created_at|placeholder }}</td> + </tr> + </table> + </div> + </div> + </div> +</div> +{% endblock content %} diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/urls.py b/plugins/netbox-cert-plugin/netbox_cert_plugin/urls.py new file mode 100644 index 0000000..c19c448 --- /dev/null +++ b/plugins/netbox-cert-plugin/netbox_cert_plugin/urls.py @@ -0,0 +1,24 @@ +"""URL definitions""" + +from django.urls import path +from netbox_cert_plugin import views, models +from netbox.views.generic import ObjectChangeLogView, ObjectJournalView + + +urlpatterns = ( + + # Cert + path('certificates/', views.CertificateListView.as_view(), name='certificate_list'), + path('certificates/add/', views.CertificateEditView.as_view(), name='certificate_add'), + path('certificates/import/', views.CertificateBulkImportView.as_view(), name='certificate_import'), + path('certificates/delete/', views.CertificateBulkDeleteView.as_view(), name='certificate_bulk_delete'), + path('certificates/<int:pk>/', views.CertificateView.as_view(), name='certificate'), + path('certificates/<int:pk>/edit/', views.CertificateEditView.as_view(), name='certificate_edit'), + path('certificates/<int:pk>/delete/', views.CertificateDeleteView.as_view(), name='certificate_delete'), + path('certificates/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='certificate_changelog', kwargs={ + 'model': models.Certificate + }), + path('certificates/<int:pk>/journal/', ObjectJournalView.as_view(), name='certificate_journal', kwargs={ + 'model': models.Certificate + }), +) diff --git a/plugins/netbox-cert-plugin/netbox_cert_plugin/views.py b/plugins/netbox-cert-plugin/netbox_cert_plugin/views.py new file mode 100644 index 0000000..b33fa59 --- /dev/null +++ b/plugins/netbox-cert-plugin/netbox_cert_plugin/views.py @@ -0,0 +1,48 @@ +"""Model views definitions""" + +from netbox.views import generic +from netbox_cert_plugin import models, tables, filtersets, forms +from django.utils.translation import gettext as _ + + +class CertificateView(generic.ObjectView): + """Certificate view definition""" + + queryset = models.Certificate.objects.all() + + +class CertificateListView(generic.ObjectListView): + """Certificate list view definition""" + + queryset = models.Certificate.objects.all() + table = tables.CertificateTable + filterset = filtersets.CertificateFilterSet + filterset_form = forms.CertificateFilterForm + + +class CertificateEditView(generic.ObjectEditView): + """Certificate edition view definition""" + + queryset = models.Certificate.objects.all() + form = forms.CertificateForm + + +class CertificateBulkImportView(generic.BulkImportView): + """Certificate bulk import view definition""" + + queryset = models.Certificate.objects.all() + model_form = forms.CertificateImportForm + + +class CertificateDeleteView(generic.ObjectDeleteView): + """Certificate delete view definition""" + + queryset = models.Certificate.objects.all() + + +class CertificateBulkDeleteView(generic.BulkDeleteView): + """Certificate bulk delete view definition""" + + queryset = models.Certificate.objects.all() + filterset = filtersets.CertificateFilterSet + table = tables.CertificateTable diff --git a/plugins/netbox-cert-plugin/setup.py b/plugins/netbox-cert-plugin/setup.py new file mode 100644 index 0000000..445c332 --- /dev/null +++ b/plugins/netbox-cert-plugin/setup.py @@ -0,0 +1,11 @@ +from setuptools import find_packages, setup + +setup( + name='netbox_cert_plugin', + version='0.0.1', + description='A Netbox plugin to manage certificates', + install_requires=[], + packages=find_packages(), + include_package_data=True, + zip_safe=False +) diff --git a/plugins/netbox-cert-plugin/tests/README.md b/plugins/netbox-cert-plugin/tests/README.md new file mode 100644 index 0000000..31370e4 --- /dev/null +++ b/plugins/netbox-cert-plugin/tests/README.md @@ -0,0 +1,12 @@ +# Testing the plugin + +## End to end + +Prepare a python environment to execute the E2E tests suite + +```shell +python3 -m venv venv +source venv/bin/activate +pip install -r tests/requirements.e2e.txt +python -m unittest discover tests/e2e +``` diff --git a/plugins/netbox-cert-plugin/tests/__init__.py b/plugins/netbox-cert-plugin/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/netbox-cert-plugin/tests/e2e/__init__.py b/plugins/netbox-cert-plugin/tests/e2e/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/netbox-cert-plugin/tests/e2e/test_certificate_creation.py b/plugins/netbox-cert-plugin/tests/e2e/test_certificate_creation.py new file mode 100644 index 0000000..9378354 --- /dev/null +++ b/plugins/netbox-cert-plugin/tests/e2e/test_certificate_creation.py @@ -0,0 +1,115 @@ +"""Test case for Mapping creation""" + +import unittest +import json +import os +import requests + + +HOST = os.getenv("HOST", default="localhost") +PORT = os.getenv("PORT", default="8080") +API_KEY = os.getenv("API_KEY", "only4testingpurpose") + + +class TestCertificateCreation(unittest.TestCase): + """Test case for Mapping creation class""" + + mapping_id = None + + def test_that_certificate_is_created(self) -> None: + """Test that certificate is created""" + + response = requests.post( + url=f"http://{HOST}:{PORT}/api/plugins/cert/certificate/", + json={ + "cn": "truc00.com", + "ca": "letsencrypt", + "expiration_time": "1m" + }, + headers={"Authorization": f"Token {API_KEY}"}, + timeout=5, + ) + + self.assertEqual(response.status_code, 201) + + self.mapping_id = json.loads(response.content)["id"] + + def test_that_cn_is_unique(self) -> None: + """Test that CN is unique""" + + response = requests.post( + url=f"http://{HOST}:{PORT}/api/plugins/cert/certificate/", + json={ + "cn": "truc00.com", + "ca": "letsencrypt", + "expiration_time": "1m" + }, + headers={"Authorization": f"Token {API_KEY}"}, + timeout=5, + ) + + self.assertEqual(response.status_code, 201) + + self.mapping_id = json.loads(response.content)["id"] + + response = requests.post( + url=f"http://{HOST}:{PORT}/api/plugins/cert/certificate/", + json={ + "cn": "truc00.com", + "ca": "comisign", + "expiration_time": "3m" + }, + headers={"Authorization": f"Token {API_KEY}"}, + timeout=5, + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual(response.content, b'{"cn":["certificate with this Common Name already exists."]}') + + def test_that_ca_is_valid(self) -> None: + """Test that CA is valid""" + + response = requests.post( + url=f"http://{HOST}:{PORT}/api/plugins/cert/certificate/", + json={ + "cn": "truc00.com", + "ca": "randomca", + "expiration_time": "3m" + }, + headers={"Authorization": f"Token {API_KEY}"}, + timeout=5, + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual(response.content, b'{"ca":["\\"randomca\\" is not a valid choice."]}') + + def test_that_expiration_time_is_valid(self) -> None: + """Test that expiration time is valid""" + + response = requests.post( + url=f"http://{HOST}:{PORT}/api/plugins/cert/certificate/", + json={ + "cn": "truc00.com", + "ca": "comisign", + "expiration_time": "10m" + }, + headers={"Authorization": f"Token {API_KEY}"}, + timeout=5, + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual(response.content, b'{"expiration_time":["\\"10m\\" is not a valid choice."]}') + + def tearDown(self) -> None: + """Teardown function""" + + requests.delete( + url=f"http://{HOST}:{PORT}/api/plugins/cert/certificate/", + json=[{"id": self.mapping_id}], + headers={"Authorization": f"Token {API_KEY}"}, + timeout=5, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/plugins/netbox-cert-plugin/tests/requirements.e2e.txt b/plugins/netbox-cert-plugin/tests/requirements.e2e.txt new file mode 100644 index 0000000..becc27f --- /dev/null +++ b/plugins/netbox-cert-plugin/tests/requirements.e2e.txt @@ -0,0 +1 @@ +requests==2.30.0 -- GitLab From 5ac7be8fb77f7453502d2aa77a5caa5464fe497d Mon Sep 17 00:00:00 2001 From: Vincent Simonin <vincent.simonin@ext.ec.europa.eu> Date: Thu, 7 Sep 2023 19:02:04 +0200 Subject: [PATCH 2/7] Add the new plugin in the build (gni) --- netbox_configuration/plugins.py | 2 +- requirements.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox_configuration/plugins.py b/netbox_configuration/plugins.py index f945904..d7789b7 100644 --- a/netbox_configuration/plugins.py +++ b/netbox_configuration/plugins.py @@ -1 +1 @@ -PLUGINS = ['netbox_dns', 'netbox_rps_plugin', 'netbox_prometheus_sd'] +PLUGINS = ['netbox_dns', 'netbox_rps_plugin', 'netbox_cert_plugin', 'netbox_prometheus_sd'] diff --git a/requirements.txt b/requirements.txt index 58ca9b9..07a41df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ /opt/netbox/plugins/netbox-rps-plugin +/opt/netbox/plugins/netbox-cert-plugin netbox-plugin-dns netbox-plugin-prometheus-sd -- GitLab From 572c69a276003d7024290c9f43d71bc899aa07e5 Mon Sep 17 00:00:00 2001 From: Vincent Simonin <vincent.simonin@ext.ec.europa.eu> Date: Thu, 7 Sep 2023 19:04:39 +0200 Subject: [PATCH 3/7] Lint the new plugin --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cc34aca..c792cc1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,7 +21,7 @@ lint-job: - source "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/venv/bin/activate" - pip install pylint script: - - pylint "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/netbox_rps_plugin" "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/tests" + - pylint "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/netbox_rps_plugin" "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/tests" "$CI_PROJECT_DIR/plugins/netbox-cert-plugin/netbox_rps_plugin" "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/netbox_cert_plugin/tests" after_script: - deactivate - rm -rf "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/venv" -- GitLab From a3822a01cf1d2c8cc4cebef29536d861b4331747 Mon Sep 17 00:00:00 2001 From: Vincent Simonin <vincent.simonin@ext.ec.europa.eu> Date: Thu, 7 Sep 2023 19:10:02 +0200 Subject: [PATCH 4/7] Fix lint configuration --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c792cc1..c4764f4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,7 +21,7 @@ lint-job: - source "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/venv/bin/activate" - pip install pylint script: - - pylint "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/netbox_rps_plugin" "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/tests" "$CI_PROJECT_DIR/plugins/netbox-cert-plugin/netbox_rps_plugin" "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/netbox_cert_plugin/tests" + - pylint "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/netbox_rps_plugin" "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/tests" "$CI_PROJECT_DIR/plugins/netbox-cert-plugin/netbox_cert_plugin" "$CI_PROJECT_DIR/plugins/netbox-cert-plugin/tests" after_script: - deactivate - rm -rf "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/venv" -- GitLab From 98e7cf0b59503243704b6d85091e0893d03a8788 Mon Sep 17 00:00:00 2001 From: Vincent Simonin <vincent.simonin@ext.ec.europa.eu> Date: Thu, 7 Sep 2023 19:19:18 +0200 Subject: [PATCH 5/7] Pylint plugins one by one (avoid duplicate between plugins...) --- .gitlab-ci.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c4764f4..ef15830 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,14 +17,15 @@ include: lint-job: stage: lint before_script: - - python3 -m venv "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/venv" - - source "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/venv/bin/activate" + - python3 -m venv "$CI_PROJECT_DIR/plugins/venv" + - source "$CI_PROJECT_DIR/plugins/venv/bin/activate" - pip install pylint script: - - pylint "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/netbox_rps_plugin" "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/tests" "$CI_PROJECT_DIR/plugins/netbox-cert-plugin/netbox_cert_plugin" "$CI_PROJECT_DIR/plugins/netbox-cert-plugin/tests" + - pylint "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/netbox_rps_plugin" "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/tests" + - pylint "$CI_PROJECT_DIR/plugins/netbox-cert-plugin/netbox_cert_plugin" "$CI_PROJECT_DIR/plugins/netbox-cert-plugin/tests" after_script: - deactivate - - rm -rf "$CI_PROJECT_DIR/plugins/netbox-rps-plugin/venv" + - rm -rf "$CI_PROJECT_DIR/plugins/venv" build-job: # This job runs in the build stage, which runs first. stage: build -- GitLab From 7ea4f1388dbf9f9022c6e4bafabbc5e8f0303665 Mon Sep 17 00:00:00 2001 From: Vincent Simonin <vincent.simonin@ext.ec.europa.eu> Date: Fri, 8 Sep 2023 08:44:26 +0200 Subject: [PATCH 6/7] Wait 60s after test env had boot... --- ansible/deploy_on_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/deploy_on_test.yml b/ansible/deploy_on_test.yml index 4f6e4dc..31831e9 100644 --- a/ansible/deploy_on_test.yml +++ b/ansible/deploy_on_test.yml @@ -35,4 +35,4 @@ - name: Pause for 10 seconds ansible.builtin.pause: - seconds: 10 + seconds: 60 -- GitLab From cc004a72cf7a16554d04950c46da2fa8519d85a7 Mon Sep 17 00:00:00 2001 From: Vincent Simonin <vincent.simonin@ext.ec.europa.eu> Date: Fri, 8 Sep 2023 10:39:19 +0200 Subject: [PATCH 7/7] Add missing template file in the build --- docker-compose.test.yml | 2 ++ plugins/netbox-cert-plugin/MANIFEST.in | 1 + 2 files changed, 3 insertions(+) create mode 100644 plugins/netbox-cert-plugin/MANIFEST.in diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 20d07c3..44c7d5f 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -15,6 +15,7 @@ services: syslog-format: "rfc5424" tag: "netbox" netbox-worker: + image: "code.europa.eu:4567/digit-c4/netbox-plugins:${TAG}" env_file: env/netbox.env logging: driver: "syslog" @@ -23,6 +24,7 @@ services: syslog-format: "rfc5424" tag: "netbox-worker" netbox-housekeeping: + image: "code.europa.eu:4567/digit-c4/netbox-plugins:${TAG}" env_file: env/netbox.env logging: driver: "syslog" diff --git a/plugins/netbox-cert-plugin/MANIFEST.in b/plugins/netbox-cert-plugin/MANIFEST.in new file mode 100644 index 0000000..f18e683 --- /dev/null +++ b/plugins/netbox-cert-plugin/MANIFEST.in @@ -0,0 +1 @@ +global-include *.html -- GitLab