Merge branch 'master' of github.com:open-cloud/xos
diff --git a/xos/cord/models.py b/xos/cord/models.py
index 483b76b..cab7909 100644
--- a/xos/cord/models.py
+++ b/xos/cord/models.py
@@ -203,7 +203,7 @@
         # There's a bit of a race here; some other user could be trying to
         # allocate a bbs_account at the same time we are.
 
-        for i in range(1,21):
+        for i in range(2,21):
              account_name = "bbs%02d@onlab.us" % i
              if (account_name not in bbs_accounts):
                  return account_name
@@ -236,7 +236,8 @@
                           "cdn_enable": False,
                           "sliver_id": None,
                           "users": [],
-                          "bbs_account": None}
+                          "bbs_account": None,
+                          "last_ansible_hash": None}
 
     def __init__(self, *args, **kwargs):
         super(VCPETenant, self).__init__(*args, **kwargs)
@@ -389,6 +390,14 @@
         return self.set_attribute("bbs_account", value)
 
     @property
+    def last_ansible_hash(self):
+        return self.get_attribute("last_ansible_hash", self.default_attributes["last_ansible_hash"])
+
+    @last_ansible_hash.setter
+    def last_ansible_hash(self, value):
+        return self.set_attribute("last_ansible_hash", value)
+
+    @property
     def ssh_command(self):
         if self.sliver:
             return self.sliver.get_ssh_command()
@@ -513,6 +522,14 @@
     def hpc_client_ip(self):
         return self.addresses.get("hpc_client",None)
 
+    @property
+    def is_synced(self):
+        return (self.enacted is not None) and (self.enacted >= self.updated)
+
+    @is_synced.setter
+    def is_synced(self, value):
+        pass
+
     def pick_node(self):
         nodes = list(Node.objects.all())
         # TODO: logic to filter nodes by which nodes are up, and which
@@ -595,7 +612,10 @@
             if not self.bbs_account:
                 # make sure we use the proxied VCPEService object, not the generic Service object
                 vcpe_service = VCPEService.objects.get(id=self.provider_service.id)
-                self.bbs_account = vcpe_service.allocate_bbs_account()
+                if self.service_specific_id=="SYNCME":
+                    self.bbs_account = "bbs01@onlab.us"
+                else:
+                    self.bbs_account = vcpe_service.allocate_bbs_account()
                 super(VCPETenant, self).save()
         else:
             if self.bbs_account:
diff --git a/xos/core/xoslib/methods/cordsubscriber.py b/xos/core/xoslib/methods/cordsubscriber.py
index 79a1442..5e8a60c 100644
--- a/xos/core/xoslib/methods/cordsubscriber.py
+++ b/xos/core/xoslib/methods/cordsubscriber.py
@@ -51,6 +51,8 @@
 
         wan_mac = ReadOnlyField()
 
+        vcpe_synced = serializers.BooleanField()
+
         humanReadableName = serializers.SerializerMethodField("getHumanReadableName")
 
         class Meta:
@@ -62,6 +64,7 @@
                       'url_filter_enable', 'url_filter_rules', 'url_filter_level',
                       'bbs_account',
                       'ssh_command',
+                      'vcpe_synced',
                       'cdn_enable', 'vbng_id', 'routeable_subnet', 'nat_ip', 'lan_ip', 'wan_ip', 'private_ip', 'wan_mac')
 
 
@@ -197,6 +200,7 @@
     @classmethod
     def get_urlpatterns(self):
         patterns = super(CordSubscriberViewSet, self).get_urlpatterns()
+        patterns.append( self.detail_url("vcpe_synced/$", {"get": "get_vcpe_synced"}, "vcpe_synced") )
         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("services/$", {"get": "get_services"}, "services") )
@@ -227,6 +231,10 @@
 
         return Response({"subscribers": serializer.data})
 
+    def get_vcpe_synced(self, request, pk=None):
+        subscriber = self.get_object()
+        return Response({"vcpe_synced": subscriber.vcpe_synced})
+
     def get_url_filter(self, request, pk=None):
         subscriber = self.get_object()
         return Response({"level": subscriber.url_filter_level})
@@ -314,10 +322,10 @@
     def setup_demo_vcpe(self, voltTenant):
         # nuke the users and start over
         voltTenant.vcpe.users = []
-        voltTenant.vcpe.create_user(name="Mom's PC",      mac="01020303040506", level="PG_13")
-        voltTenant.vcpe.create_user(name="Dad's PC",      mac="01020304040507", level="PG_13")
-        voltTenant.vcpe.create_user(name="Jack's iPhone", mac="01020304050508", level="PG_13")
-        voltTenant.vcpe.create_user(name="Jill's iPad",   mac="01020304050609", level="PG_13")
+        voltTenant.vcpe.create_user(name="Mom's PC",      mac="010203040506", level="PG_13")
+        voltTenant.vcpe.create_user(name="Dad's PC",      mac="90E2Ba82F975", level="PG_13")
+        voltTenant.vcpe.create_user(name="Jack's iPhone", mac="A85B780F2651", level="PG_13")
+        voltTenant.vcpe.create_user(name="Jill's iPad",   mac="010203040509", level="PG_13")
         voltTenant.vcpe.save()
 
     def initdemo(self, request):
diff --git a/xos/core/xoslib/objects/cordsubscriber.py b/xos/core/xoslib/objects/cordsubscriber.py
index 4719a13..1a376ad 100644
--- a/xos/core/xoslib/objects/cordsubscriber.py
+++ b/xos/core/xoslib/objects/cordsubscriber.py
@@ -50,6 +50,7 @@
                      ("private_ip", "vcpe.private_ip"),
                      ("wan_ip", "vcpe.wan_ip"),
                      ("wan_mac", "vcpe.wan_mac"),
+                     ("vcpe_synced", "vcpe.is_synced"),
                      )
 
     def __getattr__(self, key):
diff --git a/xos/core/xoslib/templates/xosCordSubscriber.html b/xos/core/xoslib/templates/xosCordSubscriber.html
index 67568ec..52a62f2 100644
--- a/xos/core/xoslib/templates/xosCordSubscriber.html
+++ b/xos/core/xoslib/templates/xosCordSubscriber.html
@@ -17,6 +17,7 @@
   <h3>vCPE</h3>

   <table class="xos-detail-table cord-subscriber-table">

   <tr><td class="xos-label-cell">Id:</td><td><%= model.attributes.vcpe_id %></td></tr>

+  <tr><td class="xos-label-cell">Synced:</td><td><% if (model.attributes.vcpe_synced) { print("Yes"); } else { print("No"); } %></td></tr>

   <tr><td class="xos-label-cell">Image:</td><td><%= model.attributes.image_name %></td></tr>

   <tr><td class="xos-label-cell">Sliver Id:</td><td><%= model.attributes.sliver %></td></tr>

   <tr><td class="xos-label-cell">Firewall:</td><td><input type="checkbox" name="firewall_enable" <% if (model.attributes.firewall_enable) print("checked"); %>>Enable<br>

diff --git a/xos/observers/vbng/steps/sync_vbngtenant.py b/xos/observers/vbng/steps/sync_vbngtenant.py
index 9dec0de..07d1635 100644
--- a/xos/observers/vbng/steps/sync_vbngtenant.py
+++ b/xos/observers/vbng/steps/sync_vbngtenant.py
@@ -77,8 +77,8 @@
             logger.info("received public IP %s from private IP %s" % (r.text, private_ip))
             o.routeable_subnet = r.text
             o.mapped_ip = private_ip
-            c.mapped_mac = private_mac
-            c.mapped_hostname = private_hostname
+            o.mapped_mac = private_mac
+            o.mapped_hostname = private_hostname
 
         o.save()
 
diff --git a/xos/observers/vcpe/steps/sync_vcpetenant.py b/xos/observers/vcpe/steps/sync_vcpetenant.py
index d2afed5..910cc4c 100644
--- a/xos/observers/vcpe/steps/sync_vcpetenant.py
+++ b/xos/observers/vcpe/steps/sync_vcpetenant.py
@@ -1,7 +1,9 @@
+import hashlib
 import os
 import socket
 import sys
 import base64
+import time
 from django.db.models import F, Q
 from xos.config import Config
 from observer.syncstep import SyncStep
@@ -110,7 +112,16 @@
                  fields[attribute_name] = getattr(o, attribute_name)
 
         fields.update(self.get_extra_attributes(o))
-        run_template_ssh(self.template_name, fields)
+
+        ansible_hash = hashlib.md5(repr(sorted(fields.items()))).hexdigest()
+        quick_update = (o.last_ansible_hash == ansible_hash)
+
+        if quick_update:
+            logger.info("quick_update triggered; skipping ansible recipe")
+        else:
+            tStart = time.time()
+            run_template_ssh(self.template_name, fields)
+            logger.info("playbook execution time %d" % int(time.time()-tStart))
 
         if o.url_filter_enable:
             if (str(o.service_specific_id) != "SYNCME"):
@@ -118,9 +129,12 @@
                 # Also fix the spot in cord/models.py
                 logger.info("skipping sync of URL filter for SSID %s" % str(o.service_specific_id))
             else:
+                tStart = time.time()
                 bbs = BBS(o.bbs_account, "123")
                 bbs.sync(o.url_filter_level, o.users)
+                logger.info("bbs update tiem %d" % int(time.time()-tStart))
 
+        o.last_ansible_hash = ansible_hash
         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 a280b33..5adb144 100644
--- a/xos/observers/vcpe/steps/sync_vcpetenant.yaml
+++ b/xos/observers/vcpe/steps/sync_vcpetenant.yaml
@@ -1,5 +1,6 @@
 ---
 - hosts: {{ sliver_name }}
+  gather_facts: False
   connection: ssh
   user: ubuntu
   sudo: yes
@@ -28,6 +29,7 @@
       wan_mac: {{ wan_mac }}
 
   tasks:
+{% if full_setup %}
   - name: Docker repository
     copy: src=/opt/xos/observers/vcpe/files/docker.list
       dest=/etc/apt/sources.list.d/docker.list
@@ -54,14 +56,17 @@
 
   - name: make sure /etc/dnsmasq.d exists
     file: path=/etc/dnsmasq.d state=directory owner=root group=root
-   
+{% endif %}
+
   - name: dnsmasq config
     template: src=/opt/xos/observers/vcpe/templates/dnsmasq_servers.j2 dest=/etc/dnsmasq.d/servers.conf owner=root group=root
     notify:
     - restart vcpe
 
-  - name: networking info
-    template: src=/opt/xos/observers/vcpe/templates/vlan_sample.j2 dest=/etc/vlan_sample owner=root group=root
+# These are samples, not necessary for correct function of demo
+
+#  - name: networking info
+#    template: src=/opt/xos/observers/vcpe/templates/vlan_sample.j2 dest=/etc/vlan_sample owner=root group=root
 
   - name: firewall info
     template: src=/opt/xos/observers/vcpe/templates/firewall_sample.j2 dest=/etc/firewall_sample owner=root group=root
diff --git a/xos/openstack_observer/syncstep.py b/xos/openstack_observer/syncstep.py
index fb06bdd..3dadbf4 100644
--- a/xos/openstack_observer/syncstep.py
+++ b/xos/openstack_observer/syncstep.py
@@ -150,7 +150,7 @@
                     try:
                         error = self.error_map.map(str_e)
                     except:
-                        error = '2 - %s'%str_e
+                        error = '%s'%str_e
 
                     if isinstance(e, InnocuousException) and not force_error:
                         o.backend_status = '1 - %s'%error