2
0
Эх сурвалжийг харах

Fixes #21095: Add IEC unit labels support and rename humanize helpers to be unit-agnostic (#21789)

Jonathan Senecal 1 өдөр өмнө
parent
commit
a19daa5466

+ 2 - 2
netbox/templates/virtualization/panels/cluster_resources.html

@@ -12,7 +12,7 @@
       <th scope="row"><i class="mdi mdi-chip"></i> {% trans "Memory" %}</th>
       <td>
         {% if memory_sum %}
-          <span title={{ memory_sum }}>{{ memory_sum|humanize_ram_megabytes }}</span>
+          <span title={{ memory_sum }}>{{ memory_sum|humanize_ram_capacity }}</span>
         {% else %}
           {{ ''|placeholder }}
         {% endif %}
@@ -24,7 +24,7 @@
       </th>
       <td>
         {% if disk_sum %}
-          {{ disk_sum|humanize_disk_megabytes }}
+          {{ disk_sum|humanize_disk_capacity }}
         {% else %}
           {{ ''|placeholder }}
         {% endif %}

+ 2 - 2
netbox/templates/virtualization/panels/virtual_machine_resources.html

@@ -12,7 +12,7 @@
       <th scope="row"><i class="mdi mdi-chip"></i> {% trans "Memory" %}</th>
       <td>
         {% if object.memory %}
-          <span title={{ object.memory }}>{{ object.memory|humanize_ram_megabytes }}</span>
+          <span title={{ object.memory }}>{{ object.memory|humanize_ram_capacity }}</span>
         {% else %}
           {{ ''|placeholder }}
         {% endif %}
@@ -24,7 +24,7 @@
       </th>
       <td>
         {% if object.disk %}
-          {{ object.disk|humanize_disk_megabytes }}
+          {{ object.disk|humanize_disk_capacity }}
         {% else %}
           {{ ''|placeholder }}
         {% endif %}

+ 1 - 1
netbox/templates/virtualization/virtualdisk/attrs/size.html

@@ -1,2 +1,2 @@
 {% load helpers %}
-{{ value|humanize_disk_megabytes }}
+{{ value|humanize_disk_capacity }}

+ 8 - 0
netbox/utilities/forms/utils.py

@@ -14,6 +14,7 @@ __all__ = (
     'expand_alphanumeric_pattern',
     'expand_ipaddress_pattern',
     'form_from_model',
+    'get_capacity_unit_label',
     'get_field_value',
     'get_selected_values',
     'parse_alphanumeric_range',
@@ -130,6 +131,13 @@ def expand_ipaddress_pattern(string, family):
             yield ''.join([lead, format(i, 'x' if family == 6 else 'd'), remnant])
 
 
+def get_capacity_unit_label(divisor=1000):
+    """
+    Return the appropriate base unit label: 'MiB' for binary (1024), 'MB' for decimal (1000).
+    """
+    return 'MiB' if divisor == 1024 else 'MB'
+
+
 def get_field_value(form, field_name):
     """
     Return the current bound or initial value associated with a form field, prior to calling

+ 30 - 20
netbox/utilities/templatetags/helpers.py

@@ -20,8 +20,8 @@ __all__ = (
     'divide',
     'get_item',
     'get_key',
-    'humanize_disk_megabytes',
-    'humanize_ram_megabytes',
+    'humanize_disk_capacity',
+    'humanize_ram_capacity',
     'humanize_speed',
     'icon_from_status',
     'kg_to_pounds',
@@ -208,42 +208,52 @@ def humanize_speed(speed):
     return '{} Kbps'.format(speed)
 
 
-def _humanize_megabytes(mb, divisor=1000):
+def _humanize_capacity(value, divisor=1000):
     """
-    Express a number of megabytes in the most suitable unit (e.g. gigabytes, terabytes, etc.).
+    Express a capacity value in the most suitable unit (e.g. GB, TiB, etc.).
+
+    The value is treated as a unitless base-unit quantity; the divisor determines
+    both the scaling thresholds and the label convention:
+      - 1000: SI labels (MB, GB, TB, PB)
+      - 1024: IEC labels (MiB, GiB, TiB, PiB)
     """
-    if not mb:
+    if not value:
         return ""
 
+    if divisor == 1024:
+        labels = ('MiB', 'GiB', 'TiB', 'PiB')
+    else:
+        labels = ('MB', 'GB', 'TB', 'PB')
+
     PB_SIZE = divisor**3
     TB_SIZE = divisor**2
     GB_SIZE = divisor
 
-    if mb >= PB_SIZE:
-        return f"{mb / PB_SIZE:.2f} PB"
-    if mb >= TB_SIZE:
-        return f"{mb / TB_SIZE:.2f} TB"
-    if mb >= GB_SIZE:
-        return f"{mb / GB_SIZE:.2f} GB"
-    return f"{mb} MB"
+    if value >= PB_SIZE:
+        return f"{value / PB_SIZE:.2f} {labels[3]}"
+    if value >= TB_SIZE:
+        return f"{value / TB_SIZE:.2f} {labels[2]}"
+    if value >= GB_SIZE:
+        return f"{value / GB_SIZE:.2f} {labels[1]}"
+    return f"{value} {labels[0]}"
 
 
 @register.filter()
-def humanize_disk_megabytes(mb):
+def humanize_disk_capacity(value):
     """
-    Express a number of megabytes in the most suitable unit (e.g. gigabytes, terabytes, etc.).
-    Use the DISK_BASE_UNIT setting to determine the divisor. Default is 1000.
+    Express a disk capacity in the most suitable unit, using the DISK_BASE_UNIT
+    setting to select SI (MB/GB) or IEC (MiB/GiB) labels.
     """
-    return _humanize_megabytes(mb, DISK_BASE_UNIT)
+    return _humanize_capacity(value, DISK_BASE_UNIT)
 
 
 @register.filter()
-def humanize_ram_megabytes(mb):
+def humanize_ram_capacity(value):
     """
-    Express a number of megabytes in the most suitable unit (e.g. gigabytes, terabytes, etc.).
-    Use the RAM_BASE_UNIT setting to determine the divisor. Default is 1000.
+    Express a RAM capacity in the most suitable unit, using the RAM_BASE_UNIT
+    setting to select SI (MB/GB) or IEC (MiB/GiB) labels.
     """
-    return _humanize_megabytes(mb, RAM_BASE_UNIT)
+    return _humanize_capacity(value, RAM_BASE_UNIT)
 
 
 @register.filter()

+ 18 - 1
netbox/utilities/tests/test_forms.py

@@ -6,7 +6,12 @@ from netbox.choices import ImportFormatChoices
 from utilities.forms.bulk_import import BulkImportForm
 from utilities.forms.fields.csv import CSVSelectWidget
 from utilities.forms.forms import BulkRenameForm
-from utilities.forms.utils import expand_alphanumeric_pattern, expand_ipaddress_pattern, get_field_value
+from utilities.forms.utils import (
+    expand_alphanumeric_pattern,
+    expand_ipaddress_pattern,
+    get_capacity_unit_label,
+    get_field_value,
+)
 from utilities.forms.widgets.select import AvailableOptions, SelectedOptions
 
 
@@ -550,3 +555,15 @@ class SelectMultipleWidgetTest(TestCase):
         self.assertEqual(widget.choices[0][1], [(2, 'Option 2')])
         self.assertEqual(widget.choices[1][0], 'Group B')
         self.assertEqual(widget.choices[1][1], [(3, 'Option 3')])
+
+
+class GetCapacityUnitLabelTest(TestCase):
+    """
+    Test the get_capacity_unit_label function for correct base unit label.
+    """
+
+    def test_si_label(self):
+        self.assertEqual(get_capacity_unit_label(1000), 'MB')
+
+    def test_iec_label(self):
+        self.assertEqual(get_capacity_unit_label(1024), 'MiB')

+ 44 - 0
netbox/utilities/tests/test_templatetags.py

@@ -3,6 +3,7 @@ from unittest.mock import patch
 from django.test import TestCase, override_settings
 
 from utilities.templatetags.builtins.tags import static_with_params
+from utilities.templatetags.helpers import _humanize_capacity
 
 
 class StaticWithParamsTest(TestCase):
@@ -46,3 +47,46 @@ class StaticWithParamsTest(TestCase):
                 # Check that new parameter value is used
                 self.assertIn('v=new_version', result)
                 self.assertNotIn('v=old_version', result)
+
+
+class HumanizeCapacityTest(TestCase):
+    """
+    Test the _humanize_capacity function for correct SI/IEC unit label selection.
+    """
+
+    # Tests with divisor=1000 (SI/decimal units)
+
+    def test_si_megabytes(self):
+        self.assertEqual(_humanize_capacity(500, divisor=1000), '500 MB')
+
+    def test_si_gigabytes(self):
+        self.assertEqual(_humanize_capacity(2000, divisor=1000), '2.00 GB')
+
+    def test_si_terabytes(self):
+        self.assertEqual(_humanize_capacity(2000000, divisor=1000), '2.00 TB')
+
+    def test_si_petabytes(self):
+        self.assertEqual(_humanize_capacity(2000000000, divisor=1000), '2.00 PB')
+
+    # Tests with divisor=1024 (IEC/binary units)
+
+    def test_iec_megabytes(self):
+        self.assertEqual(_humanize_capacity(500, divisor=1024), '500 MiB')
+
+    def test_iec_gigabytes(self):
+        self.assertEqual(_humanize_capacity(2048, divisor=1024), '2.00 GiB')
+
+    def test_iec_terabytes(self):
+        self.assertEqual(_humanize_capacity(2097152, divisor=1024), '2.00 TiB')
+
+    def test_iec_petabytes(self):
+        self.assertEqual(_humanize_capacity(2147483648, divisor=1024), '2.00 PiB')
+
+    # Edge cases
+
+    def test_empty_value(self):
+        self.assertEqual(_humanize_capacity(0, divisor=1000), '')
+        self.assertEqual(_humanize_capacity(None, divisor=1000), '')
+
+    def test_default_divisor_is_1000(self):
+        self.assertEqual(_humanize_capacity(2000), '2.00 GB')

+ 18 - 3
netbox/virtualization/forms/bulk_edit.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.conf import settings
 from django.utils.translation import gettext_lazy as _
 
 from dcim.choices import InterfaceModeChoices
@@ -13,6 +14,7 @@ from tenancy.models import Tenant
 from utilities.forms import BulkRenameForm, add_blank_choice
 from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
 from utilities.forms.rendering import FieldSet
+from utilities.forms.utils import get_capacity_unit_label
 from utilities.forms.widgets import BulkEditNullBooleanSelect
 from virtualization.choices import *
 from virtualization.models import *
@@ -138,11 +140,11 @@ class VirtualMachineBulkEditForm(PrimaryModelBulkEditForm):
     )
     memory = forms.IntegerField(
         required=False,
-        label=_('Memory (MB)')
+        label=_('Memory')
     )
     disk = forms.IntegerField(
         required=False,
-        label=_('Disk (MB)')
+        label=_('Disk')
     )
     config_template = DynamicModelChoiceField(
         queryset=ConfigTemplate.objects.all(),
@@ -159,6 +161,13 @@ class VirtualMachineBulkEditForm(PrimaryModelBulkEditForm):
         'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'description', 'comments',
     )
 
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Set unit labels based on configured RAM_BASE_UNIT / DISK_BASE_UNIT (MB vs MiB)
+        self.fields['memory'].label = _('Memory ({unit})').format(unit=get_capacity_unit_label(settings.RAM_BASE_UNIT))
+        self.fields['disk'].label = _('Disk ({unit})').format(unit=get_capacity_unit_label(settings.DISK_BASE_UNIT))
+
 
 class VMInterfaceBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm):
     virtual_machine = forms.ModelChoiceField(
@@ -304,7 +313,7 @@ class VirtualDiskBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm):
     )
     size = forms.IntegerField(
         required=False,
-        label=_('Size (MB)')
+        label=_('Size')
     )
     description = forms.CharField(
         label=_('Description'),
@@ -318,6 +327,12 @@ class VirtualDiskBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm):
     )
     nullable_fields = ('description',)
 
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Set unit label based on configured DISK_BASE_UNIT (MB vs MiB)
+        self.fields['size'].label = _('Size ({unit})').format(unit=get_capacity_unit_label(settings.DISK_BASE_UNIT))
+
 
 class VirtualDiskBulkRenameForm(BulkRenameForm):
     pk = forms.ModelMultipleChoiceField(

+ 9 - 1
netbox/virtualization/forms/filtersets.py

@@ -1,4 +1,5 @@
 from django import forms
+from django.conf import settings
 from django.utils.translation import gettext_lazy as _
 
 from dcim.choices import *
@@ -12,6 +13,7 @@ from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
 from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES
 from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
 from utilities.forms.rendering import FieldSet
+from utilities.forms.utils import get_capacity_unit_label
 from virtualization.choices import *
 from virtualization.models import *
 from vpn.models import L2VPN
@@ -281,8 +283,14 @@ class VirtualDiskFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm):
         label=_('Virtual machine')
     )
     size = forms.IntegerField(
-        label=_('Size (MB)'),
+        label=_('Size'),
         required=False,
         min_value=1
     )
     tag = TagFilterField(model)
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Set unit label based on configured DISK_BASE_UNIT (MB vs MiB)
+        self.fields['size'].label = _('Size ({unit})').format(unit=get_capacity_unit_label(settings.DISK_BASE_UNIT))

+ 12 - 0
netbox/virtualization/forms/model_forms.py

@@ -1,5 +1,6 @@
 from django import forms
 from django.apps import apps
+from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
 from django.utils.translation import gettext_lazy as _
@@ -16,6 +17,7 @@ from tenancy.forms import TenancyForm
 from utilities.forms import ConfirmationForm
 from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField
 from utilities.forms.rendering import FieldSet
+from utilities.forms.utils import get_capacity_unit_label
 from utilities.forms.widgets import HTMXSelect
 from virtualization.models import *
 
@@ -236,6 +238,10 @@ class VirtualMachineForm(TenancyForm, PrimaryModelForm):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
+        # Set unit labels based on configured RAM_BASE_UNIT / DISK_BASE_UNIT (MB vs MiB)
+        self.fields['memory'].label = _('Memory ({unit})').format(unit=get_capacity_unit_label(settings.RAM_BASE_UNIT))
+        self.fields['disk'].label = _('Disk ({unit})').format(unit=get_capacity_unit_label(settings.DISK_BASE_UNIT))
+
         if self.instance.pk:
 
             # Disable the disk field if one or more VirtualDisks have been created
@@ -401,3 +407,9 @@ class VirtualDiskForm(VMComponentForm):
         fields = [
             'virtual_machine', 'name', 'size', 'description', 'owner', 'tags',
         ]
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Set unit label based on configured DISK_BASE_UNIT (MB vs MiB)
+        self.fields['size'].label = _('Size ({unit})').format(unit=get_capacity_unit_label(settings.DISK_BASE_UNIT))

+ 3 - 3
netbox/virtualization/models/virtualmachines.py

@@ -121,12 +121,12 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
     memory = models.PositiveIntegerField(
         blank=True,
         null=True,
-        verbose_name=_('memory (MB)')
+        verbose_name=_('memory')
     )
     disk = models.PositiveIntegerField(
         blank=True,
         null=True,
-        verbose_name=_('disk (MB)')
+        verbose_name=_('disk')
     )
     serial = models.CharField(
         verbose_name=_('serial number'),
@@ -425,7 +425,7 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
 
 class VirtualDisk(ComponentModel, TrackingModelMixin):
     size = models.PositiveIntegerField(
-        verbose_name=_('size (MB)'),
+        verbose_name=_('size'),
     )
 
     class Meta(ComponentModel.Meta):

+ 6 - 3
netbox/virtualization/tables/virtualmachines.py

@@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _
 from dcim.tables.devices import BaseInterfaceTable
 from netbox.tables import NetBoxTable, PrimaryModelTable, columns
 from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
-from utilities.templatetags.helpers import humanize_disk_megabytes
+from utilities.templatetags.helpers import humanize_disk_capacity, humanize_ram_capacity
 from virtualization.models import VirtualDisk, VirtualMachine, VMInterface
 
 from .template_code import *
@@ -93,8 +93,11 @@ class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModel
             'pk', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip',
         )
 
+    def render_memory(self, value):
+        return humanize_ram_capacity(value)
+
     def render_disk(self, value):
-        return humanize_disk_megabytes(value)
+        return humanize_disk_capacity(value)
 
 
 #
@@ -184,7 +187,7 @@ class VirtualDiskTable(NetBoxTable):
         }
 
     def render_size(self, value):
-        return humanize_disk_megabytes(value)
+        return humanize_disk_capacity(value)
 
 
 class VirtualMachineVirtualDiskTable(VirtualDiskTable):