From d11de6d021624208e3e913f220cc8c8404c43ac6 Mon Sep 17 00:00:00 2001 From: hellerve Date: Mon, 18 Nov 2019 15:42:42 +0100 Subject: [PATCH 01/12] dcim: add rack-elevations api endpoint (references #2248) --- netbox/dcim/api/urls.py | 1 + netbox/dcim/api/views.py | 46 ++++++++++++++++++- netbox/dcim/views.py | 2 - netbox/templates/dcim/inc/rack_elevation.html | 43 +---------------- 4 files changed, 46 insertions(+), 46 deletions(-) diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index fd55d9b05..7169fcdbb 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -25,6 +25,7 @@ router.register(r'sites', views.SiteViewSet) router.register(r'rack-groups', views.RackGroupViewSet) router.register(r'rack-roles', views.RackRoleViewSet) router.register(r'racks', views.RackViewSet) +router.register(r'rack-elevations', views.RackElevationViewSet, basename='rack-elevation') router.register(r'rack-reservations', views.RackReservationViewSet) # Device types diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index bffe4b9e4..7eb40b526 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -1,9 +1,11 @@ from collections import OrderedDict +import svgwrite from django.conf import settings from django.db.models import Count, F -from django.http import HttpResponseForbidden -from django.shortcuts import get_object_or_404 +from django.http import HttpResponseForbidden, HttpResponse +from django.shortcuts import get_object_or_404, reverse +from django.utils.http import urlencode from drf_yasg import openapi from drf_yasg.openapi import Parameter from drf_yasg.utils import swagger_auto_schema @@ -201,6 +203,46 @@ class RackViewSet(CustomFieldModelViewSet): return self.get_paginated_response(rack_units.data) +class RackElevationViewSet(ViewSet): + queryset = Rack.objects.prefetch_related( + 'devices' + ) + + def get_view_name(self): + return "Rack Elevations" + + + def retrieve(self, request, pk=None): + """ + Render rack + """ + rack = get_object_or_404(Rack, pk=pk) + + elevation = rack.get_front_elevation() + drawing = svgwrite.Drawing(size=(230, len(elevation)*20), style="box-sizing: border-box") + + for i, u in enumerate(elevation): + device = u['device'] + start = i * 20 + end = 20 + if device: + link = drawing.add(drawing.a(reverse('dcim:device', kwargs={'pk': device.pk}), fill='black')) + link.add(drawing.rect((0, start), (230, end), fill='#{}'.format(device.device_role.color), stroke='grey')) + link.add(drawing.text(device.name, insert=(0, start+20))) + else: + link = drawing.add( + drawing.a('{}?{}'.format( + reverse('dcim:device_add'), + urlencode({'rack': rack.pk, 'site': rack.site.pk, 'face': 0, 'position': u['id']}) + )) + ) + link.add(drawing.rect((0, start), (230, end), fill='white', stroke='lightgrey')) + + drawing.add(drawing.rect((0, 0), (230, len(elevation*20)), stroke='black', stroke_width=3, fill='none')) + + return HttpResponse(drawing.tostring(), content_type='image/svg+xml') + + # # Rack reservations # diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index ec16713fa..5a15198ba 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -419,8 +419,6 @@ class RackView(PermissionRequiredMixin, View): 'nonracked_devices': nonracked_devices, 'next_rack': next_rack, 'prev_rack': prev_rack, - 'front_elevation': rack.get_front_elevation(), - 'rear_elevation': rack.get_rear_elevation(), }) diff --git a/netbox/templates/dcim/inc/rack_elevation.html b/netbox/templates/dcim/inc/rack_elevation.html index fdb6f9a36..bfcb76317 100644 --- a/netbox/templates/dcim/inc/rack_elevation.html +++ b/netbox/templates/dcim/inc/rack_elevation.html @@ -8,47 +8,6 @@
- - - - - +
From 808f5c95e390923e834a3b3d1d791576dc6ed10a Mon Sep 17 00:00:00 2001 From: hellerve Date: Wed, 20 Nov 2019 18:02:07 +0100 Subject: [PATCH 02/12] dcim: make front and rear work (references #2248) --- netbox/dcim/api/views.py | 10 ++++++++-- netbox/templates/dcim/inc/rack_elevation.html | 2 +- netbox/templates/dcim/rack.html | 4 ++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 7eb40b526..d262abaac 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -3,7 +3,7 @@ from collections import OrderedDict import svgwrite from django.conf import settings from django.db.models import Count, F -from django.http import HttpResponseForbidden, HttpResponse +from django.http import HttpResponseForbidden, HttpResponseBadRequest, HttpResponse from django.shortcuts import get_object_or_404, reverse from django.utils.http import urlencode from drf_yasg import openapi @@ -218,7 +218,13 @@ class RackElevationViewSet(ViewSet): """ rack = get_object_or_404(Rack, pk=pk) - elevation = rack.get_front_elevation() + side = request.GET.get('face', 'front') + if side == 'front': + elevation = rack.get_front_elevation() + elif side == 'rear': + elevation = rack.get_rear_elevation() + else: + return HttpResponseBadRequest('side should either be "front" or "back".') drawing = svgwrite.Drawing(size=(230, len(elevation)*20), style="box-sizing: border-box") for i, u in enumerate(elevation): diff --git a/netbox/templates/dcim/inc/rack_elevation.html b/netbox/templates/dcim/inc/rack_elevation.html index bfcb76317..92ad859f7 100644 --- a/netbox/templates/dcim/inc/rack_elevation.html +++ b/netbox/templates/dcim/inc/rack_elevation.html @@ -8,6 +8,6 @@
- +
diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index f82ee0d4f..09495b4f7 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -317,13 +317,13 @@

Front

- {% include 'dcim/inc/rack_elevation.html' with primary_face=front_elevation secondary_face=rear_elevation face_id=0 reserved_units=rack.get_reserved_units %} + {% include 'dcim/inc/rack_elevation.html' with face="front" %}

Rear

- {% include 'dcim/inc/rack_elevation.html' with primary_face=rear_elevation secondary_face=front_elevation face_id=1 reserved_units=rack.get_reserved_units %} + {% include 'dcim/inc/rack_elevation.html' with face="rear" %}
From 1a9b9f50d823bbb58ad800a9bbd6d1c999a211bd Mon Sep 17 00:00:00 2001 From: hellerve Date: Wed, 20 Nov 2019 18:27:04 +0100 Subject: [PATCH 03/12] dcim: fix fonts & texts in svg --- netbox/dcim/api/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index d262abaac..61e989b3a 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -225,7 +225,9 @@ class RackElevationViewSet(ViewSet): elevation = rack.get_rear_elevation() else: return HttpResponseBadRequest('side should either be "front" or "back".') + drawing = svgwrite.Drawing(size=(230, len(elevation)*20), style="box-sizing: border-box") + drawing.defs.add(drawing.style('* { font-family: "Helvetica Neue"; }')) for i, u in enumerate(elevation): device = u['device'] @@ -234,7 +236,7 @@ class RackElevationViewSet(ViewSet): if device: link = drawing.add(drawing.a(reverse('dcim:device', kwargs={'pk': device.pk}), fill='black')) link.add(drawing.rect((0, start), (230, end), fill='#{}'.format(device.device_role.color), stroke='grey')) - link.add(drawing.text(device.name, insert=(0, start+20))) + link.add(drawing.text(device.name, insert=(115, start+10), text_anchor="middle", dominant_baseline="middle")) else: link = drawing.add( drawing.a('{}?{}'.format( From 77890ad77558b426d457b5230702f99519fba356 Mon Sep 17 00:00:00 2001 From: hellerve Date: Wed, 20 Nov 2019 19:14:54 +0100 Subject: [PATCH 04/12] dcim: add inline stylesheet to rack elevation api view --- netbox/dcim/api/views.py | 40 +++++++++++++++++-- netbox/templates/dcim/inc/rack_elevation.html | 2 +- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 61e989b3a..e747046cc 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -203,6 +203,37 @@ class RackViewSet(CustomFieldModelViewSet): return self.get_paginated_response(rack_units.data) +RACK_ELEVATION_STYLE = """ +* { + font-family: 'Helvetica Neue'; + font-size: 13px; +} +rect { + box-sizing: border-box; +} + +.rack { + background-color: #f0f0f0; + fill: none; + stroke: black; + stroke-width: 3px; +} +.empty { + fill: #f9f9f9; + stroke: grey; +} +.empty:hover { + fill: #fff; +} +.empty+.add-device { + fill: none; +} +.empty:hover+.add-device { + fill: blue; +} +""" + + class RackElevationViewSet(ViewSet): queryset = Rack.objects.prefetch_related( 'devices' @@ -226,8 +257,8 @@ class RackElevationViewSet(ViewSet): else: return HttpResponseBadRequest('side should either be "front" or "back".') - drawing = svgwrite.Drawing(size=(230, len(elevation)*20), style="box-sizing: border-box") - drawing.defs.add(drawing.style('* { font-family: "Helvetica Neue"; }')) + drawing = svgwrite.Drawing(size=(230, len(elevation)*20)) + drawing.defs.add(drawing.style(RACK_ELEVATION_STYLE)) for i, u in enumerate(elevation): device = u['device'] @@ -244,9 +275,10 @@ class RackElevationViewSet(ViewSet): urlencode({'rack': rack.pk, 'site': rack.site.pk, 'face': 0, 'position': u['id']}) )) ) - link.add(drawing.rect((0, start), (230, end), fill='white', stroke='lightgrey')) + link.add(drawing.rect((0, start), (230, end), class_='empty')) + link.add(drawing.text("add device", insert=(115, start+10), text_anchor="middle", dominant_baseline="middle", class_="add-device")) - drawing.add(drawing.rect((0, 0), (230, len(elevation*20)), stroke='black', stroke_width=3, fill='none')) + drawing.add(drawing.rect((0, 0), (230, len(elevation*20)), class_='rack')) return HttpResponse(drawing.tostring(), content_type='image/svg+xml') diff --git a/netbox/templates/dcim/inc/rack_elevation.html b/netbox/templates/dcim/inc/rack_elevation.html index 92ad859f7..1334465b3 100644 --- a/netbox/templates/dcim/inc/rack_elevation.html +++ b/netbox/templates/dcim/inc/rack_elevation.html @@ -8,6 +8,6 @@
- +
From 5d5b7e0dd92c01276c611990f87d0778cc48029a Mon Sep 17 00:00:00 2001 From: hellerve Date: Thu, 21 Nov 2019 16:58:07 +0100 Subject: [PATCH 05/12] dcim: refactor reservations and make them resizable --- netbox/dcim/api/views.py | 177 ++++++++++++++---- netbox/templates/dcim/rack.html | 4 +- .../templates/dcim/rack_elevation_list.html | 4 +- 3 files changed, 147 insertions(+), 38 deletions(-) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index e747046cc..5122b10eb 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -31,6 +31,8 @@ from utilities.api import ( get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable, ) from utilities.utils import get_subquery +# XXX: should this be moved to a util function so that we don’t have to import templatetags? +from utilities.templatetags.helpers import fgcolor from virtualization.models import VirtualMachine from . import serializers from .exceptions import MissingFilterException @@ -211,26 +213,50 @@ RACK_ELEVATION_STYLE = """ rect { box-sizing: border-box; } - +text { + text-anchor: middle; + dominant-baseline: middle; +} .rack { background-color: #f0f0f0; fill: none; stroke: black; stroke-width: 3px; } -.empty { - fill: #f9f9f9; - stroke: grey; +.slot { + fill: #f7f7f7; + stroke: #a0a0a0; } -.empty:hover { +.slot:hover { fill: #fff; } -.empty+.add-device { +.slot+.add-device { fill: none; } -.empty:hover+.add-device { +.slot:hover+.add-device { fill: blue; } +.reserved { + fill: url(#reserved); +} +.reserved:hover { + fill: url(#reserved); +} +.occupied { + fill: url(#occupied); +} +.occupied:hover { + fill: url(#occupied); +} +.blocked { + fill: url(#blocked); +} +.blocked:hover { + fill: url(#blocked); +} +.blocked:hover+.add-device { + fill: none; +} """ @@ -242,6 +268,96 @@ class RackElevationViewSet(ViewSet): def get_view_name(self): return "Rack Elevations" + def _add_gradient(self, drawing, id_, color): + gradient = drawing.linearGradient(start=('0', '20%'), end=('0', '40%'), spreadMethod='repeat', id_=id_, gradientTransform='rotate(80)') + gradient.add_stop_color(offset='0%', color='#f7f7f7') + gradient.add_stop_color(offset='50%', color='#f7f7f7') + gradient.add_stop_color(offset='50%', color=color) + gradient.add_stop_color(offset='100%', color=color) + drawing.defs.add(gradient) + + def _setup_drawing(self, width, height): + drawing = svgwrite.Drawing(size=(width, height)) + + # add the stylesheet + drawing.defs.add(drawing.style(RACK_ELEVATION_STYLE)) + + # add gradients + self._add_gradient(drawing, 'reserved', '#c7c7ff') + self._add_gradient(drawing, 'occupied', '#f0f0f0') + self._add_gradient(drawing, 'blocked', '#ffc7c7') + + return drawing + + def _draw_device_front(self, drawing, device, start, end, text): + color = device.device_role.color + link = drawing.add( + drawing.a( + reverse('dcim:device', kwargs={'pk': device.pk}), fill='black' + ) + ) + link.add( + drawing.rect(start, end, fill='#{}'.format(color)) + ) + link.add( + drawing.text(device.name, insert=text, fill=fgcolor(color)) + ) + + def _draw_device_rear(self, drawing, device, start, end, text): + drawing.add(drawing.rect(start, end, class_="blocked")) + drawing.add(drawing.text(device.name, insert=text)) + + def _draw_empty(self, rack, drawing, start, end, text, id_, face_id, class_): + link = drawing.add( + drawing.a('{}?{}'.format( + reverse('dcim:device_add'), + urlencode({'rack': rack.pk, 'site': rack.site.pk, 'face': face_id, 'position': id_}) + )) + ) + link.add(drawing.rect(start, end, class_=class_)) + link.add(drawing.text("add device", insert=text, class_='add-device')) + + def _draw_elevations(self, rack, elevation, reserved, face_id, width, slot_height): + drawing = self._setup_drawing(width, slot_height*rack.u_height) + i = 0 + for u in elevation: + device = u['device'] + height = u['height'] + start_y = i * slot_height + end_y = slot_height * height + start = (0, start_y) + end = (width, end_y) + text = (width/2, start_y + end_y/2) + if device and device.face == face_id: + self._draw_device_front(drawing, device, start, end, text) + elif device and device.device_type.is_full_depth: + self._draw_device_rear(drawing, device, start, end, text) + else: + class_ = 'slot' + if device: + class_ += ' occupied' + if u["id"] in reserved: + class_ += ' reserved' + self._draw_empty( + rack, drawing, start, end, text, u["id"], face_id, class_ + ) + i += height + drawing.add(drawing.rect((0, 0), (width, rack.u_height*slot_height), class_='rack')) + return drawing + + def _get_elevation(self, rack): + elevation = OrderedDict() + for u in rack.units: + elevation[u] = {'id': u, 'device': None, 'height': 1} + + for device in Device.objects.prefetch_related('device_role')\ + .filter(rack=rack, position__gt=0): + elevation[device.position]['device'] = device + elevation[device.position]['height'] = device.device_type.u_height + for u in range(device.position + 1, device.position + device.device_type.u_height): + elevation.pop(u, None) + + return elevation.values() def retrieve(self, request, pk=None): """ @@ -249,36 +365,29 @@ class RackElevationViewSet(ViewSet): """ rack = get_object_or_404(Rack, pk=pk) - side = request.GET.get('face', 'front') - if side == 'front': - elevation = rack.get_front_elevation() - elif side == 'rear': - elevation = rack.get_rear_elevation() - else: - return HttpResponseBadRequest('side should either be "front" or "back".') + face_id = request.GET.get('face', '0') + if face_id not in ['0', '1']: + return HttpResponseBadRequest('side should either be "0" or "1".') + # this is safe because of the validation above + face_id = int(face_id) - drawing = svgwrite.Drawing(size=(230, len(elevation)*20)) - drawing.defs.add(drawing.style(RACK_ELEVATION_STYLE)) + width = request.GET.get('width', '230') + try: + width = int(width) + except ValueError: + return HttpResponseBadRequest('width must be numeric.') - for i, u in enumerate(elevation): - device = u['device'] - start = i * 20 - end = 20 - if device: - link = drawing.add(drawing.a(reverse('dcim:device', kwargs={'pk': device.pk}), fill='black')) - link.add(drawing.rect((0, start), (230, end), fill='#{}'.format(device.device_role.color), stroke='grey')) - link.add(drawing.text(device.name, insert=(115, start+10), text_anchor="middle", dominant_baseline="middle")) - else: - link = drawing.add( - drawing.a('{}?{}'.format( - reverse('dcim:device_add'), - urlencode({'rack': rack.pk, 'site': rack.site.pk, 'face': 0, 'position': u['id']}) - )) - ) - link.add(drawing.rect((0, start), (230, end), class_='empty')) - link.add(drawing.text("add device", insert=(115, start+10), text_anchor="middle", dominant_baseline="middle", class_="add-device")) + slot_height = request.GET.get('slot_height', '20') + try: + slot_height = int(slot_height) + except ValueError: + return HttpResponseBadRequest('slot_height must be numeric.') - drawing.add(drawing.rect((0, 0), (230, len(elevation*20)), class_='rack')) + elevation = self._get_elevation(rack) + + reserved = rack.get_reserved_units().keys() + + drawing = self._draw_elevations(rack, elevation, reserved, face_id, width, slot_height) return HttpResponse(drawing.tostring(), content_type='image/svg+xml') diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 09495b4f7..e30ef19b6 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -317,13 +317,13 @@

Front

- {% include 'dcim/inc/rack_elevation.html' with face="front" %} + {% include 'dcim/inc/rack_elevation.html' with face=0 %}

Rear

- {% include 'dcim/inc/rack_elevation.html' with face="rear" %} + {% include 'dcim/inc/rack_elevation.html' with face=1 %}
diff --git a/netbox/templates/dcim/rack_elevation_list.html b/netbox/templates/dcim/rack_elevation_list.html index a500bb3bb..7fd07311e 100644 --- a/netbox/templates/dcim/rack_elevation_list.html +++ b/netbox/templates/dcim/rack_elevation_list.html @@ -18,9 +18,9 @@

{{ rack.facility_id|truncatechars:"30" }}

{% if face_id %} - {% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_rear_elevation secondary_face=rack.get_front_elevation face_id=1 reserved_units=rack.get_reserved_units %} + {% include 'dcim/inc/rack_elevation.html' with face=1 %} {% else %} - {% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_front_elevation secondary_face=rack.get_rear_elevation face_id=0 reserved_units=rack.get_reserved_units %} + {% include 'dcim/inc/rack_elevation.html' with face=0 %} {% endif %}
From e1fdc7773a921d9dfd9e15ad6890baaf609eb4f1 Mon Sep 17 00:00:00 2001 From: hellerve Date: Thu, 21 Nov 2019 17:00:43 +0100 Subject: [PATCH 06/12] requirements: fix svgwrite version --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 9a6cf297a..443b79d29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,3 +20,4 @@ psycopg2-binary==2.8.3 py-gfm==0.1.4 pycryptodome==3.8.2 redis==3.3.11 +svgwrite==1.1.9 From bc859c6139e15aa96313edc0c763ae89d2231bb0 Mon Sep 17 00:00:00 2001 From: hellerve Date: Thu, 21 Nov 2019 17:08:13 +0100 Subject: [PATCH 07/12] css: purge outdated rack styling --- netbox/project-static/css/base.css | 188 ----------------------------- 1 file changed, 188 deletions(-) diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index 9d4c099f4..45babe70b 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -187,194 +187,6 @@ ul.rack_legend li { padding: 5px 0; text-align: right; } -div.rack_frame { - float: left; - position: relative; -} -ul.rack { - border: 2px solid #404040; - float: left; - list-style-type: none; - padding: 0; - position: absolute; - width: 230px; -} -ul.rack li { - border-top: 1px solid #e0e0e0; - display: block; - font-size: 13px; - height: 20px; - overflow: hidden; - text-align: center; -} -ul.rack li.h2u { height: 40px; } -ul.rack li.h2u a, ul.rack li.h2u span { padding: 10px 0; } -ul.rack li.h3u { height: 60px; } -ul.rack li.h3u a, ul.rack li.h3u span { padding: 20px 0; } -ul.rack li.h4u { height: 80px; } -ul.rack li.h4u a, ul.rack li.h4u span { padding: 30px 0; } -ul.rack li.h5u { height: 100px; } -ul.rack li.h5u a, ul.rack li.h5u span { padding: 40px 0; } -ul.rack li.h6u { height: 120px; } -ul.rack li.h6u a, ul.rack li.h6u span { padding: 50px 0; } -ul.rack li.h7u { height: 140px; } -ul.rack li.h7u a, ul.rack li.h7u span { padding: 60px 0; } -ul.rack li.h8u { height: 160px; } -ul.rack li.h8u a, ul.rack li.h8u span { padding: 70px 0; } -ul.rack li.h9u { height: 180px; } -ul.rack li.h9u a, ul.rack li.h9u span { padding: 80px 0; } -ul.rack li.h10u { height: 200px; } -ul.rack li.h10u a, ul.rack li.h10u span { padding: 90px 0; } -ul.rack li.h11u { height: 220px; } -ul.rack li.h11u a, ul.rack li.h11u span { padding: 100px 0; } -ul.rack li.h12u { height: 240px; } -ul.rack li.h12u a, ul.rack li.h12u span { padding: 110px 0; } -ul.rack li.h13u { height: 260px; } -ul.rack li.h13u a, ul.rack li.h13u span { padding: 120px 0; } -ul.rack li.h14u { height: 280px; } -ul.rack li.h14u a, ul.rack li.h14u span { padding: 130px 0; } -ul.rack li.h15u { height: 300px; } -ul.rack li.h15u a, ul.rack li.h15u span { padding: 140px 0; } -ul.rack li.h16u { height: 320px; } -ul.rack li.h16u a, ul.rack li.h16u span { padding: 150px 0; } -ul.rack li.h17u { height: 340px; } -ul.rack li.h17u a, ul.rack li.h17u span { padding: 160px 0; } -ul.rack li.h18u { height: 360px; } -ul.rack li.h18u a, ul.rack li.h18u span { padding: 170px 0; } -ul.rack li.h19u { height: 380px; } -ul.rack li.h19u a, ul.rack li.h19u span { padding: 180px 0; } -ul.rack li.h20u { height: 400px; } -ul.rack li.h20u a, ul.rack li.h20u span { padding: 190px 0; } -ul.rack li.h21u { height: 420px; } -ul.rack li.h21u a, ul.rack li.h21u span { padding: 200px 0; } -ul.rack li.h22u { height: 440px; } -ul.rack li.h22u a, ul.rack li.h22u span { padding: 210px 0; } -ul.rack li.h23u { height: 460px; } -ul.rack li.h23u a, ul.rack li.h23u span { padding: 220px 0; } -ul.rack li.h24u { height: 480px; } -ul.rack li.h24u a, ul.rack li.h24u span { padding: 230px 0; } -ul.rack li.h25u { height: 500px; } -ul.rack li.h25u a, ul.rack li.h25u span { padding: 240px 0; } -ul.rack li.h26u { height: 520px; } -ul.rack li.h26u a, ul.rack li.h26u span { padding: 250px 0; } -ul.rack li.h27u { height: 540px; } -ul.rack li.h27u a, ul.rack li.h27u span { padding: 260px 0; } -ul.rack li.h28u { height: 560px; } -ul.rack li.h28u a, ul.rack li.h28u span { padding: 270px 0; } -ul.rack li.h29u { height: 580px; } -ul.rack li.h29u a, ul.rack li.h29u span { padding: 280px 0; } -ul.rack li.h30u { height: 600px; } -ul.rack li.h30u a, ul.rack li.h30u span { padding: 290px 0; } -ul.rack li.h31u { height: 620px; } -ul.rack li.h31u a, ul.rack li.h31u span { padding: 300px 0; } -ul.rack li.h32u { height: 640px; } -ul.rack li.h32u a, ul.rack li.h32u span { padding: 310px 0; } -ul.rack li.h33u { height: 660px; } -ul.rack li.h33u a, ul.rack li.h33u span { padding: 320px 0; } -ul.rack li.h34u { height: 680px; } -ul.rack li.h34u a, ul.rack li.h34u span { padding: 330px 0; } -ul.rack li.h35u { height: 700px; } -ul.rack li.h35u a, ul.rack li.h35u span { padding: 340px 0; } -ul.rack li.h36u { height: 720px; } -ul.rack li.h36u a, ul.rack li.h36u span { padding: 350px 0; } -ul.rack li.h37u { height: 740px; } -ul.rack li.h37u a, ul.rack li.h37u span { padding: 360px 0; } -ul.rack li.h38u { height: 760px; } -ul.rack li.h38u a, ul.rack li.h38u span { padding: 370px 0; } -ul.rack li.h39u { height: 780px; } -ul.rack li.h39u a, ul.rack li.h39u span { padding: 380px 0; } -ul.rack li.h40u { height: 800px; } -ul.rack li.h40u a, ul.rack li.h40u span { padding: 390px 0; } -ul.rack li.h41u { height: 820px; } -ul.rack li.h41u a, ul.rack li.h41u span { padding: 400px 0; } -ul.rack li.h42u { height: 840px; } -ul.rack li.h42u a, ul.rack li.h42u span { padding: 410px 0; } -ul.rack li.h43u { height: 860px; } -ul.rack li.h43u a, ul.rack li.h43u span { padding: 420px 0; } -ul.rack li.h44u { height: 880px; } -ul.rack li.h44u a, ul.rack li.h44u span { padding: 430px 0; } -ul.rack li.h45u { height: 900px; } -ul.rack li.h45u a, ul.rack li.h45u span { padding: 440px 0; } -ul.rack li.h46u { height: 920px; } -ul.rack li.h46u a, ul.rack li.h46u span { padding: 450px 0; } -ul.rack li.h47u { height: 940px; } -ul.rack li.h47u a, ul.rack li.h47u span { padding: 460px 0; } -ul.rack li.h48u { height: 960px; } -ul.rack li.h48u a, ul.rack li.h48u span { padding: 470px 0; } -ul.rack li.h49u { height: 980px; } -ul.rack li.h49u a, ul.rack li.h49u span { padding: 480px 0; } -ul.rack li.h50u { height: 1000px; } -ul.rack li.h50u a, ul.rack li.h50u span { padding: 490px 0; } -ul.rack_far_face { - background-color: #f7f7f7; - z-index: 100; -} -ul.rack_far_face li.occupied { - background: repeating-linear-gradient( - 45deg, - #f7f7f7, - #f7f7f7 7px, - #f0f0f0 7px, - #f0f0f0 14px - ); -} -ul.rack_far_face li.blocked { - background: repeating-linear-gradient( - 45deg, - #f7f7f7, - #f7f7f7 7px, - #ffc7c7 7px, - #ffc7c7 14px - ); -} -ul.rack_near_face li.reserved { - background: repeating-linear-gradient( - 45deg, - #f7f7f7, - #f7f7f7 7px, - #c7c7ff 7px, - #c7c7ff 14px - ); -} -ul.rack_near_face { - z-index: 200; -} -ul.rack_near_face li.occupied { - border-top: 1px solid #474747; - color: #474747; -} -ul.rack_near_face li.occupied:hover { - background-image: url('../img/tint_20.png'); -} -ul.rack_near_face li:first-child { - border-top: 0; -} -ul.rack_near_face li.available a { - color: #0000ff; - display: none; - text-decoration: none; -} -ul.rack_near_face li.available:hover { - background-color: #ffffff; -} -ul.rack_near_face li.available:hover a { - display: block; -} -ul.rack li.occupied a { - color: #ffffff; - display: block; - font-weight: bold; -} -ul.rack li.occupied a:hover { - text-decoration: none; -} -ul.rack li.occupied span { - cursor: default; - display: block; -} -li.occupied + li.available { - border-top: 1px solid #474747; -} /* Devices */ table.component-list td.subtable { From 82819d54290a240e63fc7137f37a12a7a635b54e Mon Sep 17 00:00:00 2001 From: hellerve Date: Thu, 21 Nov 2019 17:21:48 +0100 Subject: [PATCH 08/12] dcim: remove elevation getters --- netbox/dcim/models.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 2297909b4..0a78f862f 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -670,14 +670,13 @@ class Rack(ChangeLoggedModel, CustomFieldModel): def get_status_class(self): return self.STATUS_CLASS_MAP.get(self.status) - def get_rack_units(self, face=DeviceFaceChoices.FACE_FRONT, exclude=None, remove_redundant=False): + def get_rack_units(self, face=Device.FaceChoices.FACE_FRONT, exclude=None): """ Return a list of rack units as dictionaries. Example: {'device': None, 'face': 0, 'id': 48, 'name': 'U48'} Each key 'device' is either a Device or None. By default, multi-U devices are repeated for each U they occupy. :param face: Rack face (front or rear) :param exclude: PK of a Device to exclude (optional); helpful when relocating a Device within a Rack - :param remove_redundant: If True, rack units occupied by a device already listed will be omitted """ elevation = OrderedDict() @@ -691,22 +690,11 @@ class Rack(ChangeLoggedModel, CustomFieldModel): .exclude(pk=exclude)\ .filter(rack=self, position__gt=0)\ .filter(Q(face=face) | Q(device_type__is_full_depth=True)): - if remove_redundant: - elevation[device.position]['device'] = device - for u in range(device.position + 1, device.position + device.device_type.u_height): - elevation.pop(u, None) - else: - for u in range(device.position, device.position + device.device_type.u_height): - elevation[u]['device'] = device + for u in range(device.position, device.position + device.device_type.u_height): + elevation[u]['device'] = device return [u for u in elevation.values()] - def get_front_elevation(self): - return self.get_rack_units(face=DeviceFaceChoices.FACE_FRONT, remove_redundant=True) - - def get_rear_elevation(self): - return self.get_rack_units(face=DeviceFaceChoices.FACE_REAR, remove_redundant=True) - def get_available_units(self, u_height=1, rack_face=None, exclude=list()): """ Return a list of units within the rack available to accommodate a device of a given U height (default 1). From 66b6f586f1639020a8168424d9215344d59a2fb0 Mon Sep 17 00:00:00 2001 From: hellerve Date: Thu, 21 Nov 2019 17:36:37 +0100 Subject: [PATCH 09/12] tests: update to reflect absence of utility functions --- netbox/dcim/tests/test_models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 2c3507758..634ccb3ca 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -125,14 +125,14 @@ class RackTestCase(TestCase): self.assertEqual(list(self.rack.units), list(reversed(range(1, 43)))) # Validate inventory (front face) - rack1_inventory_front = self.rack.get_front_elevation() + rack1_inventory_front = self.rack.get_rack_units(face=RACK_FACE_FRONT) self.assertEqual(rack1_inventory_front[-10]['device'], device1) del(rack1_inventory_front[-10]) for u in rack1_inventory_front: self.assertIsNone(u['device']) # Validate inventory (rear face) - rack1_inventory_rear = self.rack.get_rear_elevation() + rack1_inventory_rear = self.rack.get_rack_units(face=RACK_FACE_REAR) self.assertEqual(rack1_inventory_rear[-10]['device'], device1) del(rack1_inventory_rear[-10]) for u in rack1_inventory_rear: From b54a05d2558b05b349b8144dfca93f0f48509420 Mon Sep 17 00:00:00 2001 From: hellerve Date: Thu, 21 Nov 2019 17:49:39 +0100 Subject: [PATCH 10/12] dcim: make linter happy --- netbox/dcim/api/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 5122b10eb..d66bc020b 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -318,7 +318,7 @@ class RackElevationViewSet(ViewSet): link.add(drawing.text("add device", insert=text, class_='add-device')) def _draw_elevations(self, rack, elevation, reserved, face_id, width, slot_height): - drawing = self._setup_drawing(width, slot_height*rack.u_height) + drawing = self._setup_drawing(width, slot_height * rack.u_height) i = 0 for u in elevation: device = u['device'] @@ -327,7 +327,7 @@ class RackElevationViewSet(ViewSet): end_y = slot_height * height start = (0, start_y) end = (width, end_y) - text = (width/2, start_y + end_y/2) + text = (width / 2, start_y + end_y / 2) if device and device.face == face_id: self._draw_device_front(drawing, device, start, end, text) elif device and device.device_type.is_full_depth: @@ -342,7 +342,7 @@ class RackElevationViewSet(ViewSet): rack, drawing, start, end, text, u["id"], face_id, class_ ) i += height - drawing.add(drawing.rect((0, 0), (width, rack.u_height*slot_height), class_='rack')) + drawing.add(drawing.rect((0, 0), (width, rack.u_height * slot_height), class_='rack')) return drawing def _get_elevation(self, rack): From e5b49e25b9153c53f9551dda065d7a0bc28c266f Mon Sep 17 00:00:00 2001 From: hellerve Date: Sun, 8 Dec 2019 18:14:59 +0100 Subject: [PATCH 11/12] dcim api: add feedback from @jeremystretch to rack elevations api --- netbox/dcim/api/serializers.py | 2 +- netbox/dcim/api/views.py | 100 ++++++------------------------- netbox/dcim/constants.py | 55 +++++++++++++++++ netbox/dcim/models.py | 2 +- netbox/dcim/tests/test_models.py | 4 +- netbox/templates/dcim/rack.html | 4 +- 6 files changed, 80 insertions(+), 87 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 3c4170373..84a08107b 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -157,7 +157,7 @@ class RackUnitSerializer(serializers.Serializer): """ id = serializers.IntegerField(read_only=True) name = serializers.CharField(read_only=True) - face = serializers.IntegerField(read_only=True) + face = ChoiceField(choices=DeviceFaceChoices, required=False, allow_null=True) device = NestedDeviceSerializer(read_only=True) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index d66bc020b..22246ae2d 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -15,7 +15,7 @@ from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet, ViewSet from circuits.models import Circuit -from dcim import filters +from dcim import constants, filters from dcim.models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, @@ -30,9 +30,7 @@ from ipam.models import Prefix, VLAN from utilities.api import ( get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable, ) -from utilities.utils import get_subquery -# XXX: should this be moved to a util function so that we don’t have to import templatetags? -from utilities.templatetags.helpers import fgcolor +from utilities.utils import get_subquery, foreground_color from virtualization.models import VirtualMachine from . import serializers from .exceptions import MissingFilterException @@ -205,61 +203,6 @@ class RackViewSet(CustomFieldModelViewSet): return self.get_paginated_response(rack_units.data) -RACK_ELEVATION_STYLE = """ -* { - font-family: 'Helvetica Neue'; - font-size: 13px; -} -rect { - box-sizing: border-box; -} -text { - text-anchor: middle; - dominant-baseline: middle; -} -.rack { - background-color: #f0f0f0; - fill: none; - stroke: black; - stroke-width: 3px; -} -.slot { - fill: #f7f7f7; - stroke: #a0a0a0; -} -.slot:hover { - fill: #fff; -} -.slot+.add-device { - fill: none; -} -.slot:hover+.add-device { - fill: blue; -} -.reserved { - fill: url(#reserved); -} -.reserved:hover { - fill: url(#reserved); -} -.occupied { - fill: url(#occupied); -} -.occupied:hover { - fill: url(#occupied); -} -.blocked { - fill: url(#blocked); -} -.blocked:hover { - fill: url(#blocked); -} -.blocked:hover+.add-device { - fill: none; -} -""" - - class RackElevationViewSet(ViewSet): queryset = Rack.objects.prefetch_related( 'devices' @@ -280,7 +223,7 @@ class RackElevationViewSet(ViewSet): drawing = svgwrite.Drawing(size=(width, height)) # add the stylesheet - drawing.defs.add(drawing.style(RACK_ELEVATION_STYLE)) + drawing.defs.add(drawing.style(constants.RACK_ELEVATION_STYLE)) # add gradients self._add_gradient(drawing, 'reserved', '#c7c7ff') @@ -296,12 +239,9 @@ class RackElevationViewSet(ViewSet): reverse('dcim:device', kwargs={'pk': device.pk}), fill='black' ) ) - link.add( - drawing.rect(start, end, fill='#{}'.format(color)) - ) - link.add( - drawing.text(device.name, insert=text, fill=fgcolor(color)) - ) + link.add(drawing.rect(start, end, fill='#{}'.format(color))) + hex_color = '#{}'.format(foreground_color(color)) + link.add(drawing.text(device.name, insert=text, fill=hex_color)) def _draw_device_rear(self, drawing, device, start, end, text): drawing.add(drawing.rect(start, end, class_="blocked")) @@ -317,14 +257,14 @@ class RackElevationViewSet(ViewSet): link.add(drawing.rect(start, end, class_=class_)) link.add(drawing.text("add device", insert=text, class_='add-device')) - def _draw_elevations(self, rack, elevation, reserved, face_id, width, slot_height): - drawing = self._setup_drawing(width, slot_height * rack.u_height) + def _draw_elevations(self, rack, elevation, reserved, face_id, width, u_height): + drawing = self._setup_drawing(width, u_height * rack.u_height) i = 0 for u in elevation: device = u['device'] height = u['height'] - start_y = i * slot_height - end_y = slot_height * height + start_y = i * u_height + end_y = u_height * height start = (0, start_y) end = (width, end_y) text = (width / 2, start_y + end_y / 2) @@ -342,7 +282,7 @@ class RackElevationViewSet(ViewSet): rack, drawing, start, end, text, u["id"], face_id, class_ ) i += height - drawing.add(drawing.rect((0, 0), (width, rack.u_height * slot_height), class_='rack')) + drawing.add(drawing.rect((0, 0), (width, rack.u_height * u_height), class_='rack')) return drawing def _get_elevation(self, rack): @@ -366,28 +306,26 @@ class RackElevationViewSet(ViewSet): rack = get_object_or_404(Rack, pk=pk) face_id = request.GET.get('face', '0') - if face_id not in ['0', '1']: - return HttpResponseBadRequest('side should either be "0" or "1".') - # this is safe because of the validation above - face_id = int(face_id) + if face_id not in ['front', 'rear']: + return HttpResponseBadRequest('face should either be "front" or "rear".') - width = request.GET.get('width', '230') + width = request.GET.get('u_width', '230') try: width = int(width) except ValueError: - return HttpResponseBadRequest('width must be numeric.') + return HttpResponseBadRequest('u_width must be numeric.') - slot_height = request.GET.get('slot_height', '20') + u_height = request.GET.get('u_height', '20') try: - slot_height = int(slot_height) + u_height = int(u_height) except ValueError: - return HttpResponseBadRequest('slot_height must be numeric.') + return HttpResponseBadRequest('u_height must be numeric.') elevation = self._get_elevation(rack) reserved = rack.get_reserved_units().keys() - drawing = self._draw_elevations(rack, elevation, reserved, face_id, width, slot_height) + drawing = self._draw_elevations(rack, elevation, reserved, face_id, width, u_height) return HttpResponse(drawing.tostring(), content_type='image/svg+xml') diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 8dacd68f5..a33682f51 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -55,3 +55,58 @@ COMPATIBLE_TERMINATION_TYPES = { 'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'], 'circuittermination': ['interface', 'frontport', 'rearport'], } + + +RACK_ELEVATION_STYLE = """ +* { + font-family: 'Helvetica Neue'; + font-size: 13px; +} +rect { + box-sizing: border-box; +} +text { + text-anchor: middle; + dominant-baseline: middle; +} +.rack { + background-color: #f0f0f0; + fill: none; + stroke: black; + stroke-width: 3px; +} +.slot { + fill: #f7f7f7; + stroke: #a0a0a0; +} +.slot:hover { + fill: #fff; +} +.slot+.add-device { + fill: none; +} +.slot:hover+.add-device { + fill: blue; +} +.reserved { + fill: url(#reserved); +} +.reserved:hover { + fill: url(#reserved); +} +.occupied { + fill: url(#occupied); +} +.occupied:hover { + fill: url(#occupied); +} +.blocked { + fill: url(#blocked); +} +.blocked:hover { + fill: url(#blocked); +} +.blocked:hover+.add-device { + fill: none; +} +""" diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 0a78f862f..244e61e54 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -670,7 +670,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel): def get_status_class(self): return self.STATUS_CLASS_MAP.get(self.status) - def get_rack_units(self, face=Device.FaceChoices.FACE_FRONT, exclude=None): + def get_rack_units(self, face=DeviceFaceChoices.FACE_FRONT, exclude=None): """ Return a list of rack units as dictionaries. Example: {'device': None, 'face': 0, 'id': 48, 'name': 'U48'} Each key 'device' is either a Device or None. By default, multi-U devices are repeated for each U they occupy. diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 634ccb3ca..9d7772804 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -125,14 +125,14 @@ class RackTestCase(TestCase): self.assertEqual(list(self.rack.units), list(reversed(range(1, 43)))) # Validate inventory (front face) - rack1_inventory_front = self.rack.get_rack_units(face=RACK_FACE_FRONT) + rack1_inventory_front = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_FRONT) self.assertEqual(rack1_inventory_front[-10]['device'], device1) del(rack1_inventory_front[-10]) for u in rack1_inventory_front: self.assertIsNone(u['device']) # Validate inventory (rear face) - rack1_inventory_rear = self.rack.get_rack_units(face=RACK_FACE_REAR) + rack1_inventory_rear = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_REAR) self.assertEqual(rack1_inventory_rear[-10]['device'], device1) del(rack1_inventory_rear[-10]) for u in rack1_inventory_rear: diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index e30ef19b6..0cc261a27 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -317,13 +317,13 @@

Front

- {% include 'dcim/inc/rack_elevation.html' with face=0 %} + {% include 'dcim/inc/rack_elevation.html' with face='front' %}

Rear

- {% include 'dcim/inc/rack_elevation.html' with face=1 %} + {% include 'dcim/inc/rack_elevation.html' with face='rear' %}
From 44b042e3fc051ceb39e8a644dbe1f5b61f9701b0 Mon Sep 17 00:00:00 2001 From: hellerve Date: Sun, 8 Dec 2019 18:24:13 +0100 Subject: [PATCH 12/12] dcim api: fix face default value in rackviewset --- netbox/dcim/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 22246ae2d..db4f4f154 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -183,7 +183,7 @@ class RackViewSet(CustomFieldModelViewSet): List rack units (by rack) """ rack = get_object_or_404(Rack, pk=pk) - face = request.GET.get('face', 0) + face = request.GET.get('face', 'front') exclude_pk = request.GET.get('exclude', None) if exclude_pk is not None: try: