Merge branch 'feature/vRouter'
diff --git a/xos/configurations/cord-pod/cord-vtn-vsg.yaml b/xos/configurations/cord-pod/cord-vtn-vsg.yaml
index f29e257..c8c6ceb 100644
--- a/xos/configurations/cord-pod/cord-vtn-vsg.yaml
+++ b/xos/configurations/cord-pod/cord-vtn-vsg.yaml
@@ -24,16 +24,25 @@
           view_url: /admin/cord/voltservice/$id$/
           kind: vOLT
 
-    public_addresses:
+    addresses_vsg:
       type: tosca.nodes.AddressPool
       properties:
           addresses: 10.168.0.0/24
+          gateway_ip: 10.168.0.1
+          gateway_mac: 02:42:0a:a8:00:01
+
+    addresses_exampleservice-public:
+      type: tosca.nodes.AddressPool
+      properties:
+          addresses: 10.168.1.0/24
+          gateway_ip: 10.168.1.1
+          gateway_mac: 02:42:0a:a8:00:01
 
     service_vsg:
       type: tosca.nodes.VSGService
       requirements:
-          - vbng_tenant:
-              node: service_vbng
+          - vrouter_tenant:
+              node: service_vrouter
               relationship: tosca.relationships.TenantOfService
       properties:
           view_url: /admin/cord/vsgservice/$id$/
@@ -47,13 +56,17 @@
       artifacts:
           pubkey: /opt/xos/synchronizers/vcpe/vcpe_public_key
 
-    service_vbng:
-      type: tosca.nodes.VBNGService
+    service_vrouter:
+      type: tosca.nodes.VRouterService
       properties:
-          view_url: /admin/cord/vbngservice/$id$/
-# if unspecified, vbng observer will look for an ONOSApp Tenant and
-# generate a URL from its IP address
-#          vbng_url: http://10.11.10.24:8181/onos/virtualbng/
+          view_url: /admin/vrouter/vrouterservice/$id$/
+      requirements:
+          - addresses_vsg:
+              node: addresses_vsg
+              relationship: tosca.relationships.ProvidesAddresses
+          - addresses_service1:
+              node: addresses_exampleservice-public
+              relationship: tosca.relationships.ProvidesAddresses
 
     Private:
       type: tosca.nodes.NetworkTemplate
diff --git a/xos/configurations/cord-pod/pod-exampleservice.yaml b/xos/configurations/cord-pod/pod-exampleservice.yaml
index 59a9c8f..4e4835c 100644
--- a/xos/configurations/cord-pod/pod-exampleservice.yaml
+++ b/xos/configurations/cord-pod/pod-exampleservice.yaml
@@ -18,11 +18,17 @@
           no-delete: true
           no-update: true
 
+    service_vrouter:
+      type: tosca.nodes.Service
+      properties:
+          no-create: true
+          no-delete: true
+          no-update: true
+
     exampleservice-public:
       type: tosca.nodes.network.Network
       properties:
           ip_version: 4
-          cidr: 10.168.1.0/24
       requirements:
           - network_template:
               node: Private
@@ -33,6 +39,9 @@
           - connection:
               node: mysite_exampleservice
               relationship: tosca.relationships.ConnectsToSlice
+          - vrouter_tenant:
+              node: service_vrouter
+              relationship: tosca.relationships.TenantOfService
 
     mysite:
       type: tosca.nodes.Site
diff --git a/xos/configurations/frontend/Makefile b/xos/configurations/frontend/Makefile
index 2b46441..ee2739c 100644
--- a/xos/configurations/frontend/Makefile
+++ b/xos/configurations/frontend/Makefile
@@ -32,3 +32,11 @@
 	sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/frontend/mocks/cord.yaml
 	sudo docker exec frontend_xos_1 cp /opt/xos/configurations/cord/xos_cord_config /opt/xos/xos_configuration/
 	sudo docker exec frontend_xos_1 touch /opt/xos/xos/settings.py
+
+mock-cord-pod:
+	echo "make sure to add '../vtn/files/xos_vtn_config:/opt/xos/xos_configuration/xos_vtn_config:ro' to volumes section of docker-compose.yml"
+	sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/fixtures.yaml
+	sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/cord-pod/mgmt-net.yaml
+	sudo docker-compose run xos bash -c "echo somekey > /opt/xos/synchronizers/vcpe/vcpe_public_key; python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/cord-pod/cord-vtn-vsg.yaml"
+	sudo docker exec frontend_xos_1 cp /opt/xos/configurations/cord/xos_cord_config /opt/xos/xos_configuration/
+	sudo docker exec frontend_xos_1 touch /opt/xos/xos/settings.py
diff --git a/xos/core/admin.py b/xos/core/admin.py
index 7a88d0e..ee6c6bf 100644
--- a/xos/core/admin.py
+++ b/xos/core/admin.py
@@ -2109,6 +2109,39 @@
 
         return tabs
 
+class AddressPoolForm(forms.ModelForm):
+    class Meta:
+        model = Program
+        widgets = {
+            'addresses': UploadTextareaWidget(attrs={'rows': 20, 'cols': 80, 'class': "input-xxlarge"}),
+        }
+
+class AddressPoolAdmin(XOSBaseAdmin):
+    list_display = ("name", "cidr")
+    list_display_links = ('name',)
+
+    form=AddressPoolForm
+
+    fieldsets = [
+        (None, {'fields': ['name', 'cidr', 'gateway_ip', 'gateway_mac', 'addresses', 'inuse', 'service'],
+                'classes':['suit-tab suit-tab-general']}),
+                ]
+
+    readonly_fields = ("status",)
+
+    @property
+    def suit_form_tabs(self):
+        tabs=[('general','Program Details'),
+              ('contents','Program Source'),
+              ('messages','Messages'),
+        ]
+
+#        request=getattr(_thread_locals, "request", None)
+#        if request and request.user.is_admin:
+#            tabs.append( ('admin-only', 'Admin-Only') )
+
+        return tabs
+
 # Now register the new UserAdmin...
 admin.site.register(User, UserAdmin)
 # ... and, since we're not using Django's builtin permissions,
@@ -2151,5 +2184,5 @@
     admin.site.register(TenantRoot, TenantRootAdmin)
     admin.site.register(TenantRootRole, TenantRootRoleAdmin)
     admin.site.register(TenantAttribute, TenantAttributeAdmin)
-#    admin.site.register(Container, ContainerAdmin)
+    admin.site.register(AddressPool, AddressPoolAdmin)
 
diff --git a/xos/core/models/network.py b/xos/core/models/network.py
index 6af72bf..8373814 100644
--- a/xos/core/models/network.py
+++ b/xos/core/models/network.py
@@ -2,7 +2,7 @@
 import socket
 import sys
 from django.db import models, transaction
-from core.models import PlCoreBase, Site, Slice, Instance, Controller
+from core.models import PlCoreBase, Site, Slice, Instance, Controller, Service
 from core.models import ControllerLinkManager,ControllerLinkDeletionManager
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
@@ -350,7 +350,11 @@
 class AddressPool(PlCoreBase):
     name = models.CharField(max_length=32)
     addresses = models.TextField(blank=True, null=True)
+    gateway_ip = models.CharField(max_length=32, null=True)
+    gateway_mac = models.CharField(max_length=32, null=True)
+    cidr = models.CharField(max_length=32, null=True)
     inuse = models.TextField(blank=True, null=True)
+    service = models.ForeignKey(Service, related_name="addresspools", null=True, blank=True)
 
     def __unicode__(self): return u'%s' % (self.name)
 
diff --git a/xos/core/models/service.py b/xos/core/models/service.py
index 96bff36..920bc3b 100644
--- a/xos/core/models/service.py
+++ b/xos/core/models/service.py
@@ -368,10 +368,12 @@
 
     # The next four things are the various type of objects that can be subscribers of this Tenancy
     # relationship. One and only one can be used at a time.
+    # XXX these should really be changed to GenericForeignKey
     subscriber_service = models.ForeignKey(Service, related_name='subscribed_tenants', blank=True, null=True)
     subscriber_tenant = models.ForeignKey("Tenant", related_name='subscribed_tenants', blank=True, null=True)
     subscriber_user = models.ForeignKey("User", related_name='subscribed_tenants', blank=True, null=True)
     subscriber_root = models.ForeignKey("TenantRoot", related_name="subscribed_tenants", blank=True, null=True)
+    subscriber_network = models.ForeignKey("Network", related_name="subscribed_tenants", blank=True, null=True)
 
     # Service_specific_attribute and service_specific_id are opaque to XOS
     service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
diff --git a/xos/services/cord/models.py b/xos/services/cord/models.py
index 07c169d..0da4809 100644
--- a/xos/services/cord/models.py
+++ b/xos/services/cord/models.py
@@ -8,6 +8,7 @@
 from operator import itemgetter, attrgetter, methodcaller
 from core.models import Tag
 from core.models.service import LeastLoadedNodeScheduler
+from services.vrouter.models import VRouterService, VRouterTenant
 import traceback
 from xos.exceptions import *
 from xos.config import Config
@@ -448,6 +449,7 @@
     def __init__(self, *args, **kwargs):
         super(VSGTenant, self).__init__(*args, **kwargs)
         self.cached_vbng=None
+        self.cached_vrouter=None
 
     @property
     def vbng(self):
@@ -468,6 +470,24 @@
         raise XOSConfigurationError("vCPE.vBNG cannot be set this way -- create a new vBNG object and set it's subscriber_tenant instead")
 
     @property
+    def vrouter(self):
+        vrouter = self.get_newest_subscribed_tenant(VRouterTenant)
+        if not vrouter:
+            return None
+
+        # always return the same object when possible
+        if (self.cached_vrouter) and (self.cached_vrouter.id == vrouter.id):
+            return self.cached_vrouter
+
+        vrouter.caller = self.creator
+        self.cached_vrouter = vrouter
+        return vrouter
+
+    @vrouter.setter
+    def vrouter(self, value):
+        raise XOSConfigurationError("vCPE.vRouter cannot be set this way -- create a new vRuter object and set its subscriber_tenant instead")
+
+    @property
     def volt(self):
         if not self.subscriber_tenant:
             return None
@@ -559,21 +579,20 @@
 
     @property
     def wan_container_ip(self):
-        if CORD_USE_VTN:
-            # When using VTN, wan_container_ip is stored and maintained inside
-            # of the vSG object.
-            return self.get_attribute("wan_container_ip", self.default_attributes["wan_container_ip"])
+        if self.vrouter:
+            return self.vrouter.public_ip
         else:
-            # When not using VTN, wan_container_ip is the same as wan_ip.
-            # XXX Is this broken for multiple-containers-per-VM?
-            return self.wan_ip
+            if  (CORD_USE_VTN):
+                # Should this be an error?
+                return None
+            else:
+                # When not using VTN, wan_container_ip is the same as wan_ip.
+                # XXX Is this broken for multiple-containers-per-VM?
+                return self.wan_ip
 
     @wan_container_ip.setter
     def wan_container_ip(self, value):
-        if CORD_USE_VTN:
-            self.set_attribute("wan_container_ip", value)
-        else:
-            raise Exception("wan_container_ip.setter called on non-VTN CORD")
+        raise Exception("wan_container_ip is not settable")
 
     def ip_to_mac(self, ip):
         (a, b, c, d) = ip.split('.')
@@ -582,9 +601,38 @@
     # Generate the MAC for the container interface connected to WAN
     @property
     def wan_container_mac(self):
-        if not self.wan_container_ip:
-            return None
-        return self.ip_to_mac(self.wan_container_ip)
+        if self.vrouter:
+            return self.vrouter.public_mac
+        else:
+            if (CORD_USE_VTN):
+                # Should this be an error?
+                return None
+            else:
+                return self.ip_to_mac(self.wan_container_ip)
+
+    @property
+    def wan_vm_ip(self):
+        tags = Tag.select_by_content_object(self.instance).filter(name="vm_vrouter_tenant")
+        if tags:
+            tenant = VRouterTenant.objects.get(id=tags[0].value)
+            return tenant.public_ip
+        else:
+            if CORD_USE_VTN:
+                raise Exception("no vm_vrouter_tenant tag for instance %s" % o.instance)
+            else:
+                return ""
+
+    @property
+    def wan_vm_mac(self):
+        tags = Tag.select_by_content_object(self.instance).filter(name="vm_vrouter_tenant")
+        if tags:
+            tenant = VRouterTenant.objects.get(id=tags[0].value)
+            return tenant.public_mac
+        else:
+            if CORD_USE_VTN:
+                raise Exception("no vm_vrouter_tenant tag for instance %s" % o.instance)
+            else:
+                return ""
 
     @property
     def private_ip(self):
@@ -631,6 +679,28 @@
             # print "XXX cleanup vnbg", self.vbng
             self.vbng.delete()
 
+    def get_vrouter_service(self):
+        vrouterServices = VRouterService.get_service_objects().all()
+        if not vrouterServices:
+            raise XOSConfigurationError("No VROUTER Services available")
+        return vrouterServices[0]
+
+    def manage_vrouter(self):
+        # Each vCPE object owns exactly one vRouterTenant object
+
+        if self.deleted:
+            return
+
+        if self.vrouter is None:
+            vrouter = self.get_vrouter_service().get_tenant(address_pool_name="addresses_vsg", subscriber_tenant = self)
+            vrouter.caller = self.creator
+            vrouter.save()
+
+    def cleanup_vrouter(self):
+        if self.vrouter:
+            # print "XXX cleanup vrouter", self.vrouter
+            self.vrouter.delete()
+
     def cleanup_orphans(self):
         # ensure vCPE only has one vBNG
         cur_vbng = self.vbng
@@ -639,6 +709,13 @@
                 # print "XXX clean up orphaned vbng", vbng
                 vbng.delete()
 
+        # ensure vCPE only has one vRouter
+        cur_vrouter = self.vrouter
+        for vrouter in list(self.get_subscribed_tenants(VRouterTenant)):
+            if (not cur_vrouter) or (vrouter.id != cur_vrouter.id):
+                # print "XXX clean up orphaned vrouter", vrouter
+                vrouter.delete()
+
         if self.orig_instance_id and (self.orig_instance_id != self.get_attribute("instance_id")):
             instances=Instance.objects.filter(id=self.orig_instance_id)
             if instances:
@@ -741,33 +818,6 @@
                 self.bbs_account = None
                 super(VSGTenant, self).save()
 
-    def get_wan_address_from_pool(self):
-        ap = AddressPool.objects.filter(name="public_addresses")
-        if not ap:
-            raise Exception("AddressPool 'public_addresses' does not exist. Please configure it.")
-        ap = ap[0]
-
-        addr = ap.get_address()
-        if not addr:
-            raise Exception("AddressPool 'public_addresses' has run out of addresses.")
-        return addr
-
-    def put_wan_address_to_pool(self, addr):
-        AddressPool.objects.filter(name="public_addresses")[0].put_address(addr)
-
-    def manage_wan_container_ip(self):
-        if CORD_USE_VTN:
-            if not self.wan_container_ip:
-                addr = self.get_wan_address_from_pool()
-
-                self.wan_container_ip = addr
-                super(TenantWithContainer, self).save()
-
-    def cleanup_wan_container_ip(self):
-        if CORD_USE_VTN and self.wan_container_ip:
-            self.put_wan_address_to_pool(self.wan_container_ip)
-            self.wan_container_ip = None
-
     def find_or_make_port(self, instance, network, **kwargs):
         port = Port.objects.filter(instance=instance, network=network)
         if port:
@@ -829,10 +879,12 @@
             # VTN-CORD needs a WAN address for the VM, so that the VM can
             # be configured.
             if CORD_USE_VTN:
-                tags = Tag.select_by_content_object(instance).filter(name="vm_wan_addr")
+                tags = Tag.select_by_content_object(instance).filter(name="vm_vrouter_tenant")
                 if not tags:
-                    address = self.get_wan_address_from_pool()
-                    tag = Tag(service=self.provider_service, content_object=instance, name="vm_wan_addr", value="%s,%s,%s" % ("public_addresses", address, self.ip_to_mac(address)))
+                    vrouter = self.get_vrouter_service().get_tenant(address_pool_name="addresses_vsg", subscriber_service = self.provider_service)
+                    vrouter.set_attribute("tenant_for_instance_id", instance.id)
+                    vrouter.save()
+                    tag = Tag(service=self.provider_service, content_object=instance, name="vm_vrouter_tenant", value="%d" % vrouter.id)
                     tag.save()
 
     def save(self, *args, **kwargs):
@@ -849,8 +901,8 @@
 
     def delete(self, *args, **kwargs):
         self.cleanup_vbng()
+        self.cleanup_vrouter()
         self.cleanup_container()
-        self.cleanup_wan_container_ip()
         super(VSGTenant, self).delete(*args, **kwargs)
 
 def model_policy_vcpe(pk):
@@ -860,9 +912,9 @@
         if not vcpe:
             return
         vcpe = vcpe[0]
-        vcpe.manage_wan_container_ip()
         vcpe.manage_container()
-        vcpe.manage_vbng()
+        vcpe.manage_vrouter()
+        #vcpe.manage_vbng()
         vcpe.manage_bbs_account()
         vcpe.cleanup_orphans()
 
diff --git a/xos/services/vrouter/__init__.py b/xos/services/vrouter/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/xos/services/vrouter/__init__.py
@@ -0,0 +1 @@
+
diff --git a/xos/services/vrouter/admin.py b/xos/services/vrouter/admin.py
new file mode 100644
index 0000000..318b3dc
--- /dev/null
+++ b/xos/services/vrouter/admin.py
@@ -0,0 +1,113 @@
+from django.contrib import admin
+
+from services.vrouter.models import *
+from django import forms
+from django.utils.safestring import mark_safe
+from django.contrib.auth.admin import UserAdmin
+from django.contrib.admin.widgets import FilteredSelectMultiple
+from django.contrib.auth.forms import ReadOnlyPasswordHashField
+from django.contrib.auth.signals import user_logged_in
+from django.utils import timezone
+from django.contrib.contenttypes import generic
+from suit.widgets import LinkedSelect
+from core.models import AddressPool
+from core.admin import ServiceAppAdmin,SliceInline,ServiceAttrAsTabInline, ReadOnlyAwareAdmin, XOSTabularInline, ServicePrivilegeInline, TenantRootTenantInline, TenantRootPrivilegeInline
+from core.middleware import get_request
+
+from functools import update_wrapper
+from django.contrib.admin.views.main import ChangeList
+from django.core.urlresolvers import reverse
+from django.contrib.admin.utils import quote
+
+class VRouterServiceForm(forms.ModelForm):
+    def __init__(self,*args,**kwargs):
+        super (VRouterServiceForm,self ).__init__(*args,**kwargs)
+
+    def save(self, commit=True):
+        return super(VRouterServiceForm, self).save(commit=commit)
+
+    class Meta:
+        model = VRouterService
+
+class VRouterServiceAdmin(ReadOnlyAwareAdmin):
+    model = VRouterService
+    verbose_name = "vRouter Service"
+    verbose_name_plural = "vRouter Service"
+    list_display = ("backend_status_icon", "name", "enabled")
+    list_display_links = ('backend_status_icon', 'name', )
+    fieldsets = [(None, {'fields': ['backend_status_text', 'name','enabled','versionNumber', 'description', "view_url", "icon_url", ],
+                         'classes':['suit-tab suit-tab-general']})]
+    readonly_fields = ('backend_status_text', )
+    inlines = [SliceInline,ServiceAttrAsTabInline,ServicePrivilegeInline]
+    form = VRouterServiceForm
+
+    extracontext_registered_admins = True
+
+    user_readonly_fields = ["name", "enabled", "versionNumber", "description"]
+
+    suit_form_tabs =(('general', 'vRouter Service Details'),
+        ('administration', 'Administration'),
+        #('tools', 'Tools'),
+        ('slices','Slices'),
+        ('serviceattrs','Additional Attributes'),
+        ('serviceprivileges','Privileges'),
+    )
+
+    suit_form_includes = (('vrouteradmin.html', 'top', 'administration'),
+                           ) #('hpctools.html', 'top', 'tools') )
+
+    def queryset(self, request):
+        return VRouterService.get_service_objects_by_user(request.user)
+
+class VRouterTenantForm(forms.ModelForm):
+    public_ip = forms.CharField(required=True)
+    public_mac = forms.CharField(required=True)
+    gateway_ip = forms.CharField(required=False)
+    gateway_mac = forms.CharField(required=False)
+    cidr = forms.CharField(required=False)
+    address_pool = forms.ModelChoiceField(queryset=AddressPool.objects.all(),required=False)
+
+    def __init__(self,*args,**kwargs):
+        super (VRouterTenantForm,self ).__init__(*args,**kwargs)
+        self.fields['kind'].widget.attrs['readonly'] = True
+        self.fields['provider_service'].queryset = VRouterService.get_service_objects().all()
+        if self.instance:
+            # fields for the attributes
+            self.fields['address_pool'].initial = self.instance.address_pool
+            self.fields['public_ip'].initial = self.instance.public_ip
+            self.fields['public_mac'].initial = self.instance.public_mac
+            self.fields['gateway_ip'].initial = self.instance.gateway_ip
+            self.fields['gateway_mac'].initial = self.instance.gateway_mac
+            self.fields['cidr'].initial = self.instance.cidr
+        if (not self.instance) or (not self.instance.pk):
+            # default fields for an 'add' form
+            self.fields['kind'].initial = VROUTER_KIND
+            if VRouterService.get_service_objects().exists():
+               self.fields["provider_service"].initial = VRouterService.get_service_objects().all()[0]
+
+    def save(self, commit=True):
+        self.instance.public_ip = self.cleaned_data.get("public_ip")
+        self.instance.public_mac = self.cleaned_data.get("public_mac")
+        self.instance.address_pool = self.cleaned_data.get("address_pool")
+        return super(VRouterTenantForm, self).save(commit=commit)
+
+    class Meta:
+        model = VRouterTenant
+
+class VRouterTenantAdmin(ReadOnlyAwareAdmin):
+    list_display = ('backend_status_icon', 'id', 'subscriber_tenant', 'public_ip' )
+    list_display_links = ('backend_status_icon', 'id')
+    fieldsets = [ (None, {'fields': ['backend_status_text', 'kind', 'provider_service', 'subscriber_tenant', 'subscriber_service',
+                                     'address_pool', 'public_ip', 'public_mac', 'gateway_ip', 'gateway_mac', 'cidr'],
+                          'classes':['suit-tab suit-tab-general']})]
+    readonly_fields = ('backend_status_text', 'service_specific_attribute', 'gateway_ip', 'gateway_mac', 'cidr')
+    form = VRouterTenantForm
+
+    suit_form_tabs = (('general','Details'),)
+
+    def queryset(self, request):
+        return VRouterTenant.get_tenant_objects_by_user(request.user)
+
+admin.site.register(VRouterService, VRouterServiceAdmin)
+admin.site.register(VRouterTenant, VRouterTenantAdmin)
+
diff --git a/xos/services/vrouter/models.py b/xos/services/vrouter/models.py
new file mode 100644
index 0000000..5dba838
--- /dev/null
+++ b/xos/services/vrouter/models.py
@@ -0,0 +1,130 @@
+from django.db import models
+from core.models import Service, PlCoreBase, Slice, Instance, Tenant, TenantWithContainer, Node, Image, User, Flavor, Subscriber, NetworkParameter, NetworkParameterType, Port, AddressPool
+from core.models.plcorebase import StrippedCharField
+import os
+from django.db import models, transaction
+from django.forms.models import model_to_dict
+from django.db.models import Q
+from operator import itemgetter, attrgetter, methodcaller
+from core.models import Tag
+from core.models.service import LeastLoadedNodeScheduler
+import traceback
+from xos.exceptions import *
+from xos.config import Config
+
+class ConfigurationError(Exception):
+    pass
+
+VROUTER_KIND = "vROUTER"
+
+CORD_USE_VTN = getattr(Config(), "networking_use_vtn", False)
+
+class VRouterService(Service):
+    KIND = VROUTER_KIND
+
+    class Meta:
+        app_label = "vrouter"
+        verbose_name = "vRouter Service"
+        proxy = True
+
+    def ip_to_mac(self, ip):
+        (a, b, c, d) = ip.split('.')
+        return "02:42:%02x:%02x:%02x:%02x" % (int(a), int(b), int(c), int(d))
+
+    def get_gateways(self):
+        gateways=[]
+
+        aps = self.addresspools.all()
+        for ap in aps:
+            gateways.append( {"gateway_ip": ap.gateway_ip, "gateway_mac": ap.gateway_mac} )
+
+        return gateways
+
+    def get_address_pool(self, name):
+        ap = AddressPool.objects.filter(name=name, service=self)
+        if not ap:
+            raise Exception("vRouter unable to find addresspool %s" % name)
+        return ap[0]
+
+    def get_tenant(self, **kwargs):
+        address_pool_name = kwargs.pop("address_pool_name")
+
+        ap = self.get_address_pool(address_pool_name)
+
+        ip = ap.get_address()
+        if not ip:
+            raise Exception("AddressPool '%s' has run out of addresses." % ap.name)
+
+        t = VRouterTenant(provider_service=self, **kwargs)
+        t.public_ip = ip
+        t.public_mac = self.ip_to_mac(ip)
+        t.address_pool_id = ap.id
+        t.save()
+
+        return t
+
+#VRouterService.setup_simple_attributes()
+
+class VRouterTenant(Tenant):
+    class Meta:
+        proxy = True
+
+    KIND = VROUTER_KIND
+
+    simple_attributes = ( ("public_ip", None),
+                          ("public_mac", None),
+                          ("address_pool_id", None),
+                          )
+
+    @property
+    def gateway_ip(self):
+        if not self.address_pool:
+            return None
+        return self.address_pool.gateway_ip
+
+    @property
+    def gateway_mac(self):
+        if not self.address_pool:
+            return None
+        return self.address_pool.gateway_mac
+
+    @property
+    def cidr(self):
+        if not self.address_pool:
+            return None
+        return self.address_pool.cidr
+
+    @property
+    def address_pool(self):
+        if getattr(self, "cached_address_pool", None):
+            return self.cached_address_pool
+        if not self.address_pool_id:
+            return None
+        aps=AddressPool.objects.filter(id=self.address_pool_id)
+        if not aps:
+            return None
+        ap=aps[0]
+        self.cached_address_pool = ap
+        return ap
+
+    @address_pool.setter
+    def address_pool(self, value):
+        if value:
+            value = value.id
+        if (value != self.get_attribute("address_pool_id", None)):
+            self.cached_address_pool=None
+        self.set_attribute("address_pool_id", value)
+
+    def cleanup_addresspool(self):
+        if self.address_pool_id:
+            ap = AddressPool.objects.filter(id=self.address_pool_id)
+            if ap:
+                ap[0].put_address(self.public_ip)
+                self.public_ip = None
+
+    def delete(self, *args, **kwargs):
+        self.cleanup_addresspool()
+        super(VRouterTenant, self).delete(*args, **kwargs)
+
+VRouterTenant.setup_simple_attributes()
+
diff --git a/xos/synchronizers/vcpe/steps/sync_vcpetenant.py b/xos/synchronizers/vcpe/steps/sync_vcpetenant.py
index d52f075..c28b3c1 100644
--- a/xos/synchronizers/vcpe/steps/sync_vcpetenant.py
+++ b/xos/synchronizers/vcpe/steps/sync_vcpetenant.py
@@ -153,18 +153,8 @@
                         if mac:
                             safe_macs.append(mac)
 
-        wan_vm_ip=""
-        wan_vm_mac=""
-        tags = Tag.select_by_content_object(o.instance).filter(name="vm_wan_addr")
-        if tags:
-            parts=tags[0].value.split(",")
-            if len(parts)!=3:
-                raise Exception("vm_wan_addr tag is malformed: %s" % value)
-            wan_vm_ip = parts[1]
-            wan_vm_mac = parts[2]
-        else:
-            if CORD_USE_VTN:
-                raise Exception("no vm_wan_addr tag for instance %s" % o.instance)
+        wan_vm_ip=o.wan_vm_ip
+        wan_vm_mac=o.wan_vm_mac
 
         fields = {"vlan_ids": vlan_ids,   # XXX remove this
                 "s_tags": s_tags,
diff --git a/xos/synchronizers/vtn/steps/sync_port_addresses.py b/xos/synchronizers/vtn/steps/sync_port_addresses.py
index 6b48911..bbc6c99 100644
--- a/xos/synchronizers/vtn/steps/sync_port_addresses.py
+++ b/xos/synchronizers/vtn/steps/sync_port_addresses.py
@@ -70,14 +70,11 @@
 
             # now do the VM_WAN_IP from the instance
             if vsg.instance:
-                tags=Tag.select_by_content_object(vsg.instance).filter(name="vm_wan_addr")
-                if tags:
-                    parts=tags[0].value.split(",")
-                    if len(parts)!=3:
-                        raise Exception("vm_wan_addr tag is malformed: %s" % value)
-                    entry = {"mac_address": parts[2], "ip_address": parts[1]}
-                    if not entry in addr_pairs:
-                        addr_pairs.append(entry)
+                wan_vm_ip = vsg.wan_vm_ip
+                wan_vm_mac = vsg.wan_vm_mac
+                entry = {"mac_address": wan_vm_mac, "ip_address": wan_vm_ip}
+                if not entry in addr_pairs:
+                    addr_pairs.append(entry)
 
         # Get all ports in all controllers
         ports_by_id = {}
diff --git a/xos/tools/xos-manage b/xos/tools/xos-manage
index 5410f37..23cda72 100755
--- a/xos/tools/xos-manage
+++ b/xos/tools/xos-manage
@@ -147,6 +147,7 @@
     python ./manage.py makemigrations helloworldservice_complete
     python ./manage.py makemigrations onos
     python ./manage.py makemigrations vtr
+    python ./manage.py makemigrations vrouter
     #python ./manage.py makemigrations servcomp
 }
 
diff --git a/xos/tosca/custom_types/xos.m4 b/xos/tosca/custom_types/xos.m4
index 9940f7b..fd4f8ec 100644
--- a/xos/tosca/custom_types/xos.m4
+++ b/xos/tosca/custom_types/xos.m4
@@ -251,6 +251,16 @@
                 required: false
                 description: URL of REST API endpoint for vBNG Service.
 
+    tosca.nodes.VRouterService:
+        derived_from: tosca.nodes.Root
+        description: >
+            CORD: The vRouter Service.
+        capabilities:
+            xos_base_service_caps
+        properties:
+            xos_base_props
+            xos_base_service_props
+
     tosca.nodes.CDNService:
         derived_from: tosca.nodes.Root
         description: >
@@ -556,12 +566,23 @@
         derived_from: tosca.nodes.Root
         description: >
             A pool of addresses
+        capabilities:
+            addresspool:
+                type: tosca.capabilities.xos.AddressPool
         properties:
             xos_base_props
             addresses:
                 type: string
                 required: false
                 description: space-separated list of addresses
+            gateway_ip:
+                type: string
+                required: false
+                description: gateway ip address
+            gateway_mac:
+                type: string
+                required: false
+                description: gateway mac address
 
     tosca.nodes.Image:
         derived_from: tosca.nodes.Root
@@ -880,6 +901,10 @@
         derived_from: tosca.relationships.Root
         valid_target_types: [ tosca.capabilities.xos.NodeLabel ]
 
+    tosca.relationships.ProvidesAddresses:
+        derived_from: tosca.relationships.Root
+        valid_target_types: [ tosca.capabilities.xos.AddressPool ]
+
     tosca.relationships.DependsOn:
         derived_from: tosca.relationships.Root
 
@@ -942,3 +967,7 @@
     tosca.capabilities.xos.NetworkParameterType:
         derived_from: tosca.capabilities.Root
         description: An XOS NetworkParameterType
+
+    tosca.capabilities.xos.AddressPool:
+        derived_from: tosca.capabilities.Root
+        description: An XOS AddressPool
diff --git a/xos/tosca/custom_types/xos.yaml b/xos/tosca/custom_types/xos.yaml
index cd36528..9bdcfc3 100644
--- a/xos/tosca/custom_types/xos.yaml
+++ b/xos/tosca/custom_types/xos.yaml
@@ -397,6 +397,60 @@
                 required: false
                 description: URL of REST API endpoint for vBNG Service.
 
+    tosca.nodes.VRouterService:
+        derived_from: tosca.nodes.Root
+        description: >
+            CORD: The vRouter Service.
+        capabilities:
+            scalable:
+                type: tosca.capabilities.Scalable
+            service:
+                type: tosca.capabilities.xos.Service
+        properties:
+            no-delete:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to delete this object
+            no-create:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to create this object
+            no-update:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to update this object
+            kind:
+                type: string
+                default: generic
+                description: Type of service.
+            view_url:
+                type: string
+                required: false
+                description: URL to follow when icon is clicked in the Service Directory.
+            icon_url:
+                type: string
+                required: false
+                description: ICON to display in the Service Directory.
+            enabled:
+                type: boolean
+                default: true
+            published:
+                type: boolean
+                default: true
+                description: If True then display this Service in the Service Directory.
+            public_key:
+                type: string
+                required: false
+                description: Public key to install into Instances to allows Services to SSH into them.
+            private_key_fn:
+                type: string
+                required: false
+                description: Location of private key file
+            versionNumber:
+                type: string
+                required: false
+                description: Version number of Service.
+
     tosca.nodes.CDNService:
         derived_from: tosca.nodes.Root
         description: >
@@ -835,6 +889,9 @@
         derived_from: tosca.nodes.Root
         description: >
             A pool of addresses
+        capabilities:
+            addresspool:
+                type: tosca.capabilities.xos.AddressPool
         properties:
             no-delete:
                 type: boolean
@@ -852,6 +909,14 @@
                 type: string
                 required: false
                 description: space-separated list of addresses
+            gateway_ip:
+                type: string
+                required: false
+                description: gateway ip address
+            gateway_mac:
+                type: string
+                required: false
+                description: gateway mac address
 
     tosca.nodes.Image:
         derived_from: tosca.nodes.Root
@@ -1236,6 +1301,10 @@
         derived_from: tosca.relationships.Root
         valid_target_types: [ tosca.capabilities.xos.NodeLabel ]
 
+    tosca.relationships.ProvidesAddresses:
+        derived_from: tosca.relationships.Root
+        valid_target_types: [ tosca.capabilities.xos.AddressPool ]
+
     tosca.relationships.DependsOn:
         derived_from: tosca.relationships.Root
 
@@ -1298,3 +1367,7 @@
     tosca.capabilities.xos.NetworkParameterType:
         derived_from: tosca.capabilities.Root
         description: An XOS NetworkParameterType
+
+    tosca.capabilities.xos.AddressPool:
+        derived_from: tosca.capabilities.Root
+        description: An XOS AddressPool
diff --git a/xos/tosca/resources/addresspool.py b/xos/tosca/resources/addresspool.py
index e8577a2..8cd3e83 100644
--- a/xos/tosca/resources/addresspool.py
+++ b/xos/tosca/resources/addresspool.py
@@ -14,7 +14,7 @@
 class XOSAddressPool(XOSResource):
     provides = "tosca.nodes.AddressPool"
     xos_model = AddressPool
-    copyin_props = ["addresses"]
+    copyin_props = ["addresses", "gateway_ip", "gateway_mac"]
 
     def expand_cidr(self, cidr):
         (network, bits) = cidr.split("/")
@@ -31,20 +31,31 @@
             ip = ip & netmask | i
             dest.append( socket.inet_ntoa(struct.pack("!L", ip)) )
 
-        return dest
+        return (dest, bits)
 
     def get_xos_args(self):
         args = super(XOSAddressPool, self).get_xos_args()
 
         if "addresses" in args:
-            dest = []
-            for addr in args["addresses"].split():
-                addr=addr.strip()
-                if "/" in addr:
-                    dest.extend(self.expand_cidr(addr))
-                else:
-                    dest.append(addr)
-            args["addresses"] = " ".join(dest)
+            addr = args["addresses"]
+            if "," in addr:
+                raise Exception("Only one cidr per AddressPool")
+            if not "/" in addr:
+                raise Exception("AddressPool addresses must be a cidr")
+            (cidr_addrs, cidr_netbits) = self.expand_cidr(addr)
+            args["addresses"] = " ".join(cidr_addrs)
+            args["cidr"] = addr
+
+#        if "addresses" in args:
+#            dest = []
+#            for addr in args["addresses"].split():
+#                addr=addr.strip()
+#                if "/" in addr:
+#                    (cidr_addrs, cidr_netbits) = self.expand_cidr(addr)
+#                    dest.extend(cidr_addrs)
+#                else:
+#                    dest.append(addr)
+#            args["addresses"] = " ".join(dest)
 
         return args
 
diff --git a/xos/tosca/resources/network.py b/xos/tosca/resources/network.py
index 7b513c3..06a366c 100644
--- a/xos/tosca/resources/network.py
+++ b/xos/tosca/resources/network.py
@@ -6,7 +6,7 @@
 from translator.toscalib.tosca_template import ToscaTemplate
 import pdb
 
-from core.models import Slice,User,Network,NetworkTemplate,NetworkSlice
+from core.models import Slice,User,Network,NetworkTemplate,NetworkSlice,Service,Tenant
 
 from xosresource import XOSResource
 
@@ -35,7 +35,7 @@
                 if v:
                     args[prop] = v
         else:
-            # tosca.nodes.network.Netwrok is not as rich as an XOS network. So
+            # tosca.nodes.network.Network is not as rich as an XOS network. So
             # we have to manually fill in some defaults.
             args["permit_all_slices"] = True
 
@@ -54,6 +54,24 @@
                 ns = NetworkSlice(network = obj, slice=slice)
                 ns.save()
 
+        # this is really for vRouter
+        for provider_service_name in self.get_requirements("tosca.relationships.TenantOfService"):
+            provider_service = self.get_xos_object(Service, name=provider_service_name)
+
+            existing_tenancy = Tenant.objects.filter(provider_service = provider_service, subscriber_network = obj)
+            if existing_tenancy:
+                self.info("Tenancy relationship from %s to %s already exists" % (str(obj), str(provider_service)))
+            else:
+                from services.vrouter.models import VROUTER_KIND, VRouterService
+                if provider_service.kind == VROUTER_KIND:
+                    tenancy = VRouterService.objects.get(id=provider_service.id).get_tenant(address_pool_name="addresses_"+obj.name, subscriber_network=obj)
+                    tenancy.save()
+                    obj.subnet = tenancy.cidr
+                else:
+                    raise Exception("The only network tenancy relationships that are allowed are to vRouter services")
+
+                self.info("Created Tenancy relationship from %s to %s" % (str(obj), str(provider_service)))
+
 #        v = self.get_property("permitted_slices")
 #        if v:
 #            for slicename in v.split(","):
@@ -74,10 +92,14 @@
 
         network = Network(**xos_args)
         network.caller = self.user
+        network.no_sync = True        # postprocess might set the cidr
         network.save()
 
         self.postprocess(network)
 
+        network.no_sync = False
+        network.save()
+
         self.info("Created Network '%s' owned by Slice '%s'" % (str(network), str(network.owner)))
 
     def delete(self, obj):
diff --git a/xos/tosca/resources/service.py b/xos/tosca/resources/service.py
index 247be08..5a57418 100644
--- a/xos/tosca/resources/service.py
+++ b/xos/tosca/resources/service.py
@@ -6,7 +6,7 @@
 from translator.toscalib.tosca_template import ToscaTemplate
 import pdb
 
-from core.models import Service,User,CoarseTenant
+from core.models import Service,User,CoarseTenant,AddressPool
 
 from xosresource import XOSResource
 
@@ -29,6 +29,11 @@
 
                 self.info("Created Tenancy relationship  from %s to %s" % (str(obj), str(provider_service)))
 
+        for ap_name in self.get_requirements("tosca.relationships.ProvidesAddresses"):
+            ap = self.get_xos_object(AddressPool, name=ap_name)
+            ap.service = obj
+            ap.save()
+
     def can_delete(self, obj):
         if obj.slices.exists():
             self.info("Service %s has active slices; skipping delete" % obj.name)
diff --git a/xos/tosca/resources/vrouterservice.py b/xos/tosca/resources/vrouterservice.py
new file mode 100644
index 0000000..14dabcc
--- /dev/null
+++ b/xos/tosca/resources/vrouterservice.py
@@ -0,0 +1,16 @@
+import os
+import pdb
+import sys
+import tempfile
+sys.path.append("/opt/tosca")
+from translator.toscalib.tosca_template import ToscaTemplate
+
+from services.vrouter.models import VRouterService
+
+from service import XOSService
+
+class XOSVRouterService(XOSService):
+    provides = "tosca.nodes.VRouterService"
+    xos_model = VRouterService
+    copyin_props = ["view_url", "icon_url", "enabled", "published", "public_key", "versionNumber"]
+
diff --git a/xos/xos/settings.py b/xos/xos/settings.py
index aa50e7d..f457476 100644
--- a/xos/xos/settings.py
+++ b/xos/xos/settings.py
@@ -181,6 +181,7 @@
     'services.requestrouter',
     'services.syndicate_storage',
     'services.vtr',
+    'services.vrouter',
     'geoposition',
     'rest_framework_swagger',
 )