sts-sponsors team mailing list archive
-
sts-sponsors team
-
Mailing list archive
-
Message #05488
[Merge] ~ack/maas:macaddress-field-rework into maas:master
Alberto Donato has proposed merging ~ack/maas:macaddress-field-rework into maas:master.
Commit message:
rework MACAddresField to use text as storage type
Requested reviews:
Björn Tillenius (bjornt)
MAAS Lander (maas-lander)
Related bugs:
Bug #1696108 in MAAS: "Interface model validates the MAC address twice"
https://bugs.launchpad.net/maas/+bug/1696108
For more details, see:
https://code.launchpad.net/~ack/maas/+git/maas/+merge/438041
--
Your team MAAS Committers is subscribed to branch maas:master.
diff --git a/src/maasserver/api/machines.py b/src/maasserver/api/machines.py
index 766dc68..a5374f0 100644
--- a/src/maasserver/api/machines.py
+++ b/src/maasserver/api/machines.py
@@ -13,7 +13,7 @@ import json
import re
from django.conf import settings
-from django.core.exceptions import PermissionDenied, ValidationError
+from django.core.exceptions import PermissionDenied
from django.db.models import Q
from django.http import (
HttpResponse,
@@ -62,7 +62,7 @@ from maasserver.exceptions import (
NodeStateViolation,
Unauthorized,
)
-from maasserver.fields import MAC_VALIDATOR
+from maasserver.fields import MAC_RE
from maasserver.forms import (
AdminMachineForm,
get_machine_create_form,
@@ -1914,9 +1914,7 @@ class AnonMachinesHandler(AnonNodesHandler):
# caught.
macs_valid = True
for mac_address in mac_addresses:
- try:
- MAC_VALIDATOR(mac_address)
- except ValidationError:
+ if not MAC_RE.match(mac_address):
macs_valid = False
break
if macs_valid:
diff --git a/src/maasserver/api/nodes.py b/src/maasserver/api/nodes.py
index f6d58e6..5754853 100644
--- a/src/maasserver/api/nodes.py
+++ b/src/maasserver/api/nodes.py
@@ -41,7 +41,7 @@ from maasserver.exceptions import (
NoScriptsFound,
StaticIPAddressExhaustion,
)
-from maasserver.fields import MAC_RE
+from maasserver.fields import MAC_RE, normalise_macaddress
from maasserver.forms import BulkNodeSetZoneForm
from maasserver.forms.ephemeral import TestForm
from maasserver.models import Filesystem, Interface, Node, OwnerData
@@ -209,13 +209,17 @@ def filtered_nodes_list_from_request(request, model=None):
def is_registered(request, ignore_statuses=None):
"""Used by both `NodesHandler` and `AnonNodesHandler`."""
- mac_address = get_mandatory_param(request.GET, "mac_address")
- interfaces = Interface.objects.filter(mac_address=mac_address)
- interfaces = interfaces.exclude(node_config__node__isnull=True)
if ignore_statuses is None:
ignore_statuses = [NODE_STATUS.RETIRED]
- interfaces = interfaces.exclude(
- node_config__node__status__in=ignore_statuses
+
+ mac_address = normalise_macaddress(
+ get_mandatory_param(request.GET, "mac_address")
+ )
+
+ interfaces = (
+ Interface.objects.filter(mac_address=mac_address)
+ .exclude(node_config__node__isnull=True)
+ .exclude(node_config__node__status__in=ignore_statuses)
)
return interfaces.exists()
diff --git a/src/maasserver/fields.py b/src/maasserver/fields.py
index 276d4d4..c3cfb46 100644
--- a/src/maasserver/fields.py
+++ b/src/maasserver/fields.py
@@ -3,6 +3,7 @@
"""Custom model and form fields."""
+from itertools import chain
import re
from django import forms
@@ -15,6 +16,7 @@ from django.db.models import (
GenericIPAddressField,
IntegerField,
Q,
+ TextField,
URLField,
)
from django.utils.deconstruct import deconstructible
@@ -36,8 +38,30 @@ MAC_RE = re.compile(
r"([0-9a-fA-F]{3,4}.){2}[0-9a-fA-F]{3,4}" # aabb.ccdd.eeff
r"$"
)
-MAC_ERROR_MSG = "'%(value)s' is not a valid MAC address."
-MAC_VALIDATOR = RegexValidator(regex=MAC_RE, message=MAC_ERROR_MSG)
+MAC_VALIDATOR = RegexValidator(
+ regex=MAC_RE, message="'%(value)s' is not a valid MAC address."
+)
+
+MAC_SPLIT_RE = re.compile(r"[-:.]")
+
+
+def normalise_macaddress(mac: str) -> str:
+ """Return a colon-separated format for the specified MAC.
+
+ This supports converting from input formats matching the MAC_RE regexp.
+ """
+
+ tokens = MAC_SPLIT_RE.split(mac.lower())
+ match len(tokens):
+ case 1: # no separator
+ tokens = re.findall("..", tokens[0])
+ case 3: # each token is two bytes
+ tokens = chain(
+ *(re.findall("..", token.zfill(4)) for token in tokens)
+ )
+ case _: # single-byte tokens
+ tokens = (token.zfill(2) for token in tokens)
+ return ":".join(tokens)
class MACAddressFormField(forms.CharField):
@@ -45,24 +69,23 @@ class MACAddressFormField(forms.CharField):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.validators.append(
- RegexValidator(regex=MAC_RE, message=MAC_ERROR_MSG)
- )
+ self.validators.append(MAC_VALIDATOR)
-class MACAddressField(Field):
+class MACAddressField(TextField):
"""Model field type: MAC address."""
description = "MAC address"
- default_validators = [MAC_VALIDATOR]
-
- def db_type(self, *args, **kwargs):
- return "macaddr"
-
def get_prep_value(self, value):
- # Convert empty string to None.
- return super().get_prep_value(value) or None
+ value = super().get_prep_value(value)
+ if not value:
+ return value
+ return normalise_macaddress(value)
+
+ def validate(self, value, model_instance):
+ if value:
+ MAC_VALIDATOR(value)
class XMLField(Field):
diff --git a/src/maasserver/migrations/maasserver/0295_macaddress_text_field.py b/src/maasserver/migrations/maasserver/0295_macaddress_text_field.py
new file mode 100644
index 0000000..071b63a
--- /dev/null
+++ b/src/maasserver/migrations/maasserver/0295_macaddress_text_field.py
@@ -0,0 +1,16 @@
+# Generated by Django 3.2.12 on 2023-02-28 16:03
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("maasserver", "0294_keyring_data_binary_field"),
+ ]
+
+ operations = [
+ migrations.RunSQL(
+ f"ALTER TABLE maasserver_{table} ALTER COLUMN mac_address TYPE TEXT"
+ )
+ for table in ("interface", "neighbour", "virtualmachineinterface")
+ ]
diff --git a/src/maasserver/models/interface.py b/src/maasserver/models/interface.py
index 1b89447..c9b3980 100644
--- a/src/maasserver/models/interface.py
+++ b/src/maasserver/models/interface.py
@@ -49,7 +49,7 @@ from maasserver.exceptions import (
StaticIPAddressOutOfRange,
StaticIPAddressUnavailable,
)
-from maasserver.fields import MAC_VALIDATOR, MACAddressField
+from maasserver.fields import MACAddressField
from maasserver.models.cleansave import CleanSave
from maasserver.models.staticipaddress import StaticIPAddress
from maasserver.models.timestampedmodel import TimestampedModel
@@ -1482,13 +1482,6 @@ class Interface(CleanSave, TimestampedModel):
all_related |= ancestor.get_successors()
return all_related
- def clean(self):
- super().clean()
-
- # Verify that the MAC address is legal if it is not empty.
- if self.mac_address:
- MAC_VALIDATOR(self.mac_address)
-
def delete(self, remove_ip_address=True):
# We set the _skip_ip_address_removal so the signal can use it to
# skip removing the IP addresses. This is normally only done by the
diff --git a/src/maasserver/models/tests/test_interface.py b/src/maasserver/models/tests/test_interface.py
index 919074b..aca2a9e 100644
--- a/src/maasserver/models/tests/test_interface.py
+++ b/src/maasserver/models/tests/test_interface.py
@@ -1004,23 +1004,11 @@ class TestInterface(MAASServerTestCase):
INTERFACE_TYPE.PHYSICAL,
mac_address="invalid",
)
- # XXX Danilo 2017-05-26 bug #1696108: we validate the MAC address
- # twice: once as part of Interface.clean() resulting in the __all__
- # error, and once as part of field validation that happens after a
- # few queries are done, so we cannot easily get rid of
- # MAC_VALIDATOR() in clean().
- self.assertThat(
+ self.assertEqual(
exception.message_dict,
- MatchesDict(
- {
- "__all__": MatchesListwise(
- [Equals("'invalid' is not a valid MAC address.")]
- ),
- "mac_address": MatchesListwise(
- [Equals("'invalid' is not a valid MAC address.")]
- ),
- }
- ),
+ {
+ "mac_address": ["'invalid' is not a valid MAC address."],
+ },
)
def test_allows_blank_mac_address(self):
diff --git a/src/maasserver/pytest_tests/test_fields.py b/src/maasserver/pytest_tests/test_fields.py
new file mode 100644
index 0000000..87ad823
--- /dev/null
+++ b/src/maasserver/pytest_tests/test_fields.py
@@ -0,0 +1,19 @@
+import pytest
+
+from maasserver.fields import normalise_macaddress
+
+
+@pytest.mark.parametrize(
+ ("value", "expected"),
+ [
+ ("aabbccddeeff", "aa:bb:cc:dd:ee:ff"),
+ ("aa:bb:cc:dd:ee:ff", "aa:bb:cc:dd:ee:ff"),
+ ("aa:b:cc:d:ee:f", "aa:0b:cc:0d:ee:0f"),
+ ("aa-bb-cc-dd-ee-ff", "aa:bb:cc:dd:ee:ff"),
+ ("aa-b-cc-d-ee-f", "aa:0b:cc:0d:ee:0f"),
+ ("aabb.ccdd.eeff", "aa:bb:cc:dd:ee:ff"),
+ ("abb.cdd.eeff", "0a:bb:0c:dd:ee:ff"),
+ ],
+)
+def test_normalise_mac_address(value, expected):
+ assert normalise_macaddress(value) == expected
diff --git a/src/maasserver/rpc/tests/test_rackcontrollers.py b/src/maasserver/rpc/tests/test_rackcontrollers.py
index 88b0591..05de7b4 100644
--- a/src/maasserver/rpc/tests/test_rackcontrollers.py
+++ b/src/maasserver/rpc/tests/test_rackcontrollers.py
@@ -212,7 +212,7 @@ class TestRegisterRackController(MAASServerTestCase):
def test_creates_new_rackcontroller(self):
existing_machine = factory.make_Machine()
- rack_mac = (factory.make_mac_address(),)
+ rack_mac = factory.make_mac_address()
interfaces = {
factory.make_name("eth0"): {
"type": "physical",
diff --git a/src/maasserver/tests/test_fields.py b/src/maasserver/tests/test_fields.py
index 24014da..99cc72d 100644
--- a/src/maasserver/tests/test_fields.py
+++ b/src/maasserver/tests/test_fields.py
@@ -64,7 +64,7 @@ class TestModelNameValidator(MAASServerTestCase):
class TestMACAddressField(MAASServerTestCase):
def test_mac_address_is_stored_normalized_and_loaded(self):
interface = factory.make_Interface(
- INTERFACE_TYPE.PHYSICAL, mac_address=" AA-bb-CC-dd-EE-Ff "
+ INTERFACE_TYPE.PHYSICAL, mac_address="AA-bb-CC-dd-EE-Ff"
)
loaded_mac = Interface.objects.get(id=interface.id)
self.assertEqual("aa:bb:cc:dd:ee:ff", loaded_mac.mac_address)
Follow ups