Merge branch 'fixes'
diff --git a/xos/cord/__init__.py b/xos/cord/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/xos/cord/__init__.py
@@ -0,0 +1 @@
+
diff --git a/xos/cord/models.py b/xos/cord/models.py
new file mode 100644
index 0000000..594eb31
--- /dev/null
+++ b/xos/cord/models.py
@@ -0,0 +1,248 @@
+from django.db import models
+from core.models import Service, PlCoreBase, Slice, Sliver, Tenant, Node, Image
+from core.models.plcorebase import StrippedCharField
+import os
+from django.db import models
+from django.forms.models import model_to_dict
+from django.db.models import Q
+from operator import itemgetter, attrgetter, methodcaller
+
+"""
+import os
+import sys
+sys.path.append("/opt/xos")
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+import django
+from core.models import *

+from hpc.models import *

+from cord.models import *

+django.setup()
+svc = VOLTService.get_service_objects().all()[0]
+
+t = VOLTTenant(provider_service=svc)
+t.caller = User.objects.all()[0]
+t.save()
+
+for v in VOLTTenant.objects.all():
+    v.caller = User.objects.all()[0]
+    v.delete()
+
+for v in VCPETenant.objects.all():
+    v.caller = User.objects.all()[0]
+    v.delete()
+"""
+
+class ConfigurationError(Exception):
+    pass
+
+# -------------------------------------------
+# VOLT
+# -------------------------------------------
+
+class VOLTService(Service):
+    KIND = "vOLT"
+
+    class Meta:
+        app_label = "cord"
+        verbose_name = "vOLT Service"
+        proxy = True
+
+class VOLTTenant(Tenant):
+    class Meta:
+        proxy = True
+
+    KIND = "vOLT"
+
+    @property
+    def vcpe(self):
+        vcpe_id=self.get_attribute("vcpe_id")
+        if not vcpe_id:
+            return None
+        vcpes=VCPETenant.objects.filter(id=vcpe_id)
+        if not vcpes:
+            return None
+        return vcpes[0]
+
+    @vcpe.setter
+    def vcpe(self, value):
+        if value:
+            self.set_attribute("vcpe_id", value.id)
+        else:
+            self.set_attribute("vcpe_id", None)
+
+    def manage_vcpe(self):
+        # Each VOLT object owns exactly one VCPE object
+
+        if self.deleted:
+            return
+
+        if self.vcpe is None:
+            vcpeServices = VCPEService.get_service_objects().all()
+            if not vcpeServices:
+                raise ConfigurationError("No VCPE Services available")
+
+            vcpe = VCPETenant(provider_service = vcpeServices[0],
+                              subscriber_tenant = self)
+            vcpe.caller = self.caller
+            vcpe.save()
+
+            try:
+                self.vcpe = vcpe
+                self.save()
+            except:
+                vcpe.delete()
+                raise
+
+    def cleanup_vcpe(self):
+        if self.vcpe:
+            self.vcpe.delete()
+            self.vcpe = None
+
+    def save(self, *args, **kwargs):
+        if not getattr(self, "caller", None):
+            raise TypeError("VOLTTenant's self.caller was not set")
+        super(VOLTTenant, self).save(*args, **kwargs)
+        self.manage_vcpe()
+
+    def delete(self, *args, **kwargs):
+        self.cleanup_vcpe()
+        super(VOLTTenant, self).delete(*args, **kwargs)
+
+# -------------------------------------------
+# VCPE
+# -------------------------------------------
+
+class VCPEService(Service):
+    KIND = "vCPE"
+
+    class Meta:
+        app_label = "cord"
+        verbose_name = "vCPE Service"
+        proxy = True
+
+class VCPETenant(Tenant):
+    class Meta:
+        proxy = True
+
+    KIND = "vCPE"
+
+    default_attributes = {"firewall_enable": False,
+                          "firewall_rules": "accept all anywhere anywhere",
+                          "url_filter_enable": False,
+                          "url_filter_rules": "allow all",
+                          "cdn_enable": False,
+                          "sliver_id": None}
+
+    @property
+    def image(self):
+        # TODO: logic to pick an image based on the feature set
+        #    just use Ubuntu 14.04 for now...
+        return Image.objects.get(name="Ubuntu 14.04 LTS")
+
+    @property
+    def sliver(self):
+        sliver_id=self.get_attribute("sliver_id")
+        if not sliver_id:
+            return None
+        slivers=Sliver.objects.filter(id=sliver_id)
+        if not slivers:
+            return None
+        return slivers[0]
+
+    @sliver.setter
+    def sliver(self, value):
+        if value:
+            self.set_attribute("sliver_id", value.id)
+        else:
+            self.set_attribute("sliver_id", None)
+
+    @property
+    def firewall_enable(self):
+        return self.get_attribute("firewall_enable", self.default_attributes["firewall_enable"])
+
+    @firewall_enable.setter
+    def firewall_enable(self, value):
+        self.set_attribute("firewall_enable", value)
+
+    @property
+    def firewall_rules(self):
+        return self.get_attribute("firewall_rules", self.default_attributes["firewall_rules"])
+
+    @firewall_rules.setter
+    def firewall_rules(self, value):
+        self.set_attribute("firewall_rules", value)
+
+    @property
+    def url_filter_enable(self):
+        return self.get_attribute("url_filter_enable", self.default_attributes["url_filter_enable"])
+
+    @url_filter_enable.setter
+    def url_filter_enable(self, value):
+        self.set_attribute("url_filter_enable", value)
+
+    @property
+    def url_filter_rules(self):
+        return self.get_attribute("url_filter_rules", self.default_attributes["url_filter_rules"])
+
+    @url_filter_rules.setter
+    def url_filter_rules(self, value):
+        self.set_attribute("url_filter_rules", value)
+
+    @property
+    def cdn_enable(self):
+        return self.get_attribute("cdn_enable", self.default_attributes["cdn_enable"])
+
+    @cdn_enable.setter
+    def cdn_enable(self, value):
+        self.set_attribute("cdn_enable", value)
+
+    def pick_node(self):
+        nodes = list(Node.objects.all())
+        # TODO: logic to filter nodes by which nodes are up, and which
+        #   nodes the slice can instantiate on.
+        nodes = sorted(nodes, key=lambda node: node.slivers.all().count())
+        return nodes[0]
+
+    def manage_sliver(self):
+        # Each VCPE object owns exactly one sliver.
+
+        if self.deleted:
+            return
+
+        if (self.sliver is not None) and (self.sliver.image != self.image):
+            self.sliver.delete()
+            self.sliver = None
+        if self.sliver is None:
+            if not self.provider_service.slices.count():
+                raise ConfigurationError("The VCPE service has no slicers")
+
+            node =self.pick_node()
+            sliver = Sliver(slice = self.provider_service.slices.all()[0],
+                            node = node,
+                            image = self.image,
+                            creator = self.caller,
+                            deployment = node.site_deployment.deployment)
+            sliver.save()
+
+            try:
+                self.sliver = sliver
+                self.save()
+            except:
+                sliver.delete()
+                raise
+
+    def cleanup_sliver(self):
+        if self.sliver:
+            self.sliver.delete()
+            self.sliver = None
+
+    def save(self, *args, **kwargs):
+        if not getattr(self, "caller", None):
+            raise TypeError("VCPETenant's self.caller was not set")
+        super(VCPETenant, self).save(*args, **kwargs)
+        self.manage_sliver()
+
+    def delete(self, *args, **kwargs):
+        self.cleanup_sliver()
+        super(VCPETenant, self).delete(*args, **kwargs)
+
diff --git a/xos/core/admin.py b/xos/core/admin.py
index e6911be..b3cee88 100644
--- a/xos/core/admin.py
+++ b/xos/core/admin.py
@@ -180,12 +180,12 @@
 
         return super(XOSAdminMixin, self).changelist_view(request, extra_context=extra_context)
 
-    def add_view(self, request, extra_context = None):
+    def add_view(self, request, form_url='', extra_context = None):
         extra_context = extra_context or {}
 
         self.add_extra_context(request, extra_context)
 
-        return super(XOSAdminMixin, self).add_view(request, extra_context=extra_context)
+        return super(XOSAdminMixin, self).add_view(request, form_url, extra_context=extra_context)
 
     def __user_is_readonly(self, request):
         return request.user.isReadOnlyUser()
@@ -728,9 +728,9 @@
     suit_classes = 'suit-tab suit-tab-serviceattrs'
 
 class ServiceAdmin(XOSBaseAdmin):
-    list_display = ("backend_status_icon","name","description","versionNumber","enabled","published")
+    list_display = ("backend_status_icon","name","kind","versionNumber","enabled","published")
     list_display_links = ('backend_status_icon', 'name', )
-    fieldList = ["backend_status_text","name","description","versionNumber","enabled","published","view_url","icon_url"]
+    fieldList = ["backend_status_text","name","kind","description","versionNumber","enabled","published","view_url","icon_url"]
     fieldsets = [(None, {'fields': fieldList, 'classes':['suit-tab suit-tab-general']})]
     inlines = [ServiceAttrAsTabInline,SliceInline]
     readonly_fields = ('backend_status_text', )
diff --git a/xos/core/models/__init__.py b/xos/core/models/__init__.py
index 97a281a..928679b 100644
--- a/xos/core/models/__init__.py
+++ b/xos/core/models/__init__.py
@@ -1,7 +1,7 @@
 from .plcorebase import PlCoreBase,PlCoreBaseManager,PlCoreBaseDeletionManager,PlModelMixIn
 from .project import Project
 from .singletonmodel import SingletonModel
-from .service import Service
+from .service import Service, Tenant
 from .service import ServiceAttribute
 from .tag import Tag
 from .role import Role
diff --git a/xos/core/models/controlleruser.py b/xos/core/models/controlleruser.py
index 0900df7..6b11b24 100644
--- a/xos/core/models/controlleruser.py
+++ b/xos/core/models/controlleruser.py
@@ -15,7 +15,9 @@
     controller = models.ForeignKey(Controller,related_name='controllersusers')
     kuser_id = StrippedCharField(null=True, blank=True, max_length=200, help_text="Keystone user id")
 
-    composite_primary_key = ('user', 'controller', 'kuser_id')
+
+    class Meta:
+        unique_together = ('user', 'controller')
 
     def __unicode__(self):  return u'%s %s' % (self.controller, self.user)
 
@@ -40,7 +42,8 @@
     site_privilege = models.ForeignKey('SitePrivilege', related_name='controllersiteprivileges')
     role_id = StrippedCharField(null=True, blank=True, max_length=200, db_index=True, help_text="Keystone id")
 
-    composite_primary_key = ('controller', 'site_privilege', 'role_id')
+    class Meta:
+        unique_together = ('controller', 'site_privilege', 'role_id')
 
     def __unicode__(self):  return u'%s %s' % (self.controller, self.site_privilege)
 
@@ -73,7 +76,9 @@
     slice_privilege = models.ForeignKey('SlicePrivilege', related_name='controllersliceprivileges')
     role_id = StrippedCharField(null=True, blank=True, max_length=200, db_index=True, help_text="Keystone id")
 
-    composite_primary_key = ('controller', 'slice_privilege')
+
+    class Meta:
+        unique_together = ('controller', 'slice_privilege')
 
     def __unicode__(self):  return u'%s %s' % (self.controller, self.slice_privilege)
 
diff --git a/xos/core/models/image.py b/xos/core/models/image.py
index 0e12473..21d4f23 100644
--- a/xos/core/models/image.py
+++ b/xos/core/models/image.py
@@ -19,7 +19,8 @@
     image = models.ForeignKey(Image,related_name='imagedeployments')
     deployment = models.ForeignKey(Deployment,related_name='imagedeployments')
 
-    composite_primary_key = ('image', 'deployment')
+    class Meta:
+        unique_together = ('image', 'deployment')
 
     def __unicode__(self):  return u'%s %s' % (self.image, self.deployment)
 
@@ -32,7 +33,8 @@
     image = models.ForeignKey(Image,related_name='controllerimages')
     controller = models.ForeignKey(Controller,related_name='controllerimages')
     glance_image_id = StrippedCharField(null=True, blank=True, max_length=200, help_text="Glance image id") 
-    
-    composite_primary_key = ('image', 'controller')
+   
+    class Meta:
+        unique_together = ('image', 'controller')
          
     def __unicode__(self):  return u'%s %s' % (self.image, self.controller)
diff --git a/xos/core/models/network.py b/xos/core/models/network.py
index 5346785..ae70d2e 100644
--- a/xos/core/models/network.py
+++ b/xos/core/models/network.py
@@ -155,9 +155,9 @@
     router_id = models.CharField(null=True, blank=True, max_length=256, help_text="Quantum router id")
     subnet_id = models.CharField(null=True, blank=True, max_length=256, help_text="Quantum subnet id")
     subnet = models.CharField(max_length=32, blank=True)
-      
-      
-    composite_primary_key = ('network', 'controller')
+     
+    class Meta:
+        unique_together = ('network', 'controller')
         
     @staticmethod
     def select_by_user(user):
@@ -176,11 +176,12 @@
     network = models.ForeignKey(Network,related_name='networkslices')
     slice = models.ForeignKey(Slice,related_name='networkslices')
 
-    composite_primary_key = ('network', 'slice')
+    class Meta:
+        unique_together = ('network', 'slice')
 
     def save(self, *args, **kwds):
         slice = self.slice
-        if (slice not in self.network.permitted_slices.all()) and (slice != self.network.owner) and (not self.network.permitAllSlices):
+        if (slice not in self.network.permitted_slices.all()) and (slice != self.network.owner) and (not self.network.permit_all_slices):
             # to add a sliver to the network, then one of the following must be true:
             #   1) sliver's slice is in network's permittedSlices list,
             #   2) sliver's slice is network's owner, or
@@ -209,11 +210,12 @@
     ip = models.GenericIPAddressField(help_text="Sliver ip address", blank=True, null=True)
     port_id = models.CharField(null=True, blank=True, max_length=256, help_text="Quantum port id")
 
-    composite_primary_key = ('network', 'sliver')
+    class Meta:
+        unique_together = ('network', 'sliver')
 
     def save(self, *args, **kwds):
         slice = self.sliver.slice
-        if (slice not in self.network.permitted_slices.all()) and (slice != self.network.owner) and (not self.network.permitAllSlices):
+        if (slice not in self.network.permitted_slices.all()) and (slice != self.network.owner) and (not self.network.permit_all_slices):
             # to add a sliver to the network, then one of the following must be true:
             #   1) sliver's slice is in network's permittedSlices list,
             #   2) sliver's slice is network's owner, or
diff --git a/xos/core/models/plcorebase.py b/xos/core/models/plcorebase.py
index a5f73e5..3760fde 100644
--- a/xos/core/models/plcorebase.py
+++ b/xos/core/models/plcorebase.py
@@ -151,10 +151,6 @@
     deleted = models.BooleanField(default=False)
     write_protect = models.BooleanField(default=False)
 
-    # XXX Django has no official support for composite primray keys yet
-    # so we will hack in an inefficient solution here.
-    composite_primary_key = []
-
     class Meta:
         # Changing abstract to False would require the managers of subclasses of
         # PlCoreBase to be customized individually.
@@ -190,28 +186,6 @@
                 self.enacted=None
                 self.save(update_fields=['enacted','deleted'], silent=silent)
 
-    def check_composite_primary_key(self):
-        try:
-            composite_key_exists = (self.composite_primary_key!=None) and (self.composite_primary_key!=[])
-        except AttributeError:
-            composite_key_exists = False
-
-        if (not composite_key_exists):
-            return
-
-        # dictionary containing cpk field name and value
-        cpk_fields = dict([(name, getattr(self, name)) for name in self.composite_primary_key])
-        objs = self.__class__.objects.filter(**cpk_fields)
-        # we can only continue if there are no matches or
-        # if this record is updating itself
-        if (len(objs) == 0 or
-            (len(objs) == 1 and self.id and objs[0].id == self.id)):
-            return
-        # if we reach this point then we've matched more than 1
-        # existing record or we are trying to
-        msg = "%s violates composite primray key constraint on fields: %s " % (self, self.composite_primary_key)
-        raise db.Error, msg
-
 
     def save(self, *args, **kwargs):
         # let the user specify silence as either a kwarg or an instance varible
@@ -229,9 +203,6 @@
                 if not (field in ["backend_register", "backend_status", "deleted", "enacted", "updated"]):
                     ignore_composite_key_check=False
 
-        if not ignore_composite_key_check:
-            self.check_composite_primary_key()
-
         super(PlCoreBase, self).save(*args, **kwargs)
 
         # This is a no-op if observer_disabled is set
diff --git a/xos/core/models/service.py b/xos/core/models/service.py
index 979a295..48d1677 100644
--- a/xos/core/models/service.py
+++ b/xos/core/models/service.py
@@ -1,16 +1,30 @@
 from django.db import models
-from core.models import PlCoreBase,SingletonModel
+from core.models import PlCoreBase,SingletonModel,PlCoreBaseManager
 from core.models.plcorebase import StrippedCharField
+import json
 
 class Service(PlCoreBase):
+    # when subclassing a service, redefine KIND to describe the new service
+    KIND = "generic"
+
     description = models.TextField(max_length=254,null=True, blank=True,help_text="Description of Service")
     enabled = models.BooleanField(default=True)
+    kind = StrippedCharField(max_length=30, help_text="Kind of service", default=KIND)
     name = StrippedCharField(max_length=30, help_text="Service Name")
     versionNumber = StrippedCharField(max_length=30, help_text="Version of Service Definition")
     published = models.BooleanField(default=True)
     view_url = StrippedCharField(blank=True, null=True, max_length=1024)
     icon_url = StrippedCharField(blank=True, null=True, max_length=1024)
 
+    def __init__(self, *args, **kwargs):
+        # for subclasses, set the default kind appropriately
+        self._meta.get_field("kind").default = self.KIND
+        super(Service, self).__init__(*args, **kwargs)
+
+    @classmethod
+    def get_service_objects(cls):
+        return cls.objects.filter(kind = cls.KIND)
+
     def __unicode__(self): return u'%s' % (self.name)
 
 class ServiceAttribute(PlCoreBase):
@@ -18,4 +32,64 @@
     value = StrippedCharField(help_text="Attribute Value", max_length=1024)
     service = models.ForeignKey(Service, related_name='serviceattributes', help_text="The Service this attribute is associated with")
 
+class Tenant(PlCoreBase):
+    """ A tenant is a relationship between two entities, a subscriber and a
+        provider.
+
+        The subscriber can be a User, a Service, or a Tenant.
+
+        The provider is always a Service.
+    """
+
+    # when subclassing a service, redefine KIND to describe the new service
+    KIND = "generic"
+
+    kind = StrippedCharField(max_length=30, default=KIND)
+    provider_service = models.ForeignKey(Service, related_name='tenants')
+    subscriber_service = models.ForeignKey(Service, related_name='subscriptions', blank=True, null=True)
+    subscriber_tenant = models.ForeignKey("Tenant", related_name='subscriptions', blank=True, null=True)
+    subscriber_user = models.ForeignKey("User", related_name='subscriptions', blank=True, null=True)
+    service_specific_id = StrippedCharField(max_length=30)
+    service_specific_attribute = models.TextField()
+
+    def __init__(self, *args, **kwargs):
+        # for subclasses, set the default kind appropriately
+        self._meta.get_field("kind").default = self.KIND
+        super(Tenant, self).__init__(*args, **kwargs)
+
+    def __unicode__(self):
+        if not hasattr(self, "provider_service"):
+           # When the REST API does a POST on a CordSubscriber object, for
+           # some reason there is no provider_service field. All of the other
+           # fields are there. Provider_service is even in the dir(). However,
+           # trying to getattr() on it will fail.
+           return "confused-tenant-object"
+
+        if self.subscriber_service:
+            return u'%s service %s on service %s' % (str(self.kind), str(self.subscriber_service.id), str(self.provider_service.id))
+        elif self.subscriber_tenant:
+            return u'%s tenant %s on service %s' % (str(self.kind), str(self.subscriber_tenant.id), str(self.provider_service.id))
+        else:
+            return u'%s on service %s' % (str(self.kind), str(self.provider_service.id))
+
+    # helper for extracting things from a json-encoded service_specific_attribute
+    def get_attribute(self, name, default=None):
+        if self.service_specific_attribute:
+            attributes = json.loads(self.service_specific_attribute)
+        else:
+            attributes = {}
+        return attributes.get(name, default)
+
+    def set_attribute(self, name, value):
+        if self.service_specific_attribute:
+            attributes = json.loads(self.service_specific_attribute)
+        else:
+            attributes = {}
+        attributes[name]=value
+        self.service_specific_attribute = json.dumps(attributes)
+
+    @classmethod
+    def get_tenant_objects(cls):
+        return cls.objects.filter(kind = cls.KIND)
+
 
diff --git a/xos/core/models/site.py b/xos/core/models/site.py
index 689dea0..1e8c7ca 100644
--- a/xos/core/models/site.py
+++ b/xos/core/models/site.py
@@ -219,7 +219,8 @@
     user = models.ForeignKey('User', related_name='deploymentprivileges')
     deployment = models.ForeignKey('Deployment', related_name='deploymentprivileges')
     role = models.ForeignKey('DeploymentRole',related_name='deploymentprivileges')
-    composite_primary_key = ('user', 'deployment', 'role')
+    class Meta:
+        unique_together = ('user', 'deployment', 'role')
 
     def __unicode__(self):  return u'%s %s %s' % (self.deployment, self.user, self.role)
 
@@ -281,7 +282,8 @@
     controller = models.ForeignKey(Controller, null=True, blank=True, related_name='sitedeployments')
     availability_zone = StrippedCharField(max_length=200, null=True, blank=True, help_text="OpenStack availability zone")
 
-    composite_primary_key = ('site', 'deployment', 'controller')
+    class Meta:
+        unique_together = ('site', 'deployment', 'controller')
 
     def __unicode__(self):  return u'%s %s' % (self.deployment, self.site)
     
@@ -291,4 +293,5 @@
     controller = models.ForeignKey(Controller, null=True, blank=True, related_name='controllersite')
     tenant_id = StrippedCharField(null=True, blank=True, max_length=200, db_index=True, help_text="Keystone tenant id")
     
-    composite_primary_key = ('site', 'controller') 
+    class Meta:
+        unique_together = ('site', 'controller') 
diff --git a/xos/core/models/slice.py b/xos/core/models/slice.py
index 44a918b..0c55791 100644
--- a/xos/core/models/slice.py
+++ b/xos/core/models/slice.py
@@ -126,7 +126,8 @@
     slice = models.ForeignKey('Slice', related_name='sliceprivileges')
     role = models.ForeignKey('SliceRole',related_name='sliceprivileges')
 
-    composite_primary_key = ('user', 'slice', 'role')
+    class Meta:
+        unique_together = ('user', 'slice', 'role')
 
     def __unicode__(self):  return u'%s %s %s' % (self.slice, self.user, self.role)
 
@@ -155,7 +156,8 @@
     slice = models.ForeignKey(Slice, related_name='controllerslices')
     tenant_id = StrippedCharField(null=True, blank=True, max_length=200, help_text="Keystone tenant id")
 
-    composite_primary_key = ('controller', 'slice')
+    class Meta:
+        unique_together = ('controller', 'slice')
      
     def __unicode__(self):  return u'%s %s'  % (self.slice, self.controller)
 
diff --git a/xos/core/views/hpc_config.py b/xos/core/views/hpc_config.py
index 41bd334..8b5bac0 100644
--- a/xos/core/views/hpc_config.py
+++ b/xos/core/views/hpc_config.py
@@ -29,6 +29,12 @@
     # to us.
     hpc=None
     for candidate in HpcService.objects.all():
+        if candidate.cmi_hostname == node_slicename:
+            # A hack for standalone CMIs that aren't managed by XOS. Set
+            # /etc/slicename to cmi_hostname that's configured in the
+            # HPCService object.
+            hpc = candidate
+
         for slice in get_service_slices(candidate):
             if slice.name == node_slicename:
                 hpc = candidate
diff --git a/xos/core/views/legacyapi.py b/xos/core/views/legacyapi.py
index 43b444c..5216351 100644
--- a/xos/core/views/legacyapi.py
+++ b/xos/core/views/legacyapi.py
@@ -146,7 +146,7 @@
 
     return sites
 
-def GetInterfaces(slicename, node_ids):
+def GetInterfaces(slicename, node_ids, return_nat=False, return_private=False):
     interfaces = []
     ps_slices = Slice.objects.filter(name=slicename)
     for ps_slice in ps_slices:
@@ -165,6 +165,28 @@
                     if (template.visibility=="public") and (template.translation=="none"):
                         ip=networkSliver.ip
 
+                if return_nat:
+                    ip = None
+                    for networkSliver in ps_sliver.networkslivers.all():
+                        if (not networkSliver.ip):
+                            continue
+                        template = networkSliver.network.template
+                        if (template.visibility=="private") and (template.translation=="NAT"):
+                            ip=networkSliver.ip
+                    if not ip:
+                        continue
+
+                if return_private:
+                    ip = None
+                    for networkSliver in ps_sliver.networkslivers.all():
+                        if (not networkSliver.ip):
+                            continue
+                        template = networkSliver.network.template
+                        if (template.visibility=="private") and (template.translation=="none"):
+                            ip=networkSliver.ip
+                    if not ip:
+                        continue
+
                 interface = {"node_id": node_id,
                              "ip": ip,
                              "broadcast": None,
@@ -197,6 +219,7 @@
     perhost = {}
     allinterfaces = {}
     hostipmap = {}
+    hostnatmap = {}
     nodes = []
     if len(slices)==1:
         slice = slices[0]
@@ -216,6 +239,18 @@
             if interface['is_primary']:
                 hostipmap[nodemap[interface['node_id']]] = interface['ip']
 
+        hostnatmap = {}
+        interfaces = GetInterfaces(slice["planetstack_name"], node_ids, return_nat=True)
+        for interface in interfaces:
+            interface['interface_tags'] = []
+            hostnatmap[nodemap[interface['node_id']]] = interface['ip']
+
+        hostprivmap = {}
+        interfaces = GetInterfaces(slice["planetstack_name"], node_ids, return_private=True)
+        for interface in interfaces:
+            interface['interface_tags'] = []
+            hostprivmap[nodemap[interface['node_id']]] = interface['ip']
+
         for nid in node_ids:
             sliver_tags = GetTags(slicename,nid)
             perhost[nodemap[nid]] = sliver_tags
@@ -234,6 +269,8 @@
             'configuration': node_sliver_tags,
             'allconfigurations':perhost,
             'hostipmap':hostipmap,
+            'hostnatmap':hostnatmap,
+            'hostprivmap':hostprivmap,
             'slivers': slivers,
             'interfaces': allinterfaces,
             'sites': sites,
diff --git a/xos/core/xoslib/dashboards/cord.html b/xos/core/xoslib/dashboards/cord.html
new file mode 100644
index 0000000..368f264
--- /dev/null
+++ b/xos/core/xoslib/dashboards/cord.html
@@ -0,0 +1,70 @@
+<script src="{{ STATIC_URL }}/js/vendor/underscore-min.js"></script>
+<script src="{{ STATIC_URL }}/js/vendor/backbone.js"></script>
+<script src="{{ STATIC_URL }}/js/vendor/backbone.syphon.js"></script>
+<script src="{{ STATIC_URL }}/js/vendor/backbone.wreqr.js"></script>
+<script src="{{ STATIC_URL }}/js/vendor/backbone.babysitter.js"></script>
+<script src="{{ STATIC_URL }}/js/vendor/backbone.marionette.js"></script>
+
+<link rel="stylesheet" href="//code.jquery.com/ui/1.11.2/themes/smoothness/jquery-ui.css">
+<link rel="stylesheet" type="text/css" href="{% static 'css/xosAdminDashboard.css' %}" media="all" >
+<link rel="stylesheet" type="text/css" href="{% static 'css/xosAdminSite.css' %}" media="all" >
+
+<script src="{{ STATIC_URL }}/js/xoslib/xos-util.js"></script>
+<script src="{{ STATIC_URL }}/js/xoslib/xos-defaults.js"></script>
+<script src="{{ STATIC_URL }}/js/xoslib/xos-validators.js"></script>
+<script src="{{ STATIC_URL }}/js/xoslib/xos-backbone.js"></script>
+<script src="{{ STATIC_URL }}/js/xoslib/xosHelper.js"></script>
+<script src="{{ STATIC_URL }}/js/picker.js"></script>
+<script src="{{ STATIC_URL }}/js/xosCord.js"></script>
+
+<script type="text/template" id="xos-log-template">
+  <tr id="<%= logMessageId %>" class="xos-log xos-<%= statusclass %>">
+     <td><%= what %><br>
+         <%= status %> <%= statusText %>
+     </td>
+  </tr>
+</script>
+
+<div id="xos-confirm-dialog" title="Confirmation Required">
+  Are you sure about this?

+</div>
+
+<div id="xos-error-dialog" title="Error Message">
+</div>

+

+<div id="xos-addchild-dialog" title="Add Child">

+<div id="xos-addchild-detail"></div>

+</div>

+
+<div id="contentPanel">
+<div id="contentTitle">
+</div>
+<div id="contentButtonPanel">
+<!-- This is really a convoluted way of handling the buttons. The onClick
+     handler for this Save button tells the save button inside the detail
+     form to click itself.
+-->
+
+<div id="rightButtonPanel"></div>
+
+<div class="box" id="logPanel">
+<table id="logTable">
+<tbody>
+</tbody>
+</table> <!-- end logTable -->
+</div> <!-- end logPanel -->
+</div> <!-- end contentButtonPanel -->
+<div id="contentInner">
+<div id="tabs">
+</div>
+<div id="detail"></div>
+<div id="linkedObjs1"></div>
+<div id="linkedObjs2"></div>
+<div id="linkedObjs3"></div>
+<div id="linkedObjs4"></div>
+</div> <!-- end contentInner -->
+</div> <!-- end contentPanel -->
+
+{% include 'xosAdmin.html' %}
+{% include 'xosCordSubscriber.html' %}
+
diff --git a/xos/core/xoslib/methods/cordsubscriber.py b/xos/core/xoslib/methods/cordsubscriber.py
new file mode 100644
index 0000000..1470323
--- /dev/null
+++ b/xos/core/xoslib/methods/cordsubscriber.py
@@ -0,0 +1,59 @@
+from rest_framework.decorators import api_view
+from rest_framework.response import Response
+from rest_framework.reverse import reverse
+from rest_framework import serializers
+from rest_framework import generics
+from core.models import *
+from django.forms import widgets
+from cord.models import VOLTTenant
+from core.xoslib.objects.cordsubscriber import CordSubscriber
+from plus import PlusSerializerMixin
+from xos.apibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView, XOSPermissionDenied
+
+if hasattr(serializers, "ReadOnlyField"):
+    # rest_framework 3.x
+    ReadOnlyField = serializers.ReadOnlyField
+else:
+    # rest_framework 2.x
+    ReadOnlyField = serializers.Field
+
+class CordSubscriberIdSerializer(serializers.ModelSerializer, PlusSerializerMixin):
+        id = ReadOnlyField()
+        vcpe_id = ReadOnlyField()
+        sliver = ReadOnlyField()
+        image = ReadOnlyField()
+        firewall_enable = serializers.BooleanField()
+        firewall_rules = serializers.CharField()
+        url_filter_enable = serializers.BooleanField()
+        url_filter_rules = serializers.CharField()
+        cdn_enable = serializers.BooleanField()
+        sliver_name = ReadOnlyField()
+        image_name = ReadOnlyField()
+
+        humanReadableName = serializers.SerializerMethodField("getHumanReadableName")
+
+        class Meta:
+            model = CordSubscriber
+            fields = ('humanReadableName', 'id',
+                      'service_specific_id',
+                      'vcpe_id', 'sliver', 'sliver_name', 'image', 'image_name', 'firewall_enable', 'firewall_rules', 'url_filter_enable', 'url_filter_rules', 'cdn_enable')
+
+
+        def getHumanReadableName(self, obj):
+            return obj.__unicode__()
+
+class CordSubscriberList(XOSListCreateAPIView):
+    queryset = CordSubscriber.get_tenant_objects().select_related().all()
+    serializer_class = CordSubscriberIdSerializer
+
+    method_kind = "list"
+    method_name = "cordsubscriber"
+
+class CordSubscriberDetail(XOSRetrieveUpdateDestroyAPIView):
+    queryset = CordSubscriber.get_tenant_objects().select_related().all()
+    serializer_class = CordSubscriberIdSerializer
+
+    method_kind = "detail"
+    method_name = "cordsubscriber"
+
+
diff --git a/xos/core/xoslib/objects/cordsubscriber.py b/xos/core/xoslib/objects/cordsubscriber.py
new file mode 100644
index 0000000..654d8a3
--- /dev/null
+++ b/xos/core/xoslib/objects/cordsubscriber.py
@@ -0,0 +1,82 @@
+from core.models import Slice, SlicePrivilege, SliceRole, Sliver, Site, Node, User
+from cord.models import VOLTTenant
+from plus import PlusObjectMixin
+from operator import itemgetter, attrgetter
+from rest_framework.exceptions import APIException
+
+"""
+import os
+import sys
+sys.path.append("/opt/xos")
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+import django
+from core.models import *

+from hpc.models import *

+from cord.models import *

+django.setup()
+from core.xoslib.objects.cordsubscriber import CordSubscriber
+c=CordSubscriber.get_tenant_objects().select_related().all()[0]
+"""
+
+class CordSubscriber(VOLTTenant, PlusObjectMixin):
+    class Meta:
+        proxy = True
+
+    def __init__(self, *args, **kwargs):
+        super(VOLTTenant, self).__init__(*args, **kwargs)
+
+    @property
+    def vcpe_id(self):
+        if self.vcpe:
+            return self.vcpe.id
+        else:
+            return None
+
+    @vcpe_id.setter
+    def vcpe_id(self, value):
+        pass
+
+    passthroughs = ( ("firewall_enable", "vcpe.firewall_enable"),
+                     ("firewall_rules", "vcpe.firewall_rules"),
+                     ("url_filter_enable", "vcpe.url_filter_enable"),
+                     ("url_filter_rules", "vcpe.url_filter_rules"),
+                     ("cdn_enable", "vcpe.cdn_enable"),
+                     ("image", "vcpe.image.id"),
+                     ("image_name", "vcpe.image.name"),
+                     ("sliver", "vcpe.sliver.id"),
+                     ("sliver_name", "vcpe.sliver.name") )
+
+    def __getattr__(self, key):
+        for (member_name, passthrough_name) in self.passthroughs:
+            if key==member_name:
+                parts = passthrough_name.split(".")
+                obj = self
+                for part in parts[:-1]:
+                    obj = getattr(obj, part)
+                    if not obj:
+                        return None
+                return getattr(obj, parts[-1])
+
+        raise AttributeError("getattr: %r object has no attribute %r" %
+                         (self.__class__, key))
+
+    def __setattr__(self, key, value):
+        for (member_name, passthrough_name) in self.passthroughs:
+            if key==member_name:
+                parts = passthrough_name.split(".")
+                obj = self
+                for part in parts[:-1]:
+                     obj = getattr(obj, part)
+                     if not obj:
+                         return
+                setattr(obj, parts[-1], value)
+
+        super(CordSubscriber, self).__setattr__(key, value)
+
+
+
+
+
+
+
+
diff --git a/xos/core/xoslib/static/js/xosCord.js b/xos/core/xoslib/static/js/xosCord.js
new file mode 100644
index 0000000..96d95f1
--- /dev/null
+++ b/xos/core/xoslib/static/js/xosCord.js
@@ -0,0 +1,195 @@
+OBJS = ['cordSubscriber', ]
+
+CordAdminApp = new XOSApplication({
+    logTableId: "#logTable",
+    statusMsgId: "#statusMsg",
+    hideTabsByDefault: true
+});
+
+CordAdminApp.addRegions({
+    navigation: "#navigationPanel",
+
+    detail: "#detail",
+    linkedObjs1: "#linkedObjs1",
+    linkedObjs2: "#linkedObjs2",
+    linkedObjs3: "#linkedObjs3",
+    linkedObjs4: "#linkedObjs4",
+
+    addChildDetail: "#xos-addchild-detail",
+
+    rightButtonPanel: "#rightButtonPanel"
+});
+
+CordAdminApp.navigate = function(what, modelName, modelId) {

+    collection_name = modelName + "s";

+    if (what=="list") {

+        CordAdminApp.Router.navigate(collection_name, {trigger: true})

+    } else if (what=="detail") {

+        CordAdminApp.Router.navigate(collection_name + "/" + modelId, {trigger: true})

+    } else if (what=="add") {

+        CordAdminApp.Router.navigate("add" + firstCharUpper(modelName), {trigger: true, force: true})

+    }

+}

+

+CordAdminApp.buildViews = function() {

+     genericAddChildClass = XOSDetailView.extend({template: "#xos-add-template",

+                                                        app: CordAdminApp});

+     CordAdminApp["genericAddChildView"] = genericAddChildClass;

+

+     genericDetailClass = XOSDetailView.extend({template: "#xos-detail-template",

+                                                           app: CordAdminApp});

+     CordAdminApp["genericDetailView"] = genericDetailClass;

+

+     genericItemViewClass = XOSItemView.extend({template: "#xos-listitem-template",

+                                                app: CordAdminApp});

+     CordAdminApp["genericItemView"] = genericItemViewClass;

+

+     //genericListViewClass = XOSListView.extend({template: "#xos-list-template",

+     //                                           app: CordAdminApp});

+

+     genericListViewClass = XOSDataTableView.extend({template: "#xos-list-template", app: CordAdminApp});

+     CordAdminApp["genericListView"] = genericListViewClass;

+

+     for (var index in OBJS) {

+         name = OBJS[index];
+         tr_template = '#xosAdmin-' + name + '-listitem-template';
+         table_template = '#xosAdmin-' + name + '-list-template';
+         detail_template = '#xosAdmin-' + name + '-detail-template';
+         add_child_template = '#xosAdmin-' + name + '-add-child-template';
+         collection_name = name + "s";
+         region_name = name + "List";
+
+         if (window["XOSDetailView_" + name]) {
+             detailClass = window["XOSDetailView_" + name].extend({template: "#xos-detail-template",
+                                                                    app: CordAdminApp});
+         } else {
+             detailClass = genericDetailClass;
+         }
+         if ($(detail_template).length) {
+             detailClass = detailClass.extend({
+                template: detail_template,

+             });

+         }

+         CordAdminApp[collection_name + "DetailView"] = detailClass;

+

+         if (window["XOSDetailView_" + name]) {

+             addClass = window["XOSDetailView_" + name].extend({template: "#xos-add-template",
+                                                                    app: CordAdminApp});
+         } else {
+             addClass = genericAddChildClass;
+         }
+         if ($(add_child_template).length) {
+             addClass = detailClass.extend({
+                template: add_child_template,

+             });

+         }

+         CordAdminApp[collection_name + "AddChildView"] = addClass;

+

+         if ($(tr_template).length) {

+             itemViewClass = XOSItemView.extend({

+                 template: tr_template,

+                 app: CordAdminApp,
+             });
+         } else {
+             itemViewClass = genericItemViewClass;
+         }
+
+         if ($(table_template).length) {
+             listViewClass = XOSListView.extend({
+                 childView: itemViewClass,
+                 template: table_template,
+                 collection: xos[collection_name],
+                 title: name + "s",
+                 app: CordAdminApp,
+             });
+         } else {
+             listViewClass = genericListViewClass.extend( { childView: itemViewClass,
+                                                            collection: xos[collection_name],
+                                                            title: name + "s",
+                                                           } );
+         }
+
+         CordAdminApp[collection_name + "ListView"] = listViewClass;
+
+         xos[collection_name].fetch(); //startPolling();
+     }

+};

+

+CordAdminApp.initRouter = function() {

+    router = XOSRouter;

+    var api = {};

+    var routes = {};

+

+    for (var index in OBJS) {

+        name = OBJS[index];

+        collection_name = name + "s";

+        nav_url = collection_name;

+        api_command = "list" + firstCharUpper(collection_name);

+        listViewName = collection_name + "ListView";

+        detailViewName = collection_name + "DetailView";

+        addChildViewName = collection_name + "AddChildView";

+

+        api[api_command] = CordAdminApp.createListHandler(listViewName, collection_name, "detail", collection_name);

+        routes[nav_url] = api_command;

+

+        nav_url = collection_name + "/:id";

+        api_command = "detail" + firstCharUpper(collection_name);

+

+        api[api_command] = CordAdminApp.createDetailHandler(detailViewName, collection_name, "detail", name);

+        routes[nav_url] = api_command;

+

+        nav_url = "add" + firstCharUpper(name);

+        api_command = "add" + firstCharUpper(name);

+        api[api_command] = CordAdminApp.createAddHandler(detailViewName, collection_name, "detail", name);

+        routes[nav_url] = api_command;

+

+        nav_url = "addChild" + firstCharUpper(name) + "/:parentModel/:parentField/:parentId";

+        api_command = "addChild" + firstCharUpper(name);

+        api[api_command] = CordAdminApp.createAddChildHandler(addChildViewName, collection_name);

+        routes[nav_url] = api_command;

+

+        nav_url = "delete" + firstCharUpper(name) + "/:id";

+        api_command = "delete" + firstCharUpper(name);

+        api[api_command] = CordAdminApp.createDeleteHandler(collection_name, name);

+        routes[nav_url] = api_command;

+    };

+

+    routes["*part"] = "listCordSubscribers";

+

+    CordAdminApp.Router = new router({ appRoutes: routes, controller: api });

+};

+

+CordAdminApp.startNavigation = function() {

+    Backbone.history.start();

+    CordAdminApp.navigationStarted = true;

+}

+

+CordAdminApp.collectionLoadChange = function() {

+    stats = xos.getCollectionStatus();

+

+    if (!CordAdminApp.navigationStarted) {

+        if (stats["isLoaded"] + stats["failedLoad"] >= stats["startedLoad"]) {

+            CordAdminApp.startNavigation();

+        } else {

+            $("#detail").html("<h3>Loading...</h3><div id='xos-startup-progress'></div>");

+            $("#xos-startup-progress").progressbar({value: stats["completedLoad"], max: stats["startedLoad"]});

+        }

+    }

+};

+

+CordAdminApp.on("start", function() {

+     CordAdminApp.buildViews();
+
+     CordAdminApp.initRouter();
+
+     // fire it once to initially show the progress bar
+     CordAdminApp.collectionLoadChange();
+
+     // fire it each time the collection load status is updated
+     Backbone.on("xoslib:collectionLoadChange", CordAdminApp.collectionLoadChange);
+});
+
+$(document).ready(function(){
+    CordAdminApp.start();
+});
+
diff --git a/xos/core/xoslib/static/js/xoslib/xos-backbone.js b/xos/core/xoslib/static/js/xoslib/xos-backbone.js
index a459458..f22a2e2 100644
--- a/xos/core/xoslib/static/js/xoslib/xos-backbone.js
+++ b/xos/core/xoslib/static/js/xoslib/xos-backbone.js
@@ -34,7 +34,9 @@
 
     SLICEPLUS_API = XOSLIB_BASE + "/slicesplus/";
     TENANTVIEW_API = XOSLIB_BASE + "/tenantview/";
-    HPCVIEW_API = XOSLIB_BASE + "/hpcview";
+    HPCVIEW_API = XOSLIB_BASE + "/hpcview/";
+
+    CORDSUBSCRIBER_API = XOSLIB_BASE + "/cordsubscriber/";
 
     XOSModel = Backbone.Model.extend({
         relatedCollections: [],
@@ -725,6 +727,15 @@
                             detailFields: [],
                             });
 
+        define_model(this, {urlRoot: CORDSUBSCRIBER_API,
+                            modelName: "cordSubscriber",
+                            listFields: ["id"],
+                            detailFields: ["id", "service_specific_id", "vcpe_id", "image_name", "sliver_name", "firewall_enable", "firewall_rules", "url_filter_enable", "url_filter_rules", "cdn_enable"],
+                            inputType: {"firewall_enable": "checkbox",
+                                        "url_filter_enable": "checkbox",
+                                        "cdn_enable": "checkbox"},
+                            });
+
         /* by default, have slicePlus only fetch the slices the user can see */
         this.slicesPlus.currentUserCanSee = true;
 
diff --git a/xos/core/xoslib/templates/xosCordSubscriber.html b/xos/core/xoslib/templates/xosCordSubscriber.html
new file mode 100644
index 0000000..7d86cf7
--- /dev/null
+++ b/xos/core/xoslib/templates/xosCordSubscriber.html
@@ -0,0 +1,16 @@
+<script type="text/template" id="xos-cord-subscriber-template">
+  <h3 class="xos-detail-title">CORD Subscriber</h3>
+  <form>
+  <table class="xos-detail-table">

+  <tr><td class="xos-label-cell">vOLT</td></tr>

+  <tr><td class="xos-label-cell">ID:</td><td><%= vOLT_id %></td></tr>

+  <tr><td class="xos-label-cell">internal ID:</td><td><%= vOLT_service_specific_id %></td></tr>

+  <tr><td class="xos-label-cell">vCPE</td></tr>

+  <tr><td class="xos-label-cell">ID:</td><td><%= vCPE_id %></td></tr>

+  <tr><td class="xos-label-cell">vBNG</td></tr>

+  <tr><td class="xos-label-cell">ID:</td><td></td></tr>

+  <tr><td class="xos-label-cell">Routeable Subnet:</td><td></td></tr>

+  </table>

+  </form>

+</script>

+

diff --git a/xos/hpc_observer/hpc_watcher.py b/xos/hpc_observer/hpc_watcher.py
index 5b9f181..b0587f5 100644
--- a/xos/hpc_observer/hpc_watcher.py
+++ b/xos/hpc_observer/hpc_watcher.py
@@ -33,6 +33,7 @@
 """
 
 import os
+import socket
 import sys
 sys.path.append("/opt/xos")
 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
@@ -329,8 +330,11 @@
 
             ip = sliver.get_public_ip()
             if not ip:
-                self.set_status(sliver, service, "watcher.DNS", "no public IP")
-                continue
+                ip = socket.gethostbyname(sliver.node.name)
+
+            #if not ip:
+            #    self.set_status(sliver, service, "watcher.DNS", "no public IP")
+            #    continue
 
             checks = HpcHealthCheck.objects.filter(kind="dns")
             if not checks:
diff --git a/xos/openstack_observer/steps/sync_network_slivers.py b/xos/openstack_observer/steps/sync_network_slivers.py
index 78751d1..b61b93f 100644
--- a/xos/openstack_observer/steps/sync_network_slivers.py
+++ b/xos/openstack_observer/steps/sync_network_slivers.py
@@ -133,7 +133,12 @@
                                sliver=sliver,
                                ip=ip,
                                port_id=port["id"])
-            ns.save()
+
+            try:
+                ns.save()
+            except:
+                logger.log_exc("failed to save networksliver %s" % str(ns))
+                continue
 
         # Now, handle port forwarding
         # We get the list of NetworkSlivers again, since we might have just
diff --git a/xos/tools/ansible_hosts.py b/xos/tools/ansible_hosts.py
new file mode 100644
index 0000000..e17edf6
--- /dev/null
+++ b/xos/tools/ansible_hosts.py
@@ -0,0 +1,65 @@
+#! /usr/bin/env python
+
+import json
+import os
+import requests
+import sys
+
+from operator import itemgetter, attrgetter
+
+opencloud_auth = None
+
+REST_API="http://portal.opencloud.us/xos/"
+
+NODES_API = REST_API + "nodes/"
+SITES_API = REST_API + "sites/"
+
+def get_nodes_by_site():
+    r = requests.get(SITES_API + "?no_hyperlinks=1", auth=opencloud_auth)
+    sites_list = r.json()
+    sites = {}
+    for site in sites_list:
+        site["hostnames"] = []
+        sites[str(site["id"])] = site
+
+    r = requests.get(NODES_API + "?no_hyperlinks=1", auth=opencloud_auth)
+    nodes = r.json()
+    for node in nodes:
+        site_id = str(node["site"])
+        if site_id in sites:
+            sites[site_id]["hostnames"].append(node["name"])
+
+    return sites
+
+def main():
+    global opencloud_auth
+
+    if len(sys.argv)!=3:
+        print >> sys.stderr, "syntax: get_instance_name.py <username>, <password>"
+        sys.exit(-1)
+
+    username = sys.argv[1]
+    password = sys.argv[2]
+
+    opencloud_auth=(username, password)
+
+    sites = get_nodes_by_site()
+
+    for site in sites.values():
+        if not site["hostnames"]:
+            continue
+
+        print "[%s]" % site["name"]
+        for hostname in site["hostnames"]:
+            print hostname
+        print ""
+
+    print "[all-opencloud:children]"
+    for site in sites.values():
+        if not site["hostnames"]:
+            continue
+        print site["name"]
+
+if __name__ == "__main__":
+    main()
+