-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create page for each netbox entity so additional content can be added…
… in CMS
- Loading branch information
1 parent
cb992d9
commit 53de86c
Showing
18 changed files
with
293 additions
and
273 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
from django.conf import settings | ||
from django.core.management.base import BaseCommand | ||
from integrations.netbox.client import NetboxClient | ||
|
||
from pages.infra.models import NetboxEntityPage, NetboxEntityType, NetboxInfrastructurePage | ||
|
||
|
||
class Command(BaseCommand): | ||
help = "Synchronise pages that are created from Netbox" # noqa: A003 | ||
|
||
def handle( | ||
self, | ||
*, | ||
verbosity: int, | ||
settings: str, | ||
pythonpath: str, | ||
traceback: bool, | ||
no_color: bool, | ||
force_color: bool, | ||
skip_checks: bool, | ||
) -> None: | ||
# We only expect there to be one index, but to be sure... | ||
assert NetboxInfrastructurePage.objects.count() == 1 | ||
index_page = NetboxInfrastructurePage.objects.first() | ||
client = self._get_client() | ||
self._sync_pages(index_page, client.list_devices(), NetboxEntityType.DEVICE) | ||
self._sync_pages(index_page, client.list_vms(), NetboxEntityType.VM) | ||
self.stdout.write("Synchronised all netbox pages") | ||
|
||
def _sync_pages(self, parent: NetboxInfrastructurePage, entities: list, entity_type: NetboxEntityType) -> None: | ||
synced_entity_page_ids: list[int] = [] | ||
|
||
entity_pages_qs = NetboxEntityPage.objects.child_of(parent).filter( | ||
netbox_entity_type=entity_type, | ||
) | ||
|
||
for entity in entities: | ||
try: | ||
entity_page = entity_pages_qs.get( | ||
netbox_id=entity["id"], | ||
) | ||
if entity_page.netbox_name != entity["name"]: | ||
entity_page.netbox_name = entity["name"] | ||
|
||
if entity_page.netbox_data != entity: | ||
entity_page.netbox_data = entity | ||
|
||
# TODO: Only save if needed, check update | ||
entity_page.save() | ||
except NetboxEntityPage.DoesNotExist: | ||
entity_page = NetboxEntityPage( | ||
title=entity["name"], | ||
netbox_id=entity["id"], | ||
netbox_name=entity["name"], | ||
netbox_entity_type=entity_type, | ||
netbox_data=entity, | ||
) | ||
parent.add_child(instance=entity_page) | ||
|
||
synced_entity_page_ids.append(entity_page.id) | ||
|
||
# Unpublish any entities that no longer exist in Netbox | ||
entities_to_unpublish = entity_pages_qs.exclude(id__in=synced_entity_page_ids).live() | ||
for entity_page in entities_to_unpublish: | ||
self.stdout.write(f"Unpublishing {entity_page} as it was deleted in Netbox") | ||
entity_page.unpublish() | ||
|
||
def _get_client(self) -> NetboxClient: | ||
return NetboxClient( | ||
graphql_endpoint=settings.NETBOX_GRAPHQL_ENDPOINT, | ||
api_token=settings.NETBOX_API_TOKEN, | ||
cache_ttl_seconds=settings.NETBOX_CACHE_TTL, | ||
request_timeout=settings.NETBOX_REQUEST_TIMEOUT, | ||
) |
63 changes: 63 additions & 0 deletions
63
kmicms/pages/infra/migrations/0003_add_netbox_entity_page.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
# Generated by Django 4.2.10 on 2024-02-10 13:37 | ||
|
||
import django.db.models.deletion | ||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("wagtailcore", "0091_remove_revision_submitted_for_moderation"), | ||
("infra", "0002_use_streamfield_on_infra_index"), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name="NetboxEntityPage", | ||
fields=[ | ||
( | ||
"page_ptr", | ||
models.OneToOneField( | ||
auto_created=True, | ||
on_delete=django.db.models.deletion.CASCADE, | ||
parent_link=True, | ||
primary_key=True, | ||
serialize=False, | ||
to="wagtailcore.page", | ||
), | ||
), | ||
("netbox_name", models.CharField(max_length=255)), | ||
("netbox_id", models.IntegerField(verbose_name="Netbox ID")), | ||
( | ||
"netbox_entity_type", | ||
models.CharField( | ||
choices=[("DEV", "Physical Device"), ("VM", "Virtual Machine")], | ||
max_length=3, | ||
), | ||
), | ||
("netbox_data", models.JSONField()), | ||
], | ||
options={ | ||
"verbose_name": "Netbox Entity Page", | ||
}, | ||
bases=("wagtailcore.page",), | ||
), | ||
migrations.AlterModelOptions( | ||
name="netboxinfrastructurepage", | ||
options={"verbose_name": "Netbox Index Page"}, | ||
), | ||
migrations.RemoveField( | ||
model_name="netboxinfrastructurepage", | ||
name="device_description", | ||
), | ||
migrations.RemoveField( | ||
model_name="netboxinfrastructurepage", | ||
name="vm_description", | ||
), | ||
migrations.AddConstraint( | ||
model_name="netboxentitypage", | ||
constraint=models.UniqueConstraint( | ||
fields=("netbox_id", "netbox_entity_type"), | ||
name="unique_id_for_netbox_entity", | ||
), | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,87 +1,66 @@ | ||
from typing import Any | ||
|
||
from core.blocks import StoryBlock | ||
from django.conf import settings | ||
from django.http import Http404, HttpRequest, HttpResponse | ||
from integrations.netbox import NetboxClient, NetboxRequestError | ||
from wagtail.admin.panels import FieldPanel, TitleFieldPanel | ||
from wagtail.contrib.routable_page.models import RoutablePageMixin, path | ||
from wagtail.fields import RichTextField, StreamField | ||
from django.db import models | ||
from django.db.models.functions import Lower | ||
from django.http import HttpRequest | ||
from wagtail.admin.panels import FieldPanel, MultiFieldPanel, TitleFieldPanel | ||
from wagtail.fields import StreamField | ||
from wagtail.models import Page | ||
|
||
|
||
class NetboxInfrastructurePage(RoutablePageMixin, Page): | ||
class NetboxInfrastructurePage(Page): | ||
max_count = 1 | ||
# Intentionally empty to prevent manual creation of device and VM pages | ||
subpage_types = [] | ||
|
||
content = StreamField(StoryBlock()) | ||
device_description = RichTextField() | ||
vm_description = RichTextField() | ||
|
||
content_panels = [ | ||
TitleFieldPanel("title"), | ||
FieldPanel("content"), | ||
FieldPanel("device_description"), | ||
FieldPanel("vm_description"), | ||
] | ||
|
||
def _get_client(self) -> NetboxClient: | ||
return NetboxClient( | ||
graphql_endpoint=settings.NETBOX_GRAPHQL_ENDPOINT, | ||
api_token=settings.NETBOX_API_TOKEN, | ||
cache_ttl_seconds=settings.NETBOX_CACHE_TTL, | ||
request_timeout=settings.NETBOX_REQUEST_TIMEOUT, | ||
) | ||
|
||
def _handle_error(self, request: HttpRequest) -> HttpResponse: | ||
return self.render( | ||
request, | ||
template="infra/netbox_error.html", | ||
) | ||
class Meta: | ||
verbose_name = "Netbox Index Page" | ||
|
||
@path("devices/", name="device_index") | ||
def device_index(self, request: HttpRequest) -> HttpResponse: | ||
try: | ||
client = self._get_client() | ||
devices = client.list_devices() | ||
except NetboxRequestError: | ||
return self._handle_error(request) | ||
def get_entity_pages(self, request: HttpRequest) -> models.QuerySet["NetboxEntityPage"]: | ||
return NetboxEntityPage.objects.child_of(self).live().order_by(Lower("title")) | ||
|
||
return self.render( | ||
request, | ||
context_overrides={"devices": devices}, | ||
template="infra/netbox_device_index.html", | ||
) | ||
def get_context(self, request: HttpRequest, *args: Any, **kwargs: Any) -> dict[Any]: | ||
ctx = super().get_context(request, *args, **kwargs) | ||
ctx["entity_results"] = self.get_entity_pages(request) | ||
return ctx | ||
|
||
@path("devices/<int:device_id>/", name="device_view") | ||
def device_info(self, request: HttpRequest, *, device_id: int) -> HttpResponse: | ||
try: | ||
client = self._get_client() | ||
device = client.get_device(device_id) | ||
except NetboxRequestError: | ||
return self._handle_error(request) | ||
|
||
if device is None: | ||
raise Http404() | ||
class NetboxEntityType(models.TextChoices): | ||
DEVICE = "DEV", "Physical Device" | ||
VM = "VM", "Virtual Machine" | ||
|
||
return self.render(request, context_overrides={"device": device}, template="infra/netbox_device_view.html") | ||
|
||
@path("vm/", name="vm_index") | ||
def vm_index(self, request: HttpRequest) -> HttpResponse: | ||
try: | ||
client = self._get_client() | ||
vms = client.list_vms() | ||
except NetboxRequestError: | ||
return self._handle_error(request) | ||
return self.render(request, context_overrides={"vms": vms}, template="infra/netbox_vm_index.html") | ||
class NetboxEntityPage(Page): | ||
parent_page_types = ["infra.NetboxInfrastructurePage"] | ||
subpage_types = [] | ||
|
||
@path("vm/<int:vm_id>/", name="vm_view") | ||
def vm_info(self, request: HttpRequest, *, vm_id: int) -> HttpResponse: | ||
try: | ||
client = self._get_client() | ||
vm = client.get_vm(vm_id) | ||
except NetboxRequestError: | ||
return self._handle_error(request) | ||
netbox_name = models.CharField(max_length=255) | ||
netbox_id = models.IntegerField(verbose_name="Netbox ID") | ||
netbox_entity_type = models.CharField(max_length=3, choices=NetboxEntityType.choices) | ||
netbox_data = models.JSONField() | ||
|
||
if vm is None: | ||
raise Http404() | ||
content_panels = [ | ||
TitleFieldPanel("title"), | ||
MultiFieldPanel( | ||
[ | ||
FieldPanel("netbox_name", read_only=True), | ||
FieldPanel("netbox_entity_type", read_only=True), | ||
FieldPanel("netbox_id", read_only=True), | ||
], | ||
heading="Netbox Info", | ||
), | ||
] | ||
|
||
return self.render(request, context_overrides={"vm": vm}, template="infra/netbox_vm_view.html") | ||
class Meta: | ||
verbose_name = "Netbox Entity Page" | ||
constraints = [ | ||
models.UniqueConstraint(fields=["netbox_id", "netbox_entity_type"], name="unique_id_for_netbox_entity") | ||
] |
28 changes: 0 additions & 28 deletions
28
kmicms/pages/infra/templates/infra/inc/breadcrumbs_subpage.html
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
<dt class="col-sm-3">Rack</dt> | ||
<dd class="col-sm-9">{{ device.rack.name }}</dd> | ||
<dt class="col-sm-3">Location</dt> | ||
<dd class="col-sm-9">{{ device.location.name }}</dd> | ||
<dt class="col-sm-3">Type</dt> | ||
<dd class="col-sm-9">{{ device.device_type.manufacturer.name }} {{ device.device_type.model }}</dd> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<dt class="col-sm-3">vCPUs</dt> | ||
<dd class="col-sm-9">{{ vm.vcpus }}</dd> | ||
<dt class="col-sm-3">Memory</dt> | ||
<dd class="col-sm-9">{{ vm.memory }}MB</dd> | ||
<dt class="col-sm-3">Storage</dt> | ||
<dd class="col-sm-9">{{ vm.disk }}GB</dd> | ||
<dt class="col-sm-3">Cluster / Host</dt> | ||
<dd class="col-sm-9">{{ vm.cluster.name }}</dd> |
Oops, something went wrong.