diff --git a/peeringdb_server/client_adaptor/backend.py b/peeringdb_server/client_adaptor/backend.py index 2b7575d0..6156204f 100644 --- a/peeringdb_server/client_adaptor/backend.py +++ b/peeringdb_server/client_adaptor/backend.py @@ -1,3 +1,5 @@ +import re + from collections import defaultdict from django.db.models import OneToOneRel diff --git a/peeringdb_server/inet.py b/peeringdb_server/inet.py index 09237b8a..73bf78d9 100644 --- a/peeringdb_server/inet.py +++ b/peeringdb_server/inet.py @@ -9,6 +9,36 @@ from django.utils.translation import ugettext_lazy as _ from peeringdb_server import settings +# Valid IRR Source values +# reference: http://www.irr.net/docs/list.html +IRR_SOURCE = ( + "AFRINIC", + "ALTDB", + "AOLTW", + "APNIC", + "ARIN", + "BELL", + "BBOI", + "CANARIE", + "EASYNET", + "EPOCH", + "HOST", + "JPIRR", + "LEVEL3", + "NESTEGG", + "NTTCOM", + "OPENFACE", + "OTTIX", + "PANIX", + "RADB", + "REACH", + "RGNET", + "RIPE", + "RISQ", + "ROGERS", + "TC", +) + # RFC 5398 documentation asn range ASN_RFC_5398_16BIT = (64496, 64511) ASN_RFC_5398_32BIT = (65536, 65551) diff --git a/peeringdb_server/management/commands/pdb_api_test.py b/peeringdb_server/management/commands/pdb_api_test.py index d0c9e518..c04675a8 100644 --- a/peeringdb_server/management/commands/pdb_api_test.py +++ b/peeringdb_server/management/commands/pdb_api_test.py @@ -308,7 +308,7 @@ class TestJSON(unittest.TestCase): "aka": self.make_name("Also known as"), "asn": asn, "website": WEBSITE, - "irr_as_set": "AS-ZZ-ZZZZZZ", + "irr_as_set": "AS-ZZ-ZZZZZZ@RIPE", "info_type": "NSP", "info_prefixes4": 11000, "info_prefixes6": 12000, diff --git a/peeringdb_server/mock.py b/peeringdb_server/mock.py index 2cab404b..3af22d97 100644 --- a/peeringdb_server/mock.py +++ b/peeringdb_server/mock.py @@ -203,7 +203,7 @@ class Mock(object): return self.name(data, reftag=reftag) def irr_as_set(self, data, reftag=None): - return "AS-{}".format(str(uuid.uuid4())[:8].upper()) + return "AS-{}@RIPE".format(str(uuid.uuid4())[:8].upper()) def looking_glass(self, data, reftag=None): return "{}/looking-glass".format(self.website(data, reftag=reftag)) diff --git a/peeringdb_server/models.py b/peeringdb_server/models.py index 12f7f718..184e4e1a 100644 --- a/peeringdb_server/models.py +++ b/peeringdb_server/models.py @@ -41,6 +41,7 @@ from peeringdb_server.validators import ( validate_info_prefixes6, validate_prefix_overlap, validate_phonenumber, + validate_irr_as_set, ) SPONSORSHIP_LEVELS = ( @@ -2325,18 +2326,28 @@ class Network(pdb_models.NetworkBase): """ Custom model validation """ + try: validate_info_prefixes4(self.info_prefixes4) except ValidationError as exc: - raise ValidationError({"info_prefixes4": exc[0]}) + raise ValidationError({"info_prefixes4": exc}) try: validate_info_prefixes6(self.info_prefixes6) except ValidationError as exc: - raise ValidationError({"info_prefixes6": exc[0]}) + raise ValidationError({"info_prefixes6": exc}) + + try: + if self.irr_as_set: + self.irr_as_set = validate_irr_as_set(self.irr_as_set) + except ValidationError as exc: + raise ValidationError({"irr_as_set": exc}) + + return super(Network, self).clean() + # class NetworkContact(HandleRefModel): @reversion.register class NetworkContact(pdb_models.ContactBase): diff --git a/peeringdb_server/serializers.py b/peeringdb_server/serializers.py index 412ba101..d2951b3f 100644 --- a/peeringdb_server/serializers.py +++ b/peeringdb_server/serializers.py @@ -49,6 +49,7 @@ from peeringdb_server.validators import ( validate_info_prefixes6, validate_prefix_overlap, validate_phonenumber, + validate_irr_as_set, ) from django.utils.translation import ugettext_lazy as _ @@ -1200,6 +1201,7 @@ class NetworkContactSerializer(ModelSerializer): return validate_phonenumber(value) + class NetworkIXLanSerializer(ModelSerializer): """ Serializer for peeringdb_server.models.NetworkIXLan @@ -1543,6 +1545,8 @@ class NetworkSerializer(ModelSerializer): suggest = serializers.BooleanField(required=False, write_only=True) validators = [AsnRdapValidator(), FieldMethodValidator("suggest", ["POST"])] + #irr_as_set = serializers.CharField(validators=[validate_irr_as_set]) + class Meta: model = Network depth = 1 @@ -1725,6 +1729,13 @@ class NetworkSerializer(ModelSerializer): ticket_queue_rdap_error(*rdap_error) + def validate_irr_as_set(self, value): + if value: + return validate_irr_as_set(value) + else: + return value + + class IXLanPrefixSerializer(ModelSerializer): """ Serializer for peeringdb_server.models.IXLanPrefix diff --git a/peeringdb_server/validators.py b/peeringdb_server/validators.py index 237e9988..80d1b39e 100644 --- a/peeringdb_server/validators.py +++ b/peeringdb_server/validators.py @@ -1,7 +1,7 @@ """ peeringdb model / field validators """ - +import re import ipaddress import phonenumbers @@ -9,7 +9,7 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ -from peeringdb_server.inet import network_is_pdb_valid +from peeringdb_server.inet import network_is_pdb_valid, IRR_SOURCE import peeringdb_server.models @@ -145,3 +145,65 @@ def validate_prefix_overlap(prefix): ) ) ) + + +def validate_irr_as_set(value): + """ + Validates irr as-set string + + - the as-set/rs-set name has to conform to RFC 2622 (5.1 and 5.2) + - the source may be specified by AS-SET@SOURCE or SOURCE::AS-SET + - multiple values must be separated by either comma, space or comma followed by space + + Arguments: + + - value: irr as-set string + + Returns: + + - str: validated irr as-set string + + """ + + if not isinstance(value, str): + raise ValueError(_("IRR AS-SET value must be string type")) + + # split multiple values + + # normalize value separation to commas + value = value.replace(", ",",") + value = value.replace(" ",",") + + validated = [] + + # validate + for item in value.split(","): + item = item.upper() + source = None + + # @ + parts_match = re.match("^(AS|RS)-([\w\d\-]+)@(\w+)$", item) + if parts_match: + source = parts_match.group(3) + + # :: + else: + parts_match = re.match("^(\w+)::(AS|RS)-([\w\d\-]+)$", item) + if parts_match: + source = parts_match.group(1) + else: + raise ValidationError(_("Invalid formatting: {} - should be AS-SET@SOURCE or SOURCE::AS-SET").format(item)) + + if source not in IRR_SOURCE: + raise ValidationError(_("Unknown IRR source: {}").format(source)) + + validated.append(item) + + return " ".join(validated) + + + + + + + diff --git a/tests/test_validators.py b/tests/test_validators.py index 36a19f7e..491ee514 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -12,6 +12,7 @@ from peeringdb_server.validators import ( validate_info_prefixes6, validate_prefix_overlap, validate_phonenumber, + validate_irr_as_set, ) from peeringdb_server.models import ( @@ -151,6 +152,32 @@ def test_validate_prefix_overlap(): with pytest.raises(ValidationError) as exc: validate_prefix_overlap("198.32.124.0/23") +@pytest.mark.parametrize("value,validated", [ + # success validation + ("RIPE::AS-FOO", "RIPE::AS-FOO"), + ("AS-FOO@RIPE", "AS-FOO@RIPE"), + ("ripe::as-foo", "RIPE::AS-FOO"), + ("as-foo@ripe", "AS-FOO@RIPE"), + ("as-foo@ripe as-bar@ripe", "AS-FOO@RIPE AS-BAR@RIPE"), + ("as-foo@ripe,as-bar@ripe", "AS-FOO@RIPE AS-BAR@RIPE"), + ("as-foo@ripe, as-bar@ripe", "AS-FOO@RIPE AS-BAR@RIPE"), + + # fail validation + ("AS-FOO", False), + ("UNKNOWN::AS-FOO", False), + ("AS-FOO@UNKNOWN", False), + ("ASFOO@UNKNOWN", False), + ("UNKNOWN::ASFOO", False), + ("AS-FOO RIPE:AS-FOO", False), + ("AS-FOO AS-FOO@RIPE", False), +]) +def test_validate_irr_as_set(value, validated): + if not validated: + with pytest.raises(ValidationError): + validate_irr_as_set(value) + else: + assert validate_irr_as_set(value) == validated + @pytest.mark.djangodb def test_validate_phonenumber():