Skip to content

Commit

Permalink
Create page for each netbox entity so additional content can be added…
Browse files Browse the repository at this point in the history
… in CMS
  • Loading branch information
trickeydan committed Feb 10, 2024
1 parent cb992d9 commit 53de86c
Show file tree
Hide file tree
Showing 18 changed files with 293 additions and 273 deletions.
41 changes: 40 additions & 1 deletion kmicms/integrations/netbox/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,39 @@
name
color
}
description
comments
platform {
name
}
interfaces {
description
name
enabled
ip_addresses {
display
dns_name
}
}
device_type {
model
manufacturer {
name
}
front_image
rear_image
}
tags {
name
color
}
rack {
name
}
location {
name
}
position
status
}
}
Expand Down Expand Up @@ -112,15 +133,33 @@
virtual_machine_list {
id
name
cluster {
name
}
role {
name
color
}
description
comments
platform {
name
}
cluster {
interfaces {
description
name
enabled
ip_addresses {
display
dns_name
}
}
vcpus
memory
disk
tags {
name
color
}
status
}
Expand Down
Empty file.
Empty file.
74 changes: 74 additions & 0 deletions kmicms/pages/infra/management/commands/sync_netbox.py
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 kmicms/pages/infra/migrations/0003_add_netbox_entity_page.py
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",
),
),
]
107 changes: 43 additions & 64 deletions kmicms/pages/infra/models.py
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 kmicms/pages/infra/templates/infra/inc/breadcrumbs_subpage.html

This file was deleted.

6 changes: 6 additions & 0 deletions kmicms/pages/infra/templates/infra/inc/details_device.html
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>
8 changes: 8 additions & 0 deletions kmicms/pages/infra/templates/infra/inc/details_vm.html
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>
Loading

0 comments on commit 53de86c

Please sign in to comment.