Repository: netbox-community/netbox

Issue:
Inconsistent enforcement of duplicate IP addresses
### Environment
* Python version: 3.6.9
* NetBox version: 2.10.4

### Steps to Reproduce

This is with `ENFORCE_GLOBAL_UNIQUE = True` in `configuration.py`

1. Add two interfaces to a device, say "test1" and "test2" (these can be virtual)
2. Add IP address 1.2.3.4/24 to "test1" with no role
3. Add IP address 1.2.3.4/24 to "test2" with role VIP
4. Change the test1 1.2.3.4/24 IP address to role VIP
5. Change the test1 1.2.3.4/24 IP address back to no role

### Expected Behavior
I expected duplicate IP address enforcement to be consistent: either require both addresses to be VIP, or allow one normal and one VIP to share the same IP address.  (Probably the former).

In other words: either step 3 should have failed with a duplicate address error, or step 5 should have been accepted since it puts things back to how they were at step 3.

### Observed Behavior
Step 3 is accepted, but step 5 fails with:
![image](https://user-images.githubusercontent.com/44789/106595854-150c9c80-654c-11eb-91cb-01eb5037df24.png)



Discussion:
It looks like a couple of extra tests are required around [here](https://github.com/netbox-community/netbox/blob/develop/netbox/ipam/tests/test_models.py#L262) e.g.

```
    @override_settings(ENFORCE_GLOBAL_UNIQUE=True)
    def test_duplicate_nonunique_nonrole_role(self):
        IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'))
        duplicate_ip = IPAddress(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)
        self.assertRaises(ValidationError, duplicate_ip.clean)

    @override_settings(ENFORCE_GLOBAL_UNIQUE=True)
    def test_duplicate_nonunique_role_nonrole(self):
        IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)
        duplicate_ip = IPAddress(address=netaddr.IPNetwork('192.0.2.1/24'))
        self.assertRaises(ValidationError, duplicate_ip.clean)
```

That assumes you want to disallow the duplicates between role and non-role. To allow them, `self.assertIsNone(duplicate_ip.clean())`
@candlerb want to take this one?
Not for a while... would have to set up a test/dev environment for it.
Shouldn't take more than a few minutes to get a [development environment](https://netbox.readthedocs.io/en/stable/development/getting-started/) up and running. NetBox needs all the help it can get now that I'm no longer able to work on it full-time.
> Shouldn't take more than a few minutes to get a [development environment](https://netbox.readthedocs.io/en/stable/development/getting-started/) up and running.

Looks like those instructions haven't been checked for a while :-(

```
(netbox) root@netbox-dev:~/netbox# python3 netbox/manage.py test
System check identified no issues (0 silenced).

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK
```

This is fresh installation, with `configuration.py` based on `configuration.testing.py`.

(Makes random guesses) Ah, looks like I have to be in a different directory:

```
(netbox) root@netbox-dev:~/netbox# cd netbox/
(netbox) root@netbox-dev:~/netbox/netbox# python3 ./manage.py test
Creating test database for alias 'default'...
Got an error creating the test database: permission denied to create database
```

Now, user needs credentials with permissions to create databases?

```
postgres=# ALTER USER netboxtest CREATEDB;
ALTER ROLE
```

OK, that's sufficient to make the test suite run (4 are skipped, although it doesn't show which ones).

Resolving patch:
diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py
--- a/netbox/ipam/models.py
+++ b/netbox/ipam/models.py
@@ -734,13 +734,12 @@ def clean(self):
                 })
 
             # Enforce unique IP space (if applicable)
-            if self.role not in IPADDRESS_ROLES_NONUNIQUE and ((
-                self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE
-            ) or (
-                self.vrf and self.vrf.enforce_unique
-            )):
+            if (self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
                 duplicate_ips = self.get_duplicates()
-                if duplicate_ips:
+                if duplicate_ips and (
+                        self.role not in IPADDRESS_ROLES_NONUNIQUE or
+                        any(dip.role not in IPADDRESS_ROLES_NONUNIQUE for dip in duplicate_ips)
+                ):
                     raise ValidationError({
                         'address': "Duplicate IP address found in {}: {}".format(
                             "VRF {}".format(self.vrf) if self.vrf else "global table",


Test patch:
diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py
--- a/netbox/ipam/tests/test_models.py
+++ b/netbox/ipam/tests/test_models.py
@@ -259,6 +259,18 @@ def test_duplicate_vrf_unique(self):
         duplicate_ip = IPAddress(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24'))
         self.assertRaises(ValidationError, duplicate_ip.clean)
 
+    @override_settings(ENFORCE_GLOBAL_UNIQUE=True)
+    def test_duplicate_nonunique_nonrole_role(self):
+        IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'))
+        duplicate_ip = IPAddress(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)
+        self.assertRaises(ValidationError, duplicate_ip.clean)
+
+    @override_settings(ENFORCE_GLOBAL_UNIQUE=True)
+    def test_duplicate_nonunique_role_nonrole(self):
+        IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)
+        duplicate_ip = IPAddress(address=netaddr.IPNetwork('192.0.2.1/24'))
+        self.assertRaises(ValidationError, duplicate_ip.clean)
+
     @override_settings(ENFORCE_GLOBAL_UNIQUE=True)
     def test_duplicate_nonunique_role(self):
         IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)