From 53de86cf0a84d3909ba2d6f983e9239fdc56cde7 Mon Sep 17 00:00:00 2001 From: Dan Trickey Date: Sat, 10 Feb 2024 13:38:58 +0000 Subject: [PATCH] Create page for each netbox entity so additional content can be added in CMS --- kmicms/integrations/netbox/queries.py | 41 ++++++- kmicms/pages/infra/management/__init__.py | 0 .../infra/management/commands/__init__.py | 0 .../infra/management/commands/sync_netbox.py | 74 ++++++++++++ .../migrations/0003_add_netbox_entity_page.py | 63 +++++++++++ kmicms/pages/infra/models.py | 107 +++++++----------- .../infra/inc/breadcrumbs_subpage.html | 28 ----- .../templates/infra/inc/details_device.html | 6 + .../infra/templates/infra/inc/details_vm.html | 8 ++ .../templates/infra/inc/entity_table.html | 25 ++++ .../infra/templates/infra/inc/server_dl.html | 2 +- .../templates/infra/netbox_device_index.html | 38 ------- .../templates/infra/netbox_device_view.html | 32 ------ .../templates/infra/netbox_entity_page.html | 27 +++++ .../infra/templates/infra/netbox_error.html | 14 --- .../infra/netbox_infrastructure_page.html | 29 +---- .../templates/infra/netbox_vm_index.html | 38 ------- .../infra/templates/infra/netbox_vm_view.html | 34 ------ 18 files changed, 293 insertions(+), 273 deletions(-) create mode 100644 kmicms/pages/infra/management/__init__.py create mode 100644 kmicms/pages/infra/management/commands/__init__.py create mode 100644 kmicms/pages/infra/management/commands/sync_netbox.py create mode 100644 kmicms/pages/infra/migrations/0003_add_netbox_entity_page.py delete mode 100644 kmicms/pages/infra/templates/infra/inc/breadcrumbs_subpage.html create mode 100644 kmicms/pages/infra/templates/infra/inc/details_device.html create mode 100644 kmicms/pages/infra/templates/infra/inc/details_vm.html create mode 100644 kmicms/pages/infra/templates/infra/inc/entity_table.html delete mode 100644 kmicms/pages/infra/templates/infra/netbox_device_index.html delete mode 100644 kmicms/pages/infra/templates/infra/netbox_device_view.html create mode 100644 kmicms/pages/infra/templates/infra/netbox_entity_page.html delete mode 100644 kmicms/pages/infra/templates/infra/netbox_error.html delete mode 100644 kmicms/pages/infra/templates/infra/netbox_vm_index.html delete mode 100644 kmicms/pages/infra/templates/infra/netbox_vm_view.html diff --git a/kmicms/integrations/netbox/queries.py b/kmicms/integrations/netbox/queries.py index 16de02c..acd1173 100644 --- a/kmicms/integrations/netbox/queries.py +++ b/kmicms/integrations/netbox/queries.py @@ -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 } } @@ -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 } diff --git a/kmicms/pages/infra/management/__init__.py b/kmicms/pages/infra/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kmicms/pages/infra/management/commands/__init__.py b/kmicms/pages/infra/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kmicms/pages/infra/management/commands/sync_netbox.py b/kmicms/pages/infra/management/commands/sync_netbox.py new file mode 100644 index 0000000..2008e0f --- /dev/null +++ b/kmicms/pages/infra/management/commands/sync_netbox.py @@ -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, + ) diff --git a/kmicms/pages/infra/migrations/0003_add_netbox_entity_page.py b/kmicms/pages/infra/migrations/0003_add_netbox_entity_page.py new file mode 100644 index 0000000..44c7fa7 --- /dev/null +++ b/kmicms/pages/infra/migrations/0003_add_netbox_entity_page.py @@ -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", + ), + ), + ] diff --git a/kmicms/pages/infra/models.py b/kmicms/pages/infra/models.py index ee4b9e8..6db498e 100644 --- a/kmicms/pages/infra/models.py +++ b/kmicms/pages/infra/models.py @@ -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//", 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//", 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") + ] diff --git a/kmicms/pages/infra/templates/infra/inc/breadcrumbs_subpage.html b/kmicms/pages/infra/templates/infra/inc/breadcrumbs_subpage.html deleted file mode 100644 index d95fd66..0000000 --- a/kmicms/pages/infra/templates/infra/inc/breadcrumbs_subpage.html +++ /dev/null @@ -1,28 +0,0 @@ -{% load wagtailcore_tags wagtailroutablepage_tags %} - -{% block breadcrumbs %} - {% if page.get_ancestors|length > 1 %} - - {% endif %} -{% endblock breadcrumbs %} \ No newline at end of file diff --git a/kmicms/pages/infra/templates/infra/inc/details_device.html b/kmicms/pages/infra/templates/infra/inc/details_device.html new file mode 100644 index 0000000..bb01d03 --- /dev/null +++ b/kmicms/pages/infra/templates/infra/inc/details_device.html @@ -0,0 +1,6 @@ +
Rack
+
{{ device.rack.name }}
+
Location
+
{{ device.location.name }}
+
Type
+
{{ device.device_type.manufacturer.name }} {{ device.device_type.model }}
\ No newline at end of file diff --git a/kmicms/pages/infra/templates/infra/inc/details_vm.html b/kmicms/pages/infra/templates/infra/inc/details_vm.html new file mode 100644 index 0000000..f07ba9c --- /dev/null +++ b/kmicms/pages/infra/templates/infra/inc/details_vm.html @@ -0,0 +1,8 @@ +
vCPUs
+
{{ vm.vcpus }}
+
Memory
+
{{ vm.memory }}MB
+
Storage
+
{{ vm.disk }}GB
+
Cluster / Host
+
{{ vm.cluster.name }}
\ No newline at end of file diff --git a/kmicms/pages/infra/templates/infra/inc/entity_table.html b/kmicms/pages/infra/templates/infra/inc/entity_table.html new file mode 100644 index 0000000..7949929 --- /dev/null +++ b/kmicms/pages/infra/templates/infra/inc/entity_table.html @@ -0,0 +1,25 @@ +{% load wagtailcore_tags %} + + + + + + + + + + + + + {% for subpage in entities %} + + + + + + + + + {% endfor %} + +
HostnameStatusRolePlatformActions
{{ subpage.title }}{{ subpage.netbox_data.status }}{% include 'infra/inc/colored_badge.html' with info=subpage.netbox_data.role %}{% if subpage.netbox_data.platform %}{{ subpage.netbox_data.platform.name }}{% else %}-{% endif %}View
\ No newline at end of file diff --git a/kmicms/pages/infra/templates/infra/inc/server_dl.html b/kmicms/pages/infra/templates/infra/inc/server_dl.html index db9b575..e8f1a17 100644 --- a/kmicms/pages/infra/templates/infra/inc/server_dl.html +++ b/kmicms/pages/infra/templates/infra/inc/server_dl.html @@ -6,7 +6,7 @@
{% include 'infra/inc/colored_badge.html' with info=server.role %}
Platform
-
{% if server.platform.name%}{{ server.platform.name }}{% else %}-{% endif %}
+
{% if server.platform.name %}{{ server.platform.name }}{% else %}-{% endif %}
{% if server.tags %}
Tags
diff --git a/kmicms/pages/infra/templates/infra/netbox_device_index.html b/kmicms/pages/infra/templates/infra/netbox_device_index.html deleted file mode 100644 index 4048a78..0000000 --- a/kmicms/pages/infra/templates/infra/netbox_device_index.html +++ /dev/null @@ -1,38 +0,0 @@ -{% extends "layouts/page.html" %} -{% load wagtailcore_tags wagtailroutablepage_tags %} - -{% block title %}Devices{% endblock %} - -{% block content %} -
-

Devices

- {% include "infra/inc/breadcrumbs_subpage.html" with title="Devices" %} - -

{{ page.device_description|richtext}}

- - - - - - - - - - - - - - {% for device in devices %} - - - - - - - - - {% endfor %} - -
HostnameStatusRolePlatformLocationActions
{{ device.name }}{{ device.status }}{% include 'infra/inc/colored_badge.html' with info=device.role %}{% if device.platform %}{{ device.platform.name }}{% else %}-{% endif %}{{ device.rack.name }}View
-
-{% endblock %} \ No newline at end of file diff --git a/kmicms/pages/infra/templates/infra/netbox_device_view.html b/kmicms/pages/infra/templates/infra/netbox_device_view.html deleted file mode 100644 index 0356126..0000000 --- a/kmicms/pages/infra/templates/infra/netbox_device_view.html +++ /dev/null @@ -1,32 +0,0 @@ -{% extends "layouts/page.html" %} -{% load wagtailroutablepage_tags %} - -{% block title %}Device: {{ device.name }}{% endblock %} - -{% block content %} -
-

- Device: {{ device.name }} -

- {% routablepageurl page 'device_index' as device_index_url %} - {% include "infra/inc/breadcrumbs_subpage.html" with title=device.name parent_title="Devices" parent_link=device_index_url %} - -
-
- {% include "infra/inc/server_dl.html" with server=device %} -
-
-
-
Rack
-
{{ device.rack.name }}
-
Location
-
{{ device.location.name }}
-
Type
-
{{ device.device_type.manufacturer.name }} {{ device.device_type.model }}
-
-
-
- - {% include "infra/inc/interface_table.html" with interfaces=device.interfaces %} -
-{% endblock %} \ No newline at end of file diff --git a/kmicms/pages/infra/templates/infra/netbox_entity_page.html b/kmicms/pages/infra/templates/infra/netbox_entity_page.html new file mode 100644 index 0000000..c436f30 --- /dev/null +++ b/kmicms/pages/infra/templates/infra/netbox_entity_page.html @@ -0,0 +1,27 @@ +{% extends "layouts/page.html" %} + +{% block content %} +
+

+ {{ page.title }} +

+ {% include "components/breadcrumbs.html" %} + +
+
+ {% include "infra/inc/server_dl.html" with server=page.netbox_data %} +
+
+
+ {% if page.netbox_entity_type == 'DEV' %} + {% include "infra/inc/details_device.html" with device=page.netbox_data %} + {% elif page.netbox_entity_type == 'VM' %} + {% include "infra/inc/details_vm.html" with vm=page.netbox_data %} + {% endif %} +
+
+
+ + {% include "infra/inc/interface_table.html" with interfaces=page.netbox_data.interfaces %} +
+{% endblock %} \ No newline at end of file diff --git a/kmicms/pages/infra/templates/infra/netbox_error.html b/kmicms/pages/infra/templates/infra/netbox_error.html deleted file mode 100644 index 57bafc8..0000000 --- a/kmicms/pages/infra/templates/infra/netbox_error.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "layouts/page.html" %} -{% load wagtailroutablepage_tags %} - -{% block title %}{{ page.title}}: Unable to load{% endblock %} - -{% block content %} -
-

{{ page.title}}: Unable to load

- {% include "components/breadcrumbs.html" %} - -

The page is unable to load as our IPAM database is currently not responding.

-

Please try reloading the page or try again later.

-
-{% endblock %} \ No newline at end of file diff --git a/kmicms/pages/infra/templates/infra/netbox_infrastructure_page.html b/kmicms/pages/infra/templates/infra/netbox_infrastructure_page.html index f2fba2b..a32d788 100644 --- a/kmicms/pages/infra/templates/infra/netbox_infrastructure_page.html +++ b/kmicms/pages/infra/templates/infra/netbox_infrastructure_page.html @@ -1,38 +1,21 @@ {% extends "layouts/page.html" %} - -{% load wagtailcore_tags wagtailroutablepage_tags %} +{% load wagtailcore_tags %} {% block content %}

{{ page.title }}

{% include "components/breadcrumbs.html" %} -
-
-
-
-
Devices
-

{{ page.device_description|richtext}}

- View devices -
-
-
-
-
-
-
Virtual Machines
-

{{ page.vm_description|richtext}}

- View VMs -
-
-
-
- {% for block in page.content %}
{{ block }}
{% endfor %} + {% if entity_results %} + {% include "infra/inc/entity_table.html" with entities=entity_results %} + {% else %} +

No results found

+ {% endif %}
{% endblock %} \ No newline at end of file diff --git a/kmicms/pages/infra/templates/infra/netbox_vm_index.html b/kmicms/pages/infra/templates/infra/netbox_vm_index.html deleted file mode 100644 index 3d15418..0000000 --- a/kmicms/pages/infra/templates/infra/netbox_vm_index.html +++ /dev/null @@ -1,38 +0,0 @@ -{% extends "layouts/page.html" %} -{% load wagtailcore_tags wagtailroutablepage_tags %} - -{% block title %}Virtual Machines{% endblock %} - -{% block content %} -
-

Virtual Machines

- {% include "infra/inc/breadcrumbs_subpage.html" with title="Virtual Machines" %} - -

{{ page.vm_description|richtext}}

- - - - - - - - - - - - - - {% for vm in vms %} - - - - - - - - - {% endfor %} - -
HostnameStatusRolePlatformClusterActions
{{ vm.name }}{{ vm.status }}{% include 'infra/inc/colored_badge.html' with info=vm.role %}{% if vm.platform %}{{ vm.platform.name }}{% else %}-{% endif %}{{ vm.cluster.name }}View
-
-{% endblock %} \ No newline at end of file diff --git a/kmicms/pages/infra/templates/infra/netbox_vm_view.html b/kmicms/pages/infra/templates/infra/netbox_vm_view.html deleted file mode 100644 index f3daac6..0000000 --- a/kmicms/pages/infra/templates/infra/netbox_vm_view.html +++ /dev/null @@ -1,34 +0,0 @@ -{% extends "layouts/page.html" %} -{% load wagtailroutablepage_tags %} - -{% block title %}VM: {{ vm.name }}{% endblock %} - -{% block content %} -
-

- VM: {{ vm.name }} -

- {% routablepageurl page 'vm_index' as vm_index_url %} - {% include "infra/inc/breadcrumbs_subpage.html" with title=vm.name parent_title="Virtual Machines" parent_link=vm_index_url %} - -
-
- {% include "infra/inc/server_dl.html" with server=vm %} -
-
-
-
vCPUs
-
{{ vm.vcpus }}
-
Memory
-
{{ vm.memory }}MB
-
Storage
-
{{ vm.disk }}GB
-
Cluster / Host
-
{{ vm.cluster.name }}
-
-
-
- - {% include "infra/inc/interface_table.html" with interfaces=vm.interfaces %} -
-{% endblock %} \ No newline at end of file