Skip to content

Commit

Permalink
Updated template extensions to align with standard Nautobot UI views. (
Browse files Browse the repository at this point in the history
  • Loading branch information
gsnider2195 authored Sep 16, 2024
1 parent eb4fe07 commit 4c9e46a
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 144 deletions.
1 change: 1 addition & 0 deletions changes/261.changed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Updated template extensions to align with standard Nautobot UI views.
1 change: 1 addition & 0 deletions changes/261.housekeeping
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added management command `generate_app_test_data` to generate sample data for development environments.
15 changes: 15 additions & 0 deletions docs/dev/dev_environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -482,3 +482,18 @@ invoke generate-app-config-schema
```

This command can only guess the schema, so it's up to the developer to manually update the schema as needed.

### Test Data Generation

To quickly generate test data for developing against this app, you can use the following commands:

```bash
nautobot-server generate_test_data --flush
nautobot-server generate_dlm_test_data
nautobot-server createsuperuser
```

!!! danger
The `--flush` flag will completely empty your database and replace it with test data. This command should never be run in a production environment.

This uses the [`generate_test_data`](https://docs.nautobot.com/projects/core/en/stable/user-guide/administration/tools/nautobot-server/#generate_test_data) management command from Nautobot core to generate the Statuses, Platforms, Device Types, Devices, etc. Nautobot version 2.2.0 is the minimum version required for devices to be generated. If using an older version of Nautobot, you'll need to create devices manually after running `nautobot-server generate_test_data`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""Generate test data for the Device Lifecycle Management app."""

import random

from django.core.management import call_command
from django.core.management.base import BaseCommand
from django.db import DEFAULT_DB_ALIAS
from nautobot.core.factory import get_random_instances
from nautobot.dcim.models import Device, DeviceType, InventoryItem, Platform

from nautobot_device_lifecycle_mgmt.choices import CVESeverityChoices
from nautobot_device_lifecycle_mgmt.models import (
CVELCM,
ContactLCM,
ContractLCM,
DeviceSoftwareValidationResult,
HardwareLCM,
InventoryItemSoftwareValidationResult,
ProviderLCM,
SoftwareImageLCM,
SoftwareLCM,
ValidatedSoftwareLCM,
VulnerabilityLCM,
)


class Command(BaseCommand):
"""Populate the database with various data as a baseline for testing (automated or manual)."""

help = __doc__

def add_arguments(self, parser): # noqa: D102
parser.add_argument(
"--database",
default=DEFAULT_DB_ALIAS,
help='The database to generate the test data in. Defaults to the "default" database.',
)

def _generate_static_data(self, db):
devices = get_random_instances(Device.objects.using(db), minimum=2, maximum=4)
device_types = get_random_instances(DeviceType.objects.using(db), minimum=2, maximum=4)
inventory_items = get_random_instances(InventoryItem.objects.using(db), minimum=2, maximum=4)
platforms = get_random_instances(Platform.objects.using(db), minimum=2, maximum=4)

# create HardwareLCM
for device_type in device_types:
HardwareLCM.objects.using(db).create(device_type=device_type, end_of_sale="2022-03-14")
for inventory_item in inventory_items:
HardwareLCM.objects.using(db).create(inventory_item=inventory_item, end_of_support="2020-05-04")

# create SoftwareLCM
for platform in platforms:
SoftwareLCM.objects.using(db).create(
device_platform=platform,
version=f"Test SoftwareLCM for {platform.name}",
)

# create SoftwareImageLCM
for software in SoftwareLCM.objects.using(db).all():
SoftwareImageLCM.objects.using(db).create(
software=software,
image_file_name=f"{software.device_platform.name}_vxyz.bin",
)

# create ValidatedSoftwareLCM
for software in SoftwareLCM.objects.using(db).all():
ValidatedSoftwareLCM.objects.using(db).create(
software=software,
start="2020-01-01",
end="2020-12-31",
)

# create DeviceSoftwareValidationResult
software_choices = list(SoftwareLCM.objects.using(db).all())
for device in devices:
DeviceSoftwareValidationResult.objects.using(db).create(
device=device,
software=random.choice(software_choices), # noqa: S311
)

# create InventoryItemSoftwareValidationResult
for inventory_item in inventory_items:
InventoryItemSoftwareValidationResult.objects.using(db).create(
inventory_item=inventory_item,
software=random.choice(software_choices), # noqa: S311
)

# create ProviderLCM
for i in range(1, 9):
ProviderLCM.objects.using(db).create(
name=f"Test Provider {i}",
description=f"Description for Provider {i}",
)

# create ContractLCM
for provider in ProviderLCM.objects.using(db).all():
ContractLCM.objects.using(db).create(
provider=provider,
name=f"Test Contract for {provider.name}",
)

# create ContactLCM
for contract in ContractLCM.objects.using(db).all():
ContactLCM.objects.using(db).create(
contract=contract,
name=f"Test Contact for {contract.name}",
)

# create CVELCM
for i in range(1, 5):
cve = CVELCM.objects.using(db).create(
name=f"Test CVELCM {i}",
published_date="2020-01-01",
link="http://cve.example.org/{i}/details/",
severity=random.choice(CVESeverityChoices.values()), # noqa: S311
)
cve.affected_softwares.set(get_random_instances(SoftwareLCM, minimum=1))

# create VulnerabilityLCM
VulnerabilityLCM.objects.using(db).create(cve=CVELCM.objects.using(db).get(name="Test CVELCM 1"))
VulnerabilityLCM.objects.using(db).create(software=SoftwareLCM.objects.using(db).first())
VulnerabilityLCM.objects.using(db).create(device=Device.objects.using(db).first())
VulnerabilityLCM.objects.using(db).create(inventory_item=InventoryItem.objects.using(db).first())

def handle(self, *args, **options):
"""Entry point to the management command."""
self._generate_static_data(db=options["database"])

self.stdout.write(self.style.SUCCESS(f"Database {options['database']} populated with app data successfully!"))
4 changes: 2 additions & 2 deletions nautobot_device_lifecycle_mgmt/software.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def get_validated_software_table(self):

def validate_software(self, preferred_only=False):
"""Validate software against the validated software objects."""
if not (self.software and self.validated_software_qs.count()):
if not (self.software and self.validated_software_qs.exists()):
return False

validated_software_versions = ValidatedSoftwareLCMFilterSet(
Expand All @@ -65,7 +65,7 @@ def validate_software(self, preferred_only=False):
if preferred_only:
validated_software_versions = validated_software_versions.filter(preferred_only=True)

return validated_software_versions.count() > 0
return validated_software_versions.exists()


class DeviceSoftware(ItemSoftware):
Expand Down
21 changes: 12 additions & 9 deletions nautobot_device_lifecycle_mgmt/template_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

from abc import ABCMeta

from django.conf import settings
from django.db.models import Q
from nautobot.dcim.models import InventoryItem
from nautobot.extras.plugins import PluginTemplateExtension

from nautobot_device_lifecycle_mgmt.models import HardwareLCM, ValidatedSoftwareLCM
Expand Down Expand Up @@ -67,18 +67,21 @@ class DeviceHWLCM(PluginTemplateExtension, metaclass=ABCMeta):
def right_page(self):
"""Display table on right side of page."""
dev_obj = self.context["object"]
part_ids = dev_obj.inventory_items.exclude(part_id=None).values_list("part_id", flat=True)
# order HardwareLCM queryset by field configured in expired_field setting first
order_fields = ["end_of_security_patches", "end_of_sw_releases", "end_of_support", "end_of_sale"]
expired_field = settings.PLUGINS_CONFIG["nautobot_device_lifecycle_mgmt"].get("expired_field", "end_of_support")
order_fields.remove(expired_field)
order_fields.insert(0, expired_field)

hw_notices = HardwareLCM.objects.filter(
Q(device_type=dev_obj.device_type) | Q(inventory_item__in=part_ids)
).order_by("device_type", *order_fields)

return self.render(
"nautobot_device_lifecycle_mgmt/inc/device_notice.html",
extra_context={
"hw_notices": HardwareLCM.objects.filter(
Q(device_type=dev_obj.device_type)
| Q(
inventory_item__in=[
i.part_id for i in InventoryItem.objects.filter(device__pk=dev_obj.pk) if i.part_id
]
)
)
"hw_notices": hw_notices,
},
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,108 +1,72 @@
{% load helpers %}
{% if perms.nautobot_device_lifecycle_mgmt.view_hardwarelcm and hw_notices %}
<style type="text/css">
.panel-heading .accordion-toggle:after {
/* symbol for "opening" panels */
font-family: 'Glyphicons Halflings'; /* essential for enabling glyphicon */
content: "\e114"; /* adjust as needed, taken from bootstrap.css */
float: right; /* adjust as needed */
color: grey; /* adjust as needed */
}
.panel-heading .accordion-toggle.collapsed:after {
/* symbol for "collapsed" panels */
content: "\e080"; /* adjust as needed, taken from bootstrap.css */
}
</style>

<div class="row">
<div class="col-md-12">
<!-- Nav tabs to support both hardware and software notices. -->
<ul class="nav nav-tabs" role="tablist">
{% if hw_notices %}
<li role="presentation" class="active">
<a href="#hardware" aria-controls="hardware" role="tab" data-toggle="tab">
Hardware Lifecycle Notices <span class="badge badge-pill badge-primary">{{ hw_notices.count }}</span>
</a>

</li>
{% endif %}
{% if sw_notices %}
<li role="presentation"><a href="#software" aria-controls="software" role="tab" data-toggle="tab">Software Lifecycle Notices</a></li>
{% endif %}
</ul>

<div class="tab-content">
<style>
.accordion-toggle {
font-size: 14px;
font-weight: 700;
}
</style>

<!-- Defines all hardware notice logic within this tab-panel. -->
<div role="tabpanel" class="tab-pane active" id="hardware">
<div class="panel-group" id="hwAccordion" role="tablist" aria-multiselectable="true">
{% for notice in hw_notices %}
{% if forloop.counter < 6 %}
<div class="panel panel-{% if notice.expired %}danger{% else %}warning{% endif %}">
<div class="panel-heading" role="tab" id="heading{{ forloop.counter }}">
<span role="button"
class="accordion-toggle collapsed"
data-toggle="collapse"
data-parent="#hwAccordion"
href="#collapse{{ forloop.counter }}"
aria-expanded="false"
aria-controls="collapse{{ forloop.counter }}">
{{ notice }}
</span>
</div>
<div id="collapse{{ forloop.counter }}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading{{ forloop.counter }}">
<div class="list-group">
<table class="table">
<tr>
<td>End of Sale</td>
<td>{{ notice.end_of_sale }}</td>
</tr>
<tr>
<td>End of Support</td>
<td>{{ notice.end_of_support }}</td>
</tr>
<tr>
<td>End of Software Releases</td>
<td>{{ notice.end_of_sw_releases }}</td>
</tr>
<tr>
<td>End of Security Patches</td>
<td>{{ notice.end_of_security_patches }}</td>
</tr>
<tr>
<td>Documentation URL</td>
<td>
{% if notice.documentation_url %}<a href="{{ notice.documentation_url }}">{{ notice.documentation_url }}</a>{% else %}&mdash;{% endif %}
</td>
</tr>
<tr>
<td>Comments</td>
<td>
{% if notice.comments %}<pre>{{ notice.comments }}</pre>{% else %}&mdash;{% endif %}
</td>
</tr>
</table>
</div>
</div>
</div>
{% elif forloop.counter == 6 %}
<div class="panel-footer text-right noprint">
<a href="{% url 'plugins:nautobot_device_lifecycle_mgmt:hardwarelcm_list' %}?q={% for inv in object.inventoryitems.all %}&inventory_item={{ inv.part_id }}{% endfor %}" target="_blank" class="btn btn-primary btn-xs">
<span class="mdi mdi-open-in-new" aria-hidden="true"></span> See More Notices
</a>
</div>
{% endif %}
{% endfor %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Hardware Lifecycle Notices {% badge hw_notices.count show_empty=True %}</strong>
</div>
</div>

<!-- Defines all software notice logic within this tab-panel. -->
<div role="tabpanel" class="tab-pane" id="software">
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">

<table id="accordion" class="table table-hover panel-body attr-table">
{% for notice in hw_notices|slice:":5" %}
<tbody>
<tr>
<th colspan="2">
<button type="button" class="btn-link accordion-toggle mdi mdi-chevron-right"
name="hardware-notices-accordion-{{ forloop.counter }}" data-toggle="collapse"
data-target=".collapseme-hardware-notices-accordion-{{ forloop.counter }}">
{{ notice }}
</button>
{% if notice.expired %}
<span class="label label-danger pull-right">Expired</span>
{% endif %}
</th>
</tr>
</tbody>
<tbody class="collapse collapseme-hardware-notices-accordion-{{ forloop.counter }}">
<tr>
<td>End of Sale</td>
<td>{{ notice.end_of_sale|placeholder }}</td>
</tr>
<tr>
<td>End of Support</td>
<td>{{ notice.end_of_support|placeholder }}</td>
</tr>
<tr>
<td>End of Software Releases</td>
<td>{{ notice.end_of_sw_releases|placeholder }}</td>
</tr>
<tr>
<td>End of Security Patches</td>
<td>{{ notice.end_of_security_patches|placeholder }}</td>
</tr>
<tr>
<td>Documentation URL</td>
<td>
{% if notice.documentation_url %}<a href="{{ notice.documentation_url }}">{{ notice.documentation_url }}</a>{% else %}{{ None|placeholder }}{% endif %}
</td>
</tr>
<tr>
<td>Comments</td>
<td>
{% if notice.comments %}<pre>{{ notice.comments }}</pre>{% else %}{{ None|placeholder }}{% endif %}
</td>
</tr>
</tbody>
{% endfor %}
</table>
{% if hw_notices|length > 5 %}
<div class="panel-footer text-right noprint">
<a href="{% url 'plugins:nautobot_device_lifecycle_mgmt:hardwarelcm_list' %}?q={% for inv in object.inventory_items.all %}{% if inv.part_id %}&inventory_item={{ inv.part_id|urlencode }}{% endif %}{% endfor %}" target="_blank" class="btn btn-primary btn-xs">
<span class="mdi mdi-open-in-new" aria-hidden="true"></span>
See More Notices
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
Loading

0 comments on commit 4c9e46a

Please sign in to comment.