From eb40275427047b1007b9a73d14c5d087ac6a7021 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Wed, 8 Jan 2020 17:23:09 +0000 Subject: [PATCH 1/7] Fixes #3623: Word expansion for interfaces --- docs/release-notes/version-2.6.md | 1 + netbox/utilities/forms.py | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index f3ff8798a..f5fc4beba 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -4,6 +4,7 @@ * [#2050](https://github.com/netbox-community/netbox/issues/2050) - Preview image attachments when hovering the link * [#3187](https://github.com/netbox-community/netbox/issues/3187) - Add rack selection field to rack elevations +* [#3623](https://github.com/netbox-community/netbox/issues/3623) - Add word expansion during interface creation ## Bug Fixes diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index eeee719ae..4cd92ca12 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -60,8 +60,13 @@ def parse_alphanumeric_range(string): for n in list(range(int(begin), int(end) + 1)): values.append(n) else: - for n in list(range(ord(begin), ord(end) + 1)): - values.append(chr(n)) + # Value-based + if begin == end: + values.append(begin) + # Range-based + else: + for n in list(range(ord(begin), ord(end) + 1)): + values.append(chr(n)) return values From 396bb28967b040247eea67c92ab7ea47a67a9c62 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Wed, 8 Jan 2020 17:28:31 +0000 Subject: [PATCH 2/7] Added example and handled invalid ranges gracefully --- netbox/utilities/forms.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 4cd92ca12..39422c265 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -65,6 +65,9 @@ def parse_alphanumeric_range(string): values.append(begin) # Range-based else: + # Not a valid range (more than a single character) + if not len(begin) == len(end) == 1: + raise forms.ValidationError('Range "{}" is invalid.'.format(dash_range)) for n in list(range(ord(begin), ord(end) + 1)): values.append(chr(n)) return values @@ -486,6 +489,7 @@ class ExpandableNameField(forms.CharField): 'Mixed cases and types within a single range are not supported.
' \ 'Examples:' def to_python(self, value): From c13b9d87983a1fc7b3e1434e219bb85545f90513 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Thu, 9 Jan 2020 18:26:10 +0000 Subject: [PATCH 3/7] Added tests for IPv4 --- netbox/utilities/tests/test_forms.py | 69 ++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 netbox/utilities/tests/test_forms.py diff --git a/netbox/utilities/tests/test_forms.py b/netbox/utilities/tests/test_forms.py new file mode 100644 index 000000000..0434ff137 --- /dev/null +++ b/netbox/utilities/tests/test_forms.py @@ -0,0 +1,69 @@ +from django.test import TestCase + +from utilities.forms import * + + +class ExpandIPAddress(TestCase): + """ + Validate the operation of expand_ipaddress_pattern(). + """ + def test_ipv4_range(self): + input = '1.2.3.[9-10]/32' + output = sorted([ + '1.2.3.9/32', + '1.2.3.10/32', + ]) + + self.assertEqual(sorted(list(expand_ipaddress_pattern(input, 4))), output) + + def test_ipv4_set(self): + input = '1.2.3.[4,44]/32' + output = sorted([ + '1.2.3.4/32', + '1.2.3.44/32', + ]) + + self.assertEqual(sorted(list(expand_ipaddress_pattern(input, 4))), output) + + def test_ipv4_multiple_ranges(self): + input = '1.[9-10].3.[9-11]/32' + output = sorted([ + '1.9.3.9/32', + '1.9.3.10/32', + '1.9.3.11/32', + '1.10.3.9/32', + '1.10.3.10/32', + '1.10.3.11/32', + ]) + + self.assertEqual(sorted(list(expand_ipaddress_pattern(input, 4))), output) + + def test_ipv4_multiple_sets(self): + input = '1.[2,22].3.[4,44]/32' + output = sorted([ + '1.2.3.4/32', + '1.2.3.44/32', + '1.22.3.4/32', + '1.22.3.44/32', + ]) + + self.assertEqual(sorted(list(expand_ipaddress_pattern(input, 4))), output) + + + def test_ipv4_set_and_range(self): + input = '1.[2,22].3.[9-11]/32' + output = sorted([ + '1.2.3.9/32', + '1.2.3.10/32', + '1.2.3.11/32', + '1.22.3.9/32', + '1.22.3.10/32', + '1.22.3.11/32', + ]) + + self.assertEqual(sorted(list(expand_ipaddress_pattern(input, 4))), output) + + # TODO: IPv6 + # TODO: negative tests + +# TODO: alphanumeric From fe89982d4ef663427d91e4efe67b364ccebaa811 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Fri, 10 Jan 2020 10:26:46 +0000 Subject: [PATCH 4/7] Removed redundant list call --- netbox/utilities/tests/test_forms.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/netbox/utilities/tests/test_forms.py b/netbox/utilities/tests/test_forms.py index 0434ff137..1fd09341c 100644 --- a/netbox/utilities/tests/test_forms.py +++ b/netbox/utilities/tests/test_forms.py @@ -14,7 +14,7 @@ class ExpandIPAddress(TestCase): '1.2.3.10/32', ]) - self.assertEqual(sorted(list(expand_ipaddress_pattern(input, 4))), output) + self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output) def test_ipv4_set(self): input = '1.2.3.[4,44]/32' @@ -23,7 +23,7 @@ class ExpandIPAddress(TestCase): '1.2.3.44/32', ]) - self.assertEqual(sorted(list(expand_ipaddress_pattern(input, 4))), output) + self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output) def test_ipv4_multiple_ranges(self): input = '1.[9-10].3.[9-11]/32' @@ -36,7 +36,7 @@ class ExpandIPAddress(TestCase): '1.10.3.11/32', ]) - self.assertEqual(sorted(list(expand_ipaddress_pattern(input, 4))), output) + self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output) def test_ipv4_multiple_sets(self): input = '1.[2,22].3.[4,44]/32' @@ -47,8 +47,7 @@ class ExpandIPAddress(TestCase): '1.22.3.44/32', ]) - self.assertEqual(sorted(list(expand_ipaddress_pattern(input, 4))), output) - + self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output) def test_ipv4_set_and_range(self): input = '1.[2,22].3.[9-11]/32' @@ -61,7 +60,7 @@ class ExpandIPAddress(TestCase): '1.22.3.11/32', ]) - self.assertEqual(sorted(list(expand_ipaddress_pattern(input, 4))), output) + self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output) # TODO: IPv6 # TODO: negative tests From 2eba84dad5e2c385ea3c3553f89ae09af7a31afd Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Fri, 10 Jan 2020 11:06:01 +0000 Subject: [PATCH 5/7] Added tests for IPv6 --- netbox/utilities/tests/test_forms.py | 67 +++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/netbox/utilities/tests/test_forms.py b/netbox/utilities/tests/test_forms.py index 1fd09341c..ea693f42c 100644 --- a/netbox/utilities/tests/test_forms.py +++ b/netbox/utilities/tests/test_forms.py @@ -62,7 +62,72 @@ class ExpandIPAddress(TestCase): self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output) - # TODO: IPv6 + def test_ipv6_range(self): + input = 'fec::abcd:[9-b]/64' + output = sorted([ + 'fec::abcd:9/64', + 'fec::abcd:a/64', + 'fec::abcd:b/64', + ]) + + self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output) + + def test_ipv6_range_multichar_field(self): + input = 'fec::abcd:[f-11]/64' + output = sorted([ + 'fec::abcd:f/64', + 'fec::abcd:10/64', + 'fec::abcd:11/64', + ]) + + self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output) + + def test_ipv6_set(self): + input = 'fec::abcd:[9,ab]/64' + output = sorted([ + 'fec::abcd:9/64', + 'fec::abcd:ab/64', + ]) + + self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output) + + def test_ipv6_multiple_ranges(self): + input = 'fec::[1-2]bcd:[9-b]/64' + output = sorted([ + 'fec::1bcd:9/64', + 'fec::1bcd:a/64', + 'fec::1bcd:b/64', + 'fec::2bcd:9/64', + 'fec::2bcd:a/64', + 'fec::2bcd:b/64', + ]) + + self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output) + + def test_ipv6_multiple_sets(self): + input = 'fec::[a,f]bcd:[9,ab]/64' + output = sorted([ + 'fec::abcd:9/64', + 'fec::abcd:ab/64', + 'fec::fbcd:9/64', + 'fec::fbcd:ab/64', + ]) + + self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output) + + def test_ipv6_set_and_range(self): + input = 'fec::[dead,beaf]:[9-b]/64' + output = sorted([ + 'fec::dead:9/64', + 'fec::dead:a/64', + 'fec::dead:b/64', + 'fec::beaf:9/64', + 'fec::beaf:a/64', + 'fec::beaf:b/64', + ]) + + self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output) + # TODO: negative tests # TODO: alphanumeric From acb66c7dc0f99435299bfb81ec7d626dfd632714 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Fri, 10 Jan 2020 11:21:37 +0000 Subject: [PATCH 6/7] Negative tests for expand_ipaddress_pattern --- netbox/utilities/tests/test_forms.py | 34 +++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/netbox/utilities/tests/test_forms.py b/netbox/utilities/tests/test_forms.py index ea693f42c..7bad34e3c 100644 --- a/netbox/utilities/tests/test_forms.py +++ b/netbox/utilities/tests/test_forms.py @@ -128,6 +128,38 @@ class ExpandIPAddress(TestCase): self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output) - # TODO: negative tests + def test_invalid_address_family(self): + with self.assertRaisesRegex(Exception, 'Invalid IP address family: 5'): + sorted(expand_ipaddress_pattern(None, 5)) + + def test_invalid_non_pattern(self): + with self.assertRaises(ValueError): + sorted(expand_ipaddress_pattern('1.2.3.4/32', 4)) + + def test_invalid_range(self): + with self.assertRaises(ValueError): + sorted(expand_ipaddress_pattern('1.2.3.[4-]/32', 4)) + + with self.assertRaises(ValueError): + sorted(expand_ipaddress_pattern('1.2.3.[-4]/32', 4)) + + with self.assertRaises(ValueError): + sorted(expand_ipaddress_pattern('1.2.3.[4--5]/32', 4)) + + def test_invalid_range_bounds(self): + self.assertEqual(sorted(expand_ipaddress_pattern('1.2.3.[4-3]/32', 6)), []) + + def test_invalid_set(self): + with self.assertRaises(ValueError): + sorted(expand_ipaddress_pattern('1.2.3.[4]/32', 4)) + + with self.assertRaises(ValueError): + sorted(expand_ipaddress_pattern('1.2.3.[4,]/32', 4)) + + with self.assertRaises(ValueError): + sorted(expand_ipaddress_pattern('1.2.3.[,4]/32', 4)) + + with self.assertRaises(ValueError): + sorted(expand_ipaddress_pattern('1.2.3.[4,,5]/32', 4)) # TODO: alphanumeric From 71120d9899d4f6481a07b9728c10ba3dbc0aa0bd Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Fri, 10 Jan 2020 11:54:43 +0000 Subject: [PATCH 7/7] Added tests for alphanumeric --- netbox/utilities/tests/test_forms.py | 120 ++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) diff --git a/netbox/utilities/tests/test_forms.py b/netbox/utilities/tests/test_forms.py index 7bad34e3c..2d7235505 100644 --- a/netbox/utilities/tests/test_forms.py +++ b/netbox/utilities/tests/test_forms.py @@ -1,3 +1,4 @@ +from django import forms from django.test import TestCase from utilities.forms import * @@ -162,4 +163,121 @@ class ExpandIPAddress(TestCase): with self.assertRaises(ValueError): sorted(expand_ipaddress_pattern('1.2.3.[4,,5]/32', 4)) -# TODO: alphanumeric + +class ExpandAlphanumeric(TestCase): + """ + Validate the operation of expand_alphanumeric_pattern(). + """ + def test_range_numberic(self): + input = 'r[9-11]a' + output = sorted([ + 'r9a', + 'r10a', + 'r11a', + ]) + + self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output) + + def test_range_alpha(self): + input = '[r-t]1a' + output = sorted([ + 'r1a', + 's1a', + 't1a', + ]) + + self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output) + + def test_set(self): + input = '[r,t]1a' + output = sorted([ + 'r1a', + 't1a', + ]) + + self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output) + + def test_set_multichar(self): + input = '[ra,tb]1a' + output = sorted([ + 'ra1a', + 'tb1a', + ]) + + self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output) + + def test_multiple_ranges(self): + input = '[r-t]1[a-b]' + output = sorted([ + 'r1a', + 'r1b', + 's1a', + 's1b', + 't1a', + 't1b', + ]) + + self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output) + + def test_multiple_sets(self): + input = '[ra,tb]1[ax,by]' + output = sorted([ + 'ra1ax', + 'ra1by', + 'tb1ax', + 'tb1by', + ]) + + self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output) + + def test_set_and_range(self): + input = '[ra,tb]1[a-c]' + output = sorted([ + 'ra1a', + 'ra1b', + 'ra1c', + 'tb1a', + 'tb1b', + 'tb1c', + ]) + + self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output) + + def test_invalid_non_pattern(self): + with self.assertRaises(ValueError): + sorted(expand_alphanumeric_pattern('r9a')) + + def test_invalid_range(self): + with self.assertRaises(ValueError): + sorted(expand_alphanumeric_pattern('r[8-]a')) + + with self.assertRaises(ValueError): + sorted(expand_alphanumeric_pattern('r[-8]a')) + + with self.assertRaises(ValueError): + sorted(expand_alphanumeric_pattern('r[8--9]a')) + + def test_invalid_range_alphanumeric(self): + self.assertEqual(sorted(expand_alphanumeric_pattern('r[9-a]a')), []) + self.assertEqual(sorted(expand_alphanumeric_pattern('r[a-9]a')), []) + + def test_invalid_range_bounds(self): + self.assertEqual(sorted(expand_alphanumeric_pattern('r[9-8]a')), []) + self.assertEqual(sorted(expand_alphanumeric_pattern('r[b-a]a')), []) + + def test_invalid_range_len(self): + with self.assertRaises(forms.ValidationError): + sorted(expand_alphanumeric_pattern('r[a-bb]a')) + + def test_invalid_set(self): + with self.assertRaises(ValueError): + sorted(expand_alphanumeric_pattern('r[a]a')) + + with self.assertRaises(ValueError): + sorted(expand_alphanumeric_pattern('r[a,]a')) + + with self.assertRaises(ValueError): + sorted(expand_alphanumeric_pattern('r[,a]a')) + + with self.assertRaises(ValueError): + sorted(expand_alphanumeric_pattern('r[a,,b]a'))