From 5c99e07548104c70b257501d5f4e4f66385672ca Mon Sep 17 00:00:00 2001 From: Benjamin Bertrand <benjamin.bertrand@esss.se> Date: Thu, 19 Dec 2019 15:11:11 +0100 Subject: [PATCH] Prevent network overlapping JIRA INFRA-1627 #action In Progress --- app/models.py | 30 +++++- app/network/views.py | 32 ++++--- app/utils.py | 8 ++ tests/functional/factories.py | 7 +- tests/functional/test_api.py | 165 ++++++++++++++++++++++---------- tests/functional/test_models.py | 94 ++++++++++++++++-- tests/functional/test_web.py | 44 +++++++-- 7 files changed, 292 insertions(+), 88 deletions(-) diff --git a/app/models.py b/app/models.py index fb3468f..f2b78d9 100644 --- a/app/models.py +++ b/app/models.py @@ -856,7 +856,12 @@ class Network(CreatedMixin, db.Model): # If domain_id is not passed, we set it to the network scope value if "domain_id" not in kwargs: kwargs["domain_id"] = kwargs["scope"].domain_id - super().__init__(**kwargs) + # WARNING! Setting self.scope will call validate_networks in the NetworkScope class + # For the validation to work, self.address must be set before! + # Ensure that address and vlan_name are passed before scope + vlan_name = kwargs.pop("vlan_name") + address = kwargs.pop("address") + super().__init__(vlan_name=vlan_name, address=address, **kwargs) def __str__(self): return str(self.vlan_name) @@ -1610,6 +1615,22 @@ class NetworkScope(CreatedMixin, db.Model): def __str__(self): return str(self.name) + @validates("networks") + def validate_networks(self, key, network): + """Ensure the network is included in the supernet and doesn't overlap + existing networks""" + if not network.network_ip.subnet_of(self.supernet_ip): + raise ValidationError( + f"{network.network_ip} is not a subnet of {self.supernet_ip}" + ) + existing_networks = Network.query.filter_by(scope=self).all() + for existing_network in existing_networks: + if network.network_ip.overlaps(existing_network.network_ip): + raise ValidationError( + f"{network.network_ip} overlaps {existing_network} ({existing_network.network_ip})" + ) + return network + @property def supernet_ip(self): return ipaddress.ip_network(self.supernet) @@ -1648,11 +1669,14 @@ class NetworkScope(CreatedMixin, db.Model): return sorted(network.network_ip for network in self.networks) def available_subnets(self, prefix): - """Return the list of available subnets with the given prefix""" + """Return the list of available subnets with the given prefix + + Overlapping subnets with existing networks are filtered""" + used = self.used_subnets() return [ str(subnet) for subnet in self.supernet_ip.subnets(new_prefix=prefix) - if subnet not in self.used_subnets() + if not utils.overlaps(subnet, used) ] def to_dict(self, recursive=False): diff --git a/app/network/views.py b/app/network/views.py index 672bdbf..f16ecf7 100644 --- a/app/network/views.py +++ b/app/network/views.py @@ -651,18 +651,24 @@ def create_network(): form = NetworkForm(request.form, scope_id=scope_id) if form.validate_on_submit(): scope_id = form.scope_id.data - network = models.Network( - scope=models.NetworkScope.query.get(scope_id), - vlan_name=form.vlan_name.data, - vlan_id=form.vlan_id.data, - description=form.description.data or None, - address=form.address.data, - first_ip=form.first_ip.data, - last_ip=form.last_ip.data, - gateway=form.gateway.data, - domain=models.Domain.query.get(form.domain_id.data), - admin_only=form.admin_only.data, - ) + try: + network = models.Network( + scope=models.NetworkScope.query.get(scope_id), + vlan_name=form.vlan_name.data, + vlan_id=form.vlan_id.data, + description=form.description.data or None, + address=form.address.data, + first_ip=form.first_ip.data, + last_ip=form.last_ip.data, + gateway=form.gateway.data, + domain=models.Domain.query.get(form.domain_id.data), + admin_only=form.admin_only.data, + ) + except ValidationError as e: + # Check for error raised by model validation (not implemented in form vaildation) + current_app.logger.warning(f"{e}") + flash(f"{e}", "error") + return render_template("network/create_network.html", form=form) current_app.logger.debug(f"Trying to create: {network!r}") db.session.add(network) try: @@ -676,6 +682,8 @@ def create_network(): # Save scope_id to the session to retrieve it after the redirect session["scope_id"] = scope_id return redirect(url_for("network.create_network")) + else: + current_app.logger.info(form.errors) return render_template("network/create_network.html", form=form) diff --git a/app/utils.py b/app/utils.py index cf52ea1..4ab053f 100644 --- a/app/utils.py +++ b/app/utils.py @@ -555,3 +555,11 @@ def validate_ip(ip, network): raise ValidationError( f"IP address {ip} is not in range {network.first} - {network.last}" ) + + +def overlaps(subnet, subnets): + """Return True if the subnet overlaps with any of the subnets""" + for network in subnets: + if subnet.overlaps(network): + return True + return False diff --git a/tests/functional/factories.py b/tests/functional/factories.py index be0c0e1..60bbb8c 100644 --- a/tests/functional/factories.py +++ b/tests/functional/factories.py @@ -114,7 +114,7 @@ class NetworkScopeFactory(factory.alchemy.SQLAlchemyModelFactory): name = factory.Sequence(lambda n: f"scope{n}") first_vlan = factory.Sequence(lambda n: 1600 + 10 * n) last_vlan = factory.Sequence(lambda n: 1609 + 10 * n) - supernet = factory.Faker("ipv4", network=True) + supernet = factory.Sequence(lambda n: str(ipaddress.ip_network(f"172.{n}.0.0/16"))) user = factory.SubFactory(UserFactory) domain = factory.SubFactory(DomainFactory) @@ -127,7 +127,6 @@ class NetworkFactory(factory.alchemy.SQLAlchemyModelFactory): vlan_name = factory.Sequence(lambda n: f"vlan{n}") vlan_id = factory.Sequence(lambda n: 1600 + n) - address = factory.Sequence(lambda n: f"192.168.{n}.0/24") scope = factory.SubFactory(NetworkScopeFactory) user = factory.SubFactory(UserFactory) domain = factory.SubFactory(DomainFactory) @@ -150,6 +149,10 @@ class NetworkFactory(factory.alchemy.SQLAlchemyModelFactory): hosts = list(net.hosts()) return str(hosts[-1]) + @factory.lazy_attribute + def address(self): + return self.scope.available_subnets(24)[0] + class DeviceTypeFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta: diff --git a/tests/functional/test_api.py b/tests/functional/test_api.py index 316c1dc..a33542f 100644 --- a/tests/functional/test_api.py +++ b/tests/functional/test_api.py @@ -615,16 +615,26 @@ def test_get_items(client, location_factory, item_factory, readonly_token): check_response_message(response, "Invalid query arguments", 422) -def test_get_networks(client, network_factory, readonly_token): +def test_get_networks(client, network_scope_factory, network_factory, readonly_token): # Create some networks + scope = network_scope_factory(supernet="172.16.0.0/16") network1 = network_factory( - address="172.16.1.0/24", first_ip="172.16.1.1", last_ip="172.16.1.254" + address="172.16.1.0/24", + first_ip="172.16.1.1", + last_ip="172.16.1.254", + scope=scope, ) network2 = network_factory( - address="172.16.20.0/22", first_ip="172.16.20.11", last_ip="172.16.20.250" + address="172.16.20.0/22", + first_ip="172.16.20.11", + last_ip="172.16.20.250", + scope=scope, ) network3 = network_factory( - address="172.16.5.0/24", first_ip="172.16.5.10", last_ip="172.16.5.254" + address="172.16.5.0/24", + first_ip="172.16.5.10", + last_ip="172.16.5.254", + scope=scope, ) response = get(client, f"{API_URL}/network/networks", token=readonly_token) @@ -751,21 +761,13 @@ def test_create_network(client, admin_token, network_scope_factory): assert response.get_json()["broadcast"] == "172.16.1.255" # Check that address and name shall be unique - response = post(client, f"{API_URL}/network/networks", data=data, token=admin_token) - check_response_message( - response, - "(psycopg2.IntegrityError) duplicate key value violates unique constraint", - 422, - ) data_same_address = data.copy() data_same_address["vlan_name"] = "networkX" response = post( client, f"{API_URL}/network/networks", data=data_same_address, token=admin_token ) check_response_message( - response, - "(psycopg2.IntegrityError) duplicate key value violates unique constraint", - 422, + response, "172.16.1.0/24 overlaps network1 (172.16.1.0/24)", 422, ) data_same_name = { "vlan_name": "network1", @@ -913,13 +915,22 @@ def test_create_network_invalid_range(client, session, admin_token, network_scop ) -def test_get_interfaces(client, network_factory, interface_factory, readonly_token): +def test_get_interfaces( + client, network_scope_factory, network_factory, interface_factory, readonly_token +): # Create some interfaces + scope = network_scope_factory(supernet="192.168.0.0/16") network1 = network_factory( - address="192.168.1.0/24", first_ip="192.168.1.10", last_ip="192.168.1.250" + address="192.168.1.0/24", + first_ip="192.168.1.10", + last_ip="192.168.1.250", + scope=scope, ) network2 = network_factory( - address="192.168.2.0/24", first_ip="192.168.2.10", last_ip="192.168.2.250" + address="192.168.2.0/24", + first_ip="192.168.2.10", + last_ip="192.168.2.250", + scope=scope, ) interface1 = interface_factory(network=network1, ip="192.168.1.10") interface2 = interface_factory( @@ -946,8 +957,14 @@ def test_get_interfaces(client, network_factory, interface_factory, readonly_tok def test_get_interfaces_by_domain( - client, domain_factory, network_factory, interface_factory, readonly_token + client, + domain_factory, + network_scope_factory, + network_factory, + interface_factory, + readonly_token, ): + scope = network_scope_factory(supernet="192.168.0.0/16") # Create some interfaces domain1 = domain_factory(name="tn.esss.lu.se") domain2 = domain_factory(name="ics.esss.lu.se") @@ -956,12 +973,14 @@ def test_get_interfaces_by_domain( first_ip="192.168.1.10", last_ip="192.168.1.250", domain=domain1, + scope=scope, ) network2 = network_factory( address="192.168.2.0/24", first_ip="192.168.2.10", last_ip="192.168.2.250", domain=domain2, + scope=scope, ) interface1 = interface_factory(network=network1, ip="192.168.1.10") interface2 = interface_factory(network=network1, ip="192.168.1.11") @@ -990,20 +1009,23 @@ def test_get_interfaces_by_domain( def test_get_interfaces_by_network( - client, network_factory, interface_factory, readonly_token + client, network_scope_factory, network_factory, interface_factory, readonly_token ): + scope = network_scope_factory(supernet="192.168.0.0/16") # Create some interfaces network1 = network_factory( vlan_name="MyNetwork1", address="192.168.1.0/24", first_ip="192.168.1.10", last_ip="192.168.1.250", + scope=scope, ) network2 = network_factory( vlan_name="MyNetwork2", address="192.168.2.0/24", first_ip="192.168.2.10", last_ip="192.168.2.250", + scope=scope, ) interface1 = interface_factory(network=network1, ip="192.168.1.10") interface2 = interface_factory(network=network1, ip="192.168.1.11") @@ -1038,9 +1060,15 @@ def test_get_interfaces_with_model( assert response.get_json()[0]["model"] == "EX3400" -def test_create_interface_fails(client, host, network_factory, no_login_check_token): +def test_create_interface_fails( + client, host, network_scope_factory, network_factory, no_login_check_token +): + scope = network_scope_factory(supernet="192.168.0.0/16") network = network_factory( - address="192.168.1.0/24", first_ip="192.168.1.10", last_ip="192.168.1.250" + address="192.168.1.0/24", + first_ip="192.168.1.10", + last_ip="192.168.1.250", + scope=scope, ) # check that network_id and ip are mandatory response = post( @@ -1077,9 +1105,15 @@ def test_create_interface_fails(client, host, network_factory, no_login_check_to ) -def test_create_interface(client, host, network_factory, no_login_check_token): +def test_create_interface( + client, host, network_scope_factory, network_factory, no_login_check_token +): + scope = network_scope_factory(supernet="192.168.0.0/16") network = network_factory( - address="192.168.1.0/24", first_ip="192.168.1.10", last_ip="192.168.1.250" + address="192.168.1.0/24", + first_ip="192.168.1.10", + last_ip="192.168.1.250", + scope=scope, ) data = { "network": network.vlan_name, @@ -1129,10 +1163,14 @@ def test_create_interface(client, host, network_factory, no_login_check_token): @pytest.mark.parametrize("ip", ("", "foo", "192.168")) def test_create_interface_invalid_ip( - ip, client, host, network_factory, no_login_check_token + ip, client, host, network_scope_factory, network_factory, no_login_check_token ): + scope = network_scope_factory(supernet="192.168.0.0/16") network = network_factory( - address="192.168.1.0/24", first_ip="192.168.1.10", last_ip="192.168.1.250" + address="192.168.1.0/24", + first_ip="192.168.1.10", + last_ip="192.168.1.250", + scope=scope, ) # invalid IP address data = { @@ -1150,10 +1188,14 @@ def test_create_interface_invalid_ip( def test_create_interface_ip_not_in_network( - client, host, network_factory, no_login_check_token + client, host, network_scope_factory, network_factory, no_login_check_token ): + scope = network_scope_factory(supernet="192.168.0.0/16") network = network_factory( - address="192.168.1.0/24", first_ip="192.168.1.10", last_ip="192.168.1.250" + address="192.168.1.0/24", + first_ip="192.168.1.10", + last_ip="192.168.1.250", + scope=scope, ) # IP address not in range data = { @@ -1171,10 +1213,14 @@ def test_create_interface_ip_not_in_network( def test_create_interface_ip_not_in_range( - client, host, network_factory, no_login_check_token + client, host, network_scope_factory, network_factory, no_login_check_token ): + scope = network_scope_factory(supernet="192.168.0.0/16") network = network_factory( - address="192.168.1.0/24", first_ip="192.168.1.10", last_ip="192.168.1.250" + address="192.168.1.0/24", + first_ip="192.168.1.10", + last_ip="192.168.1.250", + scope=scope, ) # IP address not in range data = { @@ -1194,10 +1240,14 @@ def test_create_interface_ip_not_in_range( def test_create_interface_ip_not_in_range_as_admin( - client, host, network_factory, admin_token + client, host, network_scope_factory, network_factory, admin_token ): + scope = network_scope_factory(supernet="192.168.0.0/16") network = network_factory( - address="192.168.1.0/24", first_ip="192.168.1.10", last_ip="192.168.1.250" + address="192.168.1.0/24", + first_ip="192.168.1.10", + last_ip="192.168.1.250", + scope=scope, ) # IP address not in range data = { @@ -1215,7 +1265,7 @@ def test_create_interface_ip_not_in_range_as_admin( def test_normal_user_can_create_interface_on_empty_host( client, host, network_scope_factory, network_factory, user_prod_token ): - scope = network_scope_factory(name="ProdNetworks") + scope = network_scope_factory(name="ProdNetworks", supernet="192.168.0.0/16") network = network_factory( address="192.168.1.0/24", first_ip="192.168.1.10", @@ -2010,7 +2060,7 @@ def test_patch_host_network_permission( interface_factory, user_token, ): - scope = network_scope_factory(name="FooNetworks") + scope = network_scope_factory(name="FooNetworks", supernet="192.168.0.0/16") network = network_factory( address="192.168.1.0/24", first_ip="192.168.1.10", @@ -2034,7 +2084,7 @@ def test_patch_host_invalid_network_permission( interface_factory, user_token, ): - scope = network_scope_factory(name="ProdNetworks") + scope = network_scope_factory(name="ProdNetworks", supernet="192.168.0.0/16") network = network_factory( address="192.168.1.0/24", first_ip="192.168.1.10", @@ -2092,9 +2142,15 @@ def test_patch_interface_mac(client, interface_factory, admin_token): assert updated_interface.mac == data["mac"] -def test_patch_interface_ip(client, interface_factory, network_factory, admin_token): +def test_patch_interface_ip( + client, interface_factory, network_scope_factory, network_factory, admin_token +): + scope = network_scope_factory(supernet="192.168.0.0/16") network = network_factory( - address="192.168.1.0/24", first_ip="192.168.1.10", last_ip="192.168.1.250" + address="192.168.1.0/24", + first_ip="192.168.1.10", + last_ip="192.168.1.250", + scope=scope, ) interface = interface_factory(network=network, ip="192.168.1.11") data = {"ip": "192.168.2.12"} @@ -2147,19 +2203,22 @@ def test_patch_interface_name(client, host_factory, interface_factory, admin_tok def test_patch_interface_network( - client, network_factory, interface_factory, admin_token + client, network_scope_factory, network_factory, interface_factory, admin_token ): + scope = network_scope_factory(supernet="192.168.0.0/16") network1 = network_factory( vlan_name="mynetwork", address="192.168.1.0/24", first_ip="192.168.1.10", last_ip="192.168.1.250", + scope=scope, ) network2 = network_factory( vlan_name="new-network", address="192.168.2.0/24", first_ip="192.168.2.10", last_ip="192.168.2.250", + scope=scope, ) interface = interface_factory(network=network1, ip="192.168.1.20") data = {"network": "unknown"} @@ -2207,8 +2266,8 @@ def test_patch_interface_network( def test_patch_interface_current_network_permission( client, network_scope_factory, network_factory, interface_factory, user_token ): - scope_prod = network_scope_factory(name="ProdNetworks") - scope_foo = network_scope_factory(name="FooNetworks") + scope_prod = network_scope_factory(name="ProdNetworks", supernet="192.168.0.0/22") + scope_foo = network_scope_factory(name="FooNetworks", supernet="192.168.4.0/22") network_prod = network_factory( vlan_name="prod-network", address="192.168.1.0/24", @@ -2218,9 +2277,9 @@ def test_patch_interface_current_network_permission( ) network_foo = network_factory( vlan_name="foo-network", - address="192.168.2.0/24", - first_ip="192.168.2.10", - last_ip="192.168.2.250", + address="192.168.4.0/24", + first_ip="192.168.4.10", + last_ip="192.168.4.250", scope=scope_foo, ) # User can't update an interface part of the ProdNetworks @@ -2234,8 +2293,8 @@ def test_patch_interface_current_network_permission( ) check_response_message(response, "User doesn't have the required group", 403) # but can on the FooNetworks - interface_foo = interface_factory(network=network_foo, ip="192.168.2.20") - data = {"ip": "192.168.2.21"} + interface_foo = interface_factory(network=network_foo, ip="192.168.4.20") + data = {"ip": "192.168.4.21"} response = patch( client, f"{API_URL}/network/interfaces/{interface_foo.id}", @@ -2248,8 +2307,8 @@ def test_patch_interface_current_network_permission( def test_patch_interface_new_network_permission( client, network_scope_factory, network_factory, interface_factory, user_token ): - scope_prod = network_scope_factory(name="ProdNetworks") - scope_foo = network_scope_factory(name="FooNetworks") + scope_prod = network_scope_factory(name="ProdNetworks", supernet="192.168.0.0/22") + scope_foo = network_scope_factory(name="FooNetworks", supernet="192.168.4.0/22") network_prod = network_factory( vlan_name="prod-network", address="192.168.1.0/24", @@ -2259,19 +2318,19 @@ def test_patch_interface_new_network_permission( ) network_foo1 = network_factory( vlan_name="foo-network1", - address="192.168.2.0/24", - first_ip="192.168.2.10", - last_ip="192.168.2.250", + address="192.168.4.0/24", + first_ip="192.168.4.10", + last_ip="192.168.4.250", scope=scope_foo, ) network_foo2 = network_factory( vlan_name="foo-network2", - address="192.168.3.0/24", - first_ip="192.168.3.10", - last_ip="192.168.3.250", + address="192.168.5.0/24", + first_ip="192.168.5.10", + last_ip="192.168.5.250", scope=scope_foo, ) - interface_foo = interface_factory(network=network_foo1, ip="192.168.2.20") + interface_foo = interface_factory(network=network_foo1, ip="192.168.4.20") # User can't change the network to the ProdNetworks data = {"network": network_prod.vlan_name} response = patch( @@ -2282,7 +2341,7 @@ def test_patch_interface_new_network_permission( ) # but can on the same scope it has access to check_response_message(response, "User doesn't have the required group", 403) - data = {"network": network_foo2.vlan_name, "ip": "192.168.3.10"} + data = {"network": network_foo2.vlan_name, "ip": "192.168.5.10"} response = patch( client, f"{API_URL}/network/interfaces/{interface_foo.id}", diff --git a/tests/functional/test_models.py b/tests/functional/test_models.py index 6471a22..fe5daff 100644 --- a/tests/functional/test_models.py +++ b/tests/functional/test_models.py @@ -235,13 +235,20 @@ def test_user_can_set_boot_profile( assert not user.can_set_boot_profile(non_physical_ioc) -def test_network_ip_properties(network_factory): +def test_network_ip_properties(network_scope_factory, network_factory): + scope = network_scope_factory(supernet="172.16.0.0/16") # Create some networks network1 = network_factory( - address="172.16.1.0/24", first_ip="172.16.1.10", last_ip="172.16.1.250" + address="172.16.1.0/24", + first_ip="172.16.1.10", + last_ip="172.16.1.250", + scope=scope, ) network2 = network_factory( - address="172.16.20.0/26", first_ip="172.16.20.11", last_ip="172.16.20.14" + address="172.16.20.0/26", + first_ip="172.16.20.11", + last_ip="172.16.20.14", + scope=scope, ) assert network1.network_ip == ipaddress.ip_network("172.16.1.0/24") @@ -265,13 +272,22 @@ def test_network_ip_properties(network_factory): assert network2.used_ips() == [] -def test_network_available_and_used_ips(network_factory, interface_factory): +def test_network_available_and_used_ips( + network_factory, interface_factory, network_scope_factory +): + scope = network_scope_factory(supernet="172.16.0.0/16") # Create some networks and interfaces network1 = network_factory( - address="172.16.1.0/24", first_ip="172.16.1.10", last_ip="172.16.1.250" + address="172.16.1.0/24", + first_ip="172.16.1.10", + last_ip="172.16.1.250", + scope=scope, ) network2 = network_factory( - address="172.16.20.0/26", first_ip="172.16.20.11", last_ip="172.16.20.14" + address="172.16.20.0/26", + first_ip="172.16.20.11", + last_ip="172.16.20.14", + scope=scope, ) for i in range(10, 20): interface_factory(network=network1, ip=f"172.16.1.{i}") @@ -309,10 +325,16 @@ def test_network_available_and_used_ips(network_factory, interface_factory): assert list(network2.available_ips()) == [] -def test_network_gateway(network_factory): - network = network_factory(address="192.168.0.0/24", gateway="192.168.0.1") +def test_network_gateway(network_scope_factory, network_factory): + scope1 = network_scope_factory(supernet="192.168.0.0/16") + scope2 = network_scope_factory(supernet="172.16.0.0/16") + network = network_factory( + address="192.168.0.0/24", gateway="192.168.0.1", scope=scope1 + ) assert str(network.gateway) == "192.168.0.1" - network = network_factory(address="172.16.110.0/23", gateway="172.16.111.254") + network = network_factory( + address="172.16.110.0/23", gateway="172.16.111.254", scope=scope2 + ) assert str(network.gateway) == "172.16.111.254" @@ -843,3 +865,57 @@ def test_item_invalid_ics_id(db, item_factory, ics_id): with pytest.raises(ValidationError) as excinfo: item_factory(ics_id=ics_id) assert r"ICS id shall match [A-Z]{3}[0-9]{3}" in str(excinfo.value) + + +@pytest.mark.parametrize( + "address", ("172.30.0.0/25", "172.30.1.0/24", "172.30.0.0/22", "172.30.0.192/26") +) +def test_network_overlapping(address, network_scope_factory, network_factory): + scope = network_scope_factory( + first_vlan=3800, last_vlan=4000, supernet="172.30.0.0/16" + ) + network1 = network_factory( + vlan_id=3800, + address="172.30.0.0/23", + first_ip="172.30.0.3", + last_ip="172.30.1.240", + scope=scope, + ) + with pytest.raises(ValidationError) as excinfo: + network_factory(vlan_id=3801, address=address, scope=scope) + assert f"{address} overlaps {network1} ({network1.network_ip})" in str( + excinfo.value + ) + + +@pytest.mark.parametrize("address", ("172.30.2.0/25", "172.30.0.0/16")) +def test_network_not_subnet_of_scope(address, network_scope_factory, network_factory): + scope = network_scope_factory( + first_vlan=3800, last_vlan=4000, supernet="172.30.0.0/23" + ) + with pytest.raises(ValidationError) as excinfo: + network_factory(vlan_id=3800, address=address, scope=scope) + assert f"{address} is not a subnet of 172.30.0.0/23" in str(excinfo.value) + + +def test_scope_available_subnets(network_scope_factory, network_factory): + address = "172.30.0.0/16" + scope_ip = ipaddress.ip_network(address) + scope = network_scope_factory(first_vlan=3800, last_vlan=4000, supernet=address) + full_24 = [str(subnet) for subnet in scope_ip.subnets(new_prefix=24)] + assert scope.available_subnets(24) == full_24 + network1 = network_factory(vlan_id=3800, address="172.30.60.0/24", scope=scope) + expected1 = [subnet for subnet in full_24 if subnet != network1.address] + assert scope.available_subnets(24) == expected1 + network_factory(vlan_id=3801, address="172.30.244.0/22", scope=scope) + network2_24 = [ + "172.30.244.0/24", + "172.30.245.0/24", + "172.30.246.0/24", + "172.30.247.0/24", + ] + expected2 = [subnet for subnet in expected1 if subnet not in network2_24] + assert scope.available_subnets(24) == expected2 + network_factory(vlan_id=3802, address="172.30.238.64/26", scope=scope) + expected3 = [subnet for subnet in expected2 if subnet != "172.30.238.0/24"] + assert scope.available_subnets(24) == expected3 diff --git a/tests/functional/test_web.py b/tests/functional/test_web.py index 1999560..5966191 100644 --- a/tests/functional/test_web.py +++ b/tests/functional/test_web.py @@ -300,7 +300,7 @@ def test_edit_item_comment_in_index( def test_create_host(client, network_scope_factory, network_factory, device_type): - scope = network_scope_factory(name="ProdNetworks") + scope = network_scope_factory(name="ProdNetworks", supernet="192.168.0.0/16") network = network_factory( address="192.168.1.0/24", first_ip="192.168.1.10", @@ -349,7 +349,7 @@ def test_create_host(client, network_scope_factory, network_factory, device_type def test_create_host_invalid_fields( session, client, network_scope_factory, network_factory, device_type ): - scope = network_scope_factory(name="ProdNetworks") + scope = network_scope_factory(name="ProdNetworks", supernet="192.168.0.0/16") network = network_factory( address="192.168.1.0/24", first_ip="192.168.1.10", @@ -396,7 +396,7 @@ def test_create_interface( client, host_factory, network_scope_factory, network_factory, interface_factory ): host = host_factory(name="myhost") - scope = network_scope_factory(name="ProdNetworks") + scope = network_scope_factory(name="ProdNetworks", supernet="192.168.0.0/16") network1 = network_factory(scope=scope) interface_factory(network=network1, host=host) network2 = network_factory( @@ -439,10 +439,10 @@ def test_create_interface( def test_add_interface_to_empty_host( - client, host_factory, network_scope_factory, network_factory, + client, host_factory, network_scope_factory, network_factory ): host = host_factory(name="myhost") - scope = network_scope_factory(name="ProdNetworks") + scope = network_scope_factory(name="ProdNetworks", supernet="192.168.0.0/16") network = network_factory( address="192.168.2.0/24", first_ip="192.168.2.10", @@ -699,10 +699,7 @@ def test_create_network_no_vlan( def test_create_item_invalid_ics_id(logged_rw_client): ics_id = "AAA1100" - form = { - "ics_id": ics_id, - "serial_number": "12345", - } + form = {"ics_id": ics_id, "serial_number": "12345"} response = logged_rw_client.post( f"/inventory/items/create", data=form, follow_redirects=True ) @@ -762,3 +759,32 @@ def test_ansible_groups_no_recursive_dependency( ) assert response.status_code == 200 assert b"creates a recursive dependency loop" in response.data + + +def test_create_network_overlapping( + network_scope_factory, network_factory, logged_admin_client +): + scope = network_scope_factory( + first_vlan=3800, last_vlan=4000, supernet="172.30.0.0/16" + ) + network_factory( + vlan_name="network1", + vlan_id=3800, + address="172.30.0.0/23", + first_ip="172.30.0.3", + last_ip="172.30.1.240", + scope=scope, + ) + form = { + "vlan_name": "network2", + "vlan_id": 3842, + "scope_id": scope.id, + "address": "172.30.1.0/24", + "first_ip": "172.30.1.5", + "last_ip": "172.30.1.245", + "gateway": "172.30.1.248", + "domain_id": scope.domain_id, + } + response = logged_admin_client.post("/network/networks/create", data=form) + assert response.status_code == 200 + assert b"172.30.1.0/24 overlaps network1 (172.30.0.0/23)" in response.data -- GitLab