Skip to content

Commit bf909cc

Browse files
authored
[fix] Dealt with small subnets #842
Fixes #842
1 parent c4a1683 commit bf909cc

3 files changed

Lines changed: 196 additions & 20 deletions

File tree

openwisp_controller/subnet_division/base/models.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ def _validate_existing_fields(self):
104104

105105
def _validate_master_subnet_consistency(self):
106106
master_subnet = self.master_subnet.subnet
107-
# Validate size of generated subnet is not greater than size of master subnet
107+
# Ensure that the size of the generated subnet
108+
# is not greater than size of master subnet
108109
try:
109110
next(master_subnet.subnets(new_prefix=self.size))
110111
except ValueError:
@@ -118,13 +119,19 @@ def _validate_master_subnet_consistency(self):
118119
}
119120
)
120121

121-
# Validate master subnet can accommodate required number of generated subnets
122-
if self.number_of_subnets > (2 ** (self.size - master_subnet.prefixlen)):
122+
# Ensure that master subnet can accommodate
123+
# the required number of generated subnets
124+
available = 2 ** (self.size - master_subnet.prefixlen)
125+
# Account for the reserved subnet
126+
available -= 1
127+
if self.number_of_subnets >= available:
123128
raise ValidationError(
124129
{
125130
'number_of_subnets': _(
126-
f'Master subnet cannot accommodate {self.number_of_subnets} '
127-
f'subnets of size /{self.size}'
131+
'The master subnet is too small to acommodate the '
132+
'requested "number of subnets" plus the reserved '
133+
'subnet, please increase the size of the master '
134+
'subnet or decrease the "size of subnets" field.'
128135
)
129136
}
130137
)
@@ -139,11 +146,10 @@ def _validate_master_subnet_consistency(self):
139146
)
140147

141148
def _validate_ip_address_consistency(self):
142-
# Validate individual generated subnet can accommodate required number of IPs
143149
try:
144150
next(
145151
ip_network(str(self.master_subnet.subnet)).subnets(new_prefix=self.size)
146-
)[self.number_of_ips]
152+
)[self.number_of_ips - 1]
147153
except IndexError:
148154
raise ValidationError(
149155
{

openwisp_controller/subnet_division/rule_types/base.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,7 @@ def get_max_subnet(master_subnet, division_rule):
194194
subnet_obj.full_clean()
195195
subnet_obj.save()
196196
max_subnet = subnet_obj.subnet
197-
finally:
198-
return max_subnet
197+
return max_subnet
199198

200199
@staticmethod
201200
def create_subnets(config, division_rule, max_subnet, generated_indexes):
@@ -234,24 +233,37 @@ def create_subnets(config, division_rule, max_subnet, generated_indexes):
234233
def create_ips(config, division_rule, generated_subnets, generated_indexes):
235234
generated_ips = []
236235
for subnet_obj in generated_subnets:
237-
for ip_id in range(1, division_rule.number_of_ips + 1):
236+
# don't assign first ip address of a subnet,
237+
# unless the rule is designed to use the whole
238+
# address space of the subnet
239+
if subnet_obj.subnet.num_addresses != division_rule.number_of_ips:
240+
index_start = 1
241+
index_end = division_rule.number_of_ips + 1
242+
# this allows handling /32, /128 or cases in which
243+
# the number of requested ip addresses matches exactly
244+
# what is available in the subnet
245+
else:
246+
index_start = 0
247+
index_end = division_rule.number_of_ips
248+
# generate IPs and indexes accordingly
249+
for ip_index in range(index_start, index_end):
238250
ip_obj = IpAddress(
239251
subnet_id=subnet_obj.id,
240-
ip_address=str(subnet_obj.subnet[ip_id]),
252+
ip_address=str(subnet_obj.subnet[ip_index]),
241253
)
242254
ip_obj.full_clean()
243255
generated_ips.append(ip_obj)
244-
256+
# ensure human friendly labels (starting from 1 instead of 0)
257+
keyword_index = ip_index if index_start == 1 else ip_index + 1
245258
generated_indexes.append(
246259
SubnetDivisionIndex(
247-
keyword=f'{subnet_obj.name}_ip{ip_id}',
260+
keyword=f'{subnet_obj.name}_ip{keyword_index}',
248261
subnet_id=subnet_obj.id,
249262
ip_id=ip_obj.id,
250263
rule_id=division_rule.id,
251264
config=config,
252265
)
253266
)
254-
255267
IpAddress.objects.bulk_create(generated_ips)
256268
return generated_ips
257269

openwisp_controller/subnet_division/tests/test_models.py

Lines changed: 164 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,11 @@ def test_field_validations(self):
174174
rule.full_clean()
175175
expected_message_dict = {
176176
'number_of_subnets': [
177-
'Master subnet cannot accommodate 99999999 subnets of size /28'
177+
'The master subnet is too small to acommodate '
178+
'the requested "number of subnets" plus the '
179+
'reserved subnet, please increase the size of '
180+
'the master subnet or decrease the '
181+
'"size of subnets" field.'
178182
]
179183
}
180184
self.assertDictEqual(
@@ -246,6 +250,73 @@ def test_field_validations(self):
246250
{'organization': ['Organization should be same as the subnet']},
247251
)
248252

253+
def test_slash_32_rule_ipv4(self):
254+
rule = self._get_vpn_subdivision_rule(
255+
size=32, number_of_ips=1, number_of_subnets=1
256+
)
257+
self.config.templates.add(self.template)
258+
rule.subnetdivisionindex_set.count()
259+
index_queryset = rule.subnetdivisionindex_set.filter(
260+
config_id=self.config.id, subnet_id__isnull=False, ip_id__isnull=False
261+
)
262+
self.assertEqual(index_queryset.count(), 1)
263+
index = index_queryset.first()
264+
self.assertEqual(str(index.subnet.subnet), '10.0.0.1/32')
265+
self.assertEqual(index.ip.ip_address, '10.0.0.1')
266+
267+
def test_slash_32_rule_ipv4_error(self):
268+
master_ipv4 = self._get_master_subnet(subnet='192.168.1.1/32')
269+
self.vpn_server.subnet = master_ipv4
270+
self.vpn_server.save()
271+
try:
272+
self._get_vpn_subdivision_rule(
273+
size=32, number_of_ips=1, number_of_subnets=1, master_subnet=master_ipv4
274+
)
275+
except ValidationError as e:
276+
self.assertIn('number_of_subnets', e.message_dict)
277+
self.assertIn(
278+
'The master subnet is too small to acommodate',
279+
e.message_dict['number_of_subnets'][0],
280+
)
281+
else:
282+
self.fail('Expected error not raised')
283+
284+
def test_slash_128_rule_ipv6(self):
285+
master_ipv6 = self._get_master_subnet(subnet='fd12:3456:7890::/48')
286+
self.vpn_server.subnet = master_ipv6
287+
self.vpn_server.save()
288+
rule = self._get_vpn_subdivision_rule(
289+
size=128, number_of_ips=1, number_of_subnets=1, master_subnet=master_ipv6
290+
)
291+
self.config.templates.add(self.template)
292+
index_queryset = rule.subnetdivisionindex_set.filter(
293+
config_id=self.config.id, subnet_id__isnull=False, ip_id__isnull=False
294+
)
295+
self.assertEqual(index_queryset.count(), 1)
296+
index = index_queryset.first()
297+
self.assertEqual(str(index.subnet.subnet), 'fd12:3456:7890::1/128')
298+
self.assertEqual(index.ip.ip_address, 'fd12:3456:7890::1')
299+
300+
def test_slash_128_rule_ipv6_error(self):
301+
master_ipv6 = self._get_master_subnet(subnet='fd12:3456:7890::/128')
302+
self.vpn_server.subnet = master_ipv6
303+
self.vpn_server.save()
304+
try:
305+
self._get_vpn_subdivision_rule(
306+
size=128,
307+
number_of_ips=1,
308+
number_of_subnets=1,
309+
master_subnet=master_ipv6,
310+
)
311+
except ValidationError as e:
312+
self.assertIn('number_of_subnets', e.message_dict)
313+
self.assertIn(
314+
'The master subnet is too small to acommodate',
315+
e.message_dict['number_of_subnets'][0],
316+
)
317+
else:
318+
self.fail('Expected error not raised')
319+
249320
def test_rule_label_updated(self):
250321
new_rule_label = 'TSDR'
251322
rule = self._get_vpn_subdivision_rule(label='VPN_OW')
@@ -418,11 +489,20 @@ def test_multiple_vpnclient_delete(self):
418489
@patch('logging.Logger.error')
419490
def test_subnets_exhausted(self, mocked_logger):
420491
subnet = self._get_master_subnet(
421-
'10.0.0.0/28', master_subnet=self.master_subnet
422-
)
492+
'10.0.0.0/29', master_subnet=self.master_subnet
493+
)
494+
# The master subnet can acommodate
495+
# this rule only once:
496+
# A /29 has 4 /31 slots available
497+
# Minus the reserved subnet = 3
498+
# Each run will eat 2 slots.
499+
# Hence we expect this to run fine the
500+
# first time but fail the second time.
423501
self._get_vpn_subdivision_rule(
424502
master_subnet=subnet,
425-
size=29,
503+
size=31,
504+
number_of_ips=2,
505+
number_of_subnets=2,
426506
)
427507
self.vpn_server.subnet = subnet
428508
self.vpn_server.save()
@@ -577,16 +657,94 @@ def test_device_subnet_division_rule(self):
577657
'backend': 'netjsonconfig.OpenWrt',
578658
}
579659
response = self.client.post(reverse('controller:device_register'), options)
580-
lines = response.content.decode().split('\n')
581-
self.assertEqual(lines[0], 'registration-result: success')
660+
self.assertEqual(response.status_code, 201)
661+
662+
# Verify generated subnets and IP addresses match expectations
663+
self.assertEqual(
664+
subnet_query.count(),
665+
rule.number_of_subnets,
666+
)
667+
self.assertEqual(
668+
self.ip_query.count(), (rule.number_of_subnets * rule.number_of_ips)
669+
)
670+
subnets = subnet_query.order_by('created')
671+
subnet1 = subnets[0]
672+
subnet2 = subnets[1]
673+
self.assertEqual(str(subnet1.subnet), '10.0.0.16/28')
674+
self.assertEqual(str(subnet2.subnet), '10.0.0.32/28')
675+
self.assertEqual(subnet1.ipaddress_set.count(), 2)
676+
self.assertEqual(subnet2.ipaddress_set.count(), 2)
677+
subnet1_ips = list(subnet1.ipaddress_set.order_by('created').all())
678+
with self.subTest('Check IP addresses of subnet1'):
679+
self.assertEqual(str(subnet1_ips[0].ip_address), '10.0.0.17')
680+
self.assertEqual(str(subnet1_ips[1].ip_address), '10.0.0.18')
681+
subnet2_ips = list(subnet2.ipaddress_set.order_by('created').all())
682+
with self.subTest('Check IP addresses of subnet2'):
683+
self.assertEqual(str(subnet2_ips[0].ip_address), '10.0.0.33')
684+
self.assertEqual(str(subnet2_ips[1].ip_address), '10.0.0.34')
685+
686+
# Verify context of config
687+
device = Device.objects.get(mac_address='FF:FF:FF:FF:FF:FF')
688+
context = get_subnet_division_config_context(device.config)
689+
self.assertIn(f'{rule.label}_prefixlen', context)
690+
for subnet_id in range(1, rule.number_of_subnets + 1):
691+
self.assertIn(f'{rule.label}_subnet{subnet_id}', context)
692+
for ip_id in range(1, rule.number_of_ips + 1):
693+
self.assertIn(f'{rule.label}_subnet{subnet_id}_ip{ip_id}', context)
694+
695+
# Verify working of delete handler
696+
device.delete()
697+
self.assertEqual(
698+
subnet_query.count(),
699+
0,
700+
)
701+
self.assertEqual(self.ip_query.count(), 0)
702+
703+
def test_device_rule_use_entire_subnet(self):
704+
self.config.delete()
705+
rule = self._get_device_subdivision_rule(size=29, number_of_ips=8)
706+
OrganizationConfigSettings.objects.create(
707+
organization=self.org, shared_secret='shared_secret'
708+
)
709+
subnet_query = self.subnet_query.filter(organization_id=self.org.id).exclude(
710+
id=self.master_subnet.id
711+
)
712+
self.assertEqual(subnet_query.count(), 0)
713+
714+
# Register device
715+
options = {
716+
'hardware_id': '1234',
717+
'secret': 'shared_secret',
718+
'name': 'FF:FF:FF:FF:FF:FF',
719+
'mac_address': 'FF:FF:FF:FF:FF:FF',
720+
'backend': 'netjsonconfig.OpenWrt',
721+
}
722+
response = self.client.post(reverse('controller:device_register'), options)
723+
self.assertEqual(response.status_code, 201)
582724

725+
# Verify generated subnets and IP addresses match expectations
583726
self.assertEqual(
584727
subnet_query.count(),
585728
rule.number_of_subnets,
586729
)
587730
self.assertEqual(
588731
self.ip_query.count(), (rule.number_of_subnets * rule.number_of_ips)
589732
)
733+
subnets = subnet_query.order_by('created')
734+
subnet1 = subnets[0]
735+
subnet2 = subnets[1]
736+
self.assertEqual(str(subnet1.subnet), '10.0.0.8/29')
737+
self.assertEqual(str(subnet2.subnet), '10.0.0.16/29')
738+
self.assertEqual(subnet1.ipaddress_set.count(), 8)
739+
self.assertEqual(subnet2.ipaddress_set.count(), 8)
740+
741+
number = 8
742+
for subnet in [subnet1, subnet2]:
743+
for ip in subnet.ipaddress_set.order_by('created').all():
744+
expected_ip = f'10.0.0.{number}'
745+
with self.subTest(f'Expect IP address: {expected_ip}'):
746+
self.assertEqual(str(ip.ip_address), expected_ip)
747+
number += 1
590748

591749
# Verify context of config
592750
device = Device.objects.get(mac_address='FF:FF:FF:FF:FF:FF')

0 commit comments

Comments
 (0)