from dataclasses import dataclass from functools import lru_cache from pathlib import Path from typing import Type import base64 import xml.dom.minidom import sys from django.conf import settings from django.db.models import Model from django.templatetags.static import static from django.utils.text import camel_case_to_spaces, re_camel_case from django.contrib import messages IMAGE_DIR = Path(settings.STATIC_ROOT) / "netbox_topology_views/img" if sys.version_info >= (3,9,0): CONF_IMAGE_DIR: Path = Path(settings.STATIC_ROOT) / settings.PLUGINS_CONFIG[ "netbox_topology_views" ]["static_image_directory"].removeprefix("/") else: prefix = "/" plugin_path = settings.PLUGINS_CONFIG["netbox_topology_views"]["static_image_directory"] if plugin_path.startswith(prefix): plugin_path_new = plugin_path[len(prefix):] CONF_IMAGE_DIR: Path = Path(settings.STATIC_ROOT) / plugin_path_new else: CONF_IMAGE_DIR: Path = Path(settings.STATIC_ROOT) / plugin_path def image_static_url(path: Path) -> str: return static( f"/{path.relative_to(Path(settings.STATIC_ROOT))}" ) def get_image_from_url(url: str) -> str: if sys.version_info >= (3,9,0): return url.removeprefix(settings.STATIC_URL) else: if url.startswith(settings.STATIC_URL): url_new = url[len(settings.STATIC_URL):] return url_new else: return url IMAGE_FILETYPES = ( "apng", "avif", "bmp", "cur", "gif", "ico", "jfif", "jpeg", "jpg", "pjp", "pjpeg", "png", "svg", "webp", ) def find_image_in_dir(glob: str, dir: Path): return next( (f for f in dir.glob(f"{glob}.*") if f.suffix.lstrip(".") in IMAGE_FILETYPES), None, ) @lru_cache(maxsize=50) def find_image_url(glob: str, dir: Path = CONF_IMAGE_DIR): """ will attempt to find a file that matches glob in given directory with any file extension, otherwise will try to find a `role-unknown` image returns static file url """ if file := find_image_in_dir(glob, dir): return image_static_url(file) if glob != "role-unknown" and (file := find_image_in_dir("role-unknown", dir)): return image_static_url(file) if dir != IMAGE_DIR and (file := find_image_in_dir("role-unknown", IMAGE_DIR)): return image_static_url(file) return "" @dataclass class Role: slug: str name: str def get_model_slug(model: Type[Model]): return camel_case_to_spaces(model.__name__).replace(" ", "-") def get_model_role(model: Type[Model]) -> Role: return Role( slug=get_model_slug(model), name=re_camel_case.sub(r" \1", model.__name__), ) def get_query_settings(request): filter_id = '' if "filter_id" in request.GET: filter_id = request.GET["filter_id"] save_coords = False if "save_coords" in request.GET: if request.GET["save_coords"] == "True": save_coords = True # General options overrides if save_coords == True and settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"] == False: save_coords = False messages.warning(request, "Coordinate saving not allowed. Setting has been overridden") elif settings.PLUGINS_CONFIG["netbox_topology_views"]["always_save_coordinates"] == True: save_coords = True # Individual options ignore_cable_type = () if "ignore_cable_type" in request.GET: ignore_cable_type = request.GET.getlist('ignore_cable_type') show_unconnected = False if "show_unconnected" in request.GET: if request.GET["show_unconnected"] == "True": show_unconnected = True show_power = False if "show_power" in request.GET: if request.GET["show_power"] == "True": show_power = True show_circuit = False if "show_circuit" in request.GET: if request.GET["show_circuit"] == "True": show_circuit = True show_logical_connections = False if "show_logical_connections" in request.GET: if request.GET["show_logical_connections"] == "True" : show_logical_connections = True show_single_cable_logical_conns = False if "show_single_cable_logical_conns" in request.GET: if request.GET["show_single_cable_logical_conns"] == "True" : show_single_cable_logical_conns = True show_cables = False if "show_cables" in request.GET: if request.GET["show_cables"] == "True" : show_cables = True show_wireless = False if "show_wireless" in request.GET: if request.GET["show_wireless"] == "True" : show_wireless = True group_sites = False if "group_sites" in request.GET: if request.GET["group_sites"] == "True" : group_sites = True group_locations = False if "group_locations" in request.GET: if request.GET["group_locations"] == "True" : group_locations = True group_racks = False if "group_racks" in request.GET: if request.GET["group_racks"] == "True" : group_racks = True group_virtualchassis = False if "group_virtualchassis" in request.GET: if request.GET["group_virtualchassis"] == "True" : group_virtualchassis = True group = False if "group" in request.GET: group = request.GET["group"] show_neighbors = False if "show_neighbors" in request.GET: if request.GET["show_neighbors"] == "True" : show_neighbors = True straight_cables = False if "straight_cables" in request.GET: if request.GET["straight_cables"] == "True": straight_cables = True draw_termination_labels = False if "draw_termination_labels" in request.GET: if request.GET["draw_termination_labels"] == "True": draw_termination_labels = True draw_cable_labels = False if "draw_cable_labels" in request.GET: if request.GET["draw_cable_labels"] == "True": draw_cable_labels = True grid_size = 0 if "grid_size" in request.GET: grid_size = request.GET.getlist('grid_size') node_label_items = () if "node_label_items" in request.GET: node_label_items = request.GET.getlist('node_label_items') return filter_id, ignore_cable_type, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, group_virtualchassis, group, show_neighbors, straight_cables, draw_termination_labels, draw_cable_labels, grid_size, node_label_items class LinePattern(): wireless = [2, 10, 2, 10] power = [5, 5, 3, 3] logical = [1, 10, 1, 10] def export_data_to_xml(data: dict): if data is None: return '' # static xml header doc = xml.dom.minidom.Document() mxfile = doc.createElement('mxfile') mxfile.setAttribute('host', 'app.diagrams.net') mxfile.setAttribute('type', 'device') diagram = doc.createElement('diagram') diagram.setAttribute('name', 'topology') diagram.setAttribute('id', 'someid') doc.appendChild(mxfile) mxfile.appendChild(diagram) # mxGraphModel mxgraphmodel = doc.createElement('mxGraphModel') mxgraphmodel.setAttribute('dx', '0') mxgraphmodel.setAttribute('dy', '0') mxgraphmodel.setAttribute('grid', '0') mxgraphmodel.setAttribute('guides', '1') mxgraphmodel.setAttribute('tooltips', '1') mxgraphmodel.setAttribute('connect', '1') mxgraphmodel.setAttribute('arrows', '1') mxgraphmodel.setAttribute('fold', '1') mxgraphmodel.setAttribute('page', '1') mxgraphmodel.setAttribute('pageScale', '1') mxgraphmodel.setAttribute('pageWidth', '827') mxgraphmodel.setAttribute('pageHeight', '1169') mxgraphmodel.setAttribute('background', 'none') mxgraphmodel.setAttribute('math', '0') mxgraphmodel.setAttribute('shadow', '0') diagram.appendChild(mxgraphmodel) # root root = doc.createElement('root') mxgraphmodel.appendChild(root) # mxCells # header mxcell = doc.createElement('mxCell') mxcell.setAttribute('id', '0') root.appendChild(mxcell) mxcell = doc.createElement('mxCell') mxcell.setAttribute('id', '1') mxcell.setAttribute('parent', '0') root.appendChild(mxcell) # edges for edge in data['edges']: dashes = '' if 'dashes' in edge: if edge['dashes'] == LinePattern().wireless: dashes = 'dashed=1;dashPattern=3 6;' if edge['dashes'] == LinePattern().power: dashes = 'dashed=1;dashPattern=6 6;' if edge['dashes'] == LinePattern().logical: dashes = 'dashed=1;dashPattern=1 4;strokeWidth=2;' mxcell = doc.createElement('mxCell') mxcell.setAttribute('id', 'edge_' + str(edge['id'])) mxcell.setAttribute('style', 'rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;strokeColor=' + edge['color'] + ';' + dashes) mxcell.setAttribute('edge', '1') mxcell.setAttribute('parent', '1') mxcell.setAttribute('source', 'node_' + str(edge['from'])) mxcell.setAttribute('target', 'node_' + str(edge['to'])) root.appendChild(mxcell) mxgeometry = doc.createElement('mxGeometry') mxgeometry.setAttribute('relative', '1') mxgeometry.setAttribute('as', 'geometry') mxcell.appendChild(mxgeometry) # nodes noPositionX = 0 noPositionY = 1000 for node in data['nodes']: with open(settings.STATIC_ROOT + '/' + get_image_from_url(node['image']), "rb") as img: svg = base64.b64encode(img.read()).decode('utf-8') mxcell = doc.createElement('mxCell') mxcell.setAttribute('id', 'node_' + str(node['id'])) mxcell.setAttribute('value', str(node['label'])) mxcell.setAttribute('style', 'shape=image;verticalLabelPosition=bottom;verticalAlign=top;imageAspect=0;aspect=fixed;image=data:image/svg+xml,' + svg + ';perimeter=elippsePerimeter;fontSize=10;strokeWidth=1;fontColor=#000000;') mxcell.setAttribute('vertex', '1') mxcell.setAttribute('parent', '1') root.appendChild(mxcell) mxgeometry = doc.createElement('mxGeometry') # Check if x and y values are stored in the database and set pseudo # coordinates if not. Otherwise icons will not be exported / KeyError is raised if 'x' in node: mxgeometry.setAttribute('x', str(node['x'])) else: # Set next pseudo coordinates if noPositionX > 2500: noPositionX = 100 noPositionY = noPositionY + 100 else: noPositionX = noPositionX + 100 mxgeometry.setAttribute('x', str(noPositionX)) if 'y' in node: mxgeometry.setAttribute('y', str(node['y'])) else: mxgeometry.setAttribute('y', str(noPositionY)) mxgeometry.setAttribute('width', '50') mxgeometry.setAttribute('height', '50') mxgeometry.setAttribute('as', 'geometry') mxcell.appendChild(mxgeometry) # Place a warning if one or more icons are misplaced because of missing coordinates if noPositionX > 0: mxcell = doc.createElement('mxCell') mxcell.setAttribute('id', 'noPositionWarning') mxcell.setAttribute('value', 'One or more icons are misplaced. This happens if no coordinates are stored for a device. Turn on coordinates saving and make sure to drag all icons in the topology to a specific position before exporting to XML.') mxcell.setAttribute('style', 'text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontColor=#FF0000;fontStyle=0;fontSize=35;labelBackgroundColor=#FFFF99;labelBorderColor=none;strokeWidth=20;') mxcell.setAttribute('vertex', '1') mxcell.setAttribute('parent', '1') root.appendChild(mxcell) mxgeometry = doc.createElement('mxGeometry') mxgeometry.setAttribute('x', '0') mxgeometry.setAttribute('y', '0') mxgeometry.setAttribute('width', '870') mxgeometry.setAttribute('height', '100') mxgeometry.setAttribute('as', 'geometry') mxcell.appendChild(mxgeometry) # debug output # print(doc.toprettyxml()) return doc.toxml(encoding='UTF-8')