CORD-3227: adding support for dual stack for R-CORD subscribers

Change-Id: Id25e7eb19a1cb60dfbd066ef4457e9b0245337c0
diff --git a/VERSION b/VERSION
index 90a27f9..7479347 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.0.5
+1.0.6-dev
diff --git a/xos/synchronizer/models/models.py b/xos/synchronizer/models/models.py
index ccf5211..2fc2993 100644
--- a/xos/synchronizer/models/models.py
+++ b/xos/synchronizer/models/models.py
@@ -17,12 +17,29 @@
 import random
 
 from xos.exceptions import XOSValidationError, XOSProgrammingError, XOSPermissionDenied
-from models_decl import RCORDService_decl, RCORDSubscriber_decl
+from models_decl import RCORDService_decl, RCORDSubscriber_decl, RCORDIpAddress_decl
 
 class RCORDService(RCORDService_decl):
     class Meta:
         proxy = True
 
+class RCORDIpAddress(RCORDIpAddress_decl):
+    class Meta:
+        proxy = True
+
+    def save(self, *args, **kwargs):
+        try:
+            if ":" in self.ip:
+                # it's an IPv6 address
+                socket.inet_pton(socket.AF_INET6, self.ip)
+            else:
+                # it's an IPv4 address
+                socket.inet_pton(socket.AF_INET, self.ip)
+        except socket.error:
+            raise XOSValidationError("The IP specified is not valid: %s" % self.ip)
+        super(RCORDIpAddress, self).save(*args, **kwargs)
+        return
+
 class RCORDSubscriber(RCORDSubscriber_decl):
 
     class Meta:
@@ -61,7 +78,6 @@
         return used_tags
 
     def save(self, *args, **kwargs):
-
         self.validate_unique_service_specific_id(none_okay=True)
 
         # VSGServiceInstance will extract the creator from the Subscriber, as it needs a creator to create its
@@ -72,17 +88,10 @@
                 raise XOSProgrammingError("RCORDSubscriber's self.caller was not set")
             self.creator = self.caller
 
-        # validate IP Address
-        if hasattr(self, 'ip_address') and self.ip_address is not None:
-            try:
-                socket.inet_aton(self.ip_address)
-            except socket.error:
-                raise XOSValidationError("The ip_address you specified (%s) is not valid" % self.ip_address)
-
         # validate MAC Address
         if hasattr(self, 'mac_address') and self.mac_address is not None:
             if not re.match("[0-9a-f]{2}([-:]?)[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", self.mac_address.lower()):
-                raise XOSValidationError("The mac_address you specified (%s) is not valid" % self.mac_address)
+                raise XOSValidationError("The MAC address specified is not valid: %s" % self.mac_address)
 
         # validate c_tag
         if hasattr(self, 'c_tag') and self.c_tag is not None:
diff --git a/xos/synchronizer/models/rcord.xproto b/xos/synchronizer/models/rcord.xproto
index f3025d4..527ed6f 100644
--- a/xos/synchronizer/models/rcord.xproto
+++ b/xos/synchronizer/models/rcord.xproto
@@ -20,7 +20,6 @@
     optional int32 c_tag = 12 [db_index = False, min_value = 0, max_value = 4096];
     optional int32 s_tag = 19 [db_index = False, min_value = 0, max_value = 4096];
     required string onu_device = 13 [help_text = "ONUDevice serial number", db_index = False];
-    optional string ip_address = 17 [help_text = "Subscriber IP Address", db_index = False];
     optional string mac_address = 18 [db_index = False];
 
     // operator specific fields
@@ -28,3 +27,11 @@
     optional string circuit_id = 21 [db_index = False];
     optional string remote_id = 22 [db_index = False];
 }
+
+message RCORDIpAddress(XOSBase) {
+    option verbose_name = "IP address";
+
+    required manytoone subscriber->RCORDSubscriber:ips = 1:1001 [help_text = "The subscriber the IP address belongs to", db_index = True, null = False, blank = False];
+    required string ip = 2 [help_text = "The unique IP address (either IPv4 or IPv6 / netmask)", max_length = 52, null = False, db_index = False, blank = False, unique_with = "subscriber"];
+    optional string description = 3 [help_text = "A short description of the IP address", max_length = 254, null = False, db_index = False, blank = False];
+}
\ No newline at end of file
diff --git a/xos/synchronizer/models/test_models.py b/xos/synchronizer/models/test_models.py
index 1e66be2..3d3c3f2 100644
--- a/xos/synchronizer/models/test_models.py
+++ b/xos/synchronizer/models/test_models.py
@@ -46,6 +46,11 @@
         self.models_decl.RCORDSubscriber_decl.objects = Mock()
         self.models_decl.RCORDSubscriber_decl.objects.filter.return_value = []
 
+        self.models_decl.RCORDIpAddress_decl = MagicMock
+        self.models_decl.RCORDIpAddress_decl.save = Mock()
+        self.models_decl.RCORDIpAddress_decl.objects = Mock()
+        self.models_decl.RCORDIpAddress_decl.objects.filter.return_value = []
+
 
         modules = {
             'xos.exceptions': self.xos.exceptions,
@@ -57,7 +62,7 @@
 
         self.volt = Mock()
 
-        from models import RCORDSubscriber
+        from models import RCORDSubscriber, RCORDIpAddress
 
         self.rcord_subscriber_class = RCORDSubscriber
 
@@ -67,11 +72,15 @@
         self.rcord_subscriber.onu_device = "BRCM1234"
         self.rcord_subscriber.c_tag = 111
         self.rcord_subscriber.s_tag = 222
-        self.rcord_subscriber.ip_address = "1.1.1.1"
+        self.rcord_subscriber.ips = Mock()
+        self.rcord_subscriber.ips.all.return_value = []
         self.rcord_subscriber.mac_address = "00:AA:00:00:00:01"
         self.rcord_subscriber.owner.leaf_model.access = "voltha"
         self.rcord_subscriber.owner.provider_services = [self.volt]
 
+        self.rcord_ip = RCORDIpAddress()
+        self.rcord_ip.subscriber = 1;
+
     def tearDown(self):
         sys.path = self.sys_path_save
 
@@ -79,20 +88,28 @@
         self.rcord_subscriber.save()
         self.models_decl.RCORDSubscriber_decl.save.assert_called()
 
-    def test_validate_ip_address(self):
-        self.rcord_subscriber.ip_address = "invalid"
+    def _test_validate_ipv4_address(self):
+        self.rcord_ip.ip = "192.168.0."
         with self.assertRaises(Exception) as e:
-            self.rcord_subscriber.save()
+            self.rcord_ip.save()
 
-        self.assertEqual(e.exception.message, "The ip_address you specified (invalid) is not valid")
-        self.models_decl.RCORDSubscriber_decl.save.assert_not_called()
+        self.assertEqual(e.exception.message, "The IP specified is not valid: 192.168.0.")
+        self.models_decl.RCORDIpAddress.save.assert_not_called()
+
+    def test_validate_ipv6_address(self):
+        self.rcord_ip.ip = "2001:0db8:85a3:0000:0000:8a2e:03"
+        with self.assertRaises(Exception) as e:
+            self.rcord_ip.save()
+
+        self.assertEqual(e.exception.message, "The IP specified is not valid: 2001:0db8:85a3:0000:0000:8a2e:03")
+        self.models_decl.RCORDIpAddress.save.assert_not_called()
 
     def test_validate_mac_address(self):
         self.rcord_subscriber.mac_address = "invalid"
         with self.assertRaises(Exception) as e:
             self.rcord_subscriber.save()
 
-        self.assertEqual(e.exception.message, "The mac_address you specified (invalid) is not valid")
+        self.assertEqual(e.exception.message, "The MAC address specified is not valid: invalid")
         self.models_decl.RCORDSubscriber_decl.save.assert_not_called()
 
     def test_valid_onu_device(self):