diff --git a/src/ralph/networks/forms.py b/src/ralph/networks/forms.py index 7a151d2ac0..37aadfb28f 100755 --- a/src/ralph/networks/forms.py +++ b/src/ralph/networks/forms.py @@ -121,6 +121,15 @@ def _validate_ip_uniquness(self, address): params={'ip': address}, ) + def _validate_hostame_uniqueness_in_dc(self): + address = self.cleaned_data['address'] + new_hostname = self.cleaned_data['hostname'] + if not self.ip or self.ip.address != address: + ip = IPAddress(address=address, hostname=new_hostname) + else: + ip = self.ip + ip.validate_hostname_uniqueness_in_dc(new_hostname) + def clean_address(self): if self._dhcp_expose_should_lock_fields: # if address is locked, just return current address @@ -164,6 +173,7 @@ def clean_dhcp_expose(self): raise ValidationError( _('Cannot expose in DHCP without MAC address'), ) + self._validate_hostame_uniqueness_in_dc() return dhcp_expose def _validate_mac_address(self): diff --git a/src/ralph/networks/models/networks.py b/src/ralph/networks/models/networks.py index 29c47a3dc1..e646272629 100644 --- a/src/ralph/networks/models/networks.py +++ b/src/ralph/networks/models/networks.py @@ -662,6 +662,33 @@ class Meta: def __str__(self): return self.address + def _hostname_is_unique_in_dc(self, hostname, dc): + from ralph.dhcp.models import DHCPEntry + entries_with_hostname = DHCPEntry.objects.filter( + hostname=hostname, + network__network_environment__data_center=dc + ) + if self.pk: + entries_with_hostname = entries_with_hostname.exclude(pk=self.pk) + return not entries_with_hostname.exists() + + def validate_hostname_uniqueness_in_dc(self, hostname): + network = self.get_network() + if network and network.network_environment: + dc = network.network_environment.data_center + if not self._hostname_is_unique_in_dc(hostname, dc): + raise ValidationError( + 'Hostname "{hostname}" is already exposed in DHCP in {dc}.' + .format( + hostname=self.hostname, dc=dc + ) + ) + + def _validate_hostname_uniqueness_in_dc(self): + if not self.dhcp_expose: + return + self.validate_hostname_uniqueness_in_dc(self.hostname) + def _validate_expose_in_dhcp_and_mac(self): if ( (not self.ethernet_id or (self.ethernet and not self.ethernet.mac)) and # noqa @@ -708,6 +735,7 @@ def clean(self): self._validate_expose_in_dhcp_and_mac, self._validate_expose_in_dhcp_and_hostname, self._validate_change_when_exposing_in_dhcp, + self._validate_hostname_uniqueness_in_dc, ]: try: validator() diff --git a/src/ralph/networks/tests/test_forms.py b/src/ralph/networks/tests/test_forms.py index eadab77334..f5a64ead2c 100755 --- a/src/ralph/networks/tests/test_forms.py +++ b/src/ralph/networks/tests/test_forms.py @@ -2,8 +2,13 @@ from django.test import RequestFactory from ralph.assets.models.components import Ethernet, EthernetSpeed +from ralph.data_center.tests.factories import DataCenterFactory from ralph.networks.models import IPAddress -from ralph.networks.tests.factories import IPAddressFactory +from ralph.networks.tests.factories import ( + IPAddressFactory, + NetworkEnvironmentFactory, + NetworkFactory +) from ralph.tests import RalphTestCase from ralph.tests.models import PolymorphicTestModel @@ -395,6 +400,103 @@ def test_dhcp_expose_for_new_record_should_pass(self): self.assertEqual(ip.ethernet.label, 'eth10') self.assertEqual(ip.ethernet.base_object.pk, self.obj1.pk) + def test_dhcp_expose_for_new_record_duplicate_hostname_should_not_pass(self): # noqa + network = NetworkFactory( + address='127.0.0.0/24', + network_environment=NetworkEnvironmentFactory( + data_center=DataCenterFactory( + name='DC1' + ) + ) + ) + self.test_dhcp_expose_for_new_record_should_pass() # generate duplicate + obj2 = PolymorphicTestModel.objects.create(hostname='xyz') + inline_data = { + 'TOTAL_FORMS': 2, + 'INITIAL_FORMS': 1, + '0-id': self.eth1.id, + '0-base_object': obj2.id, + '0-mac': 'ff:ff:ff:ff:ff:ff', + '0-label': 'eth10', + + '1-base_object': obj2.id, + '1-hostname': self.ip1.hostname, + '1-address': '127.0.0.3', + '1-mac': '11:11:11:11:11:11', + '1-label': 'eth10', + '1-dhcp_expose': 'on', + } + data = { + 'hostname': obj2.hostname, + 'id': obj2.id, + } + data.update(self._prepare_inline_data(inline_data)) + response = self.client.post( + obj2.get_absolute_url(), + data, + follow=True + ) + self.assertEqual(response.status_code, 200) + msg = 'Hostname "{hostname}" is already exposed in DHCP in {dc}'.format( # noqa + hostname=self.ip1.hostname, + dc=network.network_environment.data_center + ) + self.assertIn('errors', response.context_data) + self.assertTrue( + any([msg in err for err in response.context_data['errors']]) + ) + + def test_dhcp_expose_for_existing_record_duplicate_hostname_should_not_pass(self): # noqa + network = NetworkFactory( + address='192.168.0.0/24', + network_environment=NetworkEnvironmentFactory( + data_center=DataCenterFactory( + name='DC1' + ) + ) + ) + name = 'some_hostname' + ip = IPAddressFactory( + dhcp_expose=True, + hostname=name, + address='192.168.0.7' + ).save() + self.ip1.hostname = name + self.ip1.address = '192.168.0.12' + self.ip1.save() + inline_data = { + 'TOTAL_FORMS': 2, + 'INITIAL_FORMS': 1, + '0-id': self.eth1.id, + '0-base_object': self.obj1.id, + '0-label': 'eth10', + + '1-base_object': self.obj1.id, + '1-hostname': name, + '1-address': '192.168.0.33', + '1-mac': '10:10:10:10:10:10', + '1-label': 'eth10', + '1-dhcp_expose': 'on', + } + data = { + 'hostname': self.obj1.hostname, + 'id': self.obj1.id, + } + data.update(self._prepare_inline_data(inline_data)) + response = self.client.post( + self.obj1.get_absolute_url(), + data, + follow=True + ) + msg = 'Hostname "{hostname}" is already exposed in DHCP in {dc}'.format( # noqa + hostname=self.ip1.hostname, + dc=network.network_environment.data_center + ) + self.assertIn('errors', response.context_data) + self.assertTrue( + any([msg in err for err in response.context_data['errors']]) + ) + def test_dhcp_expose_for_existing_record_should_pass(self): inline_data = { 'TOTAL_FORMS': 1, diff --git a/src/ralph/networks/tests/test_models.py b/src/ralph/networks/tests/test_models.py index 14d866d5ce..73bb345818 100644 --- a/src/ralph/networks/tests/test_models.py +++ b/src/ralph/networks/tests/test_models.py @@ -11,7 +11,8 @@ from ralph.assets.tests.factories import EthernetFactory from ralph.data_center.tests.factories import ( ClusterFactory, - DataCenterAssetFactory + DataCenterAssetFactory, + DataCenterFactory ) from ralph.networks.admin import NetworkAdmin from ralph.networks.filters import NetworkClassFilter @@ -19,7 +20,8 @@ from ralph.networks.models.networks import IPAddress, Network from ralph.networks.tests.factories import ( IPAddressFactory, - NetworkEnvironmentFactory + NetworkEnvironmentFactory, + NetworkFactory ) from ralph.tests import RalphTestCase from ralph.virtual.tests.factories import VirtualServerFactory @@ -568,6 +570,110 @@ def test_change_ethernet_with_dhcp_exposition_should_not_pass(self): # noqa ): self.ip.clean() + def test_duplicate_hostname_with_dhcp_exposition_should_not_pass(self): + name = 'random.hostname.net' + network = NetworkFactory( + address='192.168.0.0/24', + network_environment=NetworkEnvironmentFactory() + ) + ip = IPAddressFactory( + hostname=name, address='192.168.0.1', + dhcp_expose=True + ) + self.ip.hostname = name + self.ip.address = '192.168.0.2' + self.ip.dhcp_expose = True + with self.assertRaises( + ValidationError, + msg='Hostname "{hostname}" is already exposed in DHCP in {dc}.'. + format( + hostname=name, + dc=network.network_environment.data_center + ) + ): + self.ip.validate_hostname_uniqueness_in_dc(name) + with self.assertRaises( + ValidationError, + msg='Hostname "{hostname}" is already exposed in DHCP in {dc}.'. + format( + hostname=name, + dc=network.network_environment.data_center + ) + ): + self.ip.clean() + + def test_duplicate_hostname_in_different_networks_in_same_dc_should_not_pass(self): # noqa + name = 'random.hostname.net' + network = NetworkFactory( + address='192.168.0.0/24', + network_environment=NetworkEnvironmentFactory( + data_center=DataCenterFactory( + name='DC1' + ) + ) + ) + network1 = NetworkFactory( + address='1.1.0.0/24', + network_environment=NetworkEnvironmentFactory( + data_center=DataCenterFactory( + name='DC1' + ) + ) + ) + ip = IPAddressFactory( + hostname=name, address='192.168.0.1', + dhcp_expose=True + ) + self.ip.hostname = name + self.ip.address = '1.1.0.2' + self.ip.dhcp_expose = True + with self.assertRaises( + ValidationError, + msg='Hostname "{hostname}" is already exposed in DHCP in {dc}.'. + format( + hostname=name, + dc=network.network_environment.data_center + ) + ): + self.ip.validate_hostname_uniqueness_in_dc(name) + with self.assertRaises( + ValidationError, + msg='Hostname "{hostname}" is already exposed in DHCP in {dc}.'. + format( + hostname=name, + dc=network1.network_environment.data_center + ) + ): + self.ip.clean() + + def test_duplicate_hostnames_in_different_dcs_should_pass(self): + name = 'random.hostname.net' + network1 = NetworkFactory( + address='1.1.0.0/24', + network_environment=NetworkEnvironmentFactory( + data_center=DataCenterFactory( + name='DC1' + ) + ) + ) + network2 = NetworkFactory( + address='192.168.0.0/24', + network_environment=NetworkEnvironmentFactory( + data_center=DataCenterFactory( + name='DC2' + ) + ) + ) + ip = IPAddressFactory( + hostname=name, address='1.1.0.1', + dhcp_expose=True + ) + self.ip.address = '192.168.0.1' + self.ip.hostname = name + self.ip.dhcp_expose = True + self.ip.validate_hostname_uniqueness_in_dc(name) + self.ip.clean() + class TestNetworkClassFilter(RalphTestCase):