Skip to content

Commit cb91c92

Browse files
Merge pull request #3865 from hSaria/3623-interface-word-expansion
Fixes #3623: Word expansion for interfaces
2 parents 0296aa2 + a5413a5 commit cb91c92

File tree

3 files changed

+295
-2
lines changed

3 files changed

+295
-2
lines changed

docs/release-notes/version-2.6.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* [#3187](https://github.com/netbox-community/netbox/issues/3187) - Add rack selection field to rack elevations
1111
* [#3393](https://github.com/netbox-community/netbox/issues/3393) - Paginate the circuits at the provider details view
1212
* [#3440](https://github.com/netbox-community/netbox/issues/3440) - Add total length to cable trace
13+
* [#3623](https://github.com/netbox-community/netbox/issues/3623) - Add word expansion during interface creation
1314
* [#3851](https://github.com/netbox-community/netbox/issues/3851) - Allow passing initial data to custom script forms
1415

1516
## Bug Fixes

netbox/utilities/forms.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,16 @@ def parse_alphanumeric_range(string):
6060
for n in list(range(int(begin), int(end) + 1)):
6161
values.append(n)
6262
else:
63-
for n in list(range(ord(begin), ord(end) + 1)):
64-
values.append(chr(n))
63+
# Value-based
64+
if begin == end:
65+
values.append(begin)
66+
# Range-based
67+
else:
68+
# Not a valid range (more than a single character)
69+
if not len(begin) == len(end) == 1:
70+
raise forms.ValidationError('Range "{}" is invalid.'.format(dash_range))
71+
for n in list(range(ord(begin), ord(end) + 1)):
72+
values.append(chr(n))
6573
return values
6674

6775

@@ -481,6 +489,7 @@ def __init__(self, *args, **kwargs):
481489
'Mixed cases and types within a single range are not supported.<br />' \
482490
'Examples:<ul><li><code>ge-0/0/[0-23,25,30]</code></li>' \
483491
'<li><code>e[0-3][a-d,f]</code></li>' \
492+
'<li><code>[xe,ge]-0/0/0</code></li>' \
484493
'<li><code>e[0-3,a-d,f]</code></li></ul>'
485494

486495
def to_python(self, value):

netbox/utilities/tests/test_forms.py

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
from django import forms
2+
from django.test import TestCase
3+
4+
from utilities.forms import *
5+
6+
7+
class ExpandIPAddress(TestCase):
8+
"""
9+
Validate the operation of expand_ipaddress_pattern().
10+
"""
11+
def test_ipv4_range(self):
12+
input = '1.2.3.[9-10]/32'
13+
output = sorted([
14+
'1.2.3.9/32',
15+
'1.2.3.10/32',
16+
])
17+
18+
self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output)
19+
20+
def test_ipv4_set(self):
21+
input = '1.2.3.[4,44]/32'
22+
output = sorted([
23+
'1.2.3.4/32',
24+
'1.2.3.44/32',
25+
])
26+
27+
self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output)
28+
29+
def test_ipv4_multiple_ranges(self):
30+
input = '1.[9-10].3.[9-11]/32'
31+
output = sorted([
32+
'1.9.3.9/32',
33+
'1.9.3.10/32',
34+
'1.9.3.11/32',
35+
'1.10.3.9/32',
36+
'1.10.3.10/32',
37+
'1.10.3.11/32',
38+
])
39+
40+
self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output)
41+
42+
def test_ipv4_multiple_sets(self):
43+
input = '1.[2,22].3.[4,44]/32'
44+
output = sorted([
45+
'1.2.3.4/32',
46+
'1.2.3.44/32',
47+
'1.22.3.4/32',
48+
'1.22.3.44/32',
49+
])
50+
51+
self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output)
52+
53+
def test_ipv4_set_and_range(self):
54+
input = '1.[2,22].3.[9-11]/32'
55+
output = sorted([
56+
'1.2.3.9/32',
57+
'1.2.3.10/32',
58+
'1.2.3.11/32',
59+
'1.22.3.9/32',
60+
'1.22.3.10/32',
61+
'1.22.3.11/32',
62+
])
63+
64+
self.assertEqual(sorted(expand_ipaddress_pattern(input, 4)), output)
65+
66+
def test_ipv6_range(self):
67+
input = 'fec::abcd:[9-b]/64'
68+
output = sorted([
69+
'fec::abcd:9/64',
70+
'fec::abcd:a/64',
71+
'fec::abcd:b/64',
72+
])
73+
74+
self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output)
75+
76+
def test_ipv6_range_multichar_field(self):
77+
input = 'fec::abcd:[f-11]/64'
78+
output = sorted([
79+
'fec::abcd:f/64',
80+
'fec::abcd:10/64',
81+
'fec::abcd:11/64',
82+
])
83+
84+
self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output)
85+
86+
def test_ipv6_set(self):
87+
input = 'fec::abcd:[9,ab]/64'
88+
output = sorted([
89+
'fec::abcd:9/64',
90+
'fec::abcd:ab/64',
91+
])
92+
93+
self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output)
94+
95+
def test_ipv6_multiple_ranges(self):
96+
input = 'fec::[1-2]bcd:[9-b]/64'
97+
output = sorted([
98+
'fec::1bcd:9/64',
99+
'fec::1bcd:a/64',
100+
'fec::1bcd:b/64',
101+
'fec::2bcd:9/64',
102+
'fec::2bcd:a/64',
103+
'fec::2bcd:b/64',
104+
])
105+
106+
self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output)
107+
108+
def test_ipv6_multiple_sets(self):
109+
input = 'fec::[a,f]bcd:[9,ab]/64'
110+
output = sorted([
111+
'fec::abcd:9/64',
112+
'fec::abcd:ab/64',
113+
'fec::fbcd:9/64',
114+
'fec::fbcd:ab/64',
115+
])
116+
117+
self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output)
118+
119+
def test_ipv6_set_and_range(self):
120+
input = 'fec::[dead,beaf]:[9-b]/64'
121+
output = sorted([
122+
'fec::dead:9/64',
123+
'fec::dead:a/64',
124+
'fec::dead:b/64',
125+
'fec::beaf:9/64',
126+
'fec::beaf:a/64',
127+
'fec::beaf:b/64',
128+
])
129+
130+
self.assertEqual(sorted(expand_ipaddress_pattern(input, 6)), output)
131+
132+
def test_invalid_address_family(self):
133+
with self.assertRaisesRegex(Exception, 'Invalid IP address family: 5'):
134+
sorted(expand_ipaddress_pattern(None, 5))
135+
136+
def test_invalid_non_pattern(self):
137+
with self.assertRaises(ValueError):
138+
sorted(expand_ipaddress_pattern('1.2.3.4/32', 4))
139+
140+
def test_invalid_range(self):
141+
with self.assertRaises(ValueError):
142+
sorted(expand_ipaddress_pattern('1.2.3.[4-]/32', 4))
143+
144+
with self.assertRaises(ValueError):
145+
sorted(expand_ipaddress_pattern('1.2.3.[-4]/32', 4))
146+
147+
with self.assertRaises(ValueError):
148+
sorted(expand_ipaddress_pattern('1.2.3.[4--5]/32', 4))
149+
150+
def test_invalid_range_bounds(self):
151+
self.assertEqual(sorted(expand_ipaddress_pattern('1.2.3.[4-3]/32', 6)), [])
152+
153+
def test_invalid_set(self):
154+
with self.assertRaises(ValueError):
155+
sorted(expand_ipaddress_pattern('1.2.3.[4]/32', 4))
156+
157+
with self.assertRaises(ValueError):
158+
sorted(expand_ipaddress_pattern('1.2.3.[4,]/32', 4))
159+
160+
with self.assertRaises(ValueError):
161+
sorted(expand_ipaddress_pattern('1.2.3.[,4]/32', 4))
162+
163+
with self.assertRaises(ValueError):
164+
sorted(expand_ipaddress_pattern('1.2.3.[4,,5]/32', 4))
165+
166+
167+
class ExpandAlphanumeric(TestCase):
168+
"""
169+
Validate the operation of expand_alphanumeric_pattern().
170+
"""
171+
def test_range_numberic(self):
172+
input = 'r[9-11]a'
173+
output = sorted([
174+
'r9a',
175+
'r10a',
176+
'r11a',
177+
])
178+
179+
self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
180+
181+
def test_range_alpha(self):
182+
input = '[r-t]1a'
183+
output = sorted([
184+
'r1a',
185+
's1a',
186+
't1a',
187+
])
188+
189+
self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
190+
191+
def test_set(self):
192+
input = '[r,t]1a'
193+
output = sorted([
194+
'r1a',
195+
't1a',
196+
])
197+
198+
self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
199+
200+
def test_set_multichar(self):
201+
input = '[ra,tb]1a'
202+
output = sorted([
203+
'ra1a',
204+
'tb1a',
205+
])
206+
207+
self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
208+
209+
def test_multiple_ranges(self):
210+
input = '[r-t]1[a-b]'
211+
output = sorted([
212+
'r1a',
213+
'r1b',
214+
's1a',
215+
's1b',
216+
't1a',
217+
't1b',
218+
])
219+
220+
self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
221+
222+
def test_multiple_sets(self):
223+
input = '[ra,tb]1[ax,by]'
224+
output = sorted([
225+
'ra1ax',
226+
'ra1by',
227+
'tb1ax',
228+
'tb1by',
229+
])
230+
231+
self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
232+
233+
def test_set_and_range(self):
234+
input = '[ra,tb]1[a-c]'
235+
output = sorted([
236+
'ra1a',
237+
'ra1b',
238+
'ra1c',
239+
'tb1a',
240+
'tb1b',
241+
'tb1c',
242+
])
243+
244+
self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
245+
246+
def test_invalid_non_pattern(self):
247+
with self.assertRaises(ValueError):
248+
sorted(expand_alphanumeric_pattern('r9a'))
249+
250+
def test_invalid_range(self):
251+
with self.assertRaises(ValueError):
252+
sorted(expand_alphanumeric_pattern('r[8-]a'))
253+
254+
with self.assertRaises(ValueError):
255+
sorted(expand_alphanumeric_pattern('r[-8]a'))
256+
257+
with self.assertRaises(ValueError):
258+
sorted(expand_alphanumeric_pattern('r[8--9]a'))
259+
260+
def test_invalid_range_alphanumeric(self):
261+
self.assertEqual(sorted(expand_alphanumeric_pattern('r[9-a]a')), [])
262+
self.assertEqual(sorted(expand_alphanumeric_pattern('r[a-9]a')), [])
263+
264+
def test_invalid_range_bounds(self):
265+
self.assertEqual(sorted(expand_alphanumeric_pattern('r[9-8]a')), [])
266+
self.assertEqual(sorted(expand_alphanumeric_pattern('r[b-a]a')), [])
267+
268+
def test_invalid_range_len(self):
269+
with self.assertRaises(forms.ValidationError):
270+
sorted(expand_alphanumeric_pattern('r[a-bb]a'))
271+
272+
def test_invalid_set(self):
273+
with self.assertRaises(ValueError):
274+
sorted(expand_alphanumeric_pattern('r[a]a'))
275+
276+
with self.assertRaises(ValueError):
277+
sorted(expand_alphanumeric_pattern('r[a,]a'))
278+
279+
with self.assertRaises(ValueError):
280+
sorted(expand_alphanumeric_pattern('r[,a]a'))
281+
282+
with self.assertRaises(ValueError):
283+
sorted(expand_alphanumeric_pattern('r[a,,b]a'))

0 commit comments

Comments
 (0)