diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ed91e2..f936636 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ * German translation for navigation, forms, tables, detail views, choices, and validation messages * Optional lifetime or recurring renewal interval for licenses * Optional license file, license key, and provider portal URL +* Daily license expiration notifications for users who subscribe to a license +* Default reminders one month and one week before expiration plus an optional custom interval +* Optional SMTP email in addition to mandatory NetBox notifications ### Changed @@ -21,6 +24,7 @@ * Render tenant group and tenant in a dedicated license form section * Bump the package version so Git/Pip upgrades replace older installations * Make the license type field optional +* Show the plugin under Licences in the main menu ## [1.9.0](https://github.com/ICTU/netbox_slm/releases/tag/1.9.0) - 2026-06-25 diff --git a/README.md b/README.md index 2481d4b..dc8d615 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ Benutzer ausgewählten Sprache. - Softwarelizenzen einschließlich Laufzeit, Umfang und Ablageort verwalten - Lifetime-Lizenzen oder Erneuerungsintervalle in Tagen, Monaten oder Jahren erfassen - Lizenzdateien, Lizenzschlüssel und Links zum Anbieterportal hinterlegen +- Ablaufwarnungen standardmäßig einen Monat und eine Woche vor Lizenzende +- Zusätzliche eigene Vorwarnzeit in Tagen, Wochen oder Monaten +- NetBox-Benachrichtigungen und optionaler E-Mail-Versand über die NetBox-SMTP-Konfiguration - Lizenzen Mandantengruppen und Mandanten zuordnen - Bedienung über die NetBox-Oberfläche und REST-API - Filter, Bulk-Import und Bulk-Bearbeitung @@ -63,7 +66,25 @@ Die installierte Version kann anschließend geprüft werden: /opt/netbox/venv/bin/pip show netbox-slm ``` -Für diese Variante muss dort mindestens Version `1.11.2` stehen. +Für diese Variante muss dort mindestens Version `1.12.0` stehen. + +## Ablaufbenachrichtigungen + +Für Lizenzen mit Ablaufdatum sind Benachrichtigungen standardmäßig aktiviert. Die Standardtermine liegen einen +Monat und eine Woche vor dem Ablaufdatum. Pro Lizenz kann zusätzlich eine eigene Vorwarnzeit festgelegt oder die +Standardtermine deaktiviert werden. + +Empfänger entscheiden selbst, ob sie informiert werden: Vertrieb, zuständiger Techniker oder andere Benutzer öffnen +die Lizenz und wählen die NetBox-Aktion **Abonnieren**. Mit **Abonnement beenden** können sie die Meldungen wieder +abschalten. Ohne Abonnenten wird keine Nachricht versendet. + +Die NetBox-Nachricht wird immer erzeugt. Wird an der Lizenz zusätzlich **E-Mail-Benachrichtigungen** aktiviert, sendet +das Plugin auch eine E-Mail an die im Benutzerkonto hinterlegte Adresse. Dafür muss der SMTP-Versand in NetBox +konfiguriert sein. + +Die Prüfung läuft einmal täglich als NetBox-Systemjob. Der NetBox-RQ-Worker muss daher mit Scheduler-Unterstützung +laufen; dies ist bei der regulären NetBox-Installation mit `rqworker` standardmäßig der Fall. Das interne +Versandprotokoll verhindert doppelte Nachrichten. ### 3. Plugin in NetBox aktivieren diff --git a/netbox_slm/__init__.py b/netbox_slm/__init__.py index b498ee9..e0530f8 100644 --- a/netbox_slm/__init__.py +++ b/netbox_slm/__init__.py @@ -17,14 +17,14 @@ limitations under the License. from netbox.plugins import PluginConfig from django.utils.translation import gettext_lazy as _ -__version__ = "1.11.2" +__version__ = "1.12.0" class SLMConfig(PluginConfig): name = "netbox_slm" - verbose_name = _("Software Lifecycle Management") + verbose_name = _("Licences") version = __version__ - description = _("Software Lifecycle Management NetBox Plugin.") + description = _("Licence Management NetBox Plugin.") author = "ICTU" author_email = "open-source-projects@ictu.nl" base_url = "slm" @@ -36,5 +36,15 @@ class SLMConfig(PluginConfig): "link_virtualmachine_installations": "right", } + def ready(self): + super().ready() + + from netbox.events import EVENT_TYPE_KIND_WARNING, EventType + from netbox_slm.events import LICENSE_EXPIRING + + EventType(LICENSE_EXPIRING, _("License expiring"), kind=EVENT_TYPE_KIND_WARNING).register() + + from netbox_slm import jobs # noqa: F401 + config = SLMConfig diff --git a/netbox_slm/api/serializers.py b/netbox_slm/api/serializers.py index e6e88d9..1327ef7 100644 --- a/netbox_slm/api/serializers.py +++ b/netbox_slm/api/serializers.py @@ -32,6 +32,11 @@ class SoftwareLicenseSerializer(NetBoxModelSerializer): "license_file", "license_key", "provider_portal_url", + "expiration_notifications_enabled", + "default_notification_intervals", + "custom_notification_interval", + "custom_notification_interval_unit", + "email_notifications", "software_product", "version", "installation", @@ -69,6 +74,21 @@ class SoftwareLicenseSerializer(NetBoxModelSerializer): raise serializers.ValidationError( {"lifetime": _("A lifetime license cannot have a renewal interval or expiration date.")} ) + custom_interval = attrs.get( + "custom_notification_interval", getattr(self.instance, "custom_notification_interval", None) + ) + custom_interval_unit = attrs.get( + "custom_notification_interval_unit", + getattr(self.instance, "custom_notification_interval_unit", ""), + ) + if bool(custom_interval) != bool(custom_interval_unit): + raise serializers.ValidationError( + { + "custom_notification_interval": _( + "Custom notification interval and unit must be specified together." + ) + } + ) return attrs diff --git a/netbox_slm/events.py b/netbox_slm/events.py new file mode 100644 index 0000000..a0886e9 --- /dev/null +++ b/netbox_slm/events.py @@ -0,0 +1 @@ +LICENSE_EXPIRING = "netbox_slm_license_expiring" diff --git a/netbox_slm/filtersets.py b/netbox_slm/filtersets.py index ea9a8f3..e9d7038 100644 --- a/netbox_slm/filtersets.py +++ b/netbox_slm/filtersets.py @@ -132,7 +132,7 @@ class SoftwareLicenseFilterSet(NetBoxModelFilterSet): class Meta: model = SoftwareLicense - fields = ("support", "lifetime") + fields = ("support", "lifetime", "expiration_notifications_enabled", "email_notifications") def search(self, queryset, name, value): """Perform the filtered search.""" diff --git a/netbox_slm/forms/software_license.py b/netbox_slm/forms/software_license.py index e129473..b56aaf2 100644 --- a/netbox_slm/forms/software_license.py +++ b/netbox_slm/forms/software_license.py @@ -9,6 +9,7 @@ from netbox_slm.models import ( SoftwareProductInstallation, SoftwareLicense, RenewalIntervalUnits, + NotificationIntervalUnits, spdx_license_names, ) from tenancy.models import Tenant, TenantGroup @@ -51,6 +52,14 @@ class SoftwareLicenseForm(NetBoxModelForm): name=_("License Details"), ), FieldSet("license_file", "license_key", "provider_portal_url", name=_("License Credentials")), + FieldSet( + "expiration_notifications_enabled", + "default_notification_intervals", + "custom_notification_interval", + "custom_notification_interval_unit", + "email_notifications", + name=_("Expiration Notifications"), + ), FieldSet("tags", name=_("Tags")), ) @@ -61,6 +70,7 @@ class SoftwareLicenseForm(NetBoxModelForm): renewal_interval = IntegerField(required=False, min_value=1) license_key = CharField(required=False, widget=Textarea(attrs={"rows": 3})) provider_portal_url = LaxURLField(required=False) + custom_notification_interval = IntegerField(required=False, min_value=1) software_product = DynamicModelChoiceField( queryset=SoftwareProduct.objects.all(), @@ -113,6 +123,11 @@ class SoftwareLicenseForm(NetBoxModelForm): "license_file", "license_key", "provider_portal_url", + "expiration_notifications_enabled", + "default_notification_intervals", + "custom_notification_interval", + "custom_notification_interval_unit", + "email_notifications", "version", "installation", "tenant_group", @@ -136,6 +151,8 @@ class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm): "lifetime", "renewal_interval", "renewal_interval_unit", + "expiration_notifications_enabled", + "email_notifications", "software_product_id", "version_id", "installation_id", @@ -156,6 +173,8 @@ class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm): lifetime = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES) renewal_interval = IntegerField(required=False, min_value=1) renewal_interval_unit = ChoiceField(required=False, choices=RenewalIntervalUnits.choices) + expiration_notifications_enabled = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES) + email_notifications = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES) software_product_id = DynamicModelMultipleChoiceField( queryset=SoftwareProduct.objects.all(), @@ -208,6 +227,11 @@ class SoftwareLicenseBulkImportForm(NetBoxModelImportForm): "license_amount", "license_key", "provider_portal_url", + "expiration_notifications_enabled", + "default_notification_intervals", + "custom_notification_interval", + "custom_notification_interval_unit", + "email_notifications", "version", "installation", "tenant_group", @@ -233,6 +257,11 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm): "license_amount", "license_key", "provider_portal_url", + "expiration_notifications_enabled", + "default_notification_intervals", + "custom_notification_interval", + "custom_notification_interval_unit", + "email_notifications", "software_product", "version", "installation", @@ -253,6 +282,8 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm): "license_amount", "license_key", "provider_portal_url", + "custom_notification_interval", + "custom_notification_interval_unit", "version", "installation", "tenant_group", @@ -275,6 +306,11 @@ class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm): license_amount = IntegerField(required=False, min_value=0) license_key = CharField(required=False, widget=Textarea(attrs={"rows": 3})) provider_portal_url = LaxURLField(required=False) + expiration_notifications_enabled = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES) + default_notification_intervals = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES) + custom_notification_interval = IntegerField(required=False, min_value=1) + custom_notification_interval_unit = ChoiceField(required=False, choices=NotificationIntervalUnits.choices) + email_notifications = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES) software_product = DynamicModelChoiceField( queryset=SoftwareProduct.objects.all(), diff --git a/netbox_slm/jobs.py b/netbox_slm/jobs.py new file mode 100644 index 0000000..d28793c --- /dev/null +++ b/netbox_slm/jobs.py @@ -0,0 +1,90 @@ +from django.conf import settings +from django.contrib.contenttypes.models import ContentType +from django.core.mail import send_mail +from django.utils import timezone +from django.utils.translation import gettext as _ + +from core.choices import JobIntervalChoices +from extras.models import Notification +from netbox.jobs import JobRunner, system_job + +from netbox_slm.events import LICENSE_EXPIRING +from netbox_slm.models import SoftwareLicense, SoftwareLicenseNotificationLog + + +@system_job(interval=JobIntervalChoices.INTERVAL_DAILY) +class LicenseExpirationNotificationJob(JobRunner): + class Meta: + name = "Software License Expiration Notifications" + + def run(self, *args, **kwargs): + today = timezone.localdate() + licenses = SoftwareLicense.objects.filter( + expiration_notifications_enabled=True, + lifetime=False, + expiration_date__gte=today, + ).prefetch_related("subscriptions__user") + + for license in licenses: + due_dates = [date for date in license.get_notification_dates() if date <= today] + if not due_dates: + continue + + for subscription in license.subscriptions.all(): + user = subscription.user + if not user.is_active: + continue + # If notifications were enabled late, send only the most recent reminder instead of one per elapsed + # interval. Normally this still produces one reminder at each configured threshold. + self._notify(license, user, max(due_dates)) + + def _notify(self, license, user, notification_date): + log, _ = SoftwareLicenseNotificationLog.objects.get_or_create( + license=license, + user=user, + expiration_date=license.expiration_date, + notification_date=notification_date, + ) + now = timezone.now() + + if log.netbox_sent_at is None: + object_type = ContentType.objects.get_for_model(license) + Notification.objects.update_or_create( + object_type=object_type, + object_id=license.pk, + user=user, + defaults={ + "object_repr": Notification.get_object_repr(license), + "event_type": LICENSE_EXPIRING, + "read": None, + }, + ) + log.netbox_sent_at = now + log.save(update_fields=("netbox_sent_at",)) + self.logger.info(f"Created NetBox expiration notification for license {license.pk} and user {user}") + + if license.email_notifications and user.email and log.email_sent_at is None: + subject = _("License %(license)s expires on %(date)s") % { + "license": license, + "date": license.expiration_date, + } + body = _( + "The software license '%(license)s' for '%(product)s' expires on %(date)s." + ) % { + "license": license, + "product": license.software_product, + "date": license.expiration_date, + } + if license.tenant: + body += "\n" + _("Tenant: %(tenant)s") % {"tenant": license.tenant} + if license.provider_portal_url: + body += "\n" + _("Provider portal: %(url)s") % {"url": license.provider_portal_url} + + try: + send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [user.email], fail_silently=False) + except Exception: + self.logger.exception(f"Failed to send expiration email for license {license.pk} to user {user}") + else: + log.email_sent_at = now + log.save(update_fields=("email_sent_at",)) + self.logger.info(f"Sent expiration email for license {license.pk} to user {user}") diff --git a/netbox_slm/locale/de/LC_MESSAGES/django.mo b/netbox_slm/locale/de/LC_MESSAGES/django.mo index 6e8442e..ac4b17c 100644 Binary files a/netbox_slm/locale/de/LC_MESSAGES/django.mo and b/netbox_slm/locale/de/LC_MESSAGES/django.mo differ diff --git a/netbox_slm/locale/de/LC_MESSAGES/django.po b/netbox_slm/locale/de/LC_MESSAGES/django.po index 4662037..d331a20 100644 --- a/netbox_slm/locale/de/LC_MESSAGES/django.po +++ b/netbox_slm/locale/de/LC_MESSAGES/django.po @@ -336,3 +336,75 @@ msgstr "Leer lassen, wenn ein anderes Plattformziel ausgewählt wurde." msgid "Select exactly one platform destination: device, virtual machine, or cluster." msgstr "Wähle genau ein Plattformziel aus: Gerät, virtuelle Maschine oder Cluster." + +msgid "Weeks" +msgstr "Wochen" + +msgid "expiration notifications" +msgstr "Ablaufbenachrichtigungen" + +msgid "default notification intervals" +msgstr "Standard-Benachrichtigungsintervalle" + +msgid "custom notification interval" +msgstr "Eigene Vorwarnzeit" + +msgid "custom notification interval unit" +msgstr "Einheit der eigenen Vorwarnzeit" + +msgid "email notifications" +msgstr "E-Mail-Benachrichtigungen" + +msgid "Expiration Notifications" +msgstr "Ablaufbenachrichtigungen" + +msgid "Custom notification interval and unit must be specified together." +msgstr "Eigene Vorwarnzeit und Einheit müssen gemeinsam angegeben werden." + +msgid "License expiring" +msgstr "Lizenz läuft ab" + +msgid "License %(license)s expires on %(date)s" +msgstr "Lizenz %(license)s läuft am %(date)s ab" + +msgid "The software license '%(license)s' for '%(product)s' expires on %(date)s." +msgstr "Die Softwarelizenz ‚%(license)s‘ für ‚%(product)s‘ läuft am %(date)s ab." + +msgid "Tenant: %(tenant)s" +msgstr "Mandant: %(tenant)s" + +msgid "Provider portal: %(url)s" +msgstr "Anbieterportal: %(url)s" + +msgid "Expiration notifications" +msgstr "Ablaufbenachrichtigungen" + +msgid "Default notification intervals" +msgstr "Standard-Benachrichtigungsintervalle" + +msgid "One month and one week before expiration" +msgstr "Einen Monat und eine Woche vor Ablauf" + +msgid "Disabled" +msgstr "Deaktiviert" + +msgid "Enabled" +msgstr "Aktiviert" + +msgid "Custom notification interval" +msgstr "Eigene Vorwarnzeit" + +msgid "Email notifications" +msgstr "E-Mail-Benachrichtigungen" + +msgid "Notification recipients" +msgstr "Benachrichtigungsempfänger" + +msgid "No subscribers. Users can subscribe to this license using the Subscribe action." +msgstr "Keine Abonnenten. Benutzer können diese Lizenz über die Aktion ‚Abonnieren‘ abonnieren." + +msgid "Licences" +msgstr "Lizenzen" + +msgid "Licence Management NetBox Plugin." +msgstr "NetBox-Plugin zur Lizenzverwaltung." diff --git a/netbox_slm/migrations/0013_license_expiration_notifications.py b/netbox_slm/migrations/0013_license_expiration_notifications.py new file mode 100644 index 0000000..bd1d0f2 --- /dev/null +++ b/netbox_slm/migrations/0013_license_expiration_notifications.py @@ -0,0 +1,77 @@ +import django.core.validators +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("netbox_slm", "0012_softwarelicense_renewal_and_credentials"), + ] + + operations = [ + migrations.AddField( + model_name="softwarelicense", + name="expiration_notifications_enabled", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="softwarelicense", + name="default_notification_intervals", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="softwarelicense", + name="custom_notification_interval", + field=models.PositiveIntegerField( + blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)] + ), + ), + migrations.AddField( + model_name="softwarelicense", + name="custom_notification_interval_unit", + field=models.CharField( + blank=True, + choices=[("days", "Days"), ("weeks", "Weeks"), ("months", "Months")], + max_length=8, + ), + ), + migrations.AddField( + model_name="softwarelicense", + name="email_notifications", + field=models.BooleanField(default=False), + ), + migrations.CreateModel( + name="SoftwareLicenseNotificationLog", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("expiration_date", models.DateField()), + ("notification_date", models.DateField()), + ("netbox_sent_at", models.DateTimeField(blank=True, null=True)), + ("email_sent_at", models.DateTimeField(blank=True, null=True)), + ( + "license", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="notification_logs", + to="netbox_slm.softwarelicense", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.AddConstraint( + model_name="softwarelicensenotificationlog", + constraint=models.UniqueConstraint( + fields=("license", "user", "expiration_date", "notification_date"), + name="netbox_slm_license_notification_unique", + ), + ), + ] diff --git a/netbox_slm/models.py b/netbox_slm/models.py index fce3596..5d1b2b0 100644 --- a/netbox_slm/models.py +++ b/netbox_slm/models.py @@ -1,3 +1,7 @@ +import calendar +from datetime import timedelta + +from django.conf import settings from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator from django.db import models @@ -64,6 +68,12 @@ class RenewalIntervalUnits(models.TextChoices): YEARS = "years", _("Years") +class NotificationIntervalUnits(models.TextChoices): + DAYS = "days", _("Days") + WEEKS = "weeks", _("Weeks") + MONTHS = "months", _("Months") + + class SoftwareProductVersion(NetBoxModel): name = models.CharField(_("name"), max_length=64) comments = models.TextField(_("comments"), blank=True) @@ -205,6 +215,18 @@ class SoftwareLicense(NetBoxModel): license_file = models.FileField(_("license file"), upload_to="netbox_slm/licenses/", null=True, blank=True) license_key = models.TextField(_("license key"), blank=True) provider_portal_url = LaxURLField(_("provider portal URL"), max_length=1024, null=True, blank=True) + expiration_notifications_enabled = models.BooleanField(_("expiration notifications"), default=True) + default_notification_intervals = models.BooleanField(_("default notification intervals"), default=True) + custom_notification_interval = models.PositiveIntegerField( + _("custom notification interval"), null=True, blank=True, validators=[MinValueValidator(1)] + ) + custom_notification_interval_unit = models.CharField( + _("custom notification interval unit"), + max_length=8, + choices=NotificationIntervalUnits.choices, + blank=True, + ) + email_notifications = models.BooleanField(_("email notifications"), default=False) software_product = models.ForeignKey( to="netbox_slm.SoftwareProduct", verbose_name=_("software product"), on_delete=models.PROTECT @@ -266,9 +288,65 @@ class SoftwareLicense(NetBoxModel): raise ValidationError( {"lifetime": _("A lifetime license cannot have a renewal interval or expiration date.")} ) + if bool(self.custom_notification_interval) != bool(self.custom_notification_interval_unit): + raise ValidationError( + { + "custom_notification_interval": _( + "Custom notification interval and unit must be specified together." + ) + } + ) + + @staticmethod + def _subtract_months(value, months): + month_index = value.year * 12 + value.month - 1 - months + year, zero_based_month = divmod(month_index, 12) + month = zero_based_month + 1 + day = min(value.day, calendar.monthrange(year, month)[1]) + return value.replace(year=year, month=month, day=day) + + def get_notification_dates(self): + """Return unique reminder dates for the current expiration date.""" + if not self.expiration_notifications_enabled or self.lifetime or not self.expiration_date: + return [] + + dates = set() + if self.default_notification_intervals: + dates.add(self._subtract_months(self.expiration_date, 1)) + dates.add(self.expiration_date - timedelta(weeks=1)) + + if self.custom_notification_interval and self.custom_notification_interval_unit: + amount = self.custom_notification_interval + if self.custom_notification_interval_unit == NotificationIntervalUnits.MONTHS: + dates.add(self._subtract_months(self.expiration_date, amount)) + elif self.custom_notification_interval_unit == NotificationIntervalUnits.WEEKS: + dates.add(self.expiration_date - timedelta(weeks=amount)) + else: + dates.add(self.expiration_date - timedelta(days=amount)) + + return sorted(dates) @property def stored_location_txt(self): if self.stored_location_url and not self.stored_location: return _("Link") return self.stored_location + + +class SoftwareLicenseNotificationLog(models.Model): + license = models.ForeignKey( + to="netbox_slm.SoftwareLicense", on_delete=models.CASCADE, related_name="notification_logs" + ) + user = models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + expiration_date = models.DateField() + notification_date = models.DateField() + netbox_sent_at = models.DateTimeField(null=True, blank=True) + email_sent_at = models.DateTimeField(null=True, blank=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=("license", "user", "expiration_date", "notification_date"), + name="netbox_slm_license_notification_unique", + ) + ] diff --git a/netbox_slm/navigation.py b/netbox_slm/navigation.py index 4e29655..5c137f9 100644 --- a/netbox_slm/navigation.py +++ b/netbox_slm/navigation.py @@ -83,7 +83,7 @@ slm_items = ( if get_plugin_config("netbox_slm", "top_level_menu"): menu = PluginMenu( - label=_("Software Lifecycle"), + label=_("Licences"), groups=((SLMConfig.verbose_name, slm_items),), icon_class="mdi mdi-content-save", ) diff --git a/netbox_slm/tables.py b/netbox_slm/tables.py index 0048946..4675f55 100644 --- a/netbox_slm/tables.py +++ b/netbox_slm/tables.py @@ -174,6 +174,8 @@ class SoftwareLicenseTable(NetBoxTable): "support", "license_amount", "provider_portal_url", + "expiration_notifications_enabled", + "email_notifications", "tags", ) default_columns = ( @@ -187,6 +189,7 @@ class SoftwareLicenseTable(NetBoxTable): "tenant", "expiration_date", "lifetime", + "expiration_notifications_enabled", "tags", ) diff --git a/netbox_slm/templates/netbox_slm/softwarelicense.html b/netbox_slm/templates/netbox_slm/softwarelicense.html index b4340d3..10aeece 100644 --- a/netbox_slm/templates/netbox_slm/softwarelicense.html +++ b/netbox_slm/templates/netbox_slm/softwarelicense.html @@ -111,6 +111,44 @@ {% endif %} +