From 088d1036f51a2f474605e3ca02e02ff76648ed71 Mon Sep 17 00:00:00 2001 From: "Mees, T.D. (Ty)" Date: Mon, 27 Jan 2025 17:26:34 +0100 Subject: [PATCH] feat: implement modular V2 scan visualizer components Added a new modular implementation for the V2 scan visualizer by introducing reusable components and templates. --- .../src/hosts/scan_visualizers/__init__.py | 10 +- .../hosts/scan_visualizers/base_components.py | 147 ++++++++ .../hosts/scan_visualizers/base_visualizer.py | 73 +++- .../src/hosts/scan_visualizers/v2/__init__.py | 2 +- .../v2/artefact_visualizers.py | 319 ++++++++++++++++++ .../scan_visualizers/v2/scan_visualizer.py | 48 +++ .../hosts/scan_visualizers/v2/visualizer.py | 10 - .../hosts/templates/hosts/host_detail.html | 54 --- .../components/bars_component.html | 18 + .../components/base_component.html | 37 ++ .../components/hardware_component.html | 90 +++++ .../components/itemized_component.html | 16 + .../components/puppet_component.html | 72 ++++ .../searchable_cards_component.html | 63 ++++ .../components/zfs_component.html | 26 ++ .../hosts/scan_visualizer/v1/v1.html | 58 ++++ .../templates/hosts/scan_visualizer/v2.html | 125 +++++++ humitifier-server/src/hosts/views.py | 15 +- 18 files changed, 1105 insertions(+), 78 deletions(-) create mode 100644 humitifier-server/src/hosts/scan_visualizers/base_components.py create mode 100644 humitifier-server/src/hosts/scan_visualizers/v2/artefact_visualizers.py create mode 100644 humitifier-server/src/hosts/scan_visualizers/v2/scan_visualizer.py delete mode 100644 humitifier-server/src/hosts/scan_visualizers/v2/visualizer.py create mode 100644 humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/bars_component.html create mode 100644 humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/base_component.html create mode 100644 humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/hardware_component.html create mode 100644 humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/itemized_component.html create mode 100644 humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/puppet_component.html create mode 100644 humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/searchable_cards_component.html create mode 100644 humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/zfs_component.html create mode 100644 humitifier-server/src/hosts/templates/hosts/scan_visualizer/v2.html diff --git a/humitifier-server/src/hosts/scan_visualizers/__init__.py b/humitifier-server/src/hosts/scan_visualizers/__init__.py index e5079b7..61b99d6 100644 --- a/humitifier-server/src/hosts/scan_visualizers/__init__.py +++ b/humitifier-server/src/hosts/scan_visualizers/__init__.py @@ -1,12 +1,14 @@ from .base_visualizer import ScanVisualizer from .v1 import V1ScanVisualizer from .v2 import V2ScanVisualizer -from ..models import ScanData +from ..models import Host, ScanData -def get_scan_visualizer(scan_data: ScanData) -> ScanVisualizer: +def get_scan_visualizer( + host: Host, scan_data: ScanData, context: dict +) -> ScanVisualizer: match scan_data.version: case 1: - return V1ScanVisualizer(scan_data) + return V1ScanVisualizer(host, scan_data, context) case _: - return V2ScanVisualizer(scan_data) + return V2ScanVisualizer(host, scan_data, context) diff --git a/humitifier-server/src/hosts/scan_visualizers/base_components.py b/humitifier-server/src/hosts/scan_visualizers/base_components.py new file mode 100644 index 0000000..5f62dd9 --- /dev/null +++ b/humitifier-server/src/hosts/scan_visualizers/base_components.py @@ -0,0 +1,147 @@ +import dataclasses +from datetime import datetime +from typing import TypeVar + +from django.template.loader import render_to_string +from django.utils.functional import cached_property +from django.utils.safestring import mark_safe + +from humitifier_common.artefacts.registry.registry import ArtefactType + +T = TypeVar("T") + + +class ArtefactVisualizer: + artefact: type[T] = None + title: str | None = None + template: str = "hosts/scan_visualizer/components/base_component.html" + + def __init__(self, artefact_data: T, scan_date: datetime): + self.artefact_data = artefact_data + self.scan_date = scan_date + + def show(self): + return self.artefact_data is not None + + def get_context(self, **kwargs) -> dict: + kwargs["title"] = self.title + kwargs["is_metric"] = self.artefact.__artefact_type__ == ArtefactType.METRIC + kwargs["alpinejs_settings"] = {} + + return kwargs + + def render(self) -> str | None: + return render_to_string(self.template, context=self.get_context()) + + +class ItemizedArtefactVisualizer(ArtefactVisualizer): + template = "hosts/scan_visualizer/components/itemized_component.html" + attributes: dict[str, str] | None = None + + def get_items(self) -> list[dict[str, str]]: + data = [] + for item, label in self.attributes.items(): + value = self.get_attribute_value(item) + data.append( + { + "label": label, + "value": value, + } + ) + + return data + + def get_context(self, **kwargs) -> dict: + context = super().get_context(**kwargs) + + context["data"] = self.get_items() + + return context + + def get_attribute_value(self, item): + value = getattr(self.artefact_data, item, None) + + if value and hasattr(self, f"get_{item}_display"): + actual_value = getattr(self, f"get_{item}_display")(value) + if actual_value is not None: + return mark_safe(actual_value) + + return actual_value + + return value + + +@dataclasses.dataclass +class Bar: + label_1: str + label_2: str | None = None + used: str | None = None + total: str | None = None + percentage: float | None = None + + +class BarsArtefactVisualizer(ArtefactVisualizer): + template = "hosts/scan_visualizer/components/bars_component.html" + + def get_context(self, **kwargs) -> dict: + context = super().get_context(**kwargs) + + context["data"] = self.get_bar_items() + + return context + + def get_bar_items(self) -> list[Bar]: + raise NotImplementedError() + + +@dataclasses.dataclass +class Card: + """ + Dataclass to represent a card. + + Title will be displayed promenently as the header + Aside will be displayed next to the title, for secondary info + + Content can be specified in one of two ways, either as a string + which will just be 'pasted' as the content, or a dict of key-values + which will be displayed as a nicely formatted list. (Like ItemizedArtefactVisualizer) + + Search_value should be filled with a string that will be used to search through. + (Using a simple 'string includes search-text' method). + If multiple elements should be searched at the same time, you should just concat + them inside the string ;) + """ + + title: str | None = None + aside: str | None = None + content: str | None = None + content_items: dict[str, str] | None = None + search_value: str | None = None + + +class SearchableCardsVisualizer(ArtefactVisualizer): + template = "hosts/scan_visualizer/components/searchable_cards_component.html" + search_placeholder = "Search" + allow_search = True + min_items_for_search = 3 + + def get_items(self) -> list[Card]: + raise NotImplementedError() + + @cached_property + def _items(self): + return self.get_items() + + def show_search(self): + return self.allow_search and len(self._items) > self.min_items_for_search + + def get_context(self, **kwargs) -> dict: + context = super().get_context(**kwargs) + + context["data"] = self._items + context["show_search"] = self.show_search + context["search_placeholder"] = self.search_placeholder + + context["alpinejs_settings"]["search"] = "''" + + return context diff --git a/humitifier-server/src/hosts/scan_visualizers/base_visualizer.py b/humitifier-server/src/hosts/scan_visualizers/base_visualizer.py index bfb0a03..9d4c6ba 100644 --- a/humitifier-server/src/hosts/scan_visualizers/base_visualizer.py +++ b/humitifier-server/src/hosts/scan_visualizers/base_visualizer.py @@ -1,14 +1,19 @@ -from hosts.models import ScanData +from django.template.loader import render_to_string + +from hosts.models import Host, ScanData +from hosts.scan_visualizers.base_components import ArtefactVisualizer +from humitifier_common.artefacts.registry.registry import ArtefactType class ScanVisualizer: - def __init__(self, scan_data: ScanData): + def __init__(self, host: Host, scan_data: ScanData, context: dict): + self.host = host self.scan_data = scan_data self.request = None + self.provided_context = context def get_context(self, **kwargs): - kwargs.update( { "current_scan_date": self.scan_data.scan_date, @@ -18,10 +23,72 @@ def get_context(self, **kwargs): else self.scan_data.raw_data ), "raw_data": self.scan_data.raw_data, + "host": self.host, } ) + kwargs.update(self.provided_context) + return kwargs def render(self): raise NotImplementedError() + + +class ComponentScanVisualizer(ScanVisualizer): + template = None + + visualizers: list[type[ArtefactVisualizer]] = [] + + def render(self): + return render_to_string( + self.template, + context=self.get_context(), + request=self.request, + ) + + def get_artefact_data(self, artefact): + artefact_name = artefact.__artefact_name__ + artefact_type: ArtefactType = artefact.__artefact_type__ + + if artefact_type == ArtefactType.FACT: + return self.scan_data.parsed_data.facts.get(artefact_name, None) + else: + return self.scan_data.parsed_data.metrics.get(artefact_name, None) + + def render_components( + self, components: list[type[ArtefactVisualizer]] + ) -> dict[str, str]: + output = {} + for component in components: + data = self.get_artefact_data(component.artefact) + if data: + cmp = component(data, self.scan_data.scan_date) + if cmp.show(): + output[component.title] = cmp.render() + + return output + + @property + def fact_visualizers(self): + return [ + visualizer + for visualizer in self.visualizers + if visualizer.artefact.__artefact_type__ == ArtefactType.FACT + ] + + @property + def metric_visualizers(self): + return [ + visualizer + for visualizer in self.visualizers + if visualizer.artefact.__artefact_type__ == ArtefactType.METRIC + ] + + def get_context(self, **kwargs): + context = super().get_context(**kwargs) + + context["facts"] = self.render_components(self.fact_visualizers) + context["metrics"] = self.render_components(self.metric_visualizers) + + return context diff --git a/humitifier-server/src/hosts/scan_visualizers/v2/__init__.py b/humitifier-server/src/hosts/scan_visualizers/v2/__init__.py index 1415df0..b69ee22 100644 --- a/humitifier-server/src/hosts/scan_visualizers/v2/__init__.py +++ b/humitifier-server/src/hosts/scan_visualizers/v2/__init__.py @@ -1 +1 @@ -from .visualizer import V2ScanVisualizer +from .scan_visualizer import V2ScanVisualizer diff --git a/humitifier-server/src/hosts/scan_visualizers/v2/artefact_visualizers.py b/humitifier-server/src/hosts/scan_visualizers/v2/artefact_visualizers.py new file mode 100644 index 0000000..1c83d34 --- /dev/null +++ b/humitifier-server/src/hosts/scan_visualizers/v2/artefact_visualizers.py @@ -0,0 +1,319 @@ +from datetime import datetime + +from django.utils.safestring import mark_safe + +from hosts.scan_visualizers.base_components import ( + ArtefactVisualizer, + Bar, + BarsArtefactVisualizer, + Card, + ItemizedArtefactVisualizer, + SearchableCardsVisualizer, +) + +from hosts.scan_visualizers.base_visualizer import ComponentScanVisualizer +from hosts.templatetags.host_tags import size_from_mb, uptime +from humitifier_common.artefacts import ( + Blocks, + Groups, + Hardware, + HostMeta, + HostnameCtl, + IsWordpress, + Memory, + PackageList, + PuppetAgent, + Uptime, + Users, + ZFS, +) + + +class HostMetaVisualizer(ItemizedArtefactVisualizer): + artefact = HostMeta + title = "GenDoc metadata" + attributes = { + "update_policy": "Update policy", + "webdav": "Webdav share", + "fileservers": "Fileservers", + } + + def get_items(self) -> list[dict[str, str]]: + items = super().get_items() + + if self.artefact_data.databases: + for database_software, databases in self.artefact_data.databases.items(): + items.append( + { + "label": f"{database_software.capitalize()} DB's", + "value": ", ".join(databases), + } + ) + + return items + + @staticmethod + def get_fileservers_display(value): + return ", ".join(value) + + @staticmethod + def get_update_policy_display(value): + output = "
" + + if value["enable"]: + output += f"Enabled" + else: + output += f"Disabled" + + if value["apply_updates"]: + output += f"Applied" + else: + output += f"Not applied" + + output += "
" + + return mark_safe(output) + + +class HostMetaVHostsVisualizer(SearchableCardsVisualizer): + artefact = HostMeta + title = "Apache vhosts" + + def show(self): + if not super().show(): + return False + + return hasattr(self.artefact_data, "vhosts") and self.artefact_data.vhosts + + def get_items(self) -> list[Card]: + items = [] + + merged_dict = {} + + for vhost in self.artefact_data.vhosts: + merged_dict.update(vhost) + + for vhost, data in merged_dict.items(): + content = None + search_value = f"{vhost} {data.docroot}" + if data.serveraliases: + content = { + "aliases": ", ".join(data.serveraliases), + } + search_value += f" {" ".join(data.serveraliases)}" + + items.append( + Card( + title=vhost, + aside=data.docroot, + content_items=content, + search_value=search_value, + ) + ) + + return items + + +class HostnameCtlVisualizer(ItemizedArtefactVisualizer): + artefact = HostnameCtl + title = "Host metadata" + attributes = { + "hostname": "Hostname", + "os": "OS", + "cpe_os_name": "CPE OS name", + "kernel": "Kernel", + "virtualization": "Virtualization", + } + + +class UptimeVisualizer(ArtefactVisualizer): + title = "Host uptime" + artefact = Uptime + + def get_context(self, **kwargs): + context = super().get_context(**kwargs) + + if not self.artefact: + context["content"] = '
Unknown
' + else: + host_uptime = uptime(self.artefact_data, self.scan_date) + + context["content"] = mark_safe( + f"
{host_uptime}
" + ) + + return context + + +class MemoryVisualizer(BarsArtefactVisualizer): + title = "Memory usage" + artefact = Memory + + def get_bar_items(self) -> list[Bar]: + return [ + Bar( + label_1="Memory", + used=size_from_mb(self.artefact_data.used_mb), + total=size_from_mb(self.artefact_data.total_mb), + percentage=self.artefact_data.used_mb + / self.artefact_data.total_mb + * 100, + ), + Bar( + label_1="Swap", + used=size_from_mb(self.artefact_data.swap_used_mb), + total=size_from_mb(self.artefact_data.swap_total_mb), + percentage=self.artefact_data.swap_used_mb + / self.artefact_data.swap_total_mb + * 100, + ), + ] + + +class BlocksVisualizer(BarsArtefactVisualizer): + title = "Disk usage" + artefact = Blocks + + def get_bar_items(self) -> list[Bar]: + block_data = [] + for block in self.artefact_data: + block_data.append( + Bar( + label_1=block.name, + label_2=block.mount, + used=size_from_mb(block.used_mb), + total=size_from_mb(block.size_mb), + percentage=block.use_percent, + ) + ) + + return block_data + + +class UsersVisualizer(SearchableCardsVisualizer): + title = "Users" + artefact = Users + search_placeholder = "Search groups" + + def get_items(self) -> list[Card]: + output = [] + + for user in self.artefact_data: + output.append( + Card( + title=user.name, + content_items={ + "Info": user.info, + "Group ID": user.gid, + "User ID": user.uid, + "Homedir": user.home, + "shell": user.shell, + }, + search_value=f"{user.name} {user.uid} {user.info}", + ) + ) + + return output + + +class GroupsVisualizer(SearchableCardsVisualizer): + title = "Groups" + artefact = Groups + search_placeholder = "Search groups" + + def get_items(self) -> list[Card]: + output = [] + for group in self.artefact_data: + output.append( + Card( + title=group.name, + aside=group.gid, + content=", ".join(group.users), + search_value=f"{group.name} {group.gid}", + ) + ) + + return output + + +class PackageListVisualizer(SearchableCardsVisualizer): + title = "Packages" + artefact = PackageList + search_placeholder = "Search packages" + + def get_items(self) -> list[Card]: + output = [] + for package in self.artefact_data: + output.append( + Card( + title=package.name, + aside=package.version, + search_value=f"{package.name} {package.version}", + ) + ) + + return output + + +class PuppetAgentVisualizer(ArtefactVisualizer): + title = "Puppet Agent" + artefact = PuppetAgent + + template = "hosts/scan_visualizer/components/puppet_component.html" + + def get_context(self, **kwargs) -> dict: + context = super().get_context(**kwargs) + + last_run = None + if self.artefact_data.last_run: + last_run = datetime.fromisoformat(self.artefact_data.last_run) + + context["puppet"] = self.artefact_data + context["last_run"] = last_run + + return context + + +class IsWordpressVisualizer(ArtefactVisualizer): + title = "Is Wordpress?" + artefact = IsWordpress + + def show(self): + return self.artefact_data and self.artefact_data.is_wp + + def get_context(self, **kwargs) -> dict: + context = super().get_context(**kwargs) + + # We only show this component if it's wordpress, sooo + context["content"] = "Yes!" + + return context + + +class HardwareVisualizer(ArtefactVisualizer): + title = "Hardware" + artefact = Hardware + template = "hosts/scan_visualizer/components/hardware_component.html" + + def get_context(self, **kwargs) -> dict: + context = super().get_context(**kwargs) + + context["hardware"] = self.artefact_data + + total_memory = sum([memrange.size for memrange in self.artefact_data.memory]) + context["total_memory"] = total_memory + + return context + + +class ZFSVisualizer(ArtefactVisualizer): + title = "ZFS" + artefact = ZFS + template = "hosts/scan_visualizer/components/zfs_component.html" + + def get_context(self, **kwargs) -> dict: + context = super().get_context(**kwargs) + + context["zfs"] = self.artefact_data + + return context diff --git a/humitifier-server/src/hosts/scan_visualizers/v2/scan_visualizer.py b/humitifier-server/src/hosts/scan_visualizers/v2/scan_visualizer.py new file mode 100644 index 0000000..02ef046 --- /dev/null +++ b/humitifier-server/src/hosts/scan_visualizers/v2/scan_visualizer.py @@ -0,0 +1,48 @@ +from hosts.scan_visualizers.base_components import ArtefactVisualizer + +from hosts.scan_visualizers.base_visualizer import ComponentScanVisualizer + +import hosts.scan_visualizers.v2.artefact_visualizers as visualizers + + +class V2ScanVisualizer(ComponentScanVisualizer): + template = "hosts/scan_visualizer/v2.html" + + visualizers: list[type[ArtefactVisualizer]] = [ + visualizers.UptimeVisualizer, + visualizers.BlocksVisualizer, + visualizers.MemoryVisualizer, + visualizers.ZFSVisualizer, + visualizers.HardwareVisualizer, + visualizers.HostMetaVisualizer, + visualizers.HostMetaVHostsVisualizer, + visualizers.HostnameCtlVisualizer, + visualizers.PuppetAgentVisualizer, + visualizers.IsWordpressVisualizer, + visualizers.PackageListVisualizer, + visualizers.UsersVisualizer, + visualizers.GroupsVisualizer, + ] + + static_data = { + "data_source": "Data source", + "has_tofu_config": "Has OpenTofu config", + "otap_stage": "OTAP stage", + "department": "Department", + "customer": "Customer", + "contact": "Contact", + } + + def get_context(self, **kwargs): + context = super().get_context(**kwargs) + context["static_data"] = [] + + for attr, label in self.static_data.items(): + context["static_data"].append( + { + "label": label, + "value": getattr(self.host, attr, None), + } + ) + + return context diff --git a/humitifier-server/src/hosts/scan_visualizers/v2/visualizer.py b/humitifier-server/src/hosts/scan_visualizers/v2/visualizer.py deleted file mode 100644 index a0d5c28..0000000 --- a/humitifier-server/src/hosts/scan_visualizers/v2/visualizer.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.template.loader import render_to_string - -from hosts.scan_visualizers import ScanVisualizer - - -class V2ScanVisualizer(ScanVisualizer): - - def render(self): - # TODO: implement! - return "v2!" diff --git a/humitifier-server/src/hosts/templates/hosts/host_detail.html b/humitifier-server/src/hosts/templates/hosts/host_detail.html index a7c6ab8..e3b795e 100644 --- a/humitifier-server/src/hosts/templates/hosts/host_detail.html +++ b/humitifier-server/src/hosts/templates/hosts/host_detail.html @@ -5,59 +5,5 @@ {% block page_title %}{{ host.fqdn }} | {{ block.super }}{% endblock %} {% block content %} -
-
{{ host.fqdn }}
-
- {{ current_scan_date|date:"Y-m-d H:i" }} -
-
-
- - -
- - -
-
- - {% if is_latest_scan %} - {% for alert in alerts %} - {% if alert.level == 'critical' %} - {% include 'hosts/host_detail_parts/alerts/critical.html' %} - {% elif alert.level == 'warning' %} - {% include 'hosts/host_detail_parts/alerts/warning.html' %} - {% elif alert.level == 'info' %} - {% include 'hosts/host_detail_parts/alerts/info.html' %} - {% endif %} - {% endfor %} - {% else %} - {% with alert="This is historical data, it does not reflect the current configuration." %} - {% include 'hosts/host_detail_parts/alerts/warning.html' %} - {% endwith %} - {% endif %} - - {% if host.archived %} - {% with alert=host.archived_string %} - {% include 'hosts/host_detail_parts/alerts/info.html' %} - {% endwith %} - {% endif %} - {{ scan_visualizer.render }} {% endblock %} diff --git a/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/bars_component.html b/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/bars_component.html new file mode 100644 index 0000000..f3348f7 --- /dev/null +++ b/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/bars_component.html @@ -0,0 +1,18 @@ +{% extends 'hosts/scan_visualizer/components/base_component.html' %} + +{% block content %} +
+ {% for item in data %} + {{ item.label_1 }} + + {% if item.label_2 %} + {{ item.label_2 }} + {% endif %} + + {{ item.used }} / {{ item.total }} +
+
+
+ {% endfor %} +
+{% endblock %} diff --git a/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/base_component.html b/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/base_component.html new file mode 100644 index 0000000..3f83c17 --- /dev/null +++ b/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/base_component.html @@ -0,0 +1,37 @@ +
+ +
+
{{ title }}
+ {% block header %}{% endblock %} +
+ {% if is_metric %} +
+ {% include 'icons/reload.html' %} +
+ {% endif %} + +
+
+ {% with icon_size='size-5' %} + {% include 'icons/chevron-up.html' %} + {% endwith %} +
+
+ {% with icon_size='size-5' %} + {% include 'icons/chevron-down.html' %} + {% endwith %} +
+
+
+
+ +
+ {% block content %} + {{ content }} + {% endblock %} +
+
diff --git a/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/hardware_component.html b/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/hardware_component.html new file mode 100644 index 0000000..12504e0 --- /dev/null +++ b/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/hardware_component.html @@ -0,0 +1,90 @@ +{% extends 'hosts/scan_visualizer/components/base_component.html' %} + +{% load host_tags %} + +{% block content %} +
+
Num. CPUs
+
+ {% if hardware.num_cpus %} + {{ hardware.num_cpus }} + {% else %} +
Unknown
+ {% endif %} +
+
Memory
+
+ {% if total_memory %} + {{ total_memory|filesizeformat }} + {% else %} +
Unknown
+ {% endif %} +
+ {% if hardware.block_devices %} +
Block devices
+
+ Show + Hide +
+
+ + + + + + + + + + + {% for block in hardware.block_devices %} + + + + + + + + {% endfor %} + +
NameTypeModelSize
+ {{ block.name }} + + {{ block.type }} + + {{ block.model }} + + {{ block.size }} +
+
+ {% endif %} + {% if hardware.pci_devices %} +
PCI(e) devices
+
+ Show + Hide +
+
+ {% for pci_device in hardware.pci_devices %} +
+ {{ pci_device }} +
+ {% endfor %} +
+ {% endif %} + {% if hardware.usb_devices %} +
USB devices
+
+ Show + Hide +
+
+ {% for usb_device in hardware.usb_devices %} +
+ {{ usb_device }} +
+ {% endfor %} +
+ {% endif %} +
+{% endblock %} diff --git a/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/itemized_component.html b/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/itemized_component.html new file mode 100644 index 0000000..8acfed9 --- /dev/null +++ b/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/itemized_component.html @@ -0,0 +1,16 @@ +{% extends 'hosts/scan_visualizer/components/base_component.html' %} + +{% block content %} +
+ {% for item in data %} +
{{ item.label }}
+
+ {% if item.value %} + {{ item.value }} + {% else %} +
Unknown
+ {% endif %} +
+ {% endfor %} +
+{% endblock %} diff --git a/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/puppet_component.html b/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/puppet_component.html new file mode 100644 index 0000000..16fba87 --- /dev/null +++ b/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/puppet_component.html @@ -0,0 +1,72 @@ +{% extends 'hosts/scan_visualizer/components/base_component.html' %} + +{% block header %} + +{% endblock %} + +{% block content %} +
+
+
Enabled
+ {% if puppet.enabled %} +
+
+ Enabled +
+
+ {% else %} +
+
+ Disabled +
+
+ +
Reason
+
{{ puppet.disabled_message }}
+ {% endif %} + +
Last run timestamp
+
{{ last_run|date:"Y-m-d H:i" }}
+
Last run status
+ {% if puppet.is_failing %} +
+
+ Failure +
+
+ {% else %} +
+ Successful +
+ {% endif %} +
+ +
+ +
+
Environment
+
{{ puppet.environment }}
+
+ +
+ +
+
Code role(s)
+
{{ puppet.code_roles|join:", " }}
+ +
Profile(s)
+
{{ puppet.profiles|join:", " }}
+
+ +
+ +
+
Data role
+
{{ puppet.data_role }}
+ +
Data role variant
+
{{ puppet.data_role_variant }}
+
+ +
+{% endblock %} diff --git a/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/searchable_cards_component.html b/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/searchable_cards_component.html new file mode 100644 index 0000000..1ae0ca8 --- /dev/null +++ b/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/searchable_cards_component.html @@ -0,0 +1,63 @@ +{% extends 'hosts/scan_visualizer/components/base_component.html' %} + +{% block header %} + {% if show_search %} + + {% endif %} +{% endblock %} + +{% block content %} +
+
+ {% for item in data %} +
+
+
+ {{ item.title }} +
+ {% if item.aside %} +
+ {{ item.aside }} +
+ {% endif %} +
+ +
+ {% if item.content %} +
+ {{ item.content }} +
+ {% elif item.content_items %} +
+ {% for label, value in item.content_items.items %} +
{{ label }}
+
+ {% if value %} + {{ value }} + {% else %} +
+ Unknown +
+ {% endif %} +
+ {% endfor %} +
+ {% endif %} +
+
+ {% endfor %} +
+
+{% endblock %} diff --git a/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/zfs_component.html b/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/zfs_component.html new file mode 100644 index 0000000..bc6c11c --- /dev/null +++ b/humitifier-server/src/hosts/templates/hosts/scan_visualizer/components/zfs_component.html @@ -0,0 +1,26 @@ +{% extends 'hosts/scan_visualizer/components/base_component.html' %} + +{% load host_tags %} + +{% block content %} +
ZFS Pools
+
+ {% for pool in zfs.pools %} + {{ pool.name }} + {{ pool.used_mb|size_from_mb }} / {{ pool.size_mb|size_from_mb }} +
+
+
+ {% endfor %} +
+
ZFS volumes
+
+ {% for volume in zfs.volumes %} + {{ volume.name }} +
{{ volume.used_mb|size_from_mb }} / {{ volume.size_mb|size_from_mb }}
+
+
+
+ {% endfor %} +
+{% endblock %} diff --git a/humitifier-server/src/hosts/templates/hosts/scan_visualizer/v1/v1.html b/humitifier-server/src/hosts/templates/hosts/scan_visualizer/v1/v1.html index bc1d938..0928c8a 100644 --- a/humitifier-server/src/hosts/templates/hosts/scan_visualizer/v1/v1.html +++ b/humitifier-server/src/hosts/templates/hosts/scan_visualizer/v1/v1.html @@ -1,3 +1,61 @@ +{% load param_replace %} + +
+
{{ host.fqdn }}
+
+ {{ current_scan_date|date:"Y-m-d H:i" }} +
+
+
+ + +
+ + {% if host.can_manually_edit %} + + {% endif %} +
+
+ +{% if is_latest_scan %} + {% for alert in alerts %} + {% if alert.level == 'critical' %} + {% include 'hosts/host_detail_parts/alerts/critical.html' %} + {% elif alert.level == 'warning' %} + {% include 'hosts/host_detail_parts/alerts/warning.html' %} + {% elif alert.level == 'info' %} + {% include 'hosts/host_detail_parts/alerts/info.html' %} + {% endif %} + {% endfor %} +{% else %} + {% with alert="This is historical data, it does not reflect the current configuration." %} + {% include 'hosts/host_detail_parts/alerts/warning.html' %} + {% endwith %} +{% endif %} + +{% if host.archived %} + {% with alert=host.archived_string %} + {% include 'hosts/host_detail_parts/alerts/info.html' %} + {% endwith %} +{% endif %} +
{% include 'hosts/scan_visualizer/v1/host_meta.html' %} diff --git a/humitifier-server/src/hosts/templates/hosts/scan_visualizer/v2.html b/humitifier-server/src/hosts/templates/hosts/scan_visualizer/v2.html new file mode 100644 index 0000000..22b9b2a --- /dev/null +++ b/humitifier-server/src/hosts/templates/hosts/scan_visualizer/v2.html @@ -0,0 +1,125 @@ +{% load host_tags %} +{% load param_replace %} + +
+
+
{{ host.fqdn }}
+
+ +
+ +
+ + +
+ +

+ Static config +

+
+ {% for item in static_data %} +
{{ item.label }}
+
+ {% if item.value %} + {{ item.value }} + {% else %} +
Unknown
+ {% endif %} +
+ {% endfor %} +
+ +

+ Actions +

+
+ + + Download raw data + +
+ Archive +
+
+ Edit host config +
+
+ Queue metric rescan +
+
+ Queue full rescan +
+
+
+ + +
+ {% if is_latest_scan %} + {% for alert in alerts %} + {% if alert.level == 'critical' %} + {% include 'hosts/host_detail_parts/alerts/critical.html' %} + {% elif alert.level == 'warning' %} + {% include 'hosts/host_detail_parts/alerts/warning.html' %} + {% elif alert.level == 'info' %} + {% include 'hosts/host_detail_parts/alerts/info.html' %} + {% endif %} + {% endfor %} + {% else %} + {% with alert="This is historical data, it does not reflect the current configuration." %} + {% include 'hosts/host_detail_parts/alerts/warning.html' %} + {% endwith %} + {% endif %} + + {% if host.archived %} + {% with alert=host.archived_string %} + {% include 'hosts/host_detail_parts/alerts/info.html' %} + {% endwith %} + {% endif %} + +
+
+

Metrics

+ + +
+
+ {% for component, content in metrics.items %} +
+ {{ content }} +
+ {% endfor %} +
+
+ +
+
+

Facts

+ +
+ - {{ current_scan_date|date:"Y-m-d H:i" }} +
+ + +
+ + +
+ {% for component, content in facts.items %} +
+ {{ content }} +
+ {% endfor %} +
+
+
+
+ diff --git a/humitifier-server/src/hosts/views.py b/humitifier-server/src/hosts/views.py index 5c11ca9..da068a6 100644 --- a/humitifier-server/src/hosts/views.py +++ b/humitifier-server/src/hosts/views.py @@ -173,16 +173,19 @@ def get_context_data(self, **kwargs): current_scan == self.LATEST_KEY or scan.created_at == host.last_scan_date ) - visualizer = get_scan_visualizer(scan_data) + visualizer_context = { + "current_scan": current_scan, + "current_scan_date": current_scan_date, + "all_scans": host.scans.values_list("created_at", flat=True), + "is_latest_scan": is_latest_scan, + "alerts": host.alerts.order_by("level"), + } + + visualizer = get_scan_visualizer(host, scan_data, visualizer_context) visualizer.request = self.request context["host"] = host context["scan_visualizer"] = visualizer - context["current_scan"] = current_scan - context["current_scan_date"] = current_scan_date - context["all_scans"] = host.scans.values_list("created_at", flat=True) - context["is_latest_scan"] = is_latest_scan - context["alerts"] = host.alerts.order_by("level") return context