SEBA-324 Automatic offsetting of reverse ids

Change-Id: I1bb0bfb245656b36a466f1550093d896043586f4
diff --git a/VERSION b/VERSION
index dfb6396..0ddcadb 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.1.20
+2.1.21
diff --git a/containers/chameleon/Dockerfile.chameleon b/containers/chameleon/Dockerfile.chameleon
index c844f73..a6f38b6 100644
--- a/containers/chameleon/Dockerfile.chameleon
+++ b/containers/chameleon/Dockerfile.chameleon
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 # xosproject/chameleon
-FROM xosproject/xos-base:2.1.20
+FROM xosproject/xos-base:2.1.21
 
 # xos-base already has protoc and dependencies installed
 
diff --git a/containers/xos/Dockerfile.client b/containers/xos/Dockerfile.client
index 90fe477..89b578b 100644
--- a/containers/xos/Dockerfile.client
+++ b/containers/xos/Dockerfile.client
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 # xosproject/xos-client
-FROM xosproject/xos-libraries:2.1.20
+FROM xosproject/xos-libraries:2.1.21
 
 # Install XOS client
 COPY xos/xos_client /tmp/xos_client
diff --git a/containers/xos/Dockerfile.libraries b/containers/xos/Dockerfile.libraries
index 5d289f8..4513a2d 100644
--- a/containers/xos/Dockerfile.libraries
+++ b/containers/xos/Dockerfile.libraries
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 # xosproject/xos-libraries
-FROM xosproject/xos-base:2.1.20
+FROM xosproject/xos-base:2.1.21
 
 # Add libraries
 COPY lib /opt/xos/lib
diff --git a/containers/xos/Dockerfile.synchronizer-base b/containers/xos/Dockerfile.synchronizer-base
index 57a7ba5..35590ab 100644
--- a/containers/xos/Dockerfile.synchronizer-base
+++ b/containers/xos/Dockerfile.synchronizer-base
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 # xosproject/xos-synchronizer-base
-FROM xosproject/xos-client:2.1.20
+FROM xosproject/xos-client:2.1.21
 
 COPY xos/synchronizers/new_base /opt/xos/synchronizers/new_base
 COPY xos/xos/logger.py /opt/xos/xos/logger.py
diff --git a/containers/xos/Dockerfile.xos-core b/containers/xos/Dockerfile.xos-core
index 1f52a75..b291bc6 100644
--- a/containers/xos/Dockerfile.xos-core
+++ b/containers/xos/Dockerfile.xos-core
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 # xosproject/xos-core
-FROM xosproject/xos-libraries:2.1.20
+FROM xosproject/xos-libraries:2.1.21
 
 # Install XOS
 ADD xos /opt/xos
diff --git a/lib/xos-genx/xos-genx-tests/test_generator.py b/lib/xos-genx/xos-genx-tests/test_generator.py
index 06da7ee..3daa594 100644
--- a/lib/xos-genx/xos-genx-tests/test_generator.py
+++ b/lib/xos-genx/xos-genx-tests/test_generator.py
@@ -33,6 +33,7 @@
 BASE_XPROTO = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/xproto/base.xproto")
 TEST_XPROTO = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/xproto/test.xproto")
 FIELDTEST_XPROTO = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/xproto/fieldtest.xproto")
+REVERSEFIELDTEST_XPROTO = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/xproto/reversefieldtest.xproto")
 FILTERTEST_XPROTO = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/xproto/filtertest.xproto")
 SKIP_DJANGO_XPROTO = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/xproto/skip_django.xproto")
 VROUTER_XPROTO = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/xproto/vrouterport.xproto")
@@ -222,6 +223,31 @@
         _assert_field("Slice", "slice_field", 101)
         _assert_field("Slice", "site", 102)
 
+    def test_field_numbers(self):
+        args = XOSProcessorArgs(files = [REVERSEFIELDTEST_XPROTO],
+                                target = FIELDTEST_TARGET)
+        output = XOSProcessor.process(args)
+
+        def _assert_field(modelname, fieldname, id):
+            self.assertIn("%s,%s,%s" % (modelname, fieldname, id), output)
+
+        # rel_int1s_ids is the reverse link from RelatedToIntermediate1. It gets the related id with no offset, so it
+        # will be assigned 1001. rel_leaf1as_ids inherits from Intermediate1, so its reverse links will all be offset
+        # by 100
+        _assert_field("Leaf1a", "rel_int1s_ids", 1001)
+        _assert_field("Leaf1a", "rel_leaf1as_ids", 1101)
+
+        # rel_int2s_ids is the reverse link from RelatedToIntermediate1. It gets the related id with no offset, so it
+        # will be assigned 1001. rel_leaf1bs_ids inherits from Intermediate1, so its reverse links will all be offset
+        # by 100
+        _assert_field("Leaf1b", "rel_int1s_ids", 1001)
+        _assert_field("Leaf1b", "rel_leaf1bs_ids", 1101)
+
+        # There are no reverse numbers specified for Intermediate2 or Leaf2, so xproto will fall back to automatic
+        # numbering starting at 1900.
+        _assert_field("Leaf2", "rel_int2s_ids", 1900)
+        _assert_field("Leaf2", "rel_leaf2s_ids", 1901)
+
     def test_unfiltered(self):
         """ With no include_* args, should get all models """
         args = XOSProcessorArgs(files = [FILTERTEST_XPROTO],
diff --git a/lib/xos-genx/xos-genx-tests/xproto/reversefieldtest.xproto b/lib/xos-genx/xos-genx-tests/xproto/reversefieldtest.xproto
new file mode 100644
index 0000000..93b04e3
--- /dev/null
+++ b/lib/xos-genx/xos-genx-tests/xproto/reversefieldtest.xproto
@@ -0,0 +1,42 @@
+option app_label = "core";
+
+message XOSBase {
+    optional string base_field = 2 [default = "stuff", max_length = 1024];
+    optional string base_field2 = 3 [default = "stuff", max_length = 1024];
+}
+
+message Intermediate1(XOSBase) {
+}
+
+message Leaf1a(Intermediate1) {
+}
+
+message Leaf1b(Intermediate1) {
+}
+
+
+message Intermediate2(XOSBase) {
+}
+
+message Leaf2(Intermediate2) {
+}
+
+message RelatedToIntermediate1(XOSBase) {
+    required manytoone int1->Intermediate1:rel_int1s = 2:1001 [help_text = "The Intermediate1 this model is attached to", null = False, db_index = True, blank = False];
+}
+
+message RelatedToLeaf1a(XOSBase) {
+    required manytoone leaf1a->Leaf1a:rel_leaf1as = 2:1001 [help_text = "The Leaf1a this model is attached to", null = False, db_index = True, blank = False];
+}
+
+message RelatedToLeaf1b(XOSBase) {
+    required manytoone leaf1b->Leaf1b:rel_leaf1bs = 2:1001 [help_text = "The Leaf1b this model is attached to", null = False, db_index = True, blank = False];
+}
+
+message RelatedToIntermediate2(XOSBase) {
+    required manytoone int2->Intermediate2:rel_int2s = 2 [help_text = "The Intermediate2 this model is attached to", null = False, db_index = True, blank = False];
+}
+
+message RelatedToLeaf2(XOSBase) {
+    required manytoone leaf2->Leaf2:rel_leaf2s = 2 [help_text = "The Leaf2 this model is attached to", null = False, db_index = True, blank = False];
+}
diff --git a/lib/xos-genx/xosgenx/jinja2_extensions/base.py b/lib/xos-genx/xosgenx/jinja2_extensions/base.py
index af1c241..e11d2ec 100644
--- a/lib/xos-genx/xosgenx/jinja2_extensions/base.py
+++ b/lib/xos-genx/xosgenx/jinja2_extensions/base.py
@@ -203,7 +203,10 @@
         if b in table:
             base_rlinks = xproto_base_rlinks(table[b], table)
 
-            model_rlinks = table[b]['rlinks']
+            model_rlinks = [x.copy() for x in table[b]['rlinks']]
+            for link in model_rlinks:
+                link["accessor"] = b
+
             links.extend(base_rlinks)
             links.extend(model_rlinks)
 
@@ -212,31 +215,48 @@
 def xproto_rlinks(m, table):
     """ Return the reverse links for the xproto message `m`.
 
-        If the link includes a reverse_id, then it will be used for the protobuf field id. If there is no
-        reverse_id, then one will automatically be allocated started at id 1900. It is incouraged that all links
-        include reverse_ids, so that field identifiers are deterministic across all protobuf messages.
+        If the link includes a reverse_id, then it will be used for the protobuf field id. Each level of inheritance
+        will add an offset of 100 to the supplied reverse_id.
+
+        If there is no reverse_id, then one will automatically be allocated started at id 1900. It is encouraged that
+        all links include reverse_ids, so that field identifiers are deterministic across all protobuf messages.
     """
 
-    index = 1900
-    links = xproto_base_rlinks(m, table) + m["rlinks"]
+    model_rlinks = [x.copy() for x in m["rlinks"]]
+    for link in model_rlinks:
+        link["accessor"] = m["fqn"]
+
+    links = xproto_base_rlinks(m, table) + model_rlinks
 
     links = [x for x in links if ("+" not in x["src_port"]) and ("+" not in x["dst_port"])]
 
-    for link in links:
-        if link["reverse_id"]:
-            link["id"] = int(link["reverse_id"])
-        else:
-            link["id"] = index
-            index += 1
+    if links:
+        last_accessor = links[0]["accessor"]
+        offset = 0
+        index = 1900
+        for link in links:
+            if (link["accessor"] != last_accessor):
+                last_accessor = link["accessor"]
+                offset += 100
 
-    # check for duplicates
-    links_by_number={}
-    for link in links:
-        id = link["id"]
-        dup=links_by_number.get(id)
-        if dup:
-            raise Exception("Field %s has duplicate number %d with field %s in model %s" % (link["src_port"], id, link["src_port"], m["name"]))
-        links_by_number[id] = link
+            if link["reverse_id"]:
+                # Statically numbered reverse links. Use the id that the developer supplied, adding the offset based on
+                # inheritance depth.
+                link["id"] = int(link["reverse_id"]) + offset
+            else:
+                # Automatically numbered reverse links. These will eventually go away.
+                link["id"] = index
+                index += 1
+
+        # check for duplicates
+        links_by_number={}
+        for link in links:
+            id = link["id"]
+            dup=links_by_number.get(id)
+            if dup:
+                raise Exception("Field %s has duplicate number %d in model %s with reverse field %s" %
+                                (link["src_port"], id, m["name"], dup["src_port"]))
+            links_by_number[id] = link
 
     return links
 
diff --git a/scripts/xproto_sanity_check.py b/scripts/xproto_sanity_check.py
new file mode 100755
index 0000000..f977b28
--- /dev/null
+++ b/scripts/xproto_sanity_check.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+from xosgenx.generator import XOSProcessor, XOSProcessorArgs
+
+# These assume a traditional CORD/SEBA hierarchy is checked out using `repo`
+
+SCRIPT_DIR=os.path.dirname(os.path.realpath(__file__))
+BASE_DIR=os.path.join(SCRIPT_DIR,"..","..","..")
+SERVICES_DIR=os.path.join(BASE_DIR,"orchestration","xos_services")
+CORE_XPROTO=os.path.join(BASE_DIR,"orchestration","xos","xos","core","models","core.xproto")
+TARGET=os.path.join(BASE_DIR,"orchestration","xos","lib","xos-genx","xosgenx","targets","fieldlist.xtarget")
+
+def get_all_xproto():
+    xprotos=[]
+    for service_name in os.listdir(SERVICES_DIR):
+        if service_name.startswith("."):
+            continue
+        service_path = os.path.join(SERVICES_DIR, service_name)
+        if not os.path.isdir(service_path):
+            continue
+        models_dir = os.path.join(service_path, "xos", "synchronizer", "models")
+        if not os.path.isdir(models_dir):
+            continue
+        for xproto_name in os.listdir(models_dir):
+            if xproto_name.startswith("."):
+                continue
+            if not xproto_name.endswith(".xproto"):
+                continue
+            xproto_pathname = os.path.join(models_dir, xproto_name)
+            xprotos.append(xproto_pathname)
+    return xprotos
+
+xprotos = get_all_xproto() + [CORE_XPROTO]
+args = XOSProcessorArgs(files=xprotos,
+                        target=TARGET,
+                        verbosity=1
+                        )
+output = XOSProcessor.process(args)