Repository: allegro/ralph

Issue:
min_ip, max_ip fields erroring despite being specified via JSON REST API
As of commit 4794c4c04fced29b397b58a7689ff725c20ff6bd I'm having a problem where the JSON rest API won't let me create networks - https://22.0.0.2/api/networks/ (docker container instance of Ralph)
a request sent to 
```json
{
   "name":"test3",
   "address":"2.0.0.0/8",
   "remarks":"",
   "vlan":1,
   "dhcp_broadcast":false,
   "reserved_from_beginning":0,
   "reserved_from_end":0,
   "gateway":null,
   "network_environment":null,
   "kind":null,
   "service_env":null,
   "terminators":[

   ],
   "racks":[

   ],
   "dns_servers":[

   ],
   "min_ip":16777216,
   "max_ip":33554431
}
```
produces the following result:
```json
{
  "min_ip": [
    "This field is required."
  ],
  "max_ip": [
    "This field is required."
  ]
}
```

This seems to be a known issue with Django REST API, and is possibly a result of min_ip and max_ip being set as non-editable in the model? 

The same request works via the form request mechanism.


Discussion:
Traced this down a little - I can remediate by setting the min_ip and max_ip fields to be editable in the `network.py` model file - whatever I try to set is ignored (but still required to be present) but the code actually gets to the save handler now (which is where the override happens).
Found it - the problem is here:
https://github.com/allegro/ralph/blob/4794c4c04fced29b397b58a7689ff725c20ff6bd/src/ralph/networks/models/networks.py#L400

The `unique_together` constraint enforces a `required` constraint for all its arguments on any attached serializer (see here: http://www.django-rest-framework.org/api-guide/validators/#uniquetogethervalidator).



Resolving patch:
diff --git a/src/ralph/networks/api.py b/src/ralph/networks/api.py
--- a/src/ralph/networks/api.py
+++ b/src/ralph/networks/api.py
@@ -34,6 +34,13 @@ class Meta:
         )
 
 
+class NetworkSaveSerializer(RalphAPISerializer):
+    class Meta:
+        model = Network
+        depth = 1
+        exclude = ('min_ip', 'max_ip')
+
+
 class NetworkSerializer(RalphAPISerializer):
     class Meta:
         model = Network
@@ -95,6 +102,7 @@ def destroy(self, request, *args, **kwargs):
 class NetworkViewSet(RalphAPIViewSet):
     queryset = Network.objects.all()
     serializer_class = NetworkSerializer
+    save_serializer_class = NetworkSaveSerializer
     select_related = ['network_environment', 'kind']
     prefetch_related = ['racks']
     extended_filter_fields = {


Test patch:
diff --git a/src/ralph/networks/tests/test_api.py b/src/ralph/networks/tests/test_api.py
--- a/src/ralph/networks/tests/test_api.py
+++ b/src/ralph/networks/tests/test_api.py
@@ -4,6 +4,7 @@
 
 from ralph.api.tests._base import RalphAPITestCase
 from ralph.assets.tests.factories import EthernetFactory
+from ralph.networks.models import Network
 from ralph.networks.tests.factories import IPAddressFactory, NetworkFactory
 
 
@@ -161,3 +162,25 @@ def test_get_ip_list_filter_by_mac(self):
         response = self.client.get(url, format='json')
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.data['count'], 1)
+
+        network = response.data['results'][0]
+        self.assertIsNotNone(network['min_ip'])
+        self.assertIsNotNone(network['max_ip'])
+
+    def test_create_network_sets_min_and_max_ip_and_ignores_passed_value(self):
+        data = {
+            "name": "test1",
+            "address": "1.0.0.0/16",
+            "min_ip": 222,
+            "max_ip": 111,
+        }
+
+        url = reverse('network-list')
+        response = self.client.post(url, format='json', data=data)
+        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+        network = Network.objects.get(pk=response.data['id'])
+
+        # min and max ip are automatically assigned
+        self.assertEqual(network.min_ip, 16777216)  # 1.0.0.0
+        self.assertEqual(network.max_ip, 16842751)  # 1.0.255.255
+
diff --git a/src/ralph/networks/tests/test_models.py b/src/ralph/networks/tests/test_models.py
--- a/src/ralph/networks/tests/test_models.py
+++ b/src/ralph/networks/tests/test_models.py
@@ -243,6 +243,14 @@ def is_in_dnsaas_mocked(ip):
             self.assertEqual(net.get_first_free_ip(), first_free)
             patcher.stop()
 
+    def test_min_and_max_ip_are_assigned(self):
+        net = Network.objects.create(
+            name='net', address='1.0.0.0/16'
+        )
+
+        self.assertEqual(net.min_ip, 16777216)  # 1.0.0.0
+        self.assertEqual(net.max_ip, 16842751)  # 1.0.255.255
+
     def test_sub_network_should_assign_automatically(self):
         net = Network.objects.create(
             name='net', address='192.168.5.0/24'