Repository: nautobot/nautobot

Issue:
API Unable to Create IPv6 Addresses
### Environment

* Python version: 3.6.13
* Nautobot version: 1.0.1

<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current stable release of Nautobot. Begin with the
    creation of any necessary database objects and call out every operation
    being performed explicitly. If reporting a bug in the REST API, be sure to
    reconstruct the raw HTTP request(s) being made: Don't rely on a client
    library such as pynautobot.
-->
### Steps to Reproduce
1. Start Nautobot
2. Attempt to make a POST request to `ipam/ip-addresses/` with an IPv6 with Postman

<!-- What did you expect to happen? -->
### Expected Behavior

Expected to have an IP address created


<!-- What happened instead? -->
### Observed Behavior

No IP address created

POST Body (JSON):
```json
{
    "address": "2001:db8:abcd:0012::20/64",
    "status": "active"
}
```

### Pynautobot Example

```python
>>> api.ipam.ip_addresses.create(address="192.0.2.20/24", status="active")
192.0.2.20/24
>>> api.ipam.ip_addresses.create(address="2001:db8:abcd:0012::20/64", status="active")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/joshvanderaa/.pyenv/versions/3.7.7/lib/python3.7/site-packages/pynautobot/core/endpoint.py", line 272, in create
    args[0] if args else kwargs
  File "/Users/joshvanderaa/.pyenv/versions/3.7.7/lib/python3.7/site-packages/pynautobot/core/query.py", line 324, in post
    return self._make_call(verb="post", data=data)
  File "/Users/joshvanderaa/.pyenv/versions/3.7.7/lib/python3.7/site-packages/pynautobot/core/query.py", line 226, in _make_call
    raise RequestError(req)
pynautobot.core.query.RequestError: The request failed with code 400 Bad Request: {'host': ['Ensure this value has at most 16 characters (it has 20).'], 'broadcast': ['Ensure this value has at most 16 characters (it has 36).']}
```




Discussion:

Resolving patch:
diff --git a/nautobot/ipam/api/nested_serializers.py b/nautobot/ipam/api/nested_serializers.py
--- a/nautobot/ipam/api/nested_serializers.py
+++ b/nautobot/ipam/api/nested_serializers.py
@@ -1,3 +1,4 @@
+import netaddr
 from rest_framework import serializers
 
 from nautobot.core.api import WritableNestedSerializer
@@ -20,8 +21,18 @@
 
 class IPFieldSerializer(serializers.CharField):
     def to_representation(self, value):
+        """Convert internal (IPNetwork) representation to API (string) representation."""
         return str(value)
 
+    def to_internal_value(self, value):
+        """Convert API (string) representation to internal (IPNetwork) representation."""
+        try:
+            return netaddr.IPNetwork(value)
+        except netaddr.AddrFormatError:
+            raise serializers.ValidationError("Invalid IP address format: {}".format(value))
+        except (TypeError, ValueError) as e:
+            raise serializers.ValidationError(e)
+
 
 #
 # VRFs
diff --git a/nautobot/ipam/fields.py b/nautobot/ipam/fields.py
--- a/nautobot/ipam/fields.py
+++ b/nautobot/ipam/fields.py
@@ -14,7 +14,6 @@ class VarbinaryIPField(models.BinaryField):
     description = "IP network address"
 
     def __init__(self, **kwargs):
-        kwargs["max_length"] = 16
         super().__init__(**kwargs)
 
     def db_type(self, connection):
diff --git a/nautobot/ipam/migrations/0003_remove_max_length.py b/nautobot/ipam/migrations/0003_remove_max_length.py
new file mode 100644
--- /dev/null
+++ b/nautobot/ipam/migrations/0003_remove_max_length.py
@@ -0,0 +1,44 @@
+# Generated by Django 3.1.8 on 2021-05-14 19:58
+
+from django.db import migrations
+import nautobot.ipam.fields
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("ipam", "0002_initial_part_2"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="aggregate",
+            name="broadcast",
+            field=nautobot.ipam.fields.VarbinaryIPField(db_index=True),
+        ),
+        migrations.AlterField(
+            model_name="aggregate",
+            name="network",
+            field=nautobot.ipam.fields.VarbinaryIPField(db_index=True),
+        ),
+        migrations.AlterField(
+            model_name="ipaddress",
+            name="broadcast",
+            field=nautobot.ipam.fields.VarbinaryIPField(db_index=True),
+        ),
+        migrations.AlterField(
+            model_name="ipaddress",
+            name="host",
+            field=nautobot.ipam.fields.VarbinaryIPField(db_index=True),
+        ),
+        migrations.AlterField(
+            model_name="prefix",
+            name="broadcast",
+            field=nautobot.ipam.fields.VarbinaryIPField(db_index=True),
+        ),
+        migrations.AlterField(
+            model_name="prefix",
+            name="network",
+            field=nautobot.ipam.fields.VarbinaryIPField(db_index=True),
+        ),
+    ]


Test patch:
diff --git a/nautobot/ipam/tests/test_api.py b/nautobot/ipam/tests/test_api.py
--- a/nautobot/ipam/tests/test_api.py
+++ b/nautobot/ipam/tests/test_api.py
@@ -143,9 +143,10 @@ def setUpTestData(cls):
             RIR.objects.create(name="RIR 2", slug="rir-2"),
         )
 
-        Aggregate.objects.create(prefix=IPNetwork("10.0.0.0/8"), rir=rirs[0]),
-        Aggregate.objects.create(prefix=IPNetwork("172.16.0.0/12"), rir=rirs[0]),
-        Aggregate.objects.create(prefix=IPNetwork("192.168.0.0/16"), rir=rirs[0]),
+        Aggregate.objects.create(prefix=IPNetwork("10.0.0.0/8"), rir=rirs[0])
+        Aggregate.objects.create(prefix=IPNetwork("172.16.0.0/12"), rir=rirs[0])
+        Aggregate.objects.create(prefix=IPNetwork("192.168.0.0/16"), rir=rirs[0])
+        Aggregate.objects.create(prefix=IPNetwork("2001:db8:abcd::/64"), rir=rirs[0])
 
         cls.create_data = [
             {
@@ -153,7 +154,7 @@ def setUpTestData(cls):
                 "rir": rirs[1].pk,
             },
             {
-                "prefix": "101.0.0.0/8",
+                "prefix": "2001:db8:abcd:12::/64",
                 "rir": rirs[1].pk,
             },
             {
@@ -205,7 +206,7 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
             "status": "active",
         },
         {
-            "prefix": "192.168.5.0/24",
+            "prefix": "2001:db8:abcd:12::/80",
             "status": "active",
         },
         {
@@ -230,6 +231,7 @@ def setUpTestData(cls):
         Prefix.objects.create(prefix=IPNetwork("192.168.1.0/24"), status=statuses[0])
         Prefix.objects.create(prefix=IPNetwork("192.168.2.0/24"), status=statuses[0])
         Prefix.objects.create(prefix=IPNetwork("192.168.3.0/24"), status=statuses[0])
+        Prefix.objects.create(prefix=IPNetwork("2001:db8:abcd::/80"), status=statuses[0])
 
         # FIXME(jathan): The writable serializer for `status` takes the
         # status `name` (str) and not the `pk` (int). Do not validate this
@@ -453,7 +455,7 @@ class IPAddressTest(APIViewTestCases.APIViewTestCase):
             "status": "active",
         },
         {
-            "address": "192.168.0.5/24",
+            "address": "2001:db8:abcd:12::20/128",
             "status": "active",
         },
         {
@@ -473,6 +475,7 @@ def setUpTestData(cls):
         IPAddress.objects.create(address=IPNetwork("192.168.0.1/24"), status=statuses[0])
         IPAddress.objects.create(address=IPNetwork("192.168.0.2/24"), status=statuses[0])
         IPAddress.objects.create(address=IPNetwork("192.168.0.3/24"), status=statuses[0])
+        IPAddress.objects.create(address=IPNetwork("2001:db8:abcd::20/128"), status=statuses[0])
 
         # FIXME(jathan): The writable serializer for `status` takes the
         # status `name` (str) and not the `pk` (int). Do not validate this
@@ -482,6 +485,17 @@ def setUpTestData(cls):
         # needs to be enhanced to use the actual API serializers when `api=True`
         cls.validation_excluded_fields = ["status"]
 
+    def test_create_invalid_address(self):
+        """Pass various invalid inputs and confirm they are rejected cleanly."""
+        self.add_permissions("ipam.add_ipaddress")
+
+        for bad_address in ("", "192.168.0.0.100/24", "192.168.0.0/35", "2001:db8:1:2:3:4:5:6:7:8/64"):
+            response = self.client.post(
+                self._get_list_url(), {"address": bad_address, "status": "active"}, format="json", **self.header
+            )
+            self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
+            self.assertIn("address", response.data)
+
 
 class VLANGroupTest(APIViewTestCases.APIViewTestCase):
     model = VLANGroup
diff --git a/nautobot/utilities/testing/api.py b/nautobot/utilities/testing/api.py
--- a/nautobot/utilities/testing/api.py
+++ b/nautobot/utilities/testing/api.py
@@ -259,15 +259,16 @@ def test_create_object(self):
             obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
 
             initial_count = self._get_queryset().count()
-            response = self.client.post(self._get_list_url(), self.create_data[0], format="json", **self.header)
-            self.assertHttpStatus(response, status.HTTP_201_CREATED)
-            self.assertEqual(self._get_queryset().count(), initial_count + 1)
-            self.assertInstanceEqual(
-                self._get_queryset().get(pk=response.data["id"]),
-                self.create_data[0],
-                exclude=self.validation_excluded_fields,
-                api=True,
-            )
+            for i, create_data in enumerate(self.create_data):
+                response = self.client.post(self._get_list_url(), create_data, format="json", **self.header)
+                self.assertHttpStatus(response, status.HTTP_201_CREATED)
+                self.assertEqual(self._get_queryset().count(), initial_count + i + 1)
+                self.assertInstanceEqual(
+                    self._get_queryset().get(pk=response.data["id"]),
+                    create_data,
+                    exclude=self.validation_excluded_fields,
+                    api=True,
+                )
 
         def test_bulk_create_objects(self):
             """