Repository: infobloxopen/infoblox-client

Issue:
_custom_field_processing bug in NetworkV4 fails to properly serialize members
High-level API call to NetworkV4 fails to serialize members properly. This has been confirmed on the infoblox-client 0.5.0, as well as, _master_. This was tested and confirmed on NIOS v8.5.3 and v8.5.4. 

Steps to reproduce:

1. create a DHCP-enabled network 192.168.1.0/24 and associate two (2) Grid DHCP Member servers (NOT Microsoft Servers)

2. perform the following call in the Infoblox client:
`res = objects.Network.search(conn, return_fields=['members'], network='192.168.1.0/24')`

The following is returned:
<pre>
NetworkV4: members="[Msdhcpserver: ipv4addr="192.168.1.5", name="dhcp01.example.com", 
Msdhcpserver: ipv4addr="192.168.1.6", name="dhcp02.example.com"]", 
_ref="network/ZG5zLm5ldHdvcmskMTAuMC4zMi4wLzI0LzA:192.168.1.0/24/default"
</pre>

There are a few issues with this:
* _struct is missing from the list where the value is either 'msdhcpserver' | 'dhcpmember'
* It's interpreting the members as Msdhcpserver objects 100% of the time instead of using _struct to determine the type of DHCP server that is associated with the network

This issue is also seen with the NetworkTemplate.

This issue is only related to the high-level calls to objects.py and NOT when using the connector low-level calls.

Discussion:

Patch:
diff --git a/infoblox_client/objects.py b/infoblox_client/objects.py
index 6ca7bd1..3c817b6 100644
--- a/infoblox_client/objects.py
+++ b/infoblox_client/objects.py
@@ -639,7 +639,7 @@ class Dhcpddns(SubObjects):
 
 
 class Dhcpmember(SubObjects):
-    _fields = ['ipv4addr', 'ipv6addr', 'name']
+    _fields = ['_struct', 'ipv4addr', 'ipv6addr', 'name']
 
 
 class Dhcpoption(SubObjects):
@@ -7533,7 +7533,7 @@ class NetworkV4(Network):
 
     _custom_field_processing = {
         'logic_filter_rules': Logicfilterrule.from_dict,
-        'members': Msdhcpserver.from_dict,
+        'members': Dhcpmember.from_dict,
         'options': Dhcpoption.from_dict,
         'vlans': Vlanlink.from_dict,
         'zone_associations': Zoneassociation.from_dict,
@@ -8518,7 +8518,7 @@ class NetworkTemplateV4(NetworkTemplate):
 
     _custom_field_processing = {
         'logic_filter_rules': Logicfilterrule.from_dict,
-        'members': Msdhcpserver.from_dict,
+        'members': Dhcpmember.from_dict,
         'options': Dhcpoption.from_dict,
     }
 


Test patch:
diff --git a/tests/test_objects.py b/tests/test_objects.py
index 217d4a7..bf8f4fa 100644
--- a/tests/test_objects.py
+++ b/tests/test_objects.py
@@ -121,6 +121,64 @@ class TestObjects(unittest.TestCase):
             extattrs=None, force_proxy=False, return_fields=mock.ANY,
             max_results=None)
 
+    def test_search_network_with_grid_dhcp_members(self):
+        found = {
+            '_ref': 'network/ZG5zLm5ldHdvcmskMTAuMC4zMi4wLzI0LzA:10.0.32.0/24/default',
+            'members': [
+                {'_struct': 'dhcpmember', 'ipv4addr': '192.168.10.67', 'name': 'dhcp01.example.com'},
+                {'_struct': 'dhcpmember', 'ipv4addr': '192.168.11.67', 'name': 'dhcp02.example.com'}
+            ]
+        }
+        connector = self._mock_connector(get_object=[found])
+
+        network = objects.Network.search(
+            connector,
+            network_view='some-view',
+            network='10.0.32.0/24',
+            return_fields=['members']
+        )
+        connector.get_object.assert_called_once_with(
+            'network',
+            {'network_view': 'some-view', 'network': '10.0.32.0/24'},
+            extattrs=None, force_proxy=False, return_fields=['members'],
+            max_results=None
+        )
+        self.assertIsInstance(network.members[0], objects.Dhcpmember)
+        self.assertIsInstance(network.members[1], objects.Dhcpmember)
+        self.assertEqual('dhcpmember', network.members[0]._struct)
+        self.assertEqual('dhcpmember', network.members[1]._struct)
+        self.assertEqual('192.168.10.67', network.members[0].ipv4addr)
+        self.assertEqual('192.168.11.67', network.members[1].ipv4addr)
+
+    def test_search_network_with_ms_dhcp_members(self):
+        found = {
+            '_ref': 'network/ZG5zLm5ldHdvcmskMTAuMC4zMi4wLzI0LzA:10.0.32.0/24/default',
+            'members': [
+                {'_struct': 'msdhcpserver', 'ipv4addr': '192.168.10.67', 'name': 'dhcp01.example.com'},
+                {'_struct': 'msdhcpserver', 'ipv4addr': '192.168.11.67', 'name': 'dhcp02.example.com'}
+            ]
+        }
+        connector = self._mock_connector(get_object=[found])
+
+        network = objects.Network.search(
+            connector,
+            network_view='some-view',
+            network='10.0.32.0/24',
+            return_fields=['members']
+        )
+        connector.get_object.assert_called_once_with(
+            'network',
+            {'network_view': 'some-view', 'network': '10.0.32.0/24'},
+            extattrs=None, force_proxy=False, return_fields=['members'],
+            max_results=None
+        )
+        self.assertIsInstance(network.members[0], objects.Dhcpmember)
+        self.assertIsInstance(network.members[1], objects.Dhcpmember)
+        self.assertEqual('msdhcpserver', network.members[0]._struct)
+        self.assertEqual('msdhcpserver', network.members[1]._struct)
+        self.assertEqual('192.168.10.67', network.members[0].ipv4addr)
+        self.assertEqual('192.168.11.67', network.members[1].ipv4addr)
+
     def test_search_network_with_results(self):
         found = {"_ref": "network/ZG5zLm5ldHdvcmskMTAuMzkuMTEuMC8yNC8w"
                          ":10.39.11.0/24/default",