CORD-1252 eliminate hardcoded dependencies in VTN

Change-Id: I2765829dd7e7939e98d8d01ad2f3372e18561e7f
diff --git a/xos/README.md b/xos/README.md
new file mode 100644
index 0000000..c974a1f
--- /dev/null
+++ b/xos/README.md
@@ -0,0 +1,30 @@
+# VTN development notes #
+
+## Public Address ServiceInstances ##
+
+Hardcoded dependencies to `VRouterTenant` have been eliminated. It's now assumed that any ServiceInstance that has `public_ip` and `public_mac` fields provides the addressing functionality that VRouterTenant used to provide. 
+
+## Determining Additional Address Pairs ##
+
+The VTN synchronizer will compute additional addresses attached to a port based on the following criteria:
+
+1) If an instance has an `vm_vrouter_tag` or `vm_public_service_instance` tag attached to it, and that tag points to a ServiceInstance that has `public_ip` and `public_mac` fields, then that address pair will be added to the ports for any access networks on that instance. (TODO: replace tag with link?)
+
+`vm_vrouter_tag` is deprecated in favor of the service-neutron name `vm_public_service_instance`. 
+
+2) If there exists a tenant associated with an instance, and that tenant has a `SerivceInstanceLink` to a ServiceInstance that has `public_ip` and `public_mac` fields, then that address pair will be added to the ports for any access networks on that instance.
+
+## Determining vlan_id ##
+
+A port will be given a `vlan_id` if there exists a `vlan_id` or `s_tag` tag associated with the instance, and that port is an access network.
+
+`s_tag` is deprecated in favor of the service-neutral name `vlan_id`. 
+
+## Determining access networks ##
+
+A network is an access network (i.e. supports vlan_id and address_pairs) if it's VTN kind is in the 
+set `["VSG", ]`. (TODO: Find a better way to mark address networks)
+
+## Determining Public Gateways ##
+
+The VTN synchronizer determines public gateways by examining `AddressPool` objects. Each `AddressPool` has a `gateway_ip` and `gateway_mac` field. 
\ No newline at end of file
diff --git a/xos/synchronizer/Dockerfile.synchronizer b/xos/synchronizer/Dockerfile.synchronizer
index 6c7b6b7..9de9142 100644
--- a/xos/synchronizer/Dockerfile.synchronizer
+++ b/xos/synchronizer/Dockerfile.synchronizer
@@ -20,7 +20,6 @@
 COPY . /opt/xos/synchronizers/vtn
 
 COPY __init__.py /opt/xos/services/__init__.py
-COPY __init__.py vtnnetport.py /opt/xos/services/vtn/
 
 ENTRYPOINT []
 
diff --git a/xos/synchronizer/steps/sync_onos_netcfg.py b/xos/synchronizer/steps/sync_onos_netcfg.py
index b084a66..67dc878 100644
--- a/xos/synchronizer/steps/sync_onos_netcfg.py
+++ b/xos/synchronizer/steps/sync_onos_netcfg.py
@@ -48,18 +48,18 @@
                                   name=tagname)
         return tags[0].value
 
-    def get_tenants_who_want_config(self):
-        tenants = []
+    def get_service_instances_who_want_config(self):
+        service_instances = []
         # attribute is comma-separated list
         for ta in ServiceInstanceAttribute.objects.filter(name="autogenerate"):
             if ta.value:
                 for config in ta.value.split(','):
                     if config == "vtn-network-cfg":
-                        tenants.append(ta.service_instance)
-        return tenants
+                        service_instances.append(ta.service_instance)
+        return service_instances
 
-    def save_tenant_attribute(self, tenant, name, value):
-        tas = ServiceInstanceAttribute.objects.filter(service_instance_id=tenant.id, name=name)
+    def save_service_instance_attribute(self, service_instance, name, value):
+        tas = ServiceInstanceAttribute.objects.filter(service_instance_id=service_instance.id, name=name)
         if tas:
             ta = tas[0]
             if ta.value != value:
@@ -68,7 +68,7 @@
                 ta.save()
         else:
             logger.info("saving autogenerated config %s" % name)
-            ta = model_accessor.create_obj(ServiceInstanceAttribute, service_instance=tenant, name=name, value=value)
+            ta = model_accessor.create_obj(ServiceInstanceAttribute, service_instance=service_instance, name=name, value=value)
             ta.save()
 
     # This function currently assumes a single Deployment and Site
@@ -167,23 +167,26 @@
             data["apps"]["org.opencord.vtn"]["cordvtn"]["nodes"].append(node_dict)
 
         # Generate apps->org.onosproject.cordvtn->cordvtn->publicGateways
-        # Pull the gateway information from vRouter
-        if model_accessor.has_model_class("VRouterService"):
-            vrouters = VRouterService.objects.all()
-            if vrouters:
-                for gateway in vrouters[0].get_gateways():
-                    gatewayIp = gateway['gateway_ip'].split('/',1)[0]
-                    gatewayMac = gateway['gateway_mac']
-                    gateway_dict = {
-                        "gatewayIp": gatewayIp,
-                        "gatewayMac": gatewayMac
-                    }
-                    data["apps"]["org.opencord.vtn"]["cordvtn"]["publicGateways"].append(gateway_dict)
-        else:
-            logger.info("No VRouter service present, not adding publicGateways to config")
+        # Pull the gateway information from Address Pool objects
+        for ap in AddressPool.objects.all():
+            if (not ap.gateway_ip) or (not ap.gateway_mac):
+                logger.info("Gateway_ip or gateway_mac is blank for addresspool %s. Skipping." % ap)
+                continue
+
+            gateway_dict = {
+                "gatewayIp": ap.gateway_ip,
+                "gatewayMac": ap.gateway_mac
+            }
+            data["apps"]["org.opencord.vtn"]["cordvtn"]["publicGateways"].append(gateway_dict)
+
+        if not AddressPool.objects.all().exists():
+            logger.info("No Address Pools present, not adding publicGateways to config")
 
         return json.dumps(data, indent=4, sort_keys=True)
 
+    # TODO: Does this step execute every 5 seconds regardless of whether objects have changed?
+    #       If so, what purpose does using watchers serve?
+
     def call(self, **args):
         vtn_service = VTNService.objects.all()
         if not vtn_service:
@@ -194,6 +197,6 @@
         # Check for autogenerate attribute
         netcfg = self.get_onos_netcfg(vtn_service)
 
-        tenants = self.get_tenants_who_want_config()
-        for tenant in tenants:
-            self.save_tenant_attribute(tenant, "rest_onos/v1/network/configuration/", netcfg)
+        service_instances = self.get_service_instances_who_want_config()
+        for service_instance in service_instances:
+            self.save_service_instance_attribute(service_instance, "rest_onos/v1/network/configuration/", netcfg)
diff --git a/xos/synchronizer/steps/sync_vtn_service.py b/xos/synchronizer/steps/sync_vtn_service.py
index 9aa2cb7..e4d4b4f 100644
--- a/xos/synchronizer/steps/sync_vtn_service.py
+++ b/xos/synchronizer/steps/sync_vtn_service.py
@@ -19,7 +19,7 @@
 import socket
 import sys
 import base64
-from services.vtn.vtnnetport import VTNNetwork, VTNPort
+from synchronizers.vtn.vtnnetport import VTNNetwork, VTNPort
 from synchronizers.new_base.syncstep import SyncStep
 from synchronizers.new_base.modelaccessor import *
 from xos.logger import Logger, logging
@@ -39,51 +39,52 @@
     def __init__(self, **args):
         SyncStep.__init__(self, **args)
 
-    def get_vtn_onos_service(self):
-        vtn_service = ONOSService.objects.filter(name="ONOS_CORD")  # XXX fixme - harcoded
-        if not vtn_service:
-            raise "No VTN Onos Service"
+    def get_vtn_onos_app(self, vtn_service):
+        links = vtn_service.subscribed_links.all()
+        for link in links:
+            # We're looking for an ONOS App. It's the only ServiceInstance that VTN can be implemented on.
+            if link.provider_service_instance.leaf_model_name != "ONOSApp":
+                continue
 
-        return vtn_service[0]
+            # TODO: Rather than checking model name, check for the right interface
+            # NOTE: Deferred until new Tosca engine is in place.
 
-    def get_vtn_auth(self):
-        return HTTPBasicAuth('karaf', 'karaf') # XXX fixme - hardcoded auth
+            #if not link.provider_service_interface:
+            #    logger.warning("Link %s does not have a provider_service_interface. Skipping" % link)
+            #    continue
+            #
+            #if link.provider_service_interface.interface_type.name != "onos_app_interface":
+            #    logger.warning("Link %s provider_service_interface type is not equal to onos_app_interface" % link)
+            #    continue
 
-    def get_vtn_addr(self):
-        vtn_service = self.get_vtn_onos_service()
+            # cast from ServiceInstance to ONOSApp
+            app = link.provider_service_instance.leaf_model
+            return app
 
-        if vtn_service.rest_hostname:
-            return vtn_service.rest_hostname
+        raise Exception("No ServiceInstanceLink from VTN Service to VTN ONOS App")
 
-        # code below this point is for ONOS running in a slice, and is
-        # probably outdated
+    def get_vtn_endpoint(self, vtn_service):
+        """ Get connection info for the ONOS that is hosting the VTN ONOS App.
 
-        if not vtn_service.slices.exists():
-            raise "VTN Service has no slices"
+            returns (hostname, port, auth)
+        """
+        app = self.get_vtn_onos_app(vtn_service)
+        # cast from Service to ONOSService
+        onos = app.owner.leaf_model
+        if not (onos.rest_hostname):
+            raise Exception("onos.rest_hostname is not set")
+        if not (onos.rest_port):
+            raise Exception("onos.rest_port is not set")
+        if not (onos.rest_password):
+            raise Exception("onos.rest_password is not set")
+        if not (onos.rest_username):
+            raise Exception("onos.rest_username is not set")
+        auth = HTTPBasicAuth(onos.rest_username, onos.rest_password)
+        return (onos.rest_hostname, onos.rest_port, auth)
 
-        vtn_slice = vtn_service.slices.all()[0]
-
-        if not vtn_slice.instances.exists():
-            raise "VTN Slice has no instances"
-
-        vtn_instance = vtn_slice.instances.all()[0]
-
-        return vtn_instance.node.name
-
-    def get_vtn_port(self):
-        vtn_service = self.get_vtn_onos_service()
-
-        if vtn_service.rest_port:
-            return vtn_service.rest_port
-
-        # code below this point is for ONOS running in a slice, and is
-        # probably outdated
-
-        raise Exception("Must set rest_port")
-
-    def get_method(self, url, id):
+    def get_method(self, auth, url, id):
         url_with_id = "%s/%s" % (url, id)
-        r = requests.get(url_with_id, auth=self.get_vtn_auth())
+        r = requests.get(url_with_id, auth=auth)
         if (r.status_code==200):
             method="PUT"
             url = url_with_id
@@ -95,7 +96,9 @@
             exists=False
         return (exists, url, method, req_func)
 
-    def sync_service_networks(self):
+    def sync_service_networks(self, vtn_service):
+        (onos_hostname, onos_port, onos_auth) = self.get_vtn_endpoint(vtn_service)
+
         valid_ids = []
         for network in Network.objects.all():
             network = VTNNetwork(network)
@@ -110,7 +113,7 @@
             valid_ids.append(network.id)
 
             if (glo_saved_networks.get(network.id, None) != network.to_dict()):
-                (exists, url, method, req_func) = self.get_method("http://" + self.get_vtn_addr() +  ":" + str(self.get_vtn_port()) + "/onos/cordvtn/serviceNetworks", network.id)
+                (exists, url, method, req_func) = self.get_method(onos_auth, "http://%s:%d/onos/cordvtn/serviceNetworks" % (onos_hostname, onos_port), network.id)
 
                 logger.info("%sing VTN API for network %s" % (method, network.id))
 
@@ -124,7 +127,7 @@
                         "providerNetworks": providerNetworks} }
                 logger.info("DATA: %s" % str(data))
 
-                r=req_func(url, json=data, auth=self.get_vtn_auth() )
+                r=req_func(url, json=data, auth=onos_auth )
                 if (r.status_code in [200,201]):
                     glo_saved_networks[network.id] = network.to_dict()
                 else:
@@ -135,16 +138,18 @@
             if network_id not in valid_ids:
                 logger.info("DELETEing VTN API for network %s" % network_id)
 
-                url = "http://" + self.get_vtn_addr() +  ":" + str(self.get_vtn_port()) + "/onos/cordvtn/serviceNetworks/%s" % network_id
+                url = "http://%s:%d/onos/cordvtn/serviceNetworks/%s" % (onos_hostname, onos_port, network_id)
                 logger.info("URL: %s" % url)
 
-                r = requests.delete(url, auth=self.get_vtn_auth() )
+                r = requests.delete(url, auth=onos_auth )
                 if (r.status_code in [200,204]):
                     del glo_saved_networks[network_id]
                 else:
                     logger.error("Received error from vtn service (%d)" % r.status_code)
 
-    def sync_service_ports(self):
+    def sync_service_ports(self, vtn_service):
+        (onos_hostname, onos_port, onos_auth) = self.get_vtn_endpoint(vtn_service)
+
         valid_ids = []
         for port in Port.objects.all():
             port = VTNPort(port)
@@ -159,7 +164,7 @@
             valid_ids.append(port.id)
 
             if (glo_saved_ports.get(port.id, None) != port.to_dict()):
-                (exists, url, method, req_func) = self.get_method("http://" + self.get_vtn_addr() +  ":" + str(self.get_vtn_port()) + "/onos/cordvtn/servicePorts", port.id)
+                (exists, url, method, req_func) = self.get_method(onos_auth, "http://%s:%d/onos/cordvtn/servicePorts" % (onos_hostname, onos_port), port.id)
 
                 logger.info("%sing VTN API for port %s" % (method, port.id))
 
@@ -170,7 +175,7 @@
                         "floating_address_pairs": port.floating_address_pairs} }
                 logger.info("DATA: %s" % str(data))
 
-                r=req_func(url, json=data, auth=self.get_vtn_auth() )
+                r=req_func(url, json=data, auth=onos_auth )
                 if (r.status_code in [200,201]):
                     glo_saved_ports[port.id] = port.to_dict()
                 else:
@@ -180,10 +185,10 @@
             if port_id not in valid_ids:
                 logger.info("DELETEing VTN API for port %s" % port_id)
 
-                url = "http://" + self.get_vtn_addr() +  ":" + str(self.get_vtn_port()) + "/onos/cordvtn/servicePorts/%s" % port_id
+                url = "http://%s:%d/onos/cordvtn/servicePorts/%s" % (onos_hostname, onos_port, port_id)
                 logger.info("URL: %s" % url)
 
-                r = requests.delete(url, auth=self.get_vtn_auth() )
+                r = requests.delete(url, auth=onos_auth )
                 if (r.status_code in [200,204]):
                     del glo_saved_ports[port_id]
                 else:
@@ -199,6 +204,9 @@
 
         vtn_service = vtn_service[0]
 
+        # TODO: We should check get_vtn_onos_app() and make sure that it has been synced, and that any necessary
+        #       attributes (netcfg, etc) is filled out.
+
         if (vtn_service.resync):
             # If the VTN app requested a full resync, clear our saved network
             # so we will resync everything, then reset the 'resync' flag
@@ -211,8 +219,8 @@
         if vtn_service.vtnAPIVersion>=2:
             # version 2 means use new API
             logger.info("Using New API")
-            self.sync_service_networks()
-            self.sync_service_ports()
+            self.sync_service_networks(vtn_service)
+            self.sync_service_ports(vtn_service)
         else:
             raise Exception("VTN API Version 1 is no longer supported by VTN Synchronizer")
 
diff --git a/xos/synchronizer/vtnnetport.py b/xos/synchronizer/vtnnetport.py
index 82f5a0c..eaa037f 100644
--- a/xos/synchronizer/vtnnetport.py
+++ b/xos/synchronizer/vtnnetport.py
@@ -14,24 +14,8 @@
 # limitations under the License.
 
 
-# This library can be used in two different contexts:
-#    1) From the VTN synchronizer
-#    2) From the handcrafted VTN API endpoint
-#
-# If (1) then the modelaccessor module can provide us with models from the API
-# or django as appropriate. If (2) then we must use django, until we can
-# reconcile what to do about handcrafted API endpoints
-
-import __main__ as main_program
-
-if "synchronizer" in main_program.__file__:
-    from synchronizers.new_base.modelaccessor import *
-    in_synchronizer = True
-else:
-    from core.models import *
-    in_synchronizer = False
-
-VTN_SERVCOMP_KINDS=["PRIVATE","VSG"]
+from synchronizers.new_base.modelaccessor import *
+in_synchronizer = True
 
 class VTNNetwork(object):
     def __init__(self, xos_network=None):
@@ -160,43 +144,102 @@
             return cn
         return None
 
-    def get_vsg_tenants(self):
-        # If the VSG service isn't onboarded, then return an empty list.
-        if (in_synchronizer):
-            if not model_accessor.has_model_class("VSGTenant"):
-                 print "VSGTenant model does not exist. Returning no tenants"
-                 return []
-            VSGTenant = model_accessor.get_model_class("VSGTenant")   # suppress undefined local variable error
-        else:
-            try:
-                from services.vsg.models import VSGTenant
-            except ImportError:
-                # TODO: Set up logging for this library...
-                print "Failed to import VSG, returning no tenants"
-                return []
+    def is_access_network(self):
+        """ Determines whether this port is attached to an access network. Currently we do this by examining the
+            network template's vtn_kind field. See if there is a better way...
+        """
+        return self.xos_port.network.template.vtn_kind in ["VSG", ]
 
-        vsg_tenants=[]
-        for tenant in VSGTenant.objects.all():
-            if tenant.instance.id == self.xos_port.instance.id:
-                vsg_tenants.append(tenant)
-        return vsg_tenants
+    def get_vm_addresses(self):
+        if not self.is_access_network():
+            # If not an access network, do not apply any addresses
+            return []
+
+        if not self.xos_port.instance:
+            return []
+
+        # See if the Instance has any public address (aka "VrouterTenant) service instances associated with it.
+        # If so, then add each of those to the set of address pairs.
+
+        # TODO: Perhaps this should be implemented as a link instead of a tag...
+
+        tags = Tag.objects.filter(name="vm_public_service_instance", object_id=self.xos_port.instance.id,
+                                            content_type=self.xos_port.instance.self_content_type_id)
+
+        if not tags:
+            # DEPRECATED
+            # Historically, VSG instances are tagged with "vm_vrouter_tenant" instead of "vm_public_service_instance"
+            tags = Tag.objects.filter(name="vm_vrouter_tenant", object_id=self.xos_port.instance.id,
+                                                content_type=self.xos_port.instance.self_content_type_id)
+
+        address_pairs = []
+        for tag in tags:
+            si = ServiceInstance.objects.get(id = int(tag.value))
+
+            # cast from Tenant to descendant class (VRouterTenant, etc)
+            si = si.leaf_model
+
+            if (not hasattr(si, "public_ip")) or (not hasattr(si, "public_mac")):
+                raise Exception("Object %s does not have public_ip and/or public_mac fields" % si)
+            address_pairs.append({"ip_address": si.public_ip,
+                                  "mac_address": si.public_mac})
+
+        return address_pairs
+
+    def get_container_addresses(self):
+        if not self.is_access_network():
+            # If not an access network, do not apply any addresses
+            return []
+
+        if not self.xos_port.instance:
+            return []
+
+        addrs = []
+        for si in ServiceInstance.objects.all():
+            # cast from tenant to its descendant class (VSGTenant, etc)
+            si = si.leaf_model
+
+            if not hasattr(si, "instance_id"):
+                # ignore ServiceInstance that don't have instances
+                continue
+
+            if si.instance_id != self.xos_port.instance.id:
+                # ignore ServiceInstances that don't relate to our instance
+                continue
+
+            # Check to see if there is a link public address (aka VRouterTenant)
+            links = si.subscribed_links.all()
+            for link in links:
+                # cast from ServiceInstance to descendant class (VRouterTenant, etc)
+                pubaddr_si = link.provider_service_instance.leaf_model
+                if hasattr(pubaddr_si, "public_ip") and hasattr(pubaddr_si, "public_mac"):
+                    addrs.append({"ip_address": pubaddr_si.public_ip,
+                                  "mac_address": pubaddr_si.public_mac})
+        return addrs
 
     @property
     def vlan_id(self):
+        """ Return the vlan_id associated with this instance. This assumes the instance was tagged with either a
+            vlan_id or s_tag tag.
+        """
+
+        if not self.is_access_network():
+            # If not an access network, do not apply any tags
+            return []
+
         if not self.xos_port.instance:
             return None
 
-        # Only some kinds of networks can have s-tags associated with them.
-        # Currently, only VSG access networks qualify.
-        if not self.xos_port.network.template.vtn_kind in ["VSG",]:
-            return None
+        tags = Tag.objects.filter(content_type=model_accessor.get_content_type_id(self.xos_port.instance),
+                                  object_id=self.xos_port.instance.id,
+                                  name="vlan_id")
 
-        if (in_synchronizer):
+        if not tags:
+            # DEPRECATED
+            # Historically, VSG instances are tagged with "s_tag" instead of "vlan_id"
             tags = Tag.objects.filter(content_type=model_accessor.get_content_type_id(self.xos_port.instance),
                                       object_id=self.xos_port.instance.id,
                                       name="s_tag")
-        else:
-            tags = Tag.select_by_content_object(self.xos_port.instance).filter(name="s_tag")
 
         if not tags:
             return None
@@ -208,18 +251,10 @@
         # Floating_address_pairs is the set of WAN addresses that should be
         # applied to this port.
 
-        address_pairs = []
+        # We only want to apply these addresses to an "access" network.
 
-        # only look apply the VSG addresses if the Network is of the VSG vtn_kind
-        if self.xos_port.network.template.vtn_kind in ["VSG", ]:
-            for vsg in self.get_vsg_tenants():
-                if vsg.wan_container_ip and vsg.wan_container_mac:
-                    address_pairs.append({"ip_address": vsg.wan_container_ip,
-                                          "mac_address": vsg.wan_container_mac})
 
-                if vsg.wan_vm_ip and vsg.wan_vm_mac:
-                    address_pairs.append({"ip_address": vsg.wan_vm_ip,
-                                          "mac_address": vsg.wan_vm_mac})
+        address_pairs = self.get_vm_addresses() + self.get_container_addresses()
 
         return address_pairs
 
diff --git a/xos/vtn-onboard.yaml b/xos/vtn-onboard.yaml
index fc7d6ca..9cf0fea 100644
--- a/xos/vtn-onboard.yaml
+++ b/xos/vtn-onboard.yaml
@@ -30,7 +30,6 @@
           # The following will concatenate with base_url automatically, if
           # base_url is non-null.
           xproto: ./
-          django_library: synchronizer/vtnnetport.py
           admin_template: templates/vtnadmin.html
           tosca_resource: tosca/resources/vtnservice.py
           #private_key: file:///opt/xos/key_import/vsg_rsa