Add wan_mac
diff --git a/xos/cord/models.py b/xos/cord/models.py
index c491a54..ca010cb 100644
--- a/xos/cord/models.py
+++ b/xos/cord/models.py
@@ -68,7 +68,8 @@
 
     KIND = "vOLT"
 
-    default_attributes = {"vlan_id": None, }
+    default_attributes = {"vlan_id": None,
+                          "is_demo_user": False }
 
     def __init__(self, *args, **kwargs):
         volt_services = VOLTService.get_service_objects().all()
@@ -129,6 +130,14 @@
             self.cached_creator=None
         self.set_attribute("creator_id", value)
 
+    @property
+    def is_demo_user(self):
+        return self.get_attribute("is_demo_user", self.default_attributes["is_demo_user"])
+
+    @is_demo_user.setter
+    def is_demo_user(self, value):
+        self.set_attribute("is_demo_user", value)
+
     def manage_vcpe(self):
         # Each VOLT object owns exactly one VCPE object
 
@@ -215,7 +224,9 @@
                        "nat_ip",
                        "lan_ip",
                        "wan_ip",
-                       "private_ip")
+                       "private_ip",
+                       "hpc_client_ip",
+                       "wan_mac")
 
     default_attributes = {"firewall_enable": False,
                           "firewall_rules": "accept all anywhere anywhere",
@@ -234,7 +245,8 @@
 
     @property
     def image(self):
-        LOOK_FOR_IMAGES=["Ubuntu 14.04 LTS",    # portal
+        LOOK_FOR_IMAGES=["ubuntu-vcpe2",        # ONOS demo machine -- preferred vcpe image
+                         "Ubuntu 14.04 LTS",    # portal
                          "Ubuntu-14.04-LTS",    # ONOS demo machine
                         ]
         for image_name in LOOK_FOR_IMAGES:
@@ -376,6 +388,17 @@
     def bbs_account(self, value):
         return self.set_attribute("bbs_account", value)
 
+    @property
+    def ssh_command(self):
+        if self.sliver:
+            return self.sliver.get_ssh_command()
+        else:
+            return "no-sliver"
+
+    @ssh_command.setter
+    def ssh_command(self, value):
+        pass
+
     def find_user(self, uid):
         uid = int(uid)
         for user in self.users:
@@ -393,10 +416,17 @@
                 for arg in kwargs.keys():
                     user[arg] = kwargs[arg]
                     self.users = users
-                return
+                return user
         raise ValueError("User %d not found" % uid)
 
     def create_user(self, **kwargs):
+        if "name" not in kwargs:
+            raise XOSMissingField("The name field is required")
+
+        for user in self.users:
+            if kwargs["name"] == user["name"]:
+                raise XOSDuplicateKey("User %s already exists" % kwargs["name"])
+
         uids = [x["id"] for x in self.users]
         if uids:
             uid = max(uids)+1
@@ -447,6 +477,8 @@
                 addresses["private"] = ns.ip
             elif "nat" in ns.network.name.lower():
                 addresses["nat"] = ns.ip
+            elif "hpc_client" in ns.network.name.lower():
+                addresses["hpc_client"] = ns.ip
         return addresses
 
     @property
@@ -462,9 +494,25 @@
         return self.addresses.get("wan",None)
 
     @property
+    def wan_mac(self):
+        ip = self.wan_ip
+        if not ip:
+           return None
+        try:
+           (a,b,c,d) = ip.split('.')
+           wan_mac = "02:42:%2x:%2x:%2x:%2x" % (int(a), int(b), int(c), int(d))
+        except:
+           wan_mac = "Exception"
+        return wan_mac
+
+    @property
     def private_ip(self):
         return self.addresses.get("private",None)
 
+    @property
+    def hpc_client_ip(self):
+        return self.addresses.get("hpc_client",None)
+
     def pick_node(self):
         nodes = list(Node.objects.all())
         # TODO: logic to filter nodes by which nodes are up, and which
@@ -591,7 +639,8 @@
 
     KIND = "vBNG"
 
-    default_attributes = {"routeable_subnet": ""}
+    default_attributes = {"routeable_subnet": "",
+                          "mapped_ip": ""}
 
     @property
     def routeable_subnet(self):
@@ -600,3 +649,11 @@
     @routeable_subnet.setter
     def routeable_subnet(self, value):
         self.set_attribute("routeable_subnet", value)
+
+    @property
+    def mapped_ip(self):
+        return self.get_attribute("mapped_ip", self.default_attributes["mapped_ip"])
+
+    @mapped_ip.setter
+    def mapped_ip(self, value):
+        self.set_attribute("mapped_ip", value)
diff --git a/xos/core/views/legacyapi.py b/xos/core/views/legacyapi.py
index 5216351..3c9f50a 100644
--- a/xos/core/views/legacyapi.py
+++ b/xos/core/views/legacyapi.py
@@ -157,13 +157,26 @@
 
                 ip = socket.gethostbyname(ps_node.name.strip())
 
-                # search for a dedicated public IP address
+                # If the slice has a network that's labeled for hpc_client, then
+                # return that network.
+                found_labeled_network = False
                 for networkSliver in ps_sliver.networkslivers.all():
                     if (not networkSliver.ip):
                         continue
-                    template = networkSliver.network.template
-                    if (template.visibility=="public") and (template.translation=="none"):
+                    if (networkSliver.network.owner != ps_slice):
+                        continue
+                    if networkSliver.network.labels and ("hpc_client" in networkSliver.network.labels):
                         ip=networkSliver.ip
+                        found_labeled_network = True
+
+                if not found_labeled_network:
+                    # search for a dedicated public IP address
+                    for networkSliver in ps_sliver.networkslivers.all():
+                        if (not networkSliver.ip):
+                            continue
+                        template = networkSliver.network.template
+                        if (template.visibility=="public") and (template.translation=="none"):
+                            ip=networkSliver.ip
 
                 if return_nat:
                     ip = None
@@ -218,6 +231,7 @@
     slices = GetSlices({"name": slicename}, slice_remap=slice_remap)
     perhost = {}
     allinterfaces = {}
+    hostprivmap = {}
     hostipmap = {}
     hostnatmap = {}
     nodes = []
diff --git a/xos/core/xoslib/methods/cordsubscriber.py b/xos/core/xoslib/methods/cordsubscriber.py
index 19823dc..5742c9c 100644
--- a/xos/core/xoslib/methods/cordsubscriber.py
+++ b/xos/core/xoslib/methods/cordsubscriber.py
@@ -5,10 +5,11 @@
 from rest_framework import generics
 from rest_framework import viewsets
 from rest_framework.decorators import detail_route, list_route
+from rest_framework.views import APIView
 from core.models import *
 from django.forms import widgets
 from django.conf.urls import patterns, url
-from cord.models import VOLTTenant
+from cord.models import VOLTTenant, VBNGTenant
 from core.xoslib.objects.cordsubscriber import CordSubscriber
 from plus import PlusSerializerMixin
 from django.shortcuts import get_object_or_404
@@ -40,6 +41,7 @@
         sliver_name = ReadOnlyField()
         image_name = ReadOnlyField()
         routeable_subnet = serializers.CharField(required=False)
+        ssh_command = ReadOnlyField()
         bbs_account = ReadOnlyField()
 
         lan_ip = ReadOnlyField()
@@ -47,6 +49,8 @@
         nat_ip = ReadOnlyField()
         private_ip = ReadOnlyField()
 
+        wan_mac = ReadOnlyField()
+
         humanReadableName = serializers.SerializerMethodField("getHumanReadableName")
 
         class Meta:
@@ -57,12 +61,18 @@
                       'firewall_enable', 'firewall_rules',
                       'url_filter_enable', 'url_filter_rules', 'url_filter_level',
                       'bbs_account',
-                      'cdn_enable', 'vbng_id', 'routeable_subnet', 'nat_ip', 'lan_ip', 'wan_ip', 'private_ip')
+                      'ssh_command',
+                      'cdn_enable', 'vbng_id', 'routeable_subnet', 'nat_ip', 'lan_ip', 'wan_ip', 'private_ip', 'wan_mac')
 
 
         def getHumanReadableName(self, obj):
             return obj.__unicode__()
 
+#------------------------------------------------------------------------------
+# The "old" API
+# This is used by the xoslib-based GUI
+#------------------------------------------------------------------------------
+
 class CordSubscriberList(XOSListCreateAPIView):
     queryset = CordSubscriber.get_tenant_objects().select_related().all()
     serializer_class = CordSubscriberIdSerializer
@@ -77,6 +87,71 @@
     method_kind = "detail"
     method_name = "cordsubscriber"
 
+# We fake a user object by pulling the user data struct out of the
+# subscriber object...
+
+def serialize_user(subscriber, user):
+    return {"id": "%d-%d" % (subscriber.id, user["id"]),
+            "name": user["name"],
+            "level": user.get("level",""),
+            "mac": user.get("mac", ""),
+            "subscriber": subscriber.id }
+
+class CordUserList(APIView):
+    method_kind = "list"
+    method_name = "corduser"
+
+    def get(self, request, format=None):
+        instances=[]
+        for subscriber in CordSubscriber.get_tenant_objects().all():
+            for user in subscriber.users:
+                instances.append( serialize_user(subscriber, user) )
+
+        return Response(instances)
+
+    def post(self, request, format=None):
+        data = request.DATA
+        subscriber = CordSubscriber.get_tenant_objects().get(id=int(data["subscriber"]))
+        user = subscriber.vcpe.create_user(name=data["name"],
+                                    level=data["level"],
+                                    mac=data["mac"])
+        subscriber.save()
+
+        return Response(serialize_user(subscriber,user))
+
+class CordUserDetail(APIView):
+    method_kind = "detail"
+    method_name = "corduser"
+
+    def get(self, request, format=None, pk=0):
+        parts = pk.split("-")
+        subscriber = CordSubscriber.get_tenant_objects().filter(id=parts[0])
+        for user in subscriber.users:
+            return Response( [ serialize_user(subscriber, user) ] )
+        raise XOSNotFound("Failed to find user %s" % pk)
+
+    def delete(self, request, pk):
+        parts = pk.split("-")
+        subscriber = CordSubscriber.get_tenant_objects().get(id=int(parts[0]))
+        subscriber.vcpe.delete_user(parts[1])
+        subscriber.save()
+        return Response("okay")
+
+    def put(self, request, pk):
+        kwargs={}
+        if "name" in request.DATA:
+             kwargs["name"] = request.DATA["name"]
+        if "level" in request.DATA:
+             kwargs["level"] = request.DATA["level"]
+        if "mac" in request.DATA:
+             kwargs["mac"] = request.DATA["mac"]
+
+        parts = pk.split("-")
+        subscriber = CordSubscriber.get_tenant_objects().get(id=int(parts[0]))
+        user = subscriber.vcpe.update_user(parts[1], **kwargs)
+        subscriber.save()
+        return Response(serialize_user(subscriber,user))
+
 # this may be moved into plus.py...
 
 class XOSViewSet(viewsets.ModelViewSet):
@@ -87,6 +162,12 @@
                    name=self.base_name+"_"+name)
 
     @classmethod
+    def list_url(self, pattern, viewdict, name):
+        return url(r'^' + self.method_name + r'/' + pattern,
+                   self.as_view(viewdict),
+                   name=self.base_name+"_"+name)
+
+    @classmethod
     def get_urlpatterns(self):
         patterns = []
 
@@ -95,7 +176,10 @@
 
         return patterns
 
-# the "new" API with many more REST endpoints.
+#------------------------------------------------------------------------------
+# The "new" API with many more REST endpoints.
+# This is for integration with with the subscriber GUI
+#------------------------------------------------------------------------------
 
 class CordSubscriberViewSet(XOSViewSet):
     base_name = "subscriber"
@@ -114,18 +198,25 @@
     def get_urlpatterns(self):
         patterns = super(CordSubscriberViewSet, self).get_urlpatterns()
         patterns.append( self.detail_url("url_filter/$", {"get": "get_url_filter"}, "url_filter") )
-        patterns.append( self.detail_url("url_filter/(?P<level>[a-zA-Z0-9\-]+)/$", {"put": "set_url_filter"}, "url_filter") )
+        patterns.append( self.detail_url("url_filter/(?P<level>[a-zA-Z0-9\-_]+)/$", {"put": "set_url_filter"}, "url_filter") )
         patterns.append( self.detail_url("services/$", {"get": "get_services"}, "services") )
-        patterns.append( self.detail_url("services/(?P<service>[a-zA-Z0-9\-]+)/$", {"get": "get_service"}, "get_service") )
-        patterns.append( self.detail_url("services/(?P<service>[a-zA-Z0-9\-]+)/true/$", {"put": "enable_service"}, "enable_service") )
-        patterns.append( self.detail_url("services/(?P<service>[a-zA-Z0-9\-]+)/false/$", {"put": "disable_service"}, "disable_service") )
+        patterns.append( self.detail_url("services/(?P<service>[a-zA-Z0-9\-_]+)/$", {"get": "get_service"}, "get_service") )
+        patterns.append( self.detail_url("services/(?P<service>[a-zA-Z0-9\-_]+)/true/$", {"put": "enable_service"}, "enable_service") )
+        patterns.append( self.detail_url("services/(?P<service>[a-zA-Z0-9\-_]+)/false/$", {"put": "disable_service"}, "disable_service") )
 
         patterns.append( self.detail_url("users/$", {"get": "get_users", "post": "create_user"}, "users") )
         patterns.append( self.detail_url("users/clearusers/$", {"get": "clear_users", "put": "clear_users", "post": "clear_users"}, "clearusers") )
         patterns.append( self.detail_url("users/newuser/$", {"put": "create_user", "post": "create_user"}, "newuser") )
         patterns.append( self.detail_url("users/(?P<uid>[0-9\-]+)/$", {"delete": "delete_user"}, "user") )
         patterns.append( self.detail_url("users/(?P<uid>[0-9\-]+)/url_filter/$", {"get": "get_user_level"}, "user_level") )
-        patterns.append( self.detail_url("users/(?P<uid>[0-9\-]+)/url_filter/(?P<level>[a-zA-Z0-9\-]+)/$", {"put": "set_user_level"}, "set_user_level") )
+        patterns.append( self.detail_url("users/(?P<uid>[0-9\-]+)/url_filter/(?P<level>[a-zA-Z0-9\-_]+)/$", {"put": "set_user_level"}, "set_user_level") )
+
+        patterns.append( url("^rs/initdemo/$", self.as_view({"put": "initdemo", "get": "initdemo"}), name="initdemo") )
+
+        patterns.append( url("^rs/subidlookup/(?P<ssid>[0-9\-]+)/$", self.as_view({"get": "ssiddetail"}), name="ssiddetail") )
+        patterns.append( url("^rs/subidlookup/$", self.as_view({"get": "ssidlist"}), name="ssidlist") )
+
+        patterns.append( url("^rs/vbng_mapping/$", self.as_view({"get": "get_vbng_mapping"}), name="vbng_mapping") )
 
         return patterns
 
@@ -220,6 +311,53 @@
         subscriber.save()
         return Response({service: getattr(subscriber, service_attr)})
 
+    def initdemo(self, request):
+        object_list = VOLTTenant.get_tenant_objects().all()
 
+        demo_subscribers = [o for o in object_list if o.is_demo_user]
+
+        if demo_subscribers:
+            return Response({"id": demo_subscribers[0].id})
+
+        voltTenant = VOLTTenant(service_specific_id=1234,
+                                vlan_id=1234,
+                                is_demo_user=True)
+        voltTenant.caller = User.objects.get(email="padmin@vicci.org")
+        voltTenant.save()
+
+        voltTenant.vcpe.create_user(name="Mom's PC",      mac="01020303040506", level="R")
+        voltTenant.vcpe.create_user(name="Dad's PC",      mac="01020304040507", level="R")
+        voltTenant.vcpe.create_user(name="Jack's iPhone", mac="01020304050508", level="PG")
+        voltTenant.vcpe.create_user(name="Jill's iPad",   mac="01020304050609", level="G")
+        voltTenant.vcpe.save()
+
+        return Response({"id": voltTenant.id})
+
+    def ssidlist(self, request):
+        object_list = VOLTTenant.get_tenant_objects().all()
+
+        ssidmap = [ {"service_specific_id:": x.service_specific_id, "subscriber_id": x.id} for x in object_list ]
+
+        return Response({"ssidmap": ssidmap})
+
+    def ssiddetail(self, pk=None, ssid=None):
+        object_list = VOLTTenant.get_tenant_objects().all()
+
+        ssidmap = [ {"service_specific_id:": x.service_specific_id, "subscriber_id": x.id} for x in object_list if str(x.service_specific_id)==str(ssid) ]
+
+        if len(ssidmap)==0:
+            raise XOSNotFound("didn't find ssid %s" % str(ssid))
+
+        return Response( ssidmap[0] )
+
+    def get_vbng_mapping(self, request):
+        object_list = VBNGTenant.get_tenant_objects().all()
+
+        mappings = []
+        for vbng in object_list:
+            if vbng.mapped_ip and vbng.routeable_subnet:
+                mappings.append( {"private_ip": vbng.mapped_ip, "routeable_subnet": vbng.routeable_subnet} )
+
+        return Response( {"vbng_mapping": mappings} )
 
 
diff --git a/xos/core/xoslib/methods/volttenant.py b/xos/core/xoslib/methods/volttenant.py
index a927c3f..a1ae656 100644
--- a/xos/core/xoslib/methods/volttenant.py
+++ b/xos/core/xoslib/methods/volttenant.py
@@ -30,13 +30,24 @@
 
         humanReadableName = serializers.SerializerMethodField("getHumanReadableName")
 
+        computeNodeName = serializers.SerializerMethodField("getComputeNodeName")
+
         class Meta:
             model = VOLTTenant
-            fields = ('humanReadableName', 'id', 'provider_service', 'service_specific_id', 'vlan_id' )
+            fields = ('humanReadableName', 'id', 'provider_service', 'service_specific_id', 'vlan_id', 'computeNodeName' )
 
         def getHumanReadableName(self, obj):
             return obj.__unicode__()
 
+        def getComputeNodeName(self, obj):
+            vcpe = obj.vcpe
+            if not vcpe:
+                return None
+            sliver = vcpe.sliver
+            if not sliver:
+                return None
+            return sliver.node.name
+
 class VOLTTenantList(XOSListCreateAPIView):
     serializer_class = VOLTTenantIdSerializer
 
diff --git a/xos/core/xoslib/objects/cordsubscriber.py b/xos/core/xoslib/objects/cordsubscriber.py
index 1301f90..4719a13 100644
--- a/xos/core/xoslib/objects/cordsubscriber.py
+++ b/xos/core/xoslib/objects/cordsubscriber.py
@@ -33,6 +33,7 @@
                      ("url_filter_enable", "vcpe.url_filter_enable"),
                      ("url_filter_rules", "vcpe.url_filter_rules"),
                      ("url_filter_level", "vcpe.url_filter_level"),
+                     ("ssh_command", "vcpe.ssh_command"),
                      ("bbs_account", "vcpe.bbs_account"),
                      ("users", "vcpe.users"),
                      ("services", "vcpe.services"),
@@ -48,6 +49,7 @@
                      ("lan_ip", "vcpe.lan_ip"),
                      ("private_ip", "vcpe.private_ip"),
                      ("wan_ip", "vcpe.wan_ip"),
+                     ("wan_mac", "vcpe.wan_mac"),
                      )
 
     def __getattr__(self, key):
diff --git a/xos/core/xoslib/static/js/xosCord.js b/xos/core/xoslib/static/js/xosCord.js
index 99c6e94..f666b77 100644
--- a/xos/core/xoslib/static/js/xosCord.js
+++ b/xos/core/xoslib/static/js/xosCord.js
@@ -1,4 +1,4 @@
-OBJS = ['cordSubscriber', ]
+OBJS = ['cordSubscriber', 'cordUser']
 
 CordAdminApp = new XOSApplication({
     logTableId: "#logTable",
diff --git a/xos/core/xoslib/static/js/xoslib/xos-backbone.js b/xos/core/xoslib/static/js/xoslib/xos-backbone.js
index e7ee9cc..c4f1126 100644
--- a/xos/core/xoslib/static/js/xoslib/xos-backbone.js
+++ b/xos/core/xoslib/static/js/xoslib/xos-backbone.js
@@ -37,6 +37,7 @@
     HPCVIEW_API = XOSLIB_BASE + "/hpcview/";
 
     CORDSUBSCRIBER_API = XOSLIB_BASE + "/cordsubscriber/";
+    CORDUSER_API = XOSLIB_BASE + "/corduser/";
 
     XOSModel = Backbone.Model.extend({
         relatedCollections: [],
@@ -729,7 +730,8 @@
 
         define_model(this, {urlRoot: CORDSUBSCRIBER_API,
                             modelName: "cordSubscriber",
-                            listFields: ["id", "vlan_id", "routeable_subnet"],
+                            relatedCollections: {"cordUsers": "subscriber"},
+                            listFields: ["id", "service_specific_id", "vlan_id", "routeable_subnet"],
                             detailFields: ["id", "service_specific_id", "vcpe_id", "image_name", "sliver_name",
                                            "firewall_enable", "firewall_rules", "url_filter_enable", "url_filter_rules", "cdn_enable",
                                            "nat_ip", "lan_ip", "wan_ip", "private_ip",
@@ -740,6 +742,13 @@
                             disableAdd: true,
                             });
 
+        define_model(this, {urlRoot: CORDUSER_API,
+                            modelName: "cordUser",
+                            listFields: ["id", "subscriber", "name", "level", "mac"],
+                            detailFields: ["subscriber", "name", "level", "mac"],
+                            disableAdd: true,
+                            });
+
         /* 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
index dadf14f..67568ec 100644
--- a/xos/core/xoslib/templates/xosCordSubscriber.html
+++ b/xos/core/xoslib/templates/xosCordSubscriber.html
@@ -30,6 +30,7 @@
                                                                    <%= model.attributes.lan_ip %> (lan)<br>

                                                                    <%= model.attributes.nat_ip %> (nat)<br>

                                                                    <%= model.attributes.private_ip %> (private) </td></tr>

+  <tr><td class="xos-label-cell">SSH Command:</td><td><%= model.attributes.ssh_command %></td></tr>

   </table>

   </div>

 

diff --git a/xos/observers/vbng/steps/sync_vbngtenant.py b/xos/observers/vbng/steps/sync_vbngtenant.py
index 64494f8..b0dd345 100644
--- a/xos/observers/vbng/steps/sync_vbngtenant.py
+++ b/xos/observers/vbng/steps/sync_vbngtenant.py
@@ -12,7 +12,7 @@
 from hpc.models import HpcService, CDNPrefix
 from util.logger import Logger, logging
 
-VBNG_API = "http://<vnbg-addr>/onos/virtualbng/privateip/"
+VBNG_API = "http://10.0.3.136:8181/onos/virtualbng/privateip/"
 
 # hpclibrary will be in steps/..
 parentdir = os.path.join(os.path.dirname(__file__),"..")
@@ -37,14 +37,10 @@
         return objs
 
     def defer_sync(self, o, reason):
-        o.backend_register="{}"
-        o.backend_status = "2 - " + reason
-        o.save(update_fields=['enacted','backend_status','backend_register'])
         logger.info("defer object %s due to %s" % (str(o), reason))
+        raise Exception("defer object %s due to %s" % (str(o), reason))
 
-    def sync_record(self, o):
-        logger.info("sync'ing VBNGTenant %s" % str(o))
-
+    def get_private_ip(self, o):
         vcpes = VCPETenant.get_tenant_objects().all()
         vcpes = [x for x in vcpes if (x.vbng is not None) and (x.vbng.id == o.id)]
         if not vcpes:
@@ -60,26 +56,38 @@
 
         external_ns = None
         for ns in sliver.networkslivers.all():
-            if (ns.ip) and (ns.network.template.visibility=="private") and (ns.network.template.translation=="none"):
-                # need some logic here to find the right network
+            if (ns.ip) and ("WAN" in ns.network.template.name):
                 external_ns = ns
 
         if not external_ns:
-            self.defer_sync(o, "private network is not filled in yet")
+            self.defer_sync(o, "WAN network is not filled in yet")
             return
 
-        private_ip = external_ns.ip
+        return external_ns.ip
+
+    def sync_record(self, o):
+        logger.info("sync'ing VBNGTenant %s" % str(o))
 
         if not o.routeable_subnet:
-            print "This is where we would call Pingping's API"
-            o.routeable_subnet = "placeholder-from-observer"
+            private_ip = self.get_private_ip(o)
+            logger.info("contacting vBNG service to request mapping for private ip %s" % private_ip)
 
-            # r = requests.post(VBNG_API + "%s" % private_ip, )
-            # public_ip = r.json()
-            # o.routeable_subnet = public_ip
+            r = requests.post(VBNG_API + "%s" % private_ip, )
+            if (r.status_code != 200):
+                raise Exception("Received error from bng service (%d)" % r.status_code)
+            logger.info("received public IP %s from private IP %s" % (r.text, private_ip))
+            o.routeable_subnet = r.text
+            o.mapped_ip = private_ip
 
         o.save()
 
-    def delete_record(self, m):
-        pass
+    def delete_record(self, o):
+        logger.info("deleting VBNGTenant %s" % str(o))
+
+        if o.mapped_ip:
+            private_ip = o.mapped_ip
+            logger.info("contacting vBNG service to delete private ip %s" % private_ip)
+            r = requests.delete(VBNG_API + "%s" % private_ip, )
+            if (r.status_code != 200):
+                raise Exception("Received error from bng service (%d)" % r.status_code)
 
diff --git a/xos/observers/vbng/vbng_observer_config b/xos/observers/vbng/vbng_observer_config
index 9094bb1..b75d498 100644
--- a/xos/observers/vbng/vbng_observer_config
+++ b/xos/observers/vbng/vbng_observer_config
@@ -31,6 +31,7 @@
 #/var/log/hpc.log
 driver=None
 pretend=False
+backoff_disabled=True
 
 [feefie]
 client_id='vicci_dev_central'
diff --git a/xos/observers/vcpe/broadbandshield.py b/xos/observers/vcpe/broadbandshield.py
new file mode 100644
index 0000000..96f9944
--- /dev/null
+++ b/xos/observers/vcpe/broadbandshield.py
@@ -0,0 +1,353 @@
+import requests
+import logging
+import json
+import sys
+from rest_framework.exceptions import APIException
+
+""" format of settings
+
+    ["settings"]
+        ["watershed"]
+        ["rating"]
+        ["categories"]
+        ["blocklist"]
+        ["allowlist"]
+
+    ["users"]
+        array
+            ["account_id"] - 58
+            ["reporting"] - False
+            ["name"] - Scott1
+            ["devices"]
+            ["settings"] -
+                ["watershed"]
+                ["rating"]
+                ["categories"]
+                ["blocklist"]
+                ["allowlist"]
+
+    ["devices"]
+        array
+            ["username"] - "Scott1" or "" if whole-house
+            ["uuid"] - empty
+            ["mac_address"] - mac address as hex digits in ascii
+            ["type"] - "laptop"
+            ["name"] - human readable name of device ("Scott's laptop")
+            ["settings"]
+                 ["watershed"]
+                     array
+                         array
+                             ["rating"]
+                             ["category"]
+                 ["rating"] - ["G" | "NONE"]
+                 ["categories"] - list of categories set by rating
+                 ["blocklist"] - []
+                 ["allowlist"] - []
+"""
+
+class BBS_Failure(APIException):
+    status_code=400
+    def __init__(self, why="broadbandshield error", fields={}):
+        APIException.__init__(self, {"error": "BBS_Failure",
+                            "specific_error": why,
+                            "fields": fields})
+
+
+class BBS:
+    level_map = {"PG_13": "PG-13",
+                 None: "NONE"}
+
+    def __init__(self, username, password):
+        self.username = username
+        self.password = password
+        self.api = "https://www.broadbandshield.com/api"
+        self.session = None
+        self.settings = None
+
+    def login(self):
+        self.session = requests.Session()
+        r = self.session.post(self.api + "/login", data = json.dumps({"email": self.username, "password": self.password}))
+        if (r.status_code != 200):
+            raise BBS_Failure("Failed to login (%d)" % r.status_code)
+
+    def get_account(self):
+        if not self.session:
+            self.login()
+
+        r = self.session.get(self.api + "/account")
+        if (r.status_code != 200):
+            raise BBS_Failure("Failed to get account settings (%d)" % r.status_code)
+        self.settings = r.json()
+
+        return self.settings
+
+    def post_account(self):
+        if not self.settings:
+             raise XOSProgrammingError("no settings to post")
+
+        r = self.session.post(self.api + "/account/settings", data= json.dumps(self.settings))
+        if (r.status_code != 200):
+            raise BBS_Failure("Failed to set account settings (%d)" % r.status_code)
+
+    def add_device(self, name, mac, type="tablet", username=""):
+        data = {"name": name, "mac_address": mac, "type": type, "username": username}
+        r = self.session.post(self.api + "/device", data = json.dumps(data))
+        if (r.status_code != 200):
+            raise BBS_Failure("Failed to add device (%d)" % r.status_code)
+
+    def delete_device(self, data):
+        r = self.session.delete(self.api + "/device", data = json.dumps(data))
+        if (r.status_code != 200):
+            raise BBS_Failure("Failed to delete device (%d)" % r.status_code)
+
+    def add_user(self, name, rating="NONE", categories=[]):
+        data = {"name": name, "settings": {"rating": rating, "categories": categories}}
+        r = self.session.post(self.api + "/users", data = json.dumps(data))
+        if (r.status_code != 200):
+            raise BBS_Failure("Failed to add user (%d)" % r.status_code)
+
+    def delete_user(self, data):
+        r = self.session.delete(self.api + "/users", data = json.dumps(data))
+        if (r.status_code != 200):
+            raise BBS_Failure("Failed to delete user (%d)" % r.status_code)
+
+    def clear_users_and_devices(self):
+        if not self.settings:
+            self.get_account()
+
+        for device in self.settings["devices"]:
+            self.delete_device(device)
+
+        for user in self.settings["users"]:
+            self.delete_user(user)
+
+    def get_whole_home_level(self):
+        if not self.settings:
+            self.get_account()
+
+        return self.settings["settings"]["rating"]
+
+    def sync(self, whole_home_level, users):
+        if not self.settings:
+            self.get_account()
+
+        vcpe_users = {}
+        for user in users:
+            user = user.copy()
+            user["level"] = self.level_map.get(user["level"], user["level"])
+            user["mac"] = user.get("mac", "")
+            vcpe_users[user["name"]] = user
+
+        whole_home_level = self.level_map.get(whole_home_level, whole_home_level)
+
+        if (whole_home_level != self.settings["settings"]["rating"]):
+            print "*** set whole_home", whole_home_level, "***"
+            self.settings["settings"]["rating"] = whole_home_level
+            self.post_account()
+
+        bbs_usernames = [bbs_user["name"] for bbs_user in self.settings["users"]]
+        bbs_devicenames = [bbs_device["name"] for bbs_device in self.settings["devices"]]
+
+        add_users = []
+        add_devices = []
+        delete_users = []
+        delete_devices = []
+
+        for bbs_user in self.settings["users"]:
+             bbs_username = bbs_user["name"]
+             if bbs_username in vcpe_users.keys():
+                 vcpe_user = vcpe_users[bbs_username]
+                 if bbs_user["settings"]["rating"] != vcpe_user["level"]:
+                     print "set user", vcpe_user["name"], "rating", vcpe_user["level"]
+                     #bbs_user["settings"]["rating"] = vcpe_user["level"]
+                     # add can be used as an update
+                     add_users.append(vcpe_user)
+             else:
+                 delete_users.append(bbs_user)
+
+        for bbs_device in self.settings["devices"]:
+             bbs_devicename = bbs_device["name"]
+             if bbs_devicename in vcpe_users.keys():
+                 vcpe_user = vcpe_users[bbs_devicename]
+                 if bbs_device["mac_address"] != vcpe_user["mac"]:
+                     print "set device", vcpe_user["name"], "mac", vcpe_user["mac"]
+                     #bbs_device["mac_address"] = vcpe_user["mac"]
+                     # add of a device can't be used as an update, as you'll end
+                     # up with two of them.
+                     delete_devices.append(bbs_device)
+                     add_devices.append(vcpe_user)
+             else:
+                 delete_devices.append(bbs_device)
+
+        for (username, user) in vcpe_users.iteritems():
+            if not username in bbs_usernames:
+                add_users.append(user)
+            if not username in bbs_devicenames:
+                add_devices.append(user)
+
+        for bbs_user in delete_users:
+            print "delete user", bbs_user["name"]
+            self.delete_user(bbs_user)
+
+        for bbs_device in delete_devices:
+            print "delete device", bbs_device["name"]
+            self.delete_device(bbs_device)
+
+        for vcpe_user in add_users:
+            print "add user", vcpe_user["name"], "level", vcpe_user["level"]
+            self.add_user(vcpe_user["name"], vcpe_user["level"])
+
+        for vcpe_user in add_devices:
+            print "add device", vcpe_user["name"], "mac", vcpe_user["mac"]
+            self.add_device(vcpe_user["name"], vcpe_user["mac"], "tablet", vcpe_user["name"])
+
+    def get_whole_home_rating(self):
+        return self.settings["settings"]["rating"]
+
+    def get_user(self, name):
+        for user in self.settings["users"]:
+            if user["name"]==name:
+                return user
+        return None
+
+    def get_device(self, name):
+        for device in self.settings["devices"]:
+             if device["name"]==name:
+                 return device
+        return None
+
+    def dump(self):
+        if not self.settings:
+            self.get_account()
+
+        print "whole_home_rating:", self.settings["settings"]["rating"]
+        print "users:"
+        for user in self.settings["users"]:
+            print "  user", user["name"], "rating", user["settings"]["rating"]
+
+        print "devices:"
+        for device in self.settings["devices"]:
+            print "  device", device["name"], "user", device["username"], "rating", device["settings"]["rating"], "mac", device["mac_address"]
+
+def self_test():
+    if len(sys.argv)!=3:
+        print "syntax: broadbandshield.py <email> <password>"
+        sys.exit(-1)
+
+    bbs = BBS(sys.argv[1], sys.argv[2])
+
+    print "*** initial ***"
+    bbs.dump()
+
+    open("bbs.json","w").write(json.dumps(bbs.settings))
+
+    # a new BBS account will throw a 500 error if it has no rating
+    bbs.settings["settings"]["rating"] = "R"
+    #bbs.settings["settings"]["category"] = [u'PORNOGRAPHY', u'ADULT', u'ILLEGAL', u'WEAPONS', u'DRUGS', u'GAMBLING', u'CYBERBULLY', u'ANONYMIZERS', u'SUICIDE', u'MALWARE']
+    #bbs.settings["settings"]["blocklist"] = []
+    #bbs.settings["settings"]["allowlist"] = []
+    #for water in bbs.settings["settings"]["watershed"];
+    #    water["categories"]=[]
+    # delete everything
+    bbs.post_account()
+    bbs.clear_users_and_devices()
+
+    print "*** cleared ***"
+    bbs.settings=None
+    bbs.dump()
+
+    users = [{"name": "Moms pc", "level": "R", "mac": "010203040506"},
+             {"name": "Dads pc", "level": "R", "mac": "010203040507"},
+             {"name": "Jacks ipad", "level": "PG", "mac": "010203040508"},
+             {"name": "Jills iphone", "level": "G", "mac": "010203040509"}]
+
+    print "*** syncing mom-R, Dad-R, jack-PG, Jill-G, wholehome-PG-13 ***"
+
+    bbs.settings = None
+    bbs.sync("PG-13", users)
+
+    print "*** after sync ***"
+    bbs.settings=None
+    bbs.dump()
+    assert(bbs.get_whole_home_rating() == "PG-13")
+    assert(bbs.get_user("Moms pc")["settings"]["rating"] == "R")
+    assert(bbs.get_user("Dads pc")["settings"]["rating"] == "R")
+    assert(bbs.get_user("Jacks ipad")["settings"]["rating"] == "PG")
+    assert(bbs.get_user("Jills iphone")["settings"]["rating"] == "G")
+    assert(bbs.get_device("Moms pc")["mac_address"] == "010203040506")
+    assert(bbs.get_device("Dads pc")["mac_address"] == "010203040507")
+    assert(bbs.get_device("Jacks ipad")["mac_address"] == "010203040508")
+    assert(bbs.get_device("Jills iphone")["mac_address"] == "010203040509")
+
+    print "*** update whole home level ***"
+    bbs.settings=None
+    bbs.get_account()
+    bbs.settings["settings"]["rating"] = "PG"
+    bbs.post_account()
+
+    print "*** after sync ***"
+    bbs.settings=None
+    bbs.dump()
+    assert(bbs.get_whole_home_rating() == "PG")
+    assert(bbs.get_user("Moms pc")["settings"]["rating"] == "R")
+    assert(bbs.get_user("Dads pc")["settings"]["rating"] == "R")
+    assert(bbs.get_user("Jacks ipad")["settings"]["rating"] == "PG")
+    assert(bbs.get_user("Jills iphone")["settings"]["rating"] == "G")
+    assert(bbs.get_device("Moms pc")["mac_address"] == "010203040506")
+    assert(bbs.get_device("Dads pc")["mac_address"] == "010203040507")
+    assert(bbs.get_device("Jacks ipad")["mac_address"] == "010203040508")
+    assert(bbs.get_device("Jills iphone")["mac_address"] == "010203040509")
+
+    print "*** delete dad, change moms IP, change jills level to PG, change whole home to PG-13 ***"
+    users = [{"name": "Moms pc", "level": "R", "mac": "010203040511"},
+             {"name": "Jacks ipad", "level": "PG", "mac": "010203040508"},
+             {"name": "Jills iphone", "level": "PG", "mac": "010203040509"}]
+
+    bbs.settings = None
+    bbs.sync("PG-13", users)
+
+    print "*** after sync ***"
+    bbs.settings=None
+    bbs.dump()
+    assert(bbs.get_whole_home_rating() == "PG-13")
+    assert(bbs.get_user("Moms pc")["settings"]["rating"] == "R")
+    assert(bbs.get_user("Dads pc") == None)
+    assert(bbs.get_user("Jacks ipad")["settings"]["rating"] == "PG")
+    assert(bbs.get_user("Jills iphone")["settings"]["rating"] == "PG")
+    assert(bbs.get_device("Moms pc")["mac_address"] == "010203040511")
+    assert(bbs.get_device("Dads pc") == None)
+    assert(bbs.get_device("Jacks ipad")["mac_address"] == "010203040508")
+
+    print "add dad's laptop"
+    users = [{"name": "Moms pc", "level": "R", "mac": "010203040511"},
+             {"name": "Dads laptop", "level": "PG-13", "mac": "010203040512"},
+             {"name": "Jacks ipad", "level": "PG", "mac": "010203040508"},
+             {"name": "Jills iphone", "level": "PG", "mac": "010203040509"}]
+
+    bbs.settings = None
+    bbs.sync("PG-13", users)
+
+    print "*** after sync ***"
+    bbs.settings=None
+    bbs.dump()
+    assert(bbs.get_whole_home_rating() == "PG-13")
+    assert(bbs.get_user("Moms pc")["settings"]["rating"] == "R")
+    assert(bbs.get_user("Dads pc") == None)
+    assert(bbs.get_user("Dads laptop")["settings"]["rating"] == "PG-13")
+    assert(bbs.get_user("Jacks ipad")["settings"]["rating"] == "PG")
+    assert(bbs.get_user("Jills iphone")["settings"]["rating"] == "PG")
+    assert(bbs.get_device("Moms pc")["mac_address"] == "010203040511")
+    assert(bbs.get_device("Dads pc") == None)
+    assert(bbs.get_device("Dads laptop")["mac_address"] == "010203040512")
+    assert(bbs.get_device("Jacks ipad")["mac_address"] == "010203040508")
+
+    #bbs.add_user("tom", "G", [u'PORNOGRAPHY', u'ADULT', u'ILLEGAL', u'WEAPONS', u'DRUGS', u'GAMBLING', u'SOCIAL', u'CYBERBULLY', u'GAMES', u'ANONYMIZERS', u'SUICIDE', u'MALWARE'])
+    #bbs.add_device(name="tom's iphone", mac="010203040506", type="tablet", username="tom")
+
+def main():
+    self_test()
+
+if __name__ == "__main__":
+    main()
+
+
diff --git a/xos/observers/vcpe/steps/sync_vcpetenant.py b/xos/observers/vcpe/steps/sync_vcpetenant.py
index fc6dfee..a29ba09 100644
--- a/xos/observers/vcpe/steps/sync_vcpetenant.py
+++ b/xos/observers/vcpe/steps/sync_vcpetenant.py
@@ -15,6 +15,8 @@
 parentdir = os.path.join(os.path.dirname(__file__),"..")
 sys.path.insert(0,parentdir)
 
+from broadbandshield import BBS
+
 logger = Logger(level=logging.INFO)
 
 class SyncVCPETenant(SyncStep):
@@ -28,10 +30,8 @@
         SyncStep.__init__(self, **args)
 
     def defer_sync(self, o, reason):
-        o.backend_register="{}"
-        o.backend_status = "2 - " + reason
-        o.save(update_fields=['enacted','backend_status','backend_register'])
         logger.info("defer object %s due to %s" % (str(o), reason))
+        raise Exception("defer object %s due to %s" % (str(o), reason))
 
     def fetch_pending(self, deleted):
         if (not deleted):
@@ -51,6 +51,11 @@
             for slice in service.slices.all():
                 if "dnsdemux" in slice.name:
                     for sliver in slice.slivers.all():
+                        # Connect to a dnsdemux that's on the hpc_client network
+                        # if one is available.
+                        for ns in sliver.networkslivers.all():
+                            if ns.ip and ns.network.labels and ("hpc_client" in ns.network.labels):
+                                dnsdemux_ip = ns.ip
                         if dnsdemux_ip=="none":
                             try:
                                 dnsdemux_ip = socket.gethostbyname(sliver.node.name)
@@ -107,6 +112,10 @@
         fields.update(self.get_extra_attributes(o))
         run_template_ssh(self.template_name, fields)
 
+        if o.url_filter_enable:
+            bbs = BBS(o.bbs_account, "123")
+            bbs.sync(o.url_filter_level, o.users)
+
         o.save()
 
     def delete_record(self, m):
diff --git a/xos/observers/vcpe/steps/sync_vcpetenant.yaml b/xos/observers/vcpe/steps/sync_vcpetenant.yaml
index 0dc9f48..3593020 100644
--- a/xos/observers/vcpe/steps/sync_vcpetenant.yaml
+++ b/xos/observers/vcpe/steps/sync_vcpetenant.yaml
@@ -24,6 +24,8 @@
       lan_ip: {{ lan_ip }}
       wan_ip: {{ wan_ip }}
       private_ip: {{ private_ip }}
+      hpc_client_ip: {{ hpc_client_ip }}
+      wan_mac: {{ wan_mac }}
 
   tasks:
   - name: Docker repository
diff --git a/xos/observers/vcpe/templates/start-vcpe.sh.j2 b/xos/observers/vcpe/templates/start-vcpe.sh.j2
index b01681c..efa8473 100755
--- a/xos/observers/vcpe/templates/start-vcpe.sh.j2
+++ b/xos/observers/vcpe/templates/start-vcpe.sh.j2
@@ -12,8 +12,9 @@
 fi
 
 # Set up networking via pipework
-docker exec vcpe ifconfig eth0 >> /dev/null || pipework eth3 -i eth0 vcpe {{ wan_ip }}/17@192.168.128.1 {{ wan_mac }}
-docker exec vcpe ifconfig eth1 >> /dev/null || pipework eth2 -i eth1 vcpe 192.168.0.1/24 @{{ vlan_ids[0] }}
+docker exec vcpe ifconfig eth0 >> /dev/null || pipework eth4 -i eth0 vcpe {{ wan_ip }}/17@192.168.128.1 {{ wan_mac }}
+docker exec vcpe ifconfig eth1 >> /dev/null || pipework eth3 -i eth1 vcpe 192.168.0.1/24 @{{ vlan_ids[0] }}
+docker exec vcpe ifconfig eth2 >> /dev/null || pipework eth0 -i eth2 vcpe {{ hpc_client_ip }}/16
 
 # Now can start up dnsmasq
 docker exec vcpe service dnsmasq start