From 9dcaa004496fd9f1c9d193d9cfeabce12d77377e Mon Sep 17 00:00:00 2001 From: Susan Hooks Date: Thu, 28 May 2026 14:13:27 -0600 Subject: [PATCH] normalize pkey --- .../nautobot_app_overlays/models.py | 28 +++++++++++- .../tests/test_models.py | 44 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/components/nautobot/nautobot-app-overlays/nautobot_app_overlays/models.py b/components/nautobot/nautobot-app-overlays/nautobot_app_overlays/models.py index 567a982..937d813 100644 --- a/components/nautobot/nautobot-app-overlays/nautobot_app_overlays/models.py +++ b/components/nautobot/nautobot-app-overlays/nautobot_app_overlays/models.py @@ -15,6 +15,8 @@ """Models for Overlays app.""" +import re + from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError @@ -340,16 +342,40 @@ class Meta: verbose_name_plural = "InfiniBand PKeys" unique_together = ["pkey", "overlay"] + _PKEY_RE = re.compile(r"\A0[xX][0-9a-fA-F]{1,4}\Z") + def __str__(self): """Stringify instance.""" return f"{self.name} ({self.pkey})" + @staticmethod + def normalize_pkey(value): + """Return pkey as '0x' + 4 lowercase hex digits, or the input unchanged if it doesn't match the expected format.""" + if not isinstance(value, str): + return value + stripped = value.strip() + if not InfiniBandPKey._PKEY_RE.match(stripped): + return stripped + return f"0x{int(stripped, 16):04x}" + + def clean_fields(self, exclude=None): + """Normalize pkey before field validators run so '0X1' and whitespace are accepted.""" + if isinstance(self.pkey, str): + self.pkey = self.normalize_pkey(self.pkey) + super().clean_fields(exclude=exclude) + def clean(self): - """Validate PKey can only be associated with IB PKey overlays.""" + """Validate PKey associations and normalize the pkey value.""" super().clean() + self.pkey = self.normalize_pkey(self.pkey) if self.overlay and self.overlay.isolation_type != IsolationTypeChoices.IB_PKEY: raise ValidationError({"overlay": "InfiniBand PKeys can only be associated with IB PKey overlays."}) + def save(self, *args, **kwargs): + """Normalize pkey before persisting.""" + self.pkey = self.normalize_pkey(self.pkey) + super().save(*args, **kwargs) + @extras_features("graphql", "statuses", "custom_fields") class InfiniBandMKey(PrimaryModel): diff --git a/components/nautobot/nautobot-app-overlays/nautobot_app_overlays/tests/test_models.py b/components/nautobot/nautobot-app-overlays/nautobot_app_overlays/tests/test_models.py index 2a66fc4..d3184ab 100644 --- a/components/nautobot/nautobot-app-overlays/nautobot_app_overlays/tests/test_models.py +++ b/components/nautobot/nautobot-app-overlays/nautobot_app_overlays/tests/test_models.py @@ -205,6 +205,50 @@ def test_pkey_format_validator_rejects_invalid_values(self): with self.assertRaises(ValidationError, msg=f"Expected ValidationError for pkey={invalid!r}"): pkey.full_clean() + def test_normalize_pkey_helper(self): + """normalize_pkey() produces '0x' + 4 lowercase hex digits for all valid inputs.""" + cases = { + "0x1": "0x0001", + "0x01": "0x0001", + "0x001": "0x0001", + "0x0001": "0x0001", + "0X100": "0x0100", + "0xFFFF": "0xffff", + "0xffff": "0xffff", + " 0x100 ": "0x0100", + } + for raw, expected in cases.items(): + self.assertEqual( + InfiniBandPKey.normalize_pkey(raw), + expected, + msg=f"normalize_pkey({raw!r})", + ) + + def test_normalize_pkey_passes_through_invalid_input(self): + """normalize_pkey() leaves invalid input alone so the field validator can reject it.""" + for raw in ("", "not-a-pkey", "8001", "0x1234567", None, 0x100): + self.assertEqual( + InfiniBandPKey.normalize_pkey(raw), + raw if not isinstance(raw, str) else raw.strip(), + msg=f"normalize_pkey({raw!r})", + ) + + def test_save_normalizes_pkey(self): + """Raw ORM save() normalizes pkey.""" + pkey = InfiniBandPKey.objects.create( + pkey="0x100", + name="Raw ORM PKey", + status=self.status, + ) + pkey.refresh_from_db() + self.assertEqual(pkey.pkey, "0x0100") + + def test_full_clean_normalizes_pkey(self): + """full_clean() normalizes pkey.""" + pkey = InfiniBandPKey(pkey="0x1", name="Form PKey", status=self.status) + pkey.full_clean() + self.assertEqual(pkey.pkey, "0x0001") + class InfiniBandMKeyModelTest(TestCase): """Test cases for InfiniBandMKey model."""