From a6d937b366931670d8cc19b5dd6564f2efee3d85 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 10 May 2021 11:23:21 -0400 Subject: [PATCH 1/6] Closes #5806: Add kilometer and mile as choices for cable length unit --- docs/release-notes/version-2.12.md | 4 ++++ netbox/dcim/choices.py | 7 +++++++ netbox/utilities/utils.py | 6 +++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.12.md b/docs/release-notes/version-2.12.md index 598237e76..c69923583 100644 --- a/docs/release-notes/version-2.12.md +++ b/docs/release-notes/version-2.12.md @@ -2,6 +2,10 @@ ## v2.12-beta1 (FUTURE) +### Enhancements + +* [#5806](https://github.com/netbox-community/netbox/issues/5806) - Add kilometer and mile as choices for cable length unit + ### Other Changes * [#5532](https://github.com/netbox-community/netbox/issues/5532) - Drop support for Python 3.6 diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index c5646cf2b..be35a24c5 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1064,14 +1064,21 @@ class CableStatusChoices(ChoiceSet): class CableLengthUnitChoices(ChoiceSet): + # Metric + UNIT_KILOMETER = 'km' UNIT_METER = 'm' UNIT_CENTIMETER = 'cm' + + # Imperial + UNIT_MILE = 'mi' UNIT_FOOT = 'ft' UNIT_INCH = 'in' CHOICES = ( + (UNIT_KILOMETER, 'Kilometers'), (UNIT_METER, 'Meters'), (UNIT_CENTIMETER, 'Centimeters'), + (UNIT_MILE, 'Miles'), (UNIT_FOOT, 'Feet'), (UNIT_INCH, 'Inches'), ) diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index ce6753877..a14a468b6 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -198,15 +198,19 @@ def to_meters(length, unit): "Unknown unit {}. Must be one of the following: {}".format(unit, ', '.join(valid_units)) ) + if unit == CableLengthUnitChoices.UNIT_KILOMETER: + return length * 1000 if unit == CableLengthUnitChoices.UNIT_METER: return length if unit == CableLengthUnitChoices.UNIT_CENTIMETER: return length / 100 + if unit == CableLengthUnitChoices.UNIT_MILE: + return length * 1609.344 if unit == CableLengthUnitChoices.UNIT_FOOT: return length * 0.3048 if unit == CableLengthUnitChoices.UNIT_INCH: return length * 0.3048 * 12 - raise ValueError("Unknown unit {}. Must be 'm', 'cm', 'ft', or 'in'.".format(unit)) + raise ValueError(f"Unknown unit {unit}. Must be 'km', 'm', 'cm', 'mi', 'ft', or 'in'.") def render_jinja2(template_code, context): From f3abf84e1732ea6f748f921c720afb9538903fc7 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 10 May 2021 14:14:54 -0400 Subject: [PATCH 2/6] Closes #6154: Allow decimal values for cable lengths --- docs/release-notes/version-2.12.md | 3 +++ netbox/dcim/migrations/0132_cable_length.py | 16 ++++++++++++++++ netbox/dcim/models/cables.py | 4 +++- netbox/templates/dcim/cable.html | 2 +- netbox/templates/dcim/trace/cable.html | 2 +- 5 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 netbox/dcim/migrations/0132_cable_length.py diff --git a/docs/release-notes/version-2.12.md b/docs/release-notes/version-2.12.md index c69923583..c4402d689 100644 --- a/docs/release-notes/version-2.12.md +++ b/docs/release-notes/version-2.12.md @@ -5,6 +5,7 @@ ### Enhancements * [#5806](https://github.com/netbox-community/netbox/issues/5806) - Add kilometer and mile as choices for cable length unit +* [#6154](https://github.com/netbox-community/netbox/issues/6154) - Allow decimal values for cable lengths ### Other Changes @@ -13,6 +14,8 @@ ### REST API Changes +* dcim.Cable + * `length` is now a decimal value * dcim.Device * Removed the `display_name` attribute (use `display` instead) * dcim.DeviceType diff --git a/netbox/dcim/migrations/0132_cable_length.py b/netbox/dcim/migrations/0132_cable_length.py new file mode 100644 index 000000000..e20a8b8aa --- /dev/null +++ b/netbox/dcim/migrations/0132_cable_length.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0131_consoleport_speed'), + ] + + operations = [ + migrations.AlterField( + model_name='cable', + name='length', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True), + ), + ] diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index 28d21ff68..2b8f05206 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -74,7 +74,9 @@ class Cable(PrimaryModel): color = ColorField( blank=True ) - length = models.PositiveSmallIntegerField( + length = models.DecimalField( + max_digits=8, + decimal_places=2, blank=True, null=True ) diff --git a/netbox/templates/dcim/cable.html b/netbox/templates/dcim/cable.html index 61524d345..af55cd70b 100644 --- a/netbox/templates/dcim/cable.html +++ b/netbox/templates/dcim/cable.html @@ -60,7 +60,7 @@ Length {% if object.length %} - {{ object.length }} {{ object.get_length_unit_display }} + {{ object.length|floatformat }} {{ object.get_length_unit_display }} {% else %} {% endif %} diff --git a/netbox/templates/dcim/trace/cable.html b/netbox/templates/dcim/trace/cable.html index 43b4910f4..5f8fb01eb 100644 --- a/netbox/templates/dcim/trace/cable.html +++ b/netbox/templates/dcim/trace/cable.html @@ -10,7 +10,7 @@ {{ cable.get_type_display|default:"" }} {% endif %} {% if cable.length %} - ({{ cable.length }} {{ cable.get_length_unit_display }})
+ ({{ cable.length|floatformat }} {{ cable.get_length_unit_display }})
{% endif %} {{ cable.get_status_display }}
{% for tag in cable.tags.all %} From f6823b57045269117c510ada02cc390ef1184d2b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 10 May 2021 14:22:01 -0400 Subject: [PATCH 3/6] Closes #6338: Decimal fields are no longer coerced to strings in REST API --- docs/release-notes/version-2.12.md | 5 +++++ netbox/netbox/settings.py | 1 + 2 files changed, 6 insertions(+) diff --git a/docs/release-notes/version-2.12.md b/docs/release-notes/version-2.12.md index c4402d689..3adff7728 100644 --- a/docs/release-notes/version-2.12.md +++ b/docs/release-notes/version-2.12.md @@ -11,6 +11,7 @@ * [#5532](https://github.com/netbox-community/netbox/issues/5532) - Drop support for Python 3.6 * [#5994](https://github.com/netbox-community/netbox/issues/5994) - Drop support for `display_field` argument on ObjectVar +* [#6338](https://github.com/netbox-community/netbox/issues/6338) - Decimal fields are no longer coerced to strings in REST API ### REST API Changes @@ -22,9 +23,13 @@ * Removed the `display_name` attribute (use `display` instead) * dcim.Rack * Removed the `display_name` attribute (use `display` instead) +* dcim.Site + * `latitude` and `longitude` are now decimal fields rather than strings * extras.ContentType * Removed the `display_name` attribute (use `display` instead) * ipam.VLAN * Removed the `display_name` attribute (use `display` instead) * ipam.VRF * Removed the `display_name` attribute (use `display` instead) +* virtualization.VirtualMachine + * `vcpus` is now a decimal field rather than a string diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 71edae573..0d79bca25 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -464,6 +464,7 @@ FILTERS_NULL_CHOICE_VALUE = 'null' REST_FRAMEWORK_VERSION = VERSION.rsplit('.', 1)[0] # Use major.minor as API version REST_FRAMEWORK = { 'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION], + 'COERCE_DECIMAL_TO_STRING': False, 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.SessionAuthentication', 'netbox.api.authentication.TokenAuthentication', From bf56145a093063d3c634601e0b024f14f7ad5e9b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 11 May 2021 14:03:50 -0400 Subject: [PATCH 4/6] Closes #4609: Allow marking prefixes as fully utilized --- docs/release-notes/version-2.12.md | 3 +++ netbox/ipam/api/serializers.py | 2 +- netbox/ipam/filtersets.py | 2 +- netbox/ipam/forms.py | 20 +++++++++++++++---- .../migrations/0047_prefix_mark_utilized.py | 16 +++++++++++++++ netbox/ipam/models/ip.py | 16 +++++++++++++-- netbox/ipam/tables.py | 8 ++++++-- netbox/ipam/tests/test_filtersets.py | 10 ++++++++-- netbox/templates/ipam/prefix.html | 15 +++++++++----- 9 files changed, 75 insertions(+), 17 deletions(-) create mode 100644 netbox/ipam/migrations/0047_prefix_mark_utilized.py diff --git a/docs/release-notes/version-2.12.md b/docs/release-notes/version-2.12.md index 3adff7728..1d884de72 100644 --- a/docs/release-notes/version-2.12.md +++ b/docs/release-notes/version-2.12.md @@ -4,6 +4,7 @@ ### Enhancements +* [#4609](https://github.com/netbox-community/netbox/issues/4609) - Allow marking prefixes as fully utilized * [#5806](https://github.com/netbox-community/netbox/issues/5806) - Add kilometer and mile as choices for cable length unit * [#6154](https://github.com/netbox-community/netbox/issues/6154) - Allow decimal values for cable lengths @@ -27,6 +28,8 @@ * `latitude` and `longitude` are now decimal fields rather than strings * extras.ContentType * Removed the `display_name` attribute (use `display` instead) +* ipam.Prefix + * Added the `mark_utilized` boolean field * ipam.VLAN * Removed the `display_name` attribute (use `display` instead) * ipam.VRF diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 324c4de03..203cdc3fb 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -202,7 +202,7 @@ class PrefixSerializer(PrimaryModelSerializer): model = Prefix fields = [ 'id', 'url', 'display', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', - 'description', 'tags', 'custom_fields', 'created', 'last_updated', + 'mark_utilized', 'description', 'tags', 'custom_fields', 'created', 'last_updated', ] read_only_fields = ['family'] diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index 5ab4994ea..63165d8d2 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -304,7 +304,7 @@ class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet): class Meta: model = Prefix - fields = ['id', 'is_pool'] + fields = ['id', 'is_pool', 'mark_utilized'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 6a3753859..eed92dffd 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -454,11 +454,11 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class Meta: model = Prefix fields = [ - 'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant', - 'tags', + 'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', + 'tenant_group', 'tenant', 'tags', ] fieldsets = ( - ('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'description', 'tags')), + ('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')), ('Site/VLAN Assignment', ('region', 'site_group', 'site', 'vlan_group', 'vlan')), ('Tenancy', ('tenant_group', 'tenant')), ) @@ -582,6 +582,11 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF widget=BulkEditNullBooleanSelect(), label='Is a pool' ) + mark_utilized = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect(), + label='Treat as 100% utilized' + ) description = forms.CharField( max_length=100, required=False @@ -597,7 +602,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) model = Prefix field_order = [ 'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'present_in_vrf_id', 'status', 'region_id', - 'site_group_id', 'site_id', 'role_id', 'tenant_group_id', 'tenant_id', 'is_pool', + 'site_group_id', 'site_id', 'role_id', 'tenant_group_id', 'tenant_id', 'is_pool', 'mark_utilized', ] mask_length__lte = forms.IntegerField( widget=forms.HiddenInput() @@ -675,6 +680,13 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + mark_utilized = forms.NullBooleanField( + required=False, + label=_('Marked as 100% utilized'), + widget=StaticSelect2( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) tag = TagFilterField(model) diff --git a/netbox/ipam/migrations/0047_prefix_mark_utilized.py b/netbox/ipam/migrations/0047_prefix_mark_utilized.py new file mode 100644 index 000000000..332066b04 --- /dev/null +++ b/netbox/ipam/migrations/0047_prefix_mark_utilized.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0046_set_vlangroup_scope_types'), + ] + + operations = [ + migrations.AddField( + model_name='prefix', + name='mark_utilized', + field=models.BooleanField(default=False), + ), + ] diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 2490a0c5a..cf469c930 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -288,6 +288,10 @@ class Prefix(PrimaryModel): default=False, help_text='All IP addresses within this prefix are considered usable' ) + mark_utilized = models.BooleanField( + default=False, + help_text="Treat as 100% utilized" + ) description = models.CharField( max_length=200, blank=True @@ -296,10 +300,11 @@ class Prefix(PrimaryModel): objects = PrefixQuerySet.as_manager() csv_headers = [ - 'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'description', + 'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', + 'description', ] clone_fields = [ - 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description', + 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', ] class Meta: @@ -364,6 +369,7 @@ class Prefix(PrimaryModel): self.get_status_display(), self.role.name if self.role else None, self.is_pool, + self.mark_utilized, self.description, ) @@ -422,6 +428,9 @@ class Prefix(PrimaryModel): """ Return all available IPs within this prefix as an IPSet. """ + if self.mark_utilized: + return list() + prefix = netaddr.IPSet(self.prefix) child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()]) available_ips = prefix - child_ips @@ -461,6 +470,9 @@ class Prefix(PrimaryModel): Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of "container", calculate utilization based on child prefixes. For all others, count child IP addresses. """ + if self.mark_utilized: + return 100 + if self.status == PrefixStatusChoices.STATUS_CONTAINER: queryset = Prefix.objects.filter( prefix__net_contained=str(self.prefix), diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 0bbaddb52..aa4119fa7 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -283,11 +283,15 @@ class PrefixTable(BaseTable): is_pool = BooleanColumn( verbose_name='Pool' ) + mark_utilized = BooleanColumn( + verbose_name='Marked Utilized' + ) class Meta(BaseTable.Meta): model = Prefix fields = ( - 'pk', 'prefix', 'status', 'children', 'vrf', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'description', + 'pk', 'prefix', 'status', 'children', 'vrf', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'mark_utilized', + 'description', ) default_columns = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description') row_attrs = { @@ -308,7 +312,7 @@ class PrefixDetailTable(PrefixTable): class Meta(PrefixTable.Meta): fields = ( 'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'is_pool', - 'description', 'tags', + 'mark_utilized', 'description', 'tags', ) default_columns = ( 'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description', diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index f43a44c62..e668215ad 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -389,11 +389,11 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests): Tenant.objects.bulk_create(tenants) prefixes = ( - Prefix(prefix='10.0.0.0/24', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True), + Prefix(prefix='10.0.0.0/24', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True), Prefix(prefix='10.0.1.0/24', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]), Prefix(prefix='10.0.2.0/24', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED), Prefix(prefix='10.0.3.0/24', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED), - Prefix(prefix='2001:db8::/64', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True), + Prefix(prefix='2001:db8::/64', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True), Prefix(prefix='2001:db8:0:1::/64', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]), Prefix(prefix='2001:db8:0:2::/64', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED), Prefix(prefix='2001:db8:0:3::/64', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED), @@ -417,6 +417,12 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'is_pool': 'false'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) + def test_mark_utilized(self): + params = {'mark_utilized': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'mark_utilized': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) + def test_within(self): params = {'within': '10.0.0.0/16'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index bf0039315..3b69f1423 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -7,10 +7,10 @@
- Prefix + Prefix
- +
- @@ -101,9 +100,15 @@ - + -
{{ object.get_status_display }} @@ -20,7 +20,6 @@ Not a Pool {% endif %}
Family
Utilization{% utilization_graph object.get_utilization %} + {% if object.marked_utilized %} + {% utilization_graph 100 %} + {% else %} + {% utilization_graph object.get_utilization %} + {% endif %} +
+
{% include 'inc/custom_fields_panel.html' %} From ecf51406c57d27b8bf1ebc290953fb0bb025d47f Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 11 May 2021 14:52:27 -0400 Subject: [PATCH 5/6] Permit disabling utilization graph warning/danger thresholds --- netbox/ipam/tables.py | 17 ++++++- netbox/templates/ipam/prefix.html | 5 +- .../templatetags/utilization_graph.html | 50 +++++-------------- netbox/utilities/templatetags/helpers.py | 11 +++- 4 files changed, 41 insertions(+), 42 deletions(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index aa4119fa7..e0f63a5ca 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -256,6 +256,21 @@ class RoleTable(BaseTable): # Prefixes # +class PrefixUtilizationColumn(UtilizationColumn): + """ + Extend UtilizationColumn to allow disabling the warning & danger thresholds for prefixes + marked as fully utilized. + """ + template_code = """ + {% load helpers %} + {% if record.pk and record.mark_utilized %} + {% utilization_graph value warning_threshold=0 danger_threshold=0 %} + {% elif record.pk %} + {% utilization_graph value %} + {% endif %} + """ + + class PrefixTable(BaseTable): pk = ToggleColumn() prefix = tables.TemplateColumn( @@ -300,7 +315,7 @@ class PrefixTable(BaseTable): class PrefixDetailTable(PrefixTable): - utilization = UtilizationColumn( + utilization = PrefixUtilizationColumn( accessor='get_utilization', orderable=False ) diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index 3b69f1423..36675aa13 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -101,8 +101,9 @@ Utilization - {% if object.marked_utilized %} - {% utilization_graph 100 %} + {% if object.mark_utilized %} + {% utilization_graph 100 warning_threshold=0 danger_threshold=0 %} + (Marked fully utilized) {% else %} {% utilization_graph object.get_utilization %} {% endif %} diff --git a/netbox/templates/utilities/templatetags/utilization_graph.html b/netbox/templates/utilities/templatetags/utilization_graph.html index c4a33911f..7f722c50e 100644 --- a/netbox/templates/utilities/templatetags/utilization_graph.html +++ b/netbox/templates/utilities/templatetags/utilization_graph.html @@ -1,42 +1,18 @@ {% if utilization == 0 %} -
+
{{ utilization }}% -
+
{% else %} -
- {% if utilization >= danger_threshold %} -
+
- {{ utilization }}% + {{ utilization }}%
- {% elif utilization >= warning_threshold %} -
- {{ utilization }}% -
- {% else %} -
- {{ utilization }}% -
- {% endif %} -
-{% endif %} \ No newline at end of file +
+{% endif %} diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 6abbec619..78189ec49 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -276,10 +276,17 @@ def utilization_graph(utilization, warning_threshold=75, danger_threshold=90): """ Display a horizontal bar graph indicating a percentage of utilization. """ + if danger_threshold and utilization >= danger_threshold: + bar_class = 'bg-danger' + elif warning_threshold and utilization >= warning_threshold: + bar_class = 'bg-warning' + elif warning_threshold or danger_threshold: + bar_class = 'bg-success' + else: + bar_class = 'bg-default' return { 'utilization': utilization, - 'warning_threshold': warning_threshold, - 'danger_threshold': danger_threshold, + 'bar_class': bar_class, } From 43a823d0ac42a1f3cf35c3631e1c0685e1471e9a Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 11 May 2021 15:51:36 -0400 Subject: [PATCH 6/6] Closes #3665: Enable rendering export templates via REST API --- docs/additional-features/export-templates.md | 10 ++++++++++ docs/release-notes/version-2.12.md | 1 + netbox/extras/models/models.py | 9 ++++----- netbox/netbox/api/views.py | 15 +++++++++++++++ 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/docs/additional-features/export-templates.md b/docs/additional-features/export-templates.md index b3f585bee..c80d5b8a1 100644 --- a/docs/additional-features/export-templates.md +++ b/docs/additional-features/export-templates.md @@ -33,6 +33,16 @@ The `as_attachment` attribute of an export template controls its behavior when r A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`. +## REST API Integration + +When it is necessary to provide authentication credentials (such as when [`LOGIN_REQUIRED`](../configuration/optional-settings.md#login_required) has been enabled), it is recommended to render export templates via the REST API. This allows the client to specify an authentication token. To render an export template via the REST API, make a `GET` request to the model's list endpoint and append the `export` parameter specifying the export template name. For example: + +``` +GET /api/dcim/sites/?export=MyTemplateName +``` + +Note that the body of the response will contain only the rendered export template content, as opposed to a JSON object or list. + ## Example Here's an example device export template that will generate a simple Nagios configuration from a list of devices. diff --git a/docs/release-notes/version-2.12.md b/docs/release-notes/version-2.12.md index 1d884de72..573cd68fe 100644 --- a/docs/release-notes/version-2.12.md +++ b/docs/release-notes/version-2.12.md @@ -4,6 +4,7 @@ ### Enhancements +* [#3665](https://github.com/netbox-community/netbox/issues/3665) - Enable rendering export templates via REST API * [#4609](https://github.com/netbox-community/netbox/issues/4609) - Allow marking prefixes as fully utilized * [#5806](https://github.com/netbox-community/netbox/issues/5806) - Add kilometer and mile as choices for cable length unit * [#6154](https://github.com/netbox-community/netbox/issues/6154) - Allow decimal values for cable lengths diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index c2cebe163..ab9cbe9f3 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -300,13 +300,12 @@ class ExportTemplate(BigIDModel): # Build the response response = HttpResponse(output, content_type=mime_type) - filename = 'netbox_{}{}'.format( - queryset.model._meta.verbose_name_plural, - '.{}'.format(self.file_extension) if self.file_extension else '' - ) if self.as_attachment: - response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename) + basename = queryset.model._meta.verbose_name_plural.replace(' ', '_') + extension = f'.{self.file_extension}' if self.file_extension else '' + filename = f'netbox_{basename}{extension}' + response['Content-Disposition'] = f'attachment; filename="{filename}"' return response diff --git a/netbox/netbox/api/views.py b/netbox/netbox/api/views.py index 585b75686..56566dcd7 100644 --- a/netbox/netbox/api/views.py +++ b/netbox/netbox/api/views.py @@ -5,9 +5,11 @@ from collections import OrderedDict from django import __version__ as DJANGO_VERSION from django.apps import apps from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db import transaction from django.db.models import ProtectedError +from django.shortcuts import get_object_or_404 from django_rq.queues import get_connection from rest_framework import status from rest_framework.response import Response @@ -16,6 +18,7 @@ from rest_framework.views import APIView from rest_framework.viewsets import ModelViewSet as ModelViewSet_ from rq.worker import Worker +from extras.models import ExportTemplate from netbox.api import BulkOperationSerializer from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired from netbox.api.exceptions import SerializerNotFound @@ -222,6 +225,18 @@ class ModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ModelViewSet_): # Check that the instance is matched by the view's queryset self.queryset.get(pk=instance.pk) + def list(self, request, *args, **kwargs): + """ + Overrides ListModelMixin to allow processing ExportTemplates. + """ + if 'export' in request.GET: + content_type = ContentType.objects.get_for_model(self.serializer_class.Meta.model) + et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export']) + queryset = self.filter_queryset(self.get_queryset()) + return et.render_to_response(queryset) + + return super().list(request, *args, **kwargs) + def perform_create(self, serializer): model = self.queryset.model logger = logging.getLogger('netbox.api.views.ModelViewSet')