Revert "Delete observers dir"

This reverts commit 7c5e0cae8320fa42c5127eea2ea3acf0e3a596ce.
diff --git a/xos/observers/__init__.py b/xos/observers/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/xos/observers/__init__.py
@@ -0,0 +1 @@
+
diff --git a/xos/observers/base/SyncInstanceUsingAnsible.py b/xos/observers/base/SyncInstanceUsingAnsible.py
new file mode 100644
index 0000000..aafbd85
--- /dev/null
+++ b/xos/observers/base/SyncInstanceUsingAnsible.py
@@ -0,0 +1,221 @@
+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 synchronizers.base.syncstep import SyncStep
+from synchronizers.base.ansible import run_template_ssh
+from core.models import Service, Slice, ControllerSlice, ControllerUser
+from util.logger import Logger, logging
+
+logger = Logger(level=logging.INFO)
+
+class SyncInstanceUsingAnsible(SyncStep):
+    # All of the following should be defined for classes derived from this
+    # base class. Examples below use VCPETenant.
+
+    # provides=[VCPETenant]
+    # observes=VCPETenant
+    # requested_interval=0
+    # template_name = "sync_vcpetenant.yaml"
+    # service_key_name = "/opt/xos/observers/vcpe/vcpe_private_key"
+
+    def __init__(self, **args):
+        SyncStep.__init__(self, **args)
+
+    def defer_sync(self, o, reason):
+        logger.info("defer object %s due to %s" % (str(o), reason))
+        raise Exception("defer object %s due to %s" % (str(o), reason))
+
+    def get_extra_attributes(self, o):
+        # This is a place to include extra attributes that aren't part of the
+        # object itself.
+
+        return {}
+
+    def get_instance(self, o):
+        # We need to know what instance is associated with the object. Let's
+        # assume 'o' has a field called 'instance'. If the field is called
+        # something else, or if custom logic is needed, then override this
+        # method.
+
+        return o.instance
+
+    def run_playbook(self, o, fields, template_name=None):
+        if not template_name:
+            template_name = self.template_name
+        tStart = time.time()
+        run_template_ssh(template_name, fields)
+        logger.info("playbook execution time %d" % int(time.time()-tStart))
+
+    def pre_sync_hook(self, o, fields):
+        pass
+
+    def post_sync_hook(self, o, fields):
+        pass
+
+    def sync_fields(self, o, fields):
+        self.run_playbook(o, fields)
+
+    def prepare_record(self, o):
+        pass
+
+    def get_node(self,o):
+        return o.node
+
+    def get_node_key(self, node):
+        return "/root/setup/node_key"
+
+    def get_ansible_fields(self, instance):
+        # return all of the fields that tell Ansible how to talk to the context
+        # that's setting up the container.
+
+        if (instance.isolation == "vm"):
+            # legacy where container was configured by sync_vcpetenant.py
+
+            fields = { "instance_name": instance.name,
+                       "hostname": instance.node.name,
+                       "instance_id": instance.instance_id,
+                       "username": "ubuntu",
+                     }
+            key_name = self.service_key_name
+        elif (instance.isolation == "container"):
+            # container on bare metal
+            node = self.get_node(instance)
+            hostname = node.name
+            fields = { "hostname": hostname,
+                       "baremetal_ssh": True,
+                       "instance_name": "rootcontext",
+                       "username": "root",
+                       "container_name": "%s-%s" % (instance.slice.name, str(instance.id))
+                     }
+            key_name = self.get_node_key(node)
+        else:
+            # container in a VM
+            if not instance.parent:
+                raise Exception("Container-in-VM has no parent")
+            if not instance.parent.instance_id:
+                raise Exception("Container-in-VM parent is not yet instantiated")
+            if not instance.parent.slice.service:
+                raise Exception("Container-in-VM parent has no service")
+            if not instance.parent.slice.service.private_key_fn:
+                raise Exception("Container-in-VM parent service has no private_key_fn")
+            fields = { "hostname": instance.parent.node.name,
+                       "instance_name": instance.parent.name,
+                       "instance_id": instance.parent.instance_id,
+                       "username": "ubuntu",
+                       "nat_ip": instance.parent.get_ssh_ip(),
+                       "container_name": "%s-%s" % (instance.slice.name, str(instance.id))
+                         }
+            key_name = instance.parent.slice.service.private_key_fn
+
+        if not os.path.exists(key_name):
+            raise Exception("Node key %s does not exist" % key_name)
+
+        key = file(key_name).read()
+
+        fields["private_key"] = key
+
+        # now the ceilometer stuff
+
+        cslice = ControllerSlice.objects.get(slice=instance.slice)
+        if not cslice:
+            raise Exception("Controller slice object for %s does not exist" % instance.slice.name)
+
+        cuser = ControllerUser.objects.get(user=instance.creator)
+        if not cuser:
+            raise Exception("Controller user object for %s does not exist" % instance.creator)
+
+        fields.update({"keystone_tenant_id": cslice.tenant_id,
+                       "keystone_user_id": cuser.kuser_id,
+                       "rabbit_user": instance.controller.rabbit_user,
+                       "rabbit_password": instance.controller.rabbit_password,
+                       "rabbit_host": instance.controller.rabbit_host})
+
+        return fields
+
+    def sync_record(self, o):
+        logger.info("sync'ing object %s" % str(o))
+
+        self.prepare_record(o)
+
+        instance = self.get_instance(o)
+
+        if isinstance(instance, basestring):
+            # sync to some external host
+
+            # XXX - this probably needs more work...
+
+            fields = { "hostname": instance,
+                       "instance_id": "ubuntu",     # this is the username to log into
+                       "private_key": service.key,
+                     }
+        else:
+            # sync to an XOS instance
+            if not instance:
+                self.defer_sync(o, "waiting on instance")
+                return
+
+            if not instance.instance_name:
+                self.defer_sync(o, "waiting on instance.instance_name")
+                return
+
+            fields = self.get_ansible_fields(instance)
+
+            fields["ansible_tag"] =  o.__class__.__name__ + "_" + str(o.id)
+
+        # If 'o' defines a 'sync_attributes' list, then we'll copy those
+        # attributes into the Ansible recipe's field list automatically.
+        if hasattr(o, "sync_attributes"):
+            for attribute_name in o.sync_attributes:
+                fields[attribute_name] = getattr(o, attribute_name)
+
+        fields.update(self.get_extra_attributes(o))
+
+        self.sync_fields(o, fields)
+
+        o.save()
+
+    def delete_record(self, o):
+        try:
+            controller = o.get_controller()
+            controller_register = json.loads(o.node.site_deployment.controller.backend_register)
+
+            if (controller_register.get('disabled',False)):
+                raise InnocuousException('Controller %s is disabled'%o.node.site_deployment.controller.name)
+        except AttributeError:
+            pass
+
+        instance = self.get_instance(o)
+        if isinstance(instance, basestring):
+            # sync to some external host
+
+            # XXX - this probably needs more work...
+
+            fields = { "hostname": instance,
+                       "instance_id": "ubuntu",     # this is the username to log into
+                       "private_key": service.key,
+                     }
+        else:
+            # sync to an XOS instance
+            fields = self.get_ansible_fields(instance)
+
+            fields["ansible_tag"] =  o.__class__.__name__ + "_" + str(o.id)
+
+        # If 'o' defines a 'sync_attributes' list, then we'll copy those
+        # attributes into the Ansible recipe's field list automatically.
+        if hasattr(o, "sync_attributes"):
+            for attribute_name in o.sync_attributes:
+                fields[attribute_name] = getattr(o, attribute_name)
+
+        fields.update(self.map_delete_inputs(o))
+
+        fields['delete']=True
+        res = self.run_playbook(o,fields)
+        try:
+                self.map_delete_outputs(o,res)
+        except AttributeError:
+                pass
diff --git a/xos/observers/base/SyncSliverUsingAnsible.py b/xos/observers/base/SyncSliverUsingAnsible.py
new file mode 100644
index 0000000..a76b300
--- /dev/null
+++ b/xos/observers/base/SyncSliverUsingAnsible.py
@@ -0,0 +1,95 @@
+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 synchronizers.base.syncstep import SyncStep
+from synchronizers.base.ansible import run_template_ssh
+from core.models import Service, Slice
+from util.logger import Logger, logging
+
+logger = Logger(level=logging.INFO)
+
+class SyncInstanceUsingAnsible(SyncStep):
+    # All of the following should be defined for classes derived from this
+    # base class. Examples below use VCPETenant.
+
+    # provides=[VCPETenant]
+    # observes=VCPETenant
+    # requested_interval=0
+    # template_name = "sync_vcpetenant.yaml"
+    # service_key_name = "/opt/xos/observers/vcpe/vcpe_private_key"
+
+    def __init__(self, **args):
+        SyncStep.__init__(self, **args)
+
+    def defer_sync(self, o, reason):
+        logger.info("defer object %s due to %s" % (str(o), reason))
+        raise Exception("defer object %s due to %s" % (str(o), reason))
+
+    def get_extra_attributes(self, o):
+        # This is a place to include extra attributes that aren't part of the
+        # object itself.
+
+        return {}
+
+    def get_instance(self, o):
+        # We need to know what instance is associated with the object. Let's
+        # assume 'o' has a field called 'instance'. If the field is called
+        # something else, or if custom logic is needed, then override this
+        # method.
+
+        return o.instance
+
+    def run_playbook(self, o, fields):
+        tStart = time.time()
+        run_template_ssh(self.template_name, fields)
+        logger.info("playbook execution time %d" % int(time.time()-tStart))
+
+    def pre_sync_hook(self, o, fields):
+        pass
+
+    def post_sync_hook(self, o, fields):
+        pass
+
+    def sync_fields(self, o, fields):
+        self.run_playbook(o, fields)
+
+    def sync_record(self, o):
+        logger.info("sync'ing object %s" % str(o))
+
+        instance = self.get_instance(o)
+        if not instance:
+            self.defer_sync(o, "waiting on instance")
+            return
+
+        if not os.path.exists(self.service_key_name):
+            raise Exception("Service key %s does not exist" % self.service_key_name)
+
+        service_key = file(self.service_key_name).read()
+
+        fields = { "instance_name": instance.name,
+                   "hostname": instance.node.name,
+                   "instance_id": instance.instance_id,
+                   "private_key": service_key,
+                   "ansible_tag": "vcpe_tenant_" + str(o.id)
+                 }
+
+        # If 'o' defines a 'sync_attributes' list, then we'll copy those
+        # attributes into the Ansible recipe's field list automatically.
+        if hasattr(o, "sync_attributes"):
+            for attribute_name in o.sync_attributes:
+                fields[attribute_name] = getattr(o, attribute_name)
+
+        fields.update(self.get_extra_attributes(o))
+
+        self.sync_fields(o, fields)
+
+        o.save()
+
+    def delete_record(self, m):
+        pass
+
diff --git a/xos/observers/base/__init__.py b/xos/observers/base/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/xos/observers/base/__init__.py
@@ -0,0 +1 @@
+
diff --git a/xos/observers/helloworld/helloworld-observer.py b/xos/observers/helloworld/helloworld-observer.py
new file mode 100755
index 0000000..d6a71ff
--- /dev/null
+++ b/xos/observers/helloworld/helloworld-observer.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+
+# This imports and runs ../../xos-observer.py
+
+import importlib
+import os
+import sys
+observer_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"../..")
+sys.path.append(observer_path)
+mod = importlib.import_module("xos-observer")
+mod.main()
diff --git a/xos/observers/helloworld/helloworld_config b/xos/observers/helloworld/helloworld_config
new file mode 100644
index 0000000..e32ee0c
--- /dev/null
+++ b/xos/observers/helloworld/helloworld_config
@@ -0,0 +1,47 @@
+[plc]
+name=plc
+deployment=plc
+
+[db]
+name=xos
+user=postgres
+password=password
+host=localhost
+port=5432
+
+[api]
+host=localhost
+port=8000
+ssl_key=None
+ssl_cert=None
+ca_ssl_cert=None
+ratelimit_enabled=0
+omf_enabled=0
+mail_support_address=support@localhost
+nova_enabled=True
+logfile=/var/log/xos.log
+
+[nova]
+admin_user=admin@domain.com
+admin_password=admin
+admin_tenant=admin
+url=http://localhost:5000/v2.0/
+default_image=None
+default_flavor=m1.small
+default_security_group=default
+ca_ssl_cert=/etc/ssl/certs/ca-certificates.crt
+
+[observer]
+pretend=False
+backoff_disabled=False
+images_directory=/opt/xos/images
+dependency_graph=/opt/xos/model-deps
+logfile=/var/log/xos_backend.log
+steps_dir=/opt/xos/observers/helloworld/steps
+applist=helloworld
+
+[gui]
+disable_minidashboard=True
+#branding_name=CORD
+#branding_css=/static/cord.css
+#branding_icon=/static/onos-logo.png
diff --git a/xos/observers/helloworld/model-deps b/xos/observers/helloworld/model-deps
new file mode 100644
index 0000000..63188f0
--- /dev/null
+++ b/xos/observers/helloworld/model-deps
@@ -0,0 +1,19 @@
+{
+    "OriginServer": [
+        "ContentProvider"
+    ], 
+    "ContentProvider": [
+        "ServiceProvider"
+    ], 
+    "CDNPrefix": [
+        "ContentProvider"
+    ], 
+    "AccessMap": [
+        "ContentProvider"
+    ], 
+    "SiteMap": [
+        "ContentProvider", 
+        "ServiceProvider", 
+        "CDNPrefix"
+    ]
+}
diff --git a/xos/observers/helloworld/nohup.out b/xos/observers/helloworld/nohup.out
new file mode 100644
index 0000000..74072c6
--- /dev/null
+++ b/xos/observers/helloworld/nohup.out
@@ -0,0 +1 @@
+python: can't open file 'helloworld-observer.py': [Errno 2] No such file or directory
diff --git a/xos/observers/helloworld/run.sh b/xos/observers/helloworld/run.sh
new file mode 100755
index 0000000..f56ffe3
--- /dev/null
+++ b/xos/observers/helloworld/run.sh
@@ -0,0 +1,6 @@
+#if [[ ! -e ./hpc-backend.py ]]; then
+#    ln -s ../xos-observer.py hpc-backend.py
+#fi
+
+export XOS_DIR=/opt/xos
+python helloworld-observer.py  -C $XOS_DIR/observers/helloworld/helloworld_config
diff --git a/xos/observers/helloworld/start.sh b/xos/observers/helloworld/start.sh
new file mode 100755
index 0000000..89240cd
--- /dev/null
+++ b/xos/observers/helloworld/start.sh
@@ -0,0 +1,4 @@
+export XOS_DIR=/opt/xos
+
+echo $XOS_DIR/observers/helloworld/helloworld_config
+python helloworld-observer.py -C $XOS_DIR/observers/helloworld/helloworld_config
diff --git a/xos/observers/helloworld/steps/sync_hello.py b/xos/observers/helloworld/steps/sync_hello.py
new file mode 100644
index 0000000..82600e7
--- /dev/null
+++ b/xos/observers/helloworld/steps/sync_hello.py
@@ -0,0 +1,26 @@
+import os
+import sys
+import base64
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from services.helloworld.models import Hello,World
+from util.logger import Logger, logging
+
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+logger = Logger(level=logging.INFO)
+
+class SyncHello(SyncStep):
+    provides=[Hello]
+    observes=Hello
+    requested_interval=0
+    
+    def sync_record(self, record):
+        instance = record.instance_backref        
+        instance.userData="packages:\n  - apache2\nruncmd:\n  - update-rc.d apache2 enable\n  - service apache2 start\nwrite_files:\n-   content: Hello %s\n    path: /var/www/html/hello.txt"%record.name
+        instance.save()
+        
+    def delete_record(self, m):
+        return
diff --git a/xos/observers/helloworld/steps/sync_world.py b/xos/observers/helloworld/steps/sync_world.py
new file mode 100644
index 0000000..0a121df
--- /dev/null
+++ b/xos/observers/helloworld/steps/sync_world.py
@@ -0,0 +1,25 @@
+import os
+import sys
+import base64
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from services.helloworld.models import Hello,World
+from util.logger import Logger, logging
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+logger = Logger(level=logging.INFO)
+
+class SyncWorld(SyncStep):
+    provides=[World]
+    observes=World
+    requested_interval=0
+    
+    def sync_record(self, record):
+        open('/tmp/hello-synchronizer','w').write(record.name)	
+        
+    def delete_record(self, m):
+        return
diff --git a/xos/observers/helloworld/stop.sh b/xos/observers/helloworld/stop.sh
new file mode 100755
index 0000000..a0b4a8e
--- /dev/null
+++ b/xos/observers/helloworld/stop.sh
@@ -0,0 +1 @@
+pkill -9 -f hpc-observer.py
diff --git a/xos/observers/helloworldservice_complete/helloworldservice-observer.py b/xos/observers/helloworldservice_complete/helloworldservice-observer.py
new file mode 100755
index 0000000..75dcc46
--- /dev/null
+++ b/xos/observers/helloworldservice_complete/helloworldservice-observer.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python
+
+# This imports and runs ../../xos-observer.py
+# Runs the standard XOS observer
+
+import importlib
+import os
+import sys
+observer_path = os.path.join(os.path.dirname(
+    os.path.realpath(__file__)), "../..")
+sys.path.append(observer_path)
+mod = importlib.import_module("xos-observer")
+mod.main()
diff --git a/xos/observers/helloworldservice_complete/helloworldservice_config b/xos/observers/helloworldservice_complete/helloworldservice_config
new file mode 100644
index 0000000..716e3a0
--- /dev/null
+++ b/xos/observers/helloworldservice_complete/helloworldservice_config
@@ -0,0 +1,36 @@
+# Required by XOS
+[db]
+name=xos
+user=postgres
+password=password
+host=localhost
+port=5432
+
+# Required by XOS
+[api]
+nova_enabled=True
+
+# Sets options for the observer
+[observer]
+# Optional name
+name=helloworldservice
+# This is the location to the dependency graph you generate
+dependency_graph=/opt/xos/observers/helloworldservice_complete/model-deps
+# The location of your SyncSteps
+steps_dir=/opt/xos/observers/helloworldservice_complete/steps
+# A temporary directory that will be used by ansible
+sys_dir=/opt/xos/observers/helloworldservice_complete/sys
+# Location of the file to save logging messages to the backend log is often used
+logfile=/var/log/xos_backend.log
+# If this option is true, then nothing will change, we simply pretend to run
+pretend=False
+# If this is False then XOS will use an exponential backoff when the observer
+# fails, since we will be waiting for an instance, we don't want this.
+backoff_disabled=True
+# We want the output from ansible to be logged
+save_ansible_output=True
+# This determines how we SSH to a client, if this is set to True then we try
+# to ssh using the instance name as a proxy, if this is disabled we ssh using
+# the NAT IP of the instance. On CloudLab the first option will fail so we must
+# set this to False
+proxy_ssh=False
diff --git a/xos/observers/helloworldservice_complete/model-deps b/xos/observers/helloworldservice_complete/model-deps
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/xos/observers/helloworldservice_complete/model-deps
@@ -0,0 +1 @@
+{}
diff --git a/xos/observers/helloworldservice_complete/run.sh b/xos/observers/helloworldservice_complete/run.sh
new file mode 100755
index 0000000..6bce079
--- /dev/null
+++ b/xos/observers/helloworldservice_complete/run.sh
@@ -0,0 +1,3 @@
+# Runs the XOS observer using helloworldservice_config
+export XOS_DIR=/opt/xos
+python helloworldservice-observer.py  -C $XOS_DIR/observers/helloworldservice_complete/helloworldservice_config
diff --git a/xos/observers/helloworldservice_complete/steps/sync_helloworldtenant.py b/xos/observers/helloworldservice_complete/steps/sync_helloworldtenant.py
new file mode 100644
index 0000000..5604296
--- /dev/null
+++ b/xos/observers/helloworldservice_complete/steps/sync_helloworldtenant.py
@@ -0,0 +1,48 @@
+import os
+import sys
+from django.db.models import Q, F
+from services.helloworldservice_complete.models import HelloWorldServiceComplete, HelloWorldTenantComplete
+from observers.base.SyncInstanceUsingAnsible import SyncInstanceUsingAnsible
+
+parentdir = os.path.join(os.path.dirname(__file__), "..")
+sys.path.insert(0, parentdir)
+
+# Class to define how we sync a tenant. Using SyncInstanceUsingAnsible we
+# indicate where the find the YAML for ansible, where to find the SSH key,
+# and the logic for determining what tenant needs updating, what additional
+# attributes are needed, and how to delete an instance.
+class SyncHelloWorldTenantComplete(SyncInstanceUsingAnsible):
+    # Indicates the position in the data model, this will run when XOS needs to
+    # enact a HelloWorldTenantComplete
+    provides = [HelloWorldTenantComplete]
+    # The actual model being enacted, usually the same as provides.
+    observes = HelloWorldTenantComplete
+    # Number of milliseconds between interruptions of the observer
+    requested_interval = 0
+    # The ansible template to run
+    template_name = "sync_helloworldtenant.yaml"
+    # The location of the SSH private key to use when ansible connects to
+    # instances.
+    service_key_name = "/opt/xos/observers/helloworldservice_complete/helloworldservice_private_key"
+
+    def __init__(self, *args, **kwargs):
+        super(SyncHelloWorldTenantComplete, self).__init__(*args, **kwargs)
+
+    # Defines the logic for determining what HelloWorldTenantCompletes need to be
+    # enacted.
+    def fetch_pending(self, deleted):
+        # If the update is not a deletion, then we get all of the instnaces that
+        # have been updated or have not been enacted.
+        if (not deleted):
+            objs = HelloWorldTenantComplete.get_tenant_objects().filter(
+                Q(enacted__lt=F('updated')) | Q(enacted=None), Q(lazy_blocked=False))
+        else:
+            # If this is a deletion we get all of the deleted tenants..
+            objs = HelloWorldTenantComplete.get_deleted_tenant_objects()
+
+        return objs
+
+    # Gets the attributes that are used by the Ansible template but are not
+    # part of the set of default attributes.
+    def get_extra_attributes(self, o):
+        return {"display_message": o.display_message}
diff --git a/xos/observers/helloworldservice_complete/steps/sync_helloworldtenant.yaml b/xos/observers/helloworldservice_complete/steps/sync_helloworldtenant.yaml
new file mode 100644
index 0000000..719c75f
--- /dev/null
+++ b/xos/observers/helloworldservice_complete/steps/sync_helloworldtenant.yaml
@@ -0,0 +1,18 @@
+---
+- hosts: {{ instance_name }}
+  gather_facts: False
+  connection: ssh
+  user: ubuntu
+  sudo: yes
+  tasks:
+  - name: install apache
+    apt: name=apache2 state=present update_cache=yes
+
+  - name: write message
+    shell: echo "{{ display_message }}" > /var/www/html/index.html
+
+  - name: stop apache
+    service: name=apache2 state=stopped
+
+  - name: start apache
+    service: name=apache2 state=started
diff --git a/xos/observers/helloworldservice_complete/stop.sh b/xos/observers/helloworldservice_complete/stop.sh
new file mode 100755
index 0000000..76e68d9
--- /dev/null
+++ b/xos/observers/helloworldservice_complete/stop.sh
@@ -0,0 +1,2 @@
+# Kill the observer
+pkill -9 -f helloworldservice-observer.py
diff --git a/xos/observers/hpc/Makefile b/xos/observers/hpc/Makefile
new file mode 100644
index 0000000..4a03bd4
--- /dev/null
+++ b/xos/observers/hpc/Makefile
@@ -0,0 +1,10 @@
+hpcobserver.tar.gz:
+	rm -rf BUILD/hpc_observer
+	mkdir -p BUILD/hpc_observer
+	mkdir -p BUILD/hpc_observer/steps
+	mkdir -p BUILD/hpc_observer/deleters
+	cp hpclib.py hpc_observer_config run.sh start.sh  stop.sh BUILD/hpc_observer/
+	cp steps/*.py BUILD/hpc_observer/steps/
+	cp deleters/*.py BUILD/hpc_observer/deleters/
+	mkdir -p TARS
+	cd BUILD; tar -czf ../TARS/hpc_observer.tar.gz hpc_observer
diff --git a/xos/observers/hpc/fsck.py b/xos/observers/hpc/fsck.py
new file mode 100644
index 0000000..a6f641b
--- /dev/null
+++ b/xos/observers/hpc/fsck.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python
+import argparse
+import imp
+import inspect
+import os
+import sys
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+sys.path.append("/opt/xos")
+from xos.config import Config, DEFAULT_CONFIG_FN, XOS_DIR
+from util.logger import Logger, logging
+from synchronizers.base.syncstep import SyncStep
+
+try:
+    from django import setup as django_setup # django 1.7
+except:
+    django_setup = False
+
+logger = Logger(level=logging.INFO)
+
+class XOSConsistencyCheck:
+	def __init__(self):
+                self.sync_steps = []
+		self.load_sync_step_modules()
+
+	def load_sync_step_modules(self, step_dir=None):
+		if step_dir is None:
+			if hasattr(Config(), "observer_steps_dir"):
+				step_dir = Config().observer_steps_dir
+			else:
+				step_dir = XOS_DIR+"/observer/steps"
+
+		for fn in os.listdir(step_dir):
+			pathname = os.path.join(step_dir,fn)
+			if os.path.isfile(pathname) and fn.endswith(".py") and (fn!="__init__.py"):
+				module = imp.load_source(fn[:-3],pathname)
+				for classname in dir(module):
+					c = getattr(module, classname, None)
+
+					# make sure 'c' is a descendent of SyncStep and has a
+					# provides field (this eliminates the abstract base classes
+					# since they don't have a provides)
+
+					if inspect.isclass(c) and issubclass(c, SyncStep) and hasattr(c,"provides") and (c not in self.sync_steps):
+						self.sync_steps.append(c)
+		logger.info('loaded sync steps: %s' % ",".join([x.__name__ for x in self.sync_steps]))
+
+        def run(self):
+            updated = True
+            while updated:
+                updated = False
+
+                for step in self.sync_steps:
+                    if hasattr(step, "consistency_check"):
+                        updated = updated or step(driver=None).consistency_check()
+
+                if updated:
+                    logger.info('re-running consistency checks because something changed')
+
+def main():
+    if not "-C" in sys.argv:
+        print >> sys.stderr, "You probably wanted to use -C " + XOS_DIR + "/hpc_observer/hpc_observer_config"
+
+    # Generate command line parser
+    parser = argparse.ArgumentParser(usage='%(prog)s [options]')
+    # smbaker: util/config.py parses sys.argv[] directly to get config file name; include the option here to avoid
+    #   throwing unrecognized argument exceptions
+    parser.add_argument('-C', '--config', dest='config_file', action='store', default=DEFAULT_CONFIG_FN,
+                        help='Name of config file.')
+    args = parser.parse_args()
+
+    if django_setup: # 1.7
+        django_setup()
+
+    cc = XOSConsistencyCheck()
+    cc.run()
+
+if __name__ == '__main__':
+    main()
+
diff --git a/xos/observers/hpc/hpc-observer.py b/xos/observers/hpc/hpc-observer.py
new file mode 100755
index 0000000..d6a71ff
--- /dev/null
+++ b/xos/observers/hpc/hpc-observer.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+
+# This imports and runs ../../xos-observer.py
+
+import importlib
+import os
+import sys
+observer_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"../..")
+sys.path.append(observer_path)
+mod = importlib.import_module("xos-observer")
+mod.main()
diff --git a/xos/observers/hpc/hpc_observer_config b/xos/observers/hpc/hpc_observer_config
new file mode 100644
index 0000000..326e731
--- /dev/null
+++ b/xos/observers/hpc/hpc_observer_config
@@ -0,0 +1,36 @@
+
+[plc]
+name=plc
+deployment=VICCI
+
+[db]
+name=xos
+user=postgres
+password=password
+host=localhost
+port=5432
+
+[api]
+host=128.112.171.237
+port=8000
+ssl_key=None
+ssl_cert=None
+ca_ssl_cert=None
+ratelimit_enabled=0
+omf_enabled=0
+mail_support_address=support@localhost
+nova_enabled=True
+
+[observer]
+name=hpc
+dependency_graph=/opt/xos/observers/hpc/model-deps
+steps_dir=/opt/xos/observers/hpc/steps
+deleters_dir=/opt/xos/observers/hpc/deleters
+log_file=console
+#/var/log/hpc.log
+driver=None
+#cmi_hostname=openclouddev0.internet2.edu
+
+[feefie]
+client_id='vicci_dev_central'
+user_id='pl'
diff --git a/xos/observers/hpc/hpc_watcher.py b/xos/observers/hpc/hpc_watcher.py
new file mode 100644
index 0000000..d2efdcc
--- /dev/null
+++ b/xos/observers/hpc/hpc_watcher.py
@@ -0,0 +1,629 @@
+"""
+    hpc_watcher.py
+
+    Daemon to watch the health of HPC and RR instances.
+
+    This deamon uses HpcHealthCheck objects in the Data Model to conduct
+    periodic tests of HPC and RR nodes. Two types of Health Checks are
+    supported:
+
+       kind="dns": checks the request routers to make sure that a DNS
+         name is resolveable and returns the right kind of records.
+
+         resource_name should be set to the domain name to lookup.
+
+         result_contains is option and can be used to hold "A", "CNAME", or
+            a particular address or hostname that should be contained in the
+            query's answer.
+
+       kind="http": checks the hpc nodes to make sure that a URL can be
+         retrieved from the node.
+
+         resource_name should be set to the HostName:Url to fetch. For
+         example, cdn-stream.htm.fiu.edu:/hft2441/intro.mp4
+
+     In addition to the above, HPC heartbeat probes are conducted, similar to
+     the ones that dnsredir conducts.
+
+     The results of health checks are stored in a tag attached to the Instance
+     the healthcheck was conducted against. If all healthchecks of a particular
+     variety were successful for a instance, then "success" will be stored in
+     the tag. Otherwise, the first healthcheck to fail will be stored in the
+     tag.
+
+     Ubuntu prereqs:
+         apt-get install python-pycurl
+         pip install dnslib
+"""
+
+import os
+import socket
+import sys
+sys.path.append("/opt/xos")
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+import django
+from django.contrib.contenttypes.models import ContentType
+from core.models import *
+from services.hpc.models import *
+from services.requestrouter.models import *
+django.setup()
+import time
+import pycurl
+import traceback
+import json
+from StringIO import StringIO
+
+from dnslib.dns import DNSRecord,DNSHeader,DNSQuestion,QTYPE
+from dnslib.digparser import DigParser
+
+from threading import Thread, Condition
+
+"""
+from dnslib import *
+q = DNSRecord(q=DNSQuestion("cdn-stream.htm.fiu.edu"))
+a_pkt = q.send("150.135.65.10", tcp=False, timeout=10)
+a = DNSRecord.parse(a_pkt)
+
+from dnslib import *
+q = DNSRecord(q=DNSQuestion("onlab.vicci.org"))
+a_pkt = q.send("150.135.65.10", tcp=False, timeout=10)
+a = DNSRecord.parse(a_pkt)
+"""
+
+class WorkQueue:
+    def __init__(self):
+        self.job_cv = Condition()
+        self.jobs = []
+        self.result_cv = Condition()
+        self.results = []
+        self.outstanding = 0
+
+    def get_job(self):
+        self.job_cv.acquire()
+        while not self.jobs:
+            self.job_cv.wait()
+        result = self.jobs.pop()
+        self.job_cv.release()
+        return result
+
+    def submit_job(self, job):
+        self.job_cv.acquire()
+        self.jobs.append(job)
+        self.job_cv.notify()
+        self.job_cv.release()
+        self.outstanding = self.outstanding + 1
+
+    def get_result(self):
+        self.result_cv.acquire()
+        while not self.results:
+            self.result_cv.wait()
+        result = self.results.pop()
+        self.result_cv.release()
+        self.outstanding = self.outstanding - 1
+        return result
+
+    def submit_result(self, result):
+        self.result_cv.acquire()
+        self.results.append(result)
+        self.result_cv.notify()
+        self.result_cv.release()
+
+class DnsResolver(Thread):
+    def __init__(self, queue):
+        Thread.__init__(self)
+        self.queue = queue
+        self.daemon = True
+        self.start()
+
+    def run(self):
+        while True:
+            job = self.queue.get_job()
+            self.handle_job(job)
+            self.queue.submit_result(job)
+
+    def handle_job(self, job):
+        domain = job["domain"]
+        server = job["server"]
+        port = job["port"]
+        result_contains = job.get("result_contains", None)
+
+        try:
+            q = DNSRecord(q=DNSQuestion(domain)) #, getattr(QTYPE,"A")))
+
+            a_pkt = q.send(server, port, tcp=False, timeout=10)
+            a = DNSRecord.parse(a_pkt)
+
+            found_record = False
+            for record in a.rr:
+                if (not result_contains):
+                    QTYPE_A = getattr(QTYPE,"A")
+                    QTYPE_CNAME = getattr(QTYPE, "CNAME")
+                    if ((record.rtype==QTYPE_A) or (record.qtype==QTYPE_CNAME)):
+                        found_record = True
+                else:
+                    tmp = QTYPE.get(record.rtype) + str(record.rdata)
+                    if (result_contains in tmp):
+                        found_record = True
+
+            if not found_record:
+                if result_contains:
+                    job["status"] =  "%s,No %s records" % (domain, result_contains)
+                else:
+                    job["status"] =  "%s,No A or CNAME records" % domain
+
+                return
+
+        except Exception, e:
+            job["status"] = "%s,Exception: %s" % (domain, str(e))
+            return
+
+        job["status"] = "success"
+
+class HpcHeartbeat(Thread):
+    def __init__(self, queue):
+        Thread.__init__(self)
+        self.queue = queue
+        self.daemon = True
+        self.start()
+
+    def run(self):
+        while True:
+            job = self.queue.get_job()
+            self.handle_job(job)
+            self.queue.submit_result(job)
+
+    def curl_error_message(self, e):
+        if e.args[0] == 6:
+            return "couldn't resolve host"
+        if e.args[0] == 7:
+            return "failed to connect"
+        return "curl error %d" % e.args[0]
+
+    def handle_job(self, job):
+        server = job["server"]
+        port = job["port"]
+
+        try:
+            buffer = StringIO()
+            c = pycurl.Curl()
+
+            c.setopt(c.URL, "http://%s:%s/heartbeat" % (server, port))
+            c.setopt(c.WRITEDATA, buffer)
+            c.setopt(c.HTTPHEADER, ['host: hpc-heartbeat', 'X-heartbeat: 1'])
+            c.setopt(c.TIMEOUT, 10)
+            c.setopt(c.CONNECTTIMEOUT, 10)
+            c.setopt(c.NOSIGNAL, 1)
+
+            try:
+                c.perform()
+                response_code = c.getinfo(c.RESPONSE_CODE)
+            except Exception, e:
+                #traceback.print_exc()
+                job["status"] = self.curl_error_message(e)
+                return
+            finally:
+                c.close()
+
+            if response_code != 200:
+                job["status"] = "error response %d" % response_code
+                return
+
+        except Exception, e:
+            job["status"] = "Exception: %s" % str(e)
+            return
+
+        job["status"] = "success"
+
+class HpcFetchUrl(Thread):
+    def __init__(self, queue):
+        Thread.__init__(self)
+        self.queue = queue
+        self.daemon = True
+        self.start()
+
+    def run(self):
+        while True:
+            job = self.queue.get_job()
+            self.handle_job(job)
+            self.queue.submit_result(job)
+
+    def curl_error_message(self, e):
+        if e.args[0] == 6:
+            return "couldn't resolve host"
+        if e.args[0] == 7:
+            return "failed to connect"
+        return "curl error %d" % e.args[0]
+
+    def handle_job(self, job):
+        server = job["server"]
+        port = job["port"]
+        url = job["url"]
+        domain = job["domain"]
+
+        def progress(download_t, download_d, upload_t, upload_d):
+            # limit download size to a megabyte
+            if (download_d > 1024*1024):
+                return 1
+            else:
+                return 0
+
+        try:
+            buffer = StringIO()
+            c = pycurl.Curl()
+
+            c.setopt(c.URL, "http://%s:%s/%s" % (server, port, url))
+            c.setopt(c.WRITEDATA, buffer)
+            c.setopt(c.HTTPHEADER, ['host: ' + domain])
+            c.setopt(c.TIMEOUT, 10)
+            c.setopt(c.CONNECTTIMEOUT, 10)
+            c.setopt(c.NOSIGNAL, 1)
+            c.setopt(c.NOPROGRESS, 0)
+            c.setopt(c.PROGRESSFUNCTION, progress)
+
+            try:
+                try:
+                    c.perform()
+                except Exception, e:
+                    # prevent callback abort from raising exception
+                    if (e.args[0] != pycurl.E_ABORTED_BY_CALLBACK):
+                        raise
+                response_code = c.getinfo(c.RESPONSE_CODE)
+                bytes_downloaded = int(c.getinfo(c.SIZE_DOWNLOAD))
+                total_time = float(c.getinfo(c.TOTAL_TIME))
+            except Exception, e:
+                #traceback.print_exc()
+                job["status"] = self.curl_error_message(e)
+                return
+            finally:
+                c.close()
+
+            if response_code != 200:
+                job["status"] = "error response %s" %  str(response_code)
+                return
+
+        except Exception, e:
+            #traceback.print_exc()
+            job["status"] = "Exception: %s" % str(e)
+            return
+
+        job["status"] = "success"
+        job["bytes_downloaded"] = bytes_downloaded
+        job["total_time"] = total_time
+
+class WatcherWorker(Thread):
+    def __init__(self, queue):
+        Thread.__init__(self)
+        self.queue = queue
+        self.daemon = True
+        self.start()
+
+    def run(self):
+        while True:
+            job = self.queue.get_job()
+            self.handle_job(job)
+            self.queue.submit_result(job)
+
+    def curl_error_message(self, e):
+        if e.args[0] == 6:
+            return "couldn't resolve host"
+        if e.args[0] == 7:
+            return "failed to connect"
+        return "curl error %d" % e.args[0]
+
+    def handle_job(self, job):
+        server = job["server"]
+        port = job["port"]
+
+        try:
+            buffer = StringIO()
+            c = pycurl.Curl()
+
+            c.setopt(c.URL, "http://%s:%s/" % (server, port))
+            c.setopt(c.WRITEDATA, buffer)
+            c.setopt(c.TIMEOUT, 10)
+            c.setopt(c.CONNECTTIMEOUT, 10)
+            c.setopt(c.NOSIGNAL, 1)
+
+            try:
+                c.perform()
+                response_code = c.getinfo(c.RESPONSE_CODE)
+            except Exception, e:
+                #traceback.print_exc()
+                job["status"] = json.dumps( {"status": self.curl_error_message(e)} )
+                return
+            finally:
+                c.close()
+
+            if response_code != 200:
+                job["status"] = json.dumps( {"status": "error response %d" % response_code} )
+                return
+
+            d = json.loads(buffer.getvalue())
+            d["status"] = "success";
+            job["status"] = json.dumps(d)
+
+        except Exception, e:
+            job["status"] = json.dumps( {"status": "Exception: %s" % str(e)} )
+            return
+
+class BaseWatcher(Thread):
+    def __init__(self):
+        Thread.__init__(self)
+        self.daemon = True
+
+    def get_public_ip(self, service, instance):
+        network_name = None
+        if "hpc" in instance.slice.name:
+            network_name = getattr(service, "watcher_hpc_network", None)
+        elif "demux" in instance.slice.name:
+            network_name = getattr(service, "watcher_dnsdemux_network", None)
+        elif "redir" in instance.slice.name:
+            network_name = getattr(service, "watcher_dnsredir_network", None)
+
+        if network_name and network_name.lower()=="nat":
+            return None
+
+        if (network_name is None) or (network_name=="") or (network_name.lower()=="public"):
+            return instance.get_public_ip()
+
+        for ns in instance.ports.all():
+            if (ns.ip) and (ns.network.name==network_name):
+                return ns.ip
+
+        raise ValueError("Couldn't find network %s" % str(network_name))
+
+    def set_status(self, instance, service, kind, msg, check_error=True):
+        #print instance.node.name, kind, msg
+        if check_error:
+            instance.has_error = (msg!="success")
+
+        instance_type = ContentType.objects.get_for_model(instance)
+
+        t = Tag.objects.filter(service=service, name=kind+".msg", content_type__pk=instance_type.id, object_id=instance.id)
+        if t:
+            t=t[0]
+            if (t.value != msg):
+                t.value = msg
+                t.save()
+        else:
+            Tag(service=service, name=kind+".msg", content_object = instance, value=msg).save()
+
+        t = Tag.objects.filter(service=service, name=kind+".time", content_type__pk=instance_type.id, object_id=instance.id)
+        if t:
+            t=t[0]
+            t.value = str(time.time())
+            t.save()
+        else:
+            Tag(service=service, name=kind+".time", content_object = instance, value=str(time.time())).save()
+
+    def get_service_slices(self, service, kind=None):
+        try:
+            slices = service.slices.all()
+        except:
+            # buggy data model
+            slices = service.service.all()
+
+        if kind:
+            return [x for x in slices if (kind in x.name)]
+        else:
+            return list(slices)
+
+class RRWatcher(BaseWatcher):
+    def __init__(self):
+        BaseWatcher.__init__(self)
+
+        self.resolver_queue = WorkQueue()
+        for i in range(0,10):
+            DnsResolver(queue = self.resolver_queue)
+
+    def check_request_routers(self, service, instances):
+        for instance in instances:
+            instance.has_error = False
+
+            try:
+                ip = self.get_public_ip(service, instance)
+            except Exception, e:
+                self.set_status(instance, service, "watcher.DNS", "exception: %s" % str(e))
+                continue
+            if not ip:
+                try:
+                    ip = socket.gethostbyname(instance.node.name)
+                except:
+                    self.set_status(instance, service, "watcher.DNS", "dns resolution failure")
+                    continue
+
+            if not ip:
+                self.set_status(instance, service, "watcher.DNS", "no IP address")
+                continue
+
+            checks = HpcHealthCheck.objects.filter(kind="dns")
+            if not checks:
+                self.set_status(instance, service, "watcher.DNS", "no DNS HealthCheck tests configured")
+
+            for check in checks:
+                self.resolver_queue.submit_job({"domain": check.resource_name, "server": ip, "port": 53, "instance": instance, "result_contains": check.result_contains})
+
+        while self.resolver_queue.outstanding > 0:
+            result = self.resolver_queue.get_result()
+            instance = result["instance"]
+            if (result["status"]!="success") and (not instance.has_error):
+                self.set_status(instance, service, "watcher.DNS", result["status"])
+
+        for instance in instances:
+            if not instance.has_error:
+                self.set_status(instance, service, "watcher.DNS", "success")
+
+    def run_once(self):
+        for hpcService in HpcService.objects.all():
+            for slice in self.get_service_slices(hpcService, "dnsdemux"):
+                self.check_request_routers(hpcService, slice.instances.all())
+
+        for rrService in RequestRouterService.objects.all():
+            for slice in self.get_service_slices(rrService, "dnsdemux"):
+                self.check_request_routers(rrService, slice.instances.all())
+
+    def run(self):
+        while True:
+            self.run_once()
+            time.sleep(10)
+
+            django.db.reset_queries()
+
+class HpcProber(BaseWatcher):
+    def __init__(self):
+        BaseWatcher.__init__(self)
+
+        self.heartbeat_queue = WorkQueue()
+        for i in range(0, 10):
+            HpcHeartbeat(queue = self.heartbeat_queue)
+
+    def probe_hpc(self, service, instances):
+        for instance in instances:
+            instance.has_error = False
+
+            self.heartbeat_queue.submit_job({"server": instance.node.name, "port": 8009, "instance": instance})
+
+        while self.heartbeat_queue.outstanding > 0:
+            result = self.heartbeat_queue.get_result()
+            instance = result["instance"]
+            if (result["status"]!="success") and (not instance.has_error):
+                self.set_status(instance, service, "watcher.HPC-hb", result["status"])
+
+        for instance in instances:
+            if not instance.has_error:
+                self.set_status(instance, service, "watcher.HPC-hb", "success")
+
+    def run_once(self):
+        for hpcService in HpcService.objects.all():
+            for slice in self.get_service_slices(hpcService, "hpc"):
+                self.probe_hpc(hpcService, slice.instances.all())
+
+    def run(self):
+        while True:
+            self.run_once()
+            time.sleep(10)
+
+            django.db.reset_queries()
+
+class HpcFetcher(BaseWatcher):
+    def __init__(self):
+        BaseWatcher.__init__(self)
+
+        self.fetch_queue = WorkQueue()
+        for i in range(0, 10):
+            HpcFetchUrl(queue = self.fetch_queue)
+
+    def fetch_hpc(self, service, instances):
+        for instance in instances:
+            instance.has_error = False
+            instance.url_status = []
+
+            checks = HpcHealthCheck.objects.filter(kind="http")
+            if not checks:
+                self.set_status(instance, service, "watcher.HPC-fetch", "no HTTP HealthCheck tests configured")
+
+            for check in checks:
+                if (not check.resource_name) or (":" not in check.resource_name):
+                    self.set_status(instance, service, "watcher.HPC-fetch", "malformed resource_name: " + str(check.resource_name))
+                    break
+
+                (domain, url) = check.resource_name.split(":",1)
+
+                self.fetch_queue.submit_job({"server": instance.node.name, "port": 80, "instance": instance, "domain": domain, "url": url})
+
+        while self.fetch_queue.outstanding > 0:
+            result = self.fetch_queue.get_result()
+            instance = result["instance"]
+            if (result["status"] == "success"):
+                instance.url_status.append( (result["domain"] + result["url"], "success", result["bytes_downloaded"], result["total_time"]) )
+            if (result["status"]!="success") and (not instance.has_error):
+                self.set_status(instance, service, "watcher.HPC-fetch", result["status"])
+
+        for instance in instances:
+            self.set_status(instance, service, "watcher.HPC-fetch-urls", json.dumps(instance.url_status), check_error=False)
+            if not instance.has_error:
+                self.set_status(instance, service, "watcher.HPC-fetch", "success")
+
+    def run_once(self):
+        for hpcService in HpcService.objects.all():
+            for slice in self.get_service_slices(hpcService, "hpc"):
+                try:
+                    self.fetch_hpc(hpcService, slice.instances.all())
+                except:
+                    traceback.print_exc()
+
+    def run(self):
+        while True:
+            self.run_once()
+            time.sleep(10)
+
+            django.db.reset_queries()
+
+class WatcherFetcher(BaseWatcher):
+    def __init__(self):
+        BaseWatcher.__init__(self)
+
+        self.fetch_queue = WorkQueue()
+        for i in range(0, 10):
+             WatcherWorker(queue = self.fetch_queue)
+
+    def fetch_watcher(self, service, instances):
+        for instance in instances:
+            try:
+                ip = self.get_public_ip(service, instance)
+            except Exception, e:
+                self.set_status(instance, service, "watcher.watcher", json.dumps({"status": "exception: %s" % str(e)}) )
+                continue
+            if not ip:
+                try:
+                    ip = socket.gethostbyname(instance.node.name)
+                except:
+                    self.set_status(instance, service, "watcher.watcher", json.dumps({"status": "dns resolution failure"}) )
+                    continue
+
+            if not ip:
+                self.set_status(instance, service, "watcher.watcher", json.dumps({"status": "no IP address"}) )
+                continue
+
+            port = 8015
+            if ("redir" in instance.slice.name):
+                port = 8016
+            elif ("demux" in instance.slice.name):
+                port = 8017
+
+            self.fetch_queue.submit_job({"server": ip, "port": port, "instance": instance})
+
+        while self.fetch_queue.outstanding > 0:
+            result = self.fetch_queue.get_result()
+            instance = result["instance"]
+            self.set_status(instance, service, "watcher.watcher", result["status"])
+
+    def run_once(self):
+        for hpcService in HpcService.objects.all():
+            for slice in self.get_service_slices(hpcService):
+                self.fetch_watcher(hpcService, slice.instances.all())
+
+    def run(self):
+        while True:
+            self.run_once()
+            time.sleep(10)
+
+            django.db.reset_queries()
+
+
+if __name__ == "__main__":
+    if "--once" in sys.argv:
+        RRWatcher().run_once()
+        HpcProber().run_once()
+        HpcFetcher().run_once()
+        WatcherFetcher().run_once()
+    else:
+        RRWatcher().start()
+        HpcProber().start()
+        HpcFetcher().start()
+        WatcherFetcher().start()
+
+        print "Running forever..."
+        while True:
+            time.sleep(60)
+
diff --git a/xos/observers/hpc/hpclib.py b/xos/observers/hpc/hpclib.py
new file mode 100644
index 0000000..88abf23
--- /dev/null
+++ b/xos/observers/hpc/hpclib.py
@@ -0,0 +1,126 @@
+import os
+import base64
+import string
+import sys
+import xmlrpclib
+
+if __name__ == '__main__':
+    sys.path.append("/opt/xos")
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+
+from xos.config import Config
+from core.models import Service
+from services.hpc.models import HpcService
+from services.requestrouter.models import RequestRouterService
+from util.logger import Logger, logging
+
+logger = Logger(level=logging.INFO)
+
+class APIHelper:
+    def __init__(self, proxy, auth, method=None):
+        self.proxy = proxy
+        self.auth = auth
+        self.method = method
+
+    def __getattr__(self, name):
+        if name.startswith("_"):
+            return getattr(self, name)
+        else:
+            return APIHelper(self.proxy, self.auth, name)
+
+    def __call__(self, *args):
+        method = getattr(self.proxy, self.method)
+        return method(self.auth, *args)
+
+class CmiClient:
+    def __init__(self, hostname, port=8003, username="apiuser", password="apiuser"):
+        self.connect_api(hostname, port, username, password)
+
+    def connect_api(self, hostname, port=8003, username="apiuser", password="apiuser"):
+        #print "https://%s:%d/COAPI/" % (hostname, port)
+        cob = xmlrpclib.ServerProxy("https://%s:%d/COAPI/" % (hostname, port), allow_none=True)
+        cob_auth = {}
+        cob_auth["Username"] = username
+        cob_auth["AuthString"] = password
+        cob_auth["AuthMethod"] = "password"
+
+        onev = xmlrpclib.ServerProxy("https://%s:%d/ONEV_API/" % (hostname, port), allow_none=True)
+        onev_auth = {}
+        onev_auth["Username"] = username
+        onev_auth["AuthString"] = password
+        onev_auth["AuthMethod"] = "password"
+
+        self.cob = APIHelper(cob, cob_auth)
+        self.onev = APIHelper(onev, onev_auth)
+
+class HpcLibrary:
+    def __init__(self):
+        self._client = None
+
+    def make_account_name(self, x):
+        x=x.lower()
+        y = ""
+        for c in x:
+            if (c in (string.lowercase + string.digits)):
+                y = y + c
+        return y[:20]
+
+    def get_hpc_service(self):
+        hpc_service_name = getattr(Config(), "observer_hpc_service", None)
+        if hpc_service_name:
+            hpc_service = HpcService.objects.filter(name = hpc_service_name)
+        else:
+            hpc_service = HpcService.objects.all()
+
+        if not hpc_service:
+            if hpc_service_name:
+                raise Exception("No HPC Service with name %s" % hpc_service_name)
+            else:
+                raise Exception("No HPC Services")
+        hpc_service = hpc_service[0]
+
+        return hpc_service
+
+    def get_cmi_hostname(self, hpc_service=None):
+        if getattr(Config(),"observer_cmi_hostname",None):
+            return getattr(Config(),"observer_cmi_hostname")
+
+        if (hpc_service is None):
+            hpc_service = self.get_hpc_service()
+
+        if hpc_service.cmi_hostname:
+            return hpc_service.cmi_hostname
+
+        try:
+            slices = hpc_service.slices.all()
+        except:
+            # deal with buggy data model
+            slices = hpc_service.service.all()
+
+        for slice in slices:
+            if slice.name.endswith("cmi"):
+                for instance in slice.instances.all():
+                    if instance.node:
+                         return instance.node.name
+
+        raise Exception("Failed to find a CMI instance")
+
+    @property
+    def client(self):
+        if self._client is None:
+            self._client = CmiClient(self.get_cmi_hostname())
+        return self._client
+
+if __name__ == '__main__':
+    import django
+    django.setup()
+
+    lib = HpcLibrary()
+
+    print "testing API connection to", lib.get_cmi_hostname()
+    lib.client.cob.GetNewObjects()
+    lib.client.onev.ListAll("CDN")
+
+
+
+
diff --git a/xos/observers/hpc/model-deps b/xos/observers/hpc/model-deps
new file mode 100644
index 0000000..63188f0
--- /dev/null
+++ b/xos/observers/hpc/model-deps
@@ -0,0 +1,19 @@
+{
+    "OriginServer": [
+        "ContentProvider"
+    ], 
+    "ContentProvider": [
+        "ServiceProvider"
+    ], 
+    "CDNPrefix": [
+        "ContentProvider"
+    ], 
+    "AccessMap": [
+        "ContentProvider"
+    ], 
+    "SiteMap": [
+        "ContentProvider", 
+        "ServiceProvider", 
+        "CDNPrefix"
+    ]
+}
diff --git a/xos/observers/hpc/run.sh b/xos/observers/hpc/run.sh
new file mode 100755
index 0000000..f77d751
--- /dev/null
+++ b/xos/observers/hpc/run.sh
@@ -0,0 +1,6 @@
+#if [[ ! -e ./hpc-backend.py ]]; then
+#    ln -s ../xos-observer.py hpc-backend.py
+#fi
+
+export XOS_DIR=/opt/xos
+python hpc-observer.py  -C $XOS_DIR/observers/hpc/hpc_observer_config
diff --git a/xos/observers/hpc/start.sh b/xos/observers/hpc/start.sh
new file mode 100755
index 0000000..305c07f
--- /dev/null
+++ b/xos/observers/hpc/start.sh
@@ -0,0 +1,6 @@
+#if [[ ! -e ./hpc-backend.py ]]; then
+#    ln -s ../xos-observer.py hpc-backend.py
+#fi
+
+export XOS_DIR=/opt/xos
+nohup python hpc-observer.py  -C $XOS_DIR/observers/hpc/hpc_observer_config > /dev/null 2>&1 &
diff --git a/xos/observers/hpc/steps/garbage_collector.py b/xos/observers/hpc/steps/garbage_collector.py
new file mode 100644
index 0000000..25172a3
--- /dev/null
+++ b/xos/observers/hpc/steps/garbage_collector.py
@@ -0,0 +1,67 @@
+import os
+import sys
+import base64
+import traceback
+from collections import defaultdict
+from django.db.models import F, Q
+from xos.config import Config
+from util.logger import Logger, logging
+from synchronizers.base.syncstep import SyncStep
+from services.hpc.models import ServiceProvider, ContentProvider, CDNPrefix, OriginServer
+from core.models import *
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from hpclib import HpcLibrary
+
+logger = Logger(level=logging.INFO)
+
+class GarbageCollector(SyncStep, HpcLibrary):
+#    requested_interval = 86400
+    requested_interval = 0
+    provides=[]
+
+    def __init__(self, **args):
+        SyncStep.__init__(self, **args)
+        HpcLibrary.__init__(self)
+
+    def call(self, **args):
+        logger.info("running garbage collector")
+        try:
+            self.gc_originservers()
+            self.gc_cdnprefixes()
+            self.gc_contentproviders()
+            self.gc_serviceproviders()
+        except:
+            traceback.print_exc()
+
+    def gc_onev(self, ps_class, ps_idField, onev_className, onev_idField):
+        # get the CMI's objects
+        onev_objs = self.client.onev.ListAll(onev_className)
+
+        # get the data model's objects,
+        ps_objs = ps_class.objects.filter(enacted__isnull=False)
+        ps_ids = [str(getattr(x,ps_idField,None)) for x in ps_objs]
+
+        # for each onev object, if it's id does not exist in a data model
+        # object, then delete it.
+        for onev_obj in onev_objs:
+            onev_id = onev_obj[onev_idField]
+            if str(onev_id) not in ps_ids:
+                logger.info("garbage collecting %s %s" % (onev_className, str(onev_id)))
+                self.client.onev.Delete(onev_className, onev_id)
+
+    def gc_originservers(self):
+        self.gc_onev(OriginServer, "origin_server_id", "OriginServer", "origin_server_id")
+
+    def gc_cdnprefixes(self):
+        self.gc_onev(CDNPrefix, "cdn_prefix_id", "CDNPrefix", "cdn_prefix_id")
+
+    def gc_contentproviders(self):
+        self.gc_onev(ContentProvider, "content_provider_id", "ContentProvider", "content_provider_id")
+
+    def gc_serviceproviders(self):
+        self.gc_onev(ServiceProvider, "service_provider_id", "ServiceProvider", "service_provider_id")
+
diff --git a/xos/observers/hpc/steps/sync_cdnprefix.py b/xos/observers/hpc/steps/sync_cdnprefix.py
new file mode 100644
index 0000000..d2b0276
--- /dev/null
+++ b/xos/observers/hpc/steps/sync_cdnprefix.py
@@ -0,0 +1,101 @@
+import os
+import sys
+import base64
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+from services.hpc.models import ServiceProvider, ContentProvider, CDNPrefix
+from util.logger import Logger, logging
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from hpclib import HpcLibrary
+
+logger = Logger(level=logging.INFO)
+
+class SyncCDNPrefix(SyncStep, HpcLibrary):
+    provides=[CDNPrefix]
+    observes=CDNPrefix
+    requested_interval=0
+
+    def __init__(self, **args):
+        SyncStep.__init__(self, **args)
+        HpcLibrary.__init__(self)
+
+    def filter_hpc_service(self, objs):
+        hpcService = self.get_hpc_service()
+
+        return [x for x in objs if x.contentProvider.serviceProvider.hpcService == hpcService]
+
+    def fetch_pending(self, deleted):
+        #self.consistency_check()
+
+        return self.filter_hpc_service(SyncStep.fetch_pending(self, deleted))
+
+    def consistency_check(self):
+        # set to true if something changed
+        result=False
+
+        # sanity check to make sure our PS objects have CMI objects behind them
+        all_p_ids = [x["cdn_prefix_id"] for x in self.client.onev.ListAll("CDNPrefix")]
+
+        all_p_ids = []
+        all_origins = {}
+        for x in self.client.onev.ListAll("CDNPrefix"):
+            id = x["cdn_prefix_id"]
+            all_p_ids.append(id)
+            all_origins[id] = x.get("default_origin_server", None)
+
+        for p in CDNPrefix.objects.all():
+            if (p.cdn_prefix_id is None):
+                continue
+
+            if (p.cdn_prefix_id not in all_p_ids):
+                logger.info("CDN Prefix %s was not found on CMI" % p.cdn_prefix_id)
+                p.cdn_prefix_id=None
+                p.save()
+                result = True
+
+            if (p.defaultOriginServer!=None) and (all_origins.get(p.cdn_prefix_id,None) != p.defaultOriginServer.url):
+                logger.info("CDN Prefix %s does not have default origin server on CMI" % str(p))
+                p.save() # this will set updated>enacted and force observer to re-sync
+                result = True
+
+        return result
+
+    def sync_record(self, cp):
+        logger.info("sync'ing cdn prefix %s" % str(cp))
+
+        if (not cp.contentProvider) or (not cp.contentProvider.content_provider_id):
+            raise Exception("CDN Prefix %s is linked to a contentProvider without an id" % str(cp))
+
+        cpid = cp.contentProvider.content_provider_id
+
+        cp_dict = {"service": "HyperCache", "enabled": cp.enabled, "content_provider_id": cpid, "cdn_prefix": cp.prefix}
+
+        if cp.defaultOriginServer and cp.defaultOriginServer.url:
+            if (not cp.defaultOriginServer.origin_server_id):
+                # It's probably a bad idea to try to set defaultOriginServer before
+                # we've crated defaultOriginServer.
+                raise Exception("cdn prefix %s is waiting for it's default origin server to get an id" % str(cp))
+
+            cp_dict["default_origin_server"] = cp.defaultOriginServer.url
+
+        #print cp_dict
+
+        if not cp.cdn_prefix_id:
+            id = self.client.onev.Create("CDNPrefix", cp_dict)
+            cp.cdn_prefix_id = id
+        else:
+            del cp_dict["content_provider_id"]  # this can't be updated
+            del cp_dict["cdn_prefix"] # this can't be updated either
+            self.client.onev.Update("CDNPrefix", cp.cdn_prefix_id, cp_dict)
+
+        cp.save()
+
+    def delete_record(self, m):
+        if m.cdn_prefix_id is not None:
+            self.client.onev.Delete("CDNPrefix", m.cdn_prefix_id)
diff --git a/xos/observers/hpc/steps/sync_contentprovider.py b/xos/observers/hpc/steps/sync_contentprovider.py
new file mode 100644
index 0000000..4e95c36
--- /dev/null
+++ b/xos/observers/hpc/steps/sync_contentprovider.py
@@ -0,0 +1,78 @@
+import os
+import sys
+import base64
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+from services.hpc.models import ServiceProvider, ContentProvider
+from util.logger import Logger, logging
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from hpclib import HpcLibrary
+
+logger = Logger(level=logging.INFO)
+
+class SyncContentProvider(SyncStep, HpcLibrary):
+    provides=[ContentProvider]
+    observes=ContentProvider
+    requested_interval=0
+
+    def __init__(self, **args):
+        SyncStep.__init__(self, **args)
+        HpcLibrary.__init__(self)
+
+    def filter_hpc_service(self, objs):
+        hpcService = self.get_hpc_service()
+
+        return [x for x in objs if x.serviceProvider.hpcService == hpcService]
+
+    def fetch_pending(self, deleted):
+        #self.consistency_check()
+
+        return self.filter_hpc_service(SyncStep.fetch_pending(self, deleted))
+
+    def consistency_check(self):
+        # set to true if something changed
+        result=False
+
+        # sanity check to make sure our PS objects have CMI objects behind them
+        all_cp_ids = [x["content_provider_id"] for x in self.client.onev.ListAll("ContentProvider")]
+        for cp in ContentProvider.objects.all():
+            if (cp.content_provider_id is not None) and (cp.content_provider_id not in all_cp_ids):
+                logger.info("Content provider %s was not found on CMI" % cp.content_provider_id)
+                cp.content_provider_id=None
+                cp.save()
+                result = True
+
+        return result
+
+    def sync_record(self, cp):
+        logger.info("sync'ing content provider %s" % str(cp))
+        account_name = self.make_account_name(cp.name)
+
+        if (not cp.serviceProvider) or (not cp.serviceProvider.service_provider_id):
+            raise Exception("ContentProvider %s is linked to a serviceProvider with no id" % str(cp))
+
+        spid = cp.serviceProvider.service_provider_id
+
+        cp_dict = {"account": account_name, "name": cp.name, "enabled": cp.enabled}
+
+        #print cp_dict
+
+        if not cp.content_provider_id:
+            cp_dict["service_provider_id"] = spid
+            id = self.client.onev.Create("ContentProvider", cp_dict)
+            cp.content_provider_id = id
+        else:
+            self.client.onev.Update("ContentProvider", cp.content_provider_id, cp_dict)
+
+        cp.save()
+
+    def delete_record(self, m):
+        if m.content_provider_id is not None:
+            self.client.onev.Delete("ContentProvider", m.content_provider_id)
+
diff --git a/xos/observers/hpc/steps/sync_hpcservices.py b/xos/observers/hpc/steps/sync_hpcservices.py
new file mode 100644
index 0000000..47165cc
--- /dev/null
+++ b/xos/observers/hpc/steps/sync_hpcservices.py
@@ -0,0 +1,43 @@
+import os
+import sys
+import base64
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+from services.hpc.models import HpcService
+from services.requestrouter.models import RequestRouterService
+from util.logger import Logger, logging
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from hpclib import HpcLibrary
+
+logger = Logger(level=logging.INFO)
+
+class SyncHpcService(SyncStep, HpcLibrary):
+    provides=[HpcService]
+    observes=HpcService
+    requested_interval=0
+
+    def __init__(self, **args):
+        SyncStep.__init__(self, **args)
+        HpcLibrary.__init__(self)
+
+    def filter_hpc_service(self, objs):
+        hpcService = self.get_hpc_service()
+
+        return [x for x in objs if x == hpcService]
+
+    def fetch_pending(self, deleted):
+        # Looks like deletion is not supported for this object - Sapan
+        if (deleted):
+            return []
+        else:
+            return self.filter_hpc_service(HpcService.objects.filter(Q(enacted__lt=F('updated')) | Q(enacted=None)))
+
+    def sync_record(self, hpc_service):
+        logger.info("sync'ing hpc_service %s" % str(hpc_service))
+        hpc_service.save()
diff --git a/xos/observers/hpc/steps/sync_originserver.py b/xos/observers/hpc/steps/sync_originserver.py
new file mode 100644
index 0000000..435bbc1
--- /dev/null
+++ b/xos/observers/hpc/steps/sync_originserver.py
@@ -0,0 +1,92 @@
+import os
+import sys
+import base64
+
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+from services.hpc.models import ServiceProvider, ContentProvider, CDNPrefix, OriginServer
+from util.logger import Logger, logging
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from hpclib import HpcLibrary
+
+logger = Logger(level=logging.INFO)
+
+class SyncOriginServer(SyncStep, HpcLibrary):
+    provides=[OriginServer]
+    observes=OriginServer
+    requested_interval=0
+
+    def __init__(self, **args):
+        SyncStep.__init__(self, **args)
+        HpcLibrary.__init__(self)
+
+    def filter_hpc_service(self, objs):
+        hpcService = self.get_hpc_service()
+
+        return [x for x in objs if x.contentProvider.serviceProvider.hpcService == hpcService]
+
+    def fetch_pending(self, deleted):
+        #self.consistency_check()
+
+        return self.filter_hpc_service(SyncStep.fetch_pending(self, deleted))
+
+    def consistency_check(self):
+        # set to true if something changed
+        result=False
+
+        # sanity check to make sure our PS objects have CMI objects behind them
+        all_ors_ids = [x["origin_server_id"] for x in self.client.onev.ListAll("OriginServer")]
+        for ors in OriginServer.objects.all():
+            if (ors.origin_server_id is not None) and (ors.origin_server_id not in all_ors_ids):
+                # we have an origin server ID, but it doesn't exist in the CMI
+                # something went wrong
+                # start over
+                logger.info("origin server %s was not found on CMI" % ors.origin_server_id)
+                ors.origin_server_id=None
+                ors.save()
+                result = True
+
+        return result
+
+    def sync_record(self, ors):
+        logger.info("sync'ing origin server %s" % str(ors))
+
+        if (not ors.contentProvider) or (not ors.contentProvider.content_provider_id):
+            raise Exception("Origin Server %s is linked to a contentProvider with no id" % str(ors))
+
+        cpid = ors.contentProvider.content_provider_id
+
+        # validation requires URL start with http://
+        url = ors.url
+        if not url.startswith("http://"):
+            url = "http://" + url
+
+        ors_dict = {"authenticated_content": ors.authenticated, "zone_redirects": ors.redirects, "content_provider_id": cpid, "url": url, "service_type": "HyperCache", "caching_type": "Optimistic", "description": ors.description}
+        if not ors_dict["description"]:
+            ors_dict["description"] = "blank"
+
+        #print os_dict
+
+        if not ors.origin_server_id:
+            id = self.client.onev.Create("OriginServer", ors_dict)
+            ors.origin_server_id = id
+        else:
+            self.client.onev.Update("OriginServer", ors.origin_server_id, ors_dict)
+
+        # ... something breaks (analytics) if the URL starts with http://, so we
+        # change it in cob after we added it via onev.
+        url = url[7:]
+        self.client.cob.UpdateContent(ors.origin_server_id, {"url": url})
+
+        ors.silent = True
+        ors.save()
+
+    def delete_record(self, m):
+        if m.origin_server_id is not None:
+            self.client.onev.Delete("OriginServer", m.origin_server_id)
diff --git a/xos/observers/hpc/steps/sync_serviceprovider.py b/xos/observers/hpc/steps/sync_serviceprovider.py
new file mode 100644
index 0000000..e4e31f0
--- /dev/null
+++ b/xos/observers/hpc/steps/sync_serviceprovider.py
@@ -0,0 +1,67 @@
+import os
+import sys
+import base64
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+from services.hpc.models import ServiceProvider
+from util.logger import Logger, logging
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from hpclib import HpcLibrary
+
+logger = Logger(level=logging.INFO)
+
+class SyncServiceProvider(SyncStep, HpcLibrary):
+    provides=[ServiceProvider]
+    observes=ServiceProvider
+    requested_interval=0
+
+    def __init__(self, **args):
+        SyncStep.__init__(self, **args)
+        HpcLibrary.__init__(self)
+
+    def filter_hpc_service(self, objs):
+        hpcService = self.get_hpc_service()
+
+        return [x for x in objs if x.hpcService == hpcService]
+
+    def fetch_pending(self, deleted):
+        #self.consistency_check()
+
+        return self.filter_hpc_service(SyncStep.fetch_pending(self, deleted))
+
+    def consistency_check(self):
+        # set to true if something changed
+        result=False
+
+        # sanity check to make sure our PS objects have CMI objects behind them
+        all_sp_ids = [x["service_provider_id"] for x in self.client.onev.ListAll("ServiceProvider")]
+        for sp in ServiceProvider.objects.all():
+            if (sp.service_provider_id is not None) and (sp.service_provider_id not in all_sp_ids):
+                logger.info("Service provider %s was not found on CMI" % sp.service_provider_id)
+                sp.service_provider_id=None
+                sp.save()
+                result = True
+
+        return result
+
+    def sync_record(self, sp):
+        logger.info("sync'ing service provider %s" % str(sp))
+        account_name = self.make_account_name(sp.name)
+        sp_dict = {"account": account_name, "name": sp.name, "enabled": sp.enabled}
+        if not sp.service_provider_id:
+            id = self.client.onev.Create("ServiceProvider", sp_dict)
+            sp.service_provider_id = id
+        else:
+            self.client.onev.Update("ServiceProvider", sp.service_provider_id, sp_dict)
+
+        sp.save()
+
+    def delete_record(self, m):
+        if m.service_provider_id is not None:
+            self.client.onev.Delete("ServiceProvider", m.service_provider_id)
diff --git a/xos/observers/hpc/steps/sync_sitemap.py b/xos/observers/hpc/steps/sync_sitemap.py
new file mode 100644
index 0000000..e662458
--- /dev/null
+++ b/xos/observers/hpc/steps/sync_sitemap.py
@@ -0,0 +1,118 @@
+import os
+import sys
+import base64
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+from services.hpc.models import ServiceProvider, ContentProvider, CDNPrefix, SiteMap
+from util.logger import Logger, logging
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from hpclib import HpcLibrary
+
+logger = Logger(level=logging.INFO)
+
+class SyncSiteMap(SyncStep, HpcLibrary):
+    provides=[SiteMap]
+    observes=SiteMap
+    requested_interval=0
+
+    def __init__(self, **args):
+        SyncStep.__init__(self, **args)
+        HpcLibrary.__init__(self)
+
+    def filter_hpc_service(self, objs):
+        hpcService = self.get_hpc_service()
+
+        filtered_objs = []
+        for x in objs:
+            if ((x.hpcService == hpcService) or
+               ((x.serviceProvider != None) and (x.serviceProvider.hpcService == hpcService)) or
+               ((x.contentProvider != None) and (x.contentProvider.serviceProvider.hpcService == hpcService)) or
+               ((x.cdnPrefix!= None) and (x.cdnPrefix.contentProvider.serviceProvider.hpcService == hpcService))):
+                filtered_objs.append(x)
+
+        return filtered_objs
+
+    def fetch_pending(self, deleted):
+        return self.filter_hpc_service(SyncStep.fetch_pending(self, deleted))
+
+    def consistency_check(self):
+        # set to true if something changed
+        result=False
+
+        # sanity check to make sure our PS objects have CMI objects behind them
+        all_map_ids = [x["map_id"] for x in self.client.onev.ListAll("Map")]
+        for map in SiteMap.objects.all():
+            if (map.map_id is not None) and (map.map_id not in all_map_ids):
+                logger.info("Map %s was not found on CMI" % map.map_id)
+                map.map_id=None
+                map.save()
+                result = True
+
+        return result
+
+    def update_bind(self, map, map_dict, field_name, to_name, ids):
+        for id in ids:
+            if (not id in map_dict.get(field_name, [])):
+                print "Bind Map", map.map_id, "to", to_name, id
+                self.client.onev.Bind("Map", map.map_id, to_name, id)
+
+        for id in map_dict.get(field_name, []):
+            if (not id in ids):
+                print "Unbind Map", map.map_id, "from", to_name, id
+                self.client.onev.UnBind("map", map.map_id, to_name, id)
+
+    def sync_record(self, map):
+        logger.info("sync'ing SiteMap %s" % str(map))
+
+        if not map.map:
+            # no contents
+            return
+
+        content = map.map.read()
+
+        map_dict = {"name": map.name, "type": "site", "content": content}
+
+        cdn_prefix_ids=[]
+        service_provider_ids=[]
+        content_provider_ids=[]
+
+        if (map.contentProvider):
+            if not map.contentProvider.content_provider_id:
+                raise Exception("Map %s links to a contentProvider with no id" % map.name)
+            conent_provider_ids = [map.contentProvider.content_provider_id]
+
+        if (map.serviceProvider):
+            if not map.serviceProvider.service_provider_id:
+                raise Exception("Map %s links to a serviceProvider with no id" % map.name)
+            service_provider_ids = [map.serviceProvider.service_provider_id]
+
+        if (map.cdnPrefix):
+            if not map.cdnPrefix.cdn_prefix_id:
+                raise Exception("Map %s links to a cdnPrefix with no id" % map.name)
+            cdn_prefix_ids = [map.cdnPrefix.cdn_prefix_id]
+
+        if not map.map_id:
+            print "Create Map", map_dict
+            id = self.client.onev.Create("Map", map_dict)
+            map.map_id = id
+        else:
+            print "Update Map", map_dict
+            # these things we probably cannot update
+            del map_dict["name"]
+            self.client.onev.Update("Map", map.map_id, map_dict)
+
+        cmi_map_dict = self.client.onev.Read("Map", map.map_id)
+
+        self.update_bind(map, cmi_map_dict, "cdn_prefix_ids", "CDNPrefix", cdn_prefix_ids)
+
+        map.save()
+
+    def delete_record(self, m):
+        if m.map_id is not None:
+            self.client.onev.Delete("Map", m.map_id)
diff --git a/xos/observers/hpc/stop.sh b/xos/observers/hpc/stop.sh
new file mode 100755
index 0000000..a0b4a8e
--- /dev/null
+++ b/xos/observers/hpc/stop.sh
@@ -0,0 +1 @@
+pkill -9 -f hpc-observer.py
diff --git a/xos/observers/hpc/supervisor/hpc-observer.conf b/xos/observers/hpc/supervisor/hpc-observer.conf
new file mode 100644
index 0000000..f2c79d4
--- /dev/null
+++ b/xos/observers/hpc/supervisor/hpc-observer.conf
@@ -0,0 +1,2 @@
+[program:hpc-observer]
+command=python /opt/xos/observers/hpc/hpc-observer.py -C /opt/xos/observers/hpc/hpc_observer_config
diff --git a/xos/observers/hpc/supervisor/hpc-watcher.conf b/xos/observers/hpc/supervisor/hpc-watcher.conf
new file mode 100644
index 0000000..e0f4eb1
--- /dev/null
+++ b/xos/observers/hpc/supervisor/hpc-watcher.conf
@@ -0,0 +1,2 @@
+[program:hpc-watcher]
+command=python /opt/xos/observers/hpc/hpc_watcher.py
diff --git a/xos/observers/monitoring_channel/files/docker.list b/xos/observers/monitoring_channel/files/docker.list
new file mode 100644
index 0000000..0ee9ae0
--- /dev/null
+++ b/xos/observers/monitoring_channel/files/docker.list
@@ -0,0 +1 @@
+deb https://get.docker.com/ubuntu docker main
diff --git a/xos/observers/monitoring_channel/files/vm-resolv.conf b/xos/observers/monitoring_channel/files/vm-resolv.conf
new file mode 100644
index 0000000..cae093a
--- /dev/null
+++ b/xos/observers/monitoring_channel/files/vm-resolv.conf
@@ -0,0 +1 @@
+nameserver 8.8.8.8
diff --git a/xos/observers/monitoring_channel/model-deps b/xos/observers/monitoring_channel/model-deps
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/xos/observers/monitoring_channel/model-deps
@@ -0,0 +1 @@
+{}
diff --git a/xos/observers/monitoring_channel/monitoring_channel_observer.py b/xos/observers/monitoring_channel/monitoring_channel_observer.py
new file mode 100755
index 0000000..d6a71ff
--- /dev/null
+++ b/xos/observers/monitoring_channel/monitoring_channel_observer.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+
+# This imports and runs ../../xos-observer.py
+
+import importlib
+import os
+import sys
+observer_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"../..")
+sys.path.append(observer_path)
+mod = importlib.import_module("xos-observer")
+mod.main()
diff --git a/xos/observers/monitoring_channel/monitoring_channel_observer_config b/xos/observers/monitoring_channel/monitoring_channel_observer_config
new file mode 100644
index 0000000..5657e1d
--- /dev/null
+++ b/xos/observers/monitoring_channel/monitoring_channel_observer_config
@@ -0,0 +1,41 @@
+
+[plc]
+name=plc
+deployment=VICCI
+
+[db]
+name=xos
+user=postgres
+password=password
+host=localhost
+port=5432
+
+[api]
+host=128.112.171.237
+port=8000
+ssl_key=None
+ssl_cert=None
+ca_ssl_cert=None
+ratelimit_enabled=0
+omf_enabled=0
+mail_support_address=support@localhost
+nova_enabled=True
+
+[observer]
+name=monitoring_channel
+dependency_graph=/opt/xos/observers/monitoring_channel/model-deps
+steps_dir=/opt/xos/observers/monitoring_channel/steps
+sys_dir=/opt/xos/observers/monitoring_channel/sys
+deleters_dir=/opt/xos/observers/monitoring_channel/deleters
+log_file=console
+driver=None
+pretend=False
+backoff_disabled=True
+save_ansible_output=True
+# set proxy_ssh to false on cloudlab
+proxy_ssh=False
+full_setup=True
+
+[feefie]
+client_id='vicci_dev_central'
+user_id='pl'
diff --git a/xos/observers/monitoring_channel/steps/sync_monitoringchannel.py b/xos/observers/monitoring_channel/steps/sync_monitoringchannel.py
new file mode 100644
index 0000000..c265517
--- /dev/null
+++ b/xos/observers/monitoring_channel/steps/sync_monitoringchannel.py
@@ -0,0 +1,76 @@
+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 synchronizers.base.syncstep import SyncStep
+from synchronizers.base.ansible import run_template_ssh
+from observers.base.SyncInstanceUsingAnsible import SyncInstanceUsingAnsible
+from core.models import Service, Slice
+from services.ceilometer.models import MonitoringChannel
+from util.logger import Logger, logging
+
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+logger = Logger(level=logging.INFO)
+
+class SyncMonitoringChannel(SyncInstanceUsingAnsible):
+    provides=[MonitoringChannel]
+    observes=MonitoringChannel
+    requested_interval=0
+    template_name = "sync_monitoringchannel.yaml"
+    service_key_name = "/opt/xos/observers/monitoring_channel/monitoring_channel_private_key"
+
+    def __init__(self, *args, **kwargs):
+        super(SyncMonitoringChannel, self).__init__(*args, **kwargs)
+
+    def fetch_pending(self, deleted):
+        if (not deleted):
+            objs = MonitoringChannel.get_tenant_objects().filter(Q(enacted__lt=F('updated')) | Q(enacted=None),Q(lazy_blocked=False))
+        else:
+            objs = MonitoringChannel.get_deleted_tenant_objects()
+
+        return objs
+
+    def get_extra_attributes(self, o):
+        # This is a place to include extra attributes. In the case of Monitoring Channel, we need to know
+        #   1) Allowed tenant ids
+        #   2) Ceilometer API service endpoint URL if running externally
+        #   3) Credentials to access Ceilometer API service
+
+        instance = self.get_instance(o)
+
+        try:
+            full_setup = Config().observer_full_setup
+        except:
+            full_setup = True
+
+        fields = {"unique_id": o.id,
+                  "allowed_tenant_ids": o.tenant_list,
+                  "auth_url":instance.controller.auth_url,
+                  "admin_user":instance.controller.admin_user,
+                  "admin_password":instance.controller.admin_password,
+                  "admin_tenant":instance.controller.admin_tenant,
+                  "full_setup": full_setup}
+
+        return fields
+
+    def run_playbook(self, o, 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:
+        super(SyncMonitoringChannel, self).run_playbook(o, fields)
+
+        #o.last_ansible_hash = ansible_hash
+
+    def map_delete_inputs(self, o):
+        fields = {"unique_id": o.id,
+                  "delete": True}
+        return fields
diff --git a/xos/observers/monitoring_channel/steps/sync_monitoringchannel.yaml b/xos/observers/monitoring_channel/steps/sync_monitoringchannel.yaml
new file mode 100644
index 0000000..6c5fc8c
--- /dev/null
+++ b/xos/observers/monitoring_channel/steps/sync_monitoringchannel.yaml
@@ -0,0 +1,104 @@
+---
+- hosts: {{ instance_name }}
+  gather_facts: False
+  connection: ssh
+  user: ubuntu
+  sudo: yes
+  vars:
+      unique_id: {{ unique_id }}
+      auth_url: {{ auth_url }}
+      admin_user: {{ admin_user }}
+      admin_password: {{ admin_password }}
+      admin_tenant: {{ admin_tenant }}
+      shared_lan_ip: {{ private_ip }}
+      shared_lan_mac: {{ private_mac }}
+      headnode_flat_lan_ip: {{ rabbit_host }}
+      ceilometer_client_acess_ip: {{ ceilometer_ip }}
+      ceilometer_client_acess_mac: {{ ceilometer_mac }}
+      ceilometer_host_port: {{ ceilometer_port }}
+      allowed_tenant_ids:
+        {% for allowed_tenant_id in allowed_tenant_ids %}
+        - {{ allowed_tenant_id }}
+        {% endfor %}
+
+  tasks:
+{% if delete %}
+  - name: Remove tenant
+# FIXME: Adding dummy template action to avoid "action attribute missing in task" error
+    template: src=/opt/xos/observers/monitoring_channel/templates/ceilometer_proxy_config.j2 dest=/usr/local/share/monitoring-channel-{{ unique_id }}_ceilometer_proxy_config mode=0777
+    notify:
+     - stop monitoring-channel
+     - remove container
+{% else %}
+{% if full_setup %}
+  - name: Docker repository
+    copy: src=/opt/xos/observers/monitoring_channel/files/docker.list
+      dest=/etc/apt/sources.list.d/docker.list
+
+  - name: Import the repository key
+    apt_key: keyserver=keyserver.ubuntu.com id=36A1D7869245C8950F966E92D8576A8BA88D21E9
+
+  - name: install Docker
+    apt: name=lxc-docker state=present update_cache=yes
+
+  - name: install python-setuptools
+    apt: name=python-setuptools state=present
+
+  - name: install pip
+    easy_install: name=pip
+
+  - name: install docker-py
+    pip: name=docker-py version=0.5.3
+
+  - name: install Pipework
+    get_url: url=https://raw.githubusercontent.com/jpetazzo/pipework/master/pipework
+       dest=/usr/local/bin/pipework
+       mode=0755
+
+  - name: Disable resolvconf service
+    shell: service resolvconf stop
+    shell: echo manual > /etc/init/resolvconf.override
+    shell: rm -f /etc/resolv.conf
+
+  - name: Install resolv.conf
+    copy: src=/opt/xos/observers/monitoring_channel/files/vm-resolv.conf
+      dest=/etc/resolv.conf
+{% endif %}
+
+  - name: ceilometer proxy config
+    template: src=/opt/xos/observers/monitoring_channel/templates/ceilometer_proxy_config.j2 dest=/usr/local/share/monitoring-channel-{{ unique_id }}_ceilometer_proxy_config mode=0777
+    notify:
+#    - restart monitoring-channel
+     - stop monitoring-channel
+     - remove container
+     - start monitoring-channel
+
+  - name: Monitoring channel upstart
+    template: src=/opt/xos/observers/monitoring_channel/templates/monitoring-channel.conf.j2 dest=/etc/init/monitoring-channel-{{ unique_id }}.conf
+
+  - name: Monitoring channel startup script
+    template: src=/opt/xos/observers/monitoring_channel/templates/start-monitoring-channel.sh.j2 dest=/usr/local/sbin/start-monitoring-channel-{{ unique_id }}.sh mode=0755
+    notify:
+#    - restart monitoring-channel
+     - stop monitoring-channel
+     - remove container
+     - start monitoring-channel
+
+# These are samples, not necessary for correct function of demo
+
+  - name: Make sure Monitoring channel service is running
+    service: name=monitoring-channel-{{ unique_id }} state=started
+{% endif %}
+
+  handlers:
+  - name: restart monitoring-channel
+    shell: service monitoring-channel-{{ unique_id }} stop; sleep 1; service monitoring-channel-{{ unique_id }} start
+
+  - name: stop monitoring-channel
+    service: name=monitoring-channel-{{ unique_id }} state=stopped
+
+  - name: remove container
+    docker: name=monitoring-channel-{{ unique_id }} state=absent image=monitoring-channel
+
+  - name: start monitoring-channel
+    service: name=monitoring-channel-{{ unique_id }} state=started
diff --git a/xos/observers/monitoring_channel/supervisor/monitoring_channel_observer.conf b/xos/observers/monitoring_channel/supervisor/monitoring_channel_observer.conf
new file mode 100644
index 0000000..1b78703
--- /dev/null
+++ b/xos/observers/monitoring_channel/supervisor/monitoring_channel_observer.conf
@@ -0,0 +1,2 @@
+[program:monitoring_channel_observer]
+command=python /opt/xos/observers/monitoring_channel/monitoring_channel_observer.py -C /opt/xos/observers/monitoring_channel/monitoring_channel_observer_config
diff --git a/xos/observers/monitoring_channel/templates/Dockerfile.monitoring_channel b/xos/observers/monitoring_channel/templates/Dockerfile.monitoring_channel
new file mode 100644
index 0000000..45defb8
--- /dev/null
+++ b/xos/observers/monitoring_channel/templates/Dockerfile.monitoring_channel
@@ -0,0 +1,26 @@
+FROM       ubuntu:14.04.2
+MAINTAINER Andy Bavier <acb@cs.princeton.edu>
+
+# XXX Workaround for docker bug:
+# https://github.com/docker/docker/issues/6345
+# Kernel 3.15 breaks docker, uss the line below as a workaround
+# until there is a fix
+RUN ln -s -f /bin/true /usr/bin/chfn
+# XXX End workaround
+
+# Install.
+RUN apt-get update && apt-get install -y \
+    python-pip \
+    python-dev
+
+RUN pip install web.py
+RUN pip install wsgilog
+RUN pip install python-ceilometerclient
+RUN mkdir -p /usr/local/share
+ADD ceilometer_proxy_server.py /usr/local/share/
+RUN chmod +x /usr/local/share/ceilometer_proxy_server.py
+ADD start_ceilometer_proxy /usr/local/sbin/
+RUN chmod +x /usr/local/sbin/start_ceilometer_proxy
+EXPOSE 8000
+WORKDIR /usr/local/share
+CMD /usr/local/sbin/start_ceilometer_proxy
diff --git a/xos/observers/monitoring_channel/templates/ceilometer_proxy_config.j2 b/xos/observers/monitoring_channel/templates/ceilometer_proxy_config.j2
new file mode 100644
index 0000000..4c712f1
--- /dev/null
+++ b/xos/observers/monitoring_channel/templates/ceilometer_proxy_config.j2
@@ -0,0 +1,16 @@
+# This file autogenerated by monitoring-channel observer
+# It contains a list of attributes to be used by ceilometer proxy web server
+# syntax: key=value
+
+[default]
+auth_url={{ auth_url }}
+admin_user={{ admin_user }}
+admin_tenant={{ admin_tenant }}
+admin_password={{ admin_password }}
+
+[allowed_tenants]
+{% if allowed_tenant_ids %}
+{% for tenant_id in allowed_tenant_ids %}
+{{ tenant_id }}
+{% endfor %}
+{% endif %}
diff --git a/xos/observers/monitoring_channel/templates/ceilometer_proxy_server.py b/xos/observers/monitoring_channel/templates/ceilometer_proxy_server.py
new file mode 100644
index 0000000..62f0804
--- /dev/null
+++ b/xos/observers/monitoring_channel/templates/ceilometer_proxy_server.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python
+import web
+import ConfigParser
+import io
+import json
+from ceilometerclient import client
+import logging
+import urllib
+import urllib2
+from wsgilog import WsgiLog
+
+web.config.debug=False
+
+logfile = "ceilometer_proxy_server.log"
+level=logging.INFO
+logger=logging.getLogger('ceilometer_proxy_server')
+logger.setLevel(level)
+handler=logging.handlers.RotatingFileHandler(logfile,maxBytes=1000000, backupCount=1)
+logger.addHandler(handler)
+
+class FileLog(WsgiLog):
+    def __init__(self, application):
+        WsgiLog.__init__(
+            self,
+            application,
+            logformat = '%(message)s',
+            tofile = True,
+            toprint = True,
+            prnlevel = level,
+            file = logfile,
+            backups =1
+            )
+    def __call__(self, environ, start_response):
+        def hstart_response(status, response_headers, *args):
+             out = start_response(status, response_headers, *args)
+             try:
+                 logline=environ["SERVER_PROTOCOL"]+" "+environ["REQUEST_METHOD"]+" "+environ["REQUEST_URI"]+" - "+status
+             except err:
+                 logline="Could not log <%s> due to err <%s>" % (str(environ), err)
+             logger.info(logline)
+
+             return out
+
+        return super(FileLog, self).__call__(environ, hstart_response)
+
+#TODOs:
+#-See if we can avoid using python-ceilometerclient and instead use the REST calls directly with AuthToken
+#
+urls = (
+    r'^/v2/meters$', 'meter_list',
+    r'^/v2/meters/(?P<meter_name>[A-Za-z0-9_:.\-]+)/statistics$', 'statistics_list',
+    r'^/v2/samples$', 'sample_list',
+    r'^/v2/resources$', 'resource_list',
+    r'^/v2/subscribe$', 'pubsub_handler',
+)
+
+app = web.application(urls, globals())
+
+config = None
+ceilometer_client = None
+
+
+def parse_ceilometer_proxy_config():
+    global config
+    config = ConfigParser.RawConfigParser(allow_no_value=True)
+    config.read('ceilometer_proxy_config')
+ 
+def ceilometerclient():
+    global config, ceilometer_client
+    if ceilometer_client:
+         return ceilometer_client
+
+    if not config:
+         parse_ceilometer_proxy_config()
+
+    keystone = {}
+    keystone['os_username']=config.get('default','admin_user')
+    keystone['os_password']=config.get('default','admin_password')
+    keystone['os_auth_url']=config.get('default','auth_url')
+    keystone['os_tenant_name']=config.get('default','admin_tenant')
+    ceilometer_client = client.get_client(2,**keystone)
+    logger.info('ceilometer get_client is successful')
+    return ceilometer_client
+
+def make_query(user_id=None, tenant_id=None, resource_id=None,
+               user_ids=None, tenant_ids=None, resource_ids=None):
+    """Returns query built from given parameters.
+
+    This query can be then used for querying resources, meters and
+    statistics.
+
+    :Parameters:
+      - `user_id`: user_id, has a priority over list of ids
+      - `tenant_id`: tenant_id, has a priority over list of ids
+      - `resource_id`: resource_id, has a priority over list of ids
+      - `user_ids`: list of user_ids
+      - `tenant_ids`: list of tenant_ids
+      - `resource_ids`: list of resource_ids
+    """
+    user_ids = user_ids or []
+    tenant_ids = tenant_ids or []
+    resource_ids = resource_ids or []
+
+    query = []
+    if user_id:
+        user_ids = [user_id]
+    for u_id in user_ids:
+        query.append({"field": "user_id", "op": "eq", "value": u_id})
+
+    if tenant_id:
+        tenant_ids = [tenant_id]
+    for t_id in tenant_ids:
+        query.append({"field": "project_id", "op": "eq", "value": t_id})
+
+    if resource_id:
+        resource_ids = [resource_id]
+    for r_id in resource_ids:
+        query.append({"field": "resource_id", "op": "eq", "value": r_id})
+
+    return query
+
+def filter_query_params(query_params):
+    new_query=[]
+    i=0
+    user_specified_tenants=[]
+    for field in query_params['q.field']:
+        if (field != 'project_id') and (field != 'project'):
+            query = {}
+            query['field']=field
+            if query_params['q.op'][i] != '':
+                 query['op']=query_params['q.op'][i]
+            query['value']=query_params['q.value'][i]
+            new_query.append(query)
+        else:
+            user_specified_tenants.append(query_params['q.value'][i])
+        i=i+1
+    return new_query,user_specified_tenants
+
+class meter_list:
+    def GET(self):
+        global config
+        keyword_args = {
+             "q.field": [],
+             "q.op": [],
+             "q.type": [],
+             "q.value": [],
+        }
+        query_params = web.input(**keyword_args)
+        new_query, user_specified_tenants = filter_query_params(query_params)
+
+        client = ceilometerclient()
+        meters=[]
+        for (k,v) in config.items('allowed_tenants'):
+             if user_specified_tenants and (k not in user_specified_tenants):
+                 continue
+             final_query=[]
+             final_query.extend(new_query)
+             query = make_query(tenant_id=k)
+             final_query.extend(query)
+             logger.debug('final query=%s',final_query)
+             results = client.meters.list(q=final_query)
+             meters.extend(results)
+        return json.dumps([ob._info for ob in meters])
+
+class statistics_list:
+    def GET(self, meter_name):
+        global config
+        keyword_args = {
+             "q.field": [],
+             "q.op": [],
+             "q.type": [],
+             "q.value": [],
+             "period": None
+        }
+        query_params = web.input(**keyword_args)
+        new_query, user_specified_tenants = filter_query_params(query_params)
+
+        client = ceilometerclient()
+        period = query_params.period
+        statistics = []
+        for (k,v) in config.items('allowed_tenants'):
+              if user_specified_tenants and (k not in user_specified_tenants):
+                  continue
+              final_query=[]
+              final_query.extend(new_query)
+              query = make_query(tenant_id=k)
+              final_query.extend(query)
+              logger.debug('final query=%s',final_query)
+              results = client.statistics.list(meter_name=meter_name, q=final_query, period=period)
+              statistics.extend(results)
+        return json.dumps([ob._info for ob in statistics])
+
+class sample_list:
+    def GET(self):
+        global config
+        keyword_args = {
+             "q.field": [],
+             "q.op": [],
+             "q.type": [],
+             "q.value": [],
+             "limit": None,
+        }
+        query_params = web.input(**keyword_args)
+        new_query, user_specified_tenants = filter_query_params(query_params)
+
+        client = ceilometerclient()
+        limit=query_params.limit
+        samples=[]
+        for (k,v) in config.items('allowed_tenants'):
+              if user_specified_tenants and (k not in user_specified_tenants):
+                  continue
+              final_query=[]
+              final_query.extend(new_query)
+              query = make_query(tenant_id=k)
+              final_query.extend(query)
+              logger.debug('final query=%s',final_query)
+              results = client.new_samples.list(q=final_query,limit=limit)
+              samples.extend(results)
+        return json.dumps([ob._info for ob in samples])
+
+class resource_list:
+    def GET(self):
+        global config
+        keyword_args = {
+             "q.field": [],
+             "q.op": [],
+             "q.type": [],
+             "q.value": [],
+             "limit": None,
+             "links": None,
+        }
+        query_params = web.input(**keyword_args)
+        new_query, user_specified_tenants = filter_query_params(query_params)
+
+        client = ceilometerclient()
+        limit=query_params.limit
+        links=query_params.links
+        resources=[]
+        for (k,v) in config.items('allowed_tenants'):
+              if user_specified_tenants and (k not in user_specified_tenants):
+                  continue
+              final_query=[]
+              final_query.extend(new_query)
+              query = make_query(tenant_id=k)
+              final_query.extend(query)
+              logger.debug('final query=%s',final_query)
+              results = client.resources.list(q=final_query, limit=limit, links=links)
+              resources.extend(results)
+        return json.dumps([ob._info for ob in resources])
+
+class pubsub_handler:
+    def POST(self):
+        global config
+        parse_ceilometer_proxy_config()
+        data_str = unicode(web.data(),'iso-8859-1')
+        post_data = json.loads(data_str)
+        final_query=[]
+        for (k,v) in config.items('allowed_tenants'):
+             query = make_query(tenant_id=k)
+             final_query.extend(query)
+        if not final_query:
+             raise Exception("Not allowed to subscribe to any meters")
+        post_data["query"] = final_query
+        #TODO: The PUB/SUB url needs to be read from config
+        put_request = urllib2.Request("http://10.11.10.1:4455/subscribe", json.dumps(post_data))
+        put_request.get_method = lambda: 'SUB'
+        put_request.add_header('Content-Type', 'application/json')
+        response = urllib2.urlopen(put_request)
+        response_text = response.read()
+        return json.dumps(response_text)
+
+    def DELETE(self):
+        data_str = web.data()
+        #TODO: The PUB/SUB url needs to be read from config
+        put_request = urllib2.Request("http://10.11.10.1:4455/unsubscribe", data_str)
+        put_request.get_method = lambda: 'UNSUB'
+        put_request.add_header('Content-Type', 'application/json')
+        response = urllib2.urlopen(put_request)
+        response_text = response.read()
+        return json.dumps(response_text)
+
+if __name__ == "__main__":
+    app.run(FileLog)
diff --git a/xos/observers/monitoring_channel/templates/monitoring-channel.conf.j2 b/xos/observers/monitoring_channel/templates/monitoring-channel.conf.j2
new file mode 100644
index 0000000..eb937ac
--- /dev/null
+++ b/xos/observers/monitoring_channel/templates/monitoring-channel.conf.j2
@@ -0,0 +1,10 @@
+# Upstart script for Monitoring channel
+description "Upstart script for Monitoring channel container"
+author "andy@onlab.us"
+start on filesystem and started docker
+stop on runlevel [!2345]
+respawn
+
+script
+  /usr/local/sbin/start-monitoring-channel-{{ unique_id }}.sh
+end script
diff --git a/xos/observers/monitoring_channel/templates/start-monitoring-channel.sh.j2 b/xos/observers/monitoring_channel/templates/start-monitoring-channel.sh.j2
new file mode 100755
index 0000000..f56c247
--- /dev/null
+++ b/xos/observers/monitoring_channel/templates/start-monitoring-channel.sh.j2
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+function mac_to_iface {
+    MAC=$1
+    ifconfig|grep $MAC| awk '{print $1}'|grep -v '\.'
+}
+
+function generate_mac_from_ip {
+    IP=$1
+    printf "02:42:%02x:%02x:%02x:%02x\n" `echo $IP|awk -F '.' '{print $1, $2, $3, $4}'`
+}
+
+iptables -L > /dev/null
+ip6tables -L > /dev/null
+
+MONITORING_CHANNEL=monitoring-channel-{{ unique_id }}
+HEADNODEFLATLANIP={{ headnode_flat_lan_ip }}
+HOST_FORWARDING_PORT_FOR_CEILOMETER={{ ceilometer_host_port }}
+
+docker inspect $MONITORING_CHANNEL > /dev/null 2>&1
+if [ "$?" == 1 ]
+then
+    #sudo docker build -t monitoring-channel -f Dockerfile.monitoring_channel .
+    sudo docker pull srikanthvavila/monitoring-channel
+    docker run -d --name=$MONITORING_CHANNEL --add-host="ctl:$HEADNODEFLATLANIP" --privileged=true -p $HOST_FORWARDING_PORT_FOR_CEILOMETER:8000 srikanthvavila/monitoring-channel
+else
+    docker start $MONITORING_CHANNEL
+fi
+
+# Set up networking via pipework
+#SHARED_LAN_IFACE=$( mac_to_iface {{ shared_lan_mac }} )
+#docker exec $MONITORING_CHANNEL ifconfig eth0 >> /dev/null || pipework $SHARED_LAN_IFACE -i eth0 $MONITORING_CHANNEL 192.168.0.1/24
+
+# Make sure VM's eth0 (hpc_client) has no IP address
+#ifconfig $HPC_IFACE 0.0.0.0
+
+# Now copy ceilometer proxy configuration to container
+cat /usr/local/share/monitoring-channel-{{ unique_id }}_ceilometer_proxy_config | docker exec -i $MONITORING_CHANNEL bash -c 'cat > /usr/local/share/ceilometer_proxy_config'
+
+# Attach to container
+docker start -a $MONITORING_CHANNEL
diff --git a/xos/observers/monitoring_channel/templates/start_ceilometer_proxy b/xos/observers/monitoring_channel/templates/start_ceilometer_proxy
new file mode 100644
index 0000000..ddaa9c8
--- /dev/null
+++ b/xos/observers/monitoring_channel/templates/start_ceilometer_proxy
@@ -0,0 +1 @@
+/usr/local/share/ceilometer_proxy_server.py 8000
diff --git a/xos/observers/onos/model-deps b/xos/observers/onos/model-deps
new file mode 100644
index 0000000..2da80e0
--- /dev/null
+++ b/xos/observers/onos/model-deps
@@ -0,0 +1,5 @@
+{
+    "ONOSApp": [
+        "ONOSService"
+    ]
+}
diff --git a/xos/observers/onos/onos-observer.py b/xos/observers/onos/onos-observer.py
new file mode 100755
index 0000000..d6a71ff
--- /dev/null
+++ b/xos/observers/onos/onos-observer.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+
+# This imports and runs ../../xos-observer.py
+
+import importlib
+import os
+import sys
+observer_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"../..")
+sys.path.append(observer_path)
+mod = importlib.import_module("xos-observer")
+mod.main()
diff --git a/xos/observers/onos/onos_observer_config b/xos/observers/onos/onos_observer_config
new file mode 100644
index 0000000..3c6d63d
--- /dev/null
+++ b/xos/observers/onos/onos_observer_config
@@ -0,0 +1,41 @@
+
+[plc]
+name=plc
+deployment=VICCI
+
+[db]
+name=xos
+user=postgres
+password=password
+host=localhost
+port=5432
+
+[api]
+host=128.112.171.237
+port=8000
+ssl_key=None
+ssl_cert=None
+ca_ssl_cert=None
+ratelimit_enabled=0
+omf_enabled=0
+mail_support_address=support@localhost
+nova_enabled=True
+
+[observer]
+name=onos
+dependency_graph=/opt/xos/observers/onos/model-deps
+steps_dir=/opt/xos/observers/onos/steps
+sys_dir=/opt/xos/observers/onos/sys
+deleters_dir=/opt/xos/observers/onos/deleters
+log_file=console
+driver=None
+pretend=False
+backoff_disabled=True
+save_ansible_output=True
+# set proxy_ssh to false on cloudlab
+proxy_ssh=False
+full_setup=True
+
+[feefie]
+client_id='vicci_dev_central'
+user_id='pl'
diff --git a/xos/observers/onos/run.sh b/xos/observers/onos/run.sh
new file mode 100755
index 0000000..ea4c511
--- /dev/null
+++ b/xos/observers/onos/run.sh
@@ -0,0 +1,6 @@
+#if [[ ! -e ./vcpe-observer.py ]]; then
+#    ln -s ../../xos-observer.py vcpe-observer.py
+#fi
+
+export XOS_DIR=/opt/xos
+python onos-observer.py  -C $XOS_DIR/observers/onos/onos_observer_config
diff --git a/xos/observers/onos/scripts/dockerip.sh b/xos/observers/onos/scripts/dockerip.sh
new file mode 100644
index 0000000..732c3fe
--- /dev/null
+++ b/xos/observers/onos/scripts/dockerip.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+MODE=`docker inspect --format '{{ .HostConfig.NetworkMode }}' $1  | tr -d '\n' | tr -d '\r'`
+if [[ "$MODE" == "host" ]]; then
+    echo -n "127.0.0.1"
+else
+    docker inspect --format '{{ .NetworkSettings.IPAddress }}' $1 | tr -d '\n' | tr -d '\r'
+fi
+
diff --git a/xos/observers/onos/start.sh b/xos/observers/onos/start.sh
new file mode 100755
index 0000000..c13ffbe
--- /dev/null
+++ b/xos/observers/onos/start.sh
@@ -0,0 +1,6 @@
+#if [[ ! -e ./vcpe-observer.py ]]; then
+#    ln -s ../../xos-observer.py vcpe-observer.py
+#fi
+
+export XOS_DIR=/opt/xos
+nohup python onos-observer.py  -C $XOS_DIR/observers/onos/onos_observer_config > /dev/null 2>&1 &
diff --git a/xos/observers/onos/steps/sync_onosapp.py b/xos/observers/onos/steps/sync_onosapp.py
new file mode 100644
index 0000000..047306d
--- /dev/null
+++ b/xos/observers/onos/steps/sync_onosapp.py
@@ -0,0 +1,167 @@
+import hashlib
+import os
+import socket
+import socket
+import sys
+import base64
+import time
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from synchronizers.base.ansible import run_template_ssh
+from observers.base.SyncInstanceUsingAnsible import SyncInstanceUsingAnsible
+from core.models import Service, Slice
+from services.onos.models import ONOSService, ONOSApp
+from util.logger import Logger, logging
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+logger = Logger(level=logging.INFO)
+
+class SyncONOSApp(SyncInstanceUsingAnsible):
+    provides=[ONOSApp]
+    observes=ONOSApp
+    requested_interval=0
+    template_name = "sync_onosapp.yaml"
+    service_key_name = "/opt/xos/observers/onos/onos_key"
+
+    def __init__(self, *args, **kwargs):
+        super(SyncONOSApp, self).__init__(*args, **kwargs)
+
+    def fetch_pending(self, deleted):
+        if (not deleted):
+            objs = ONOSApp.get_tenant_objects().filter(Q(enacted__lt=F('updated')) | Q(enacted=None),Q(lazy_blocked=False))
+        else:
+            objs = ONOSApp.get_deleted_tenant_objects()
+
+        return objs
+
+    def get_instance(self, o):
+        # We assume the ONOS service owns a slice, so pick one of the instances
+        # inside that slice to sync to.
+
+        serv = self.get_onos_service(o)
+
+        if serv.use_external_host:
+            return serv.use_external_host
+
+        if serv.slices.exists():
+            slice = serv.slices.all()[0]
+            if slice.instances.exists():
+                return slice.instances.all()[0]
+
+        return None
+
+    def get_onos_service(self, o):
+        if not o.provider_service:
+            return None
+
+        onoses = ONOSService.get_service_objects().filter(id=o.provider_service.id)
+        if not onoses:
+            return None
+
+        return onoses[0]
+
+    def get_files_dir(self, o):
+        if not hasattr(Config(), "observer_steps_dir"):
+            # make steps_dir mandatory; there's no valid reason for it to not
+            # be defined.
+            raise Exception("observer_steps_dir is not defined in config file")
+
+        step_dir = Config().observer_steps_dir
+
+        return os.path.join(step_dir, "..", "files", str(self.get_onos_service(o).id), o.name)
+
+    def get_cluster_configuration(self, o):
+        instance = self.get_instance(o)
+        if not instance:
+           raise "No instance for ONOS App"
+        node_ips = [socket.gethostbyname(instance.node.name)]
+
+        ipPrefix = ".".join(node_ips[0].split(".")[:3]) + ".*"
+        result = '{ "nodes": ['
+        result = result + ",".join(['{ "ip": "%s"}' % ip for ip in node_ips])
+        result = result + '], "ipPrefix": "%s"}' % ipPrefix
+        return result
+
+
+    def write_configs(self, o):
+        o.config_fns = []
+        o.rest_configs = []
+        o.files_dir = self.get_files_dir(o)
+
+        if not os.path.exists(o.files_dir):
+            os.makedirs(o.files_dir)
+
+        # Combine the service attributes with the tenant attributes. Tenant
+        # attribute can override service attributes.
+        attrs = o.provider_service.serviceattribute_dict
+        attrs.update(o.tenantattribute_dict)
+
+        ordered_attrs = attrs.keys()
+
+        o.early_rest_configs=[]
+        if ("cordvtn" in o.dependencies):
+            # For VTN, since it's running in a docker host container, we need
+            # to make sure it configures the cluster using the right ip addresses.
+            # NOTE: rest_onos/v1/cluster/configuration/ will reboot the cluster and
+            #   must go first.
+            name="rest_onos/v1/cluster/configuration/"
+            value= self.get_cluster_configuration(o)
+            fn = name[5:].replace("/","_")
+            endpoint = name[5:]
+            file(os.path.join(o.files_dir, fn),"w").write(" " +value)
+            o.early_rest_configs.append( {"endpoint": endpoint, "fn": fn} )
+
+        for name in attrs.keys():
+            value = attrs[name]
+            if name.startswith("config_"):
+                fn = name[7:] # .replace("_json",".json")
+                o.config_fns.append(fn)
+                file(os.path.join(o.files_dir, fn),"w").write(value)
+            if name.startswith("rest_"):
+                fn = name[5:].replace("/","_")
+                endpoint = name[5:]
+                # Ansible goes out of it's way to make our life difficult. If
+                # 'lookup' sees a file that it thinks contains json, then it'll
+                # insist on parsing and return a json object. We just want
+                # a string, so prepend a space and then strip the space off
+                # later.
+                file(os.path.join(o.files_dir, fn),"w").write(" " +value)
+                o.rest_configs.append( {"endpoint": endpoint, "fn": fn} )
+
+    def prepare_record(self, o):
+        self.write_configs(o)
+
+    def get_extra_attributes(self, o):
+        instance = self.get_instance(o)
+
+        fields={}
+        fields["files_dir"] = o.files_dir
+        fields["appname"] = o.name
+        fields["nat_ip"] = instance.get_ssh_ip()
+        fields["config_fns"] = o.config_fns
+        fields["rest_configs"] = o.rest_configs
+        fields["early_rest_configs"] = o.early_rest_configs
+        if o.dependencies:
+            fields["dependencies"] = [x.strip() for x in o.dependencies.split(",")]
+        else:
+            fields["dependencies"] = []
+
+        if (instance.isolation=="container"):
+            fields["ONOS_container"] = "%s-%s" % (instance.slice.name, str(instance.id))
+        else:
+            fields["ONOS_container"] = "ONOS"
+        return fields
+
+    def sync_fields(self, o, fields):
+        # the super causes the playbook to be run
+        super(SyncONOSApp, self).sync_fields(o, fields)
+
+    def run_playbook(self, o, fields):
+        super(SyncONOSApp, self).run_playbook(o, fields)
+
+    def delete_record(self, m):
+        pass
diff --git a/xos/observers/onos/steps/sync_onosapp.yaml b/xos/observers/onos/steps/sync_onosapp.yaml
new file mode 100644
index 0000000..a03368b
--- /dev/null
+++ b/xos/observers/onos/steps/sync_onosapp.yaml
@@ -0,0 +1,114 @@
+---
+- hosts: {{ instance_name }}
+  gather_facts: False
+  connection: ssh
+  user: {{ username }}
+  sudo: yes
+  vars:
+    appname: {{ appname }}
+    dependencies: {{ dependencies }}
+{% if rest_configs %}
+    rest_configs:
+{% for rest_config in rest_configs %}
+       - endpoint: {{ rest_config.endpoint }}
+         body: "{{ '{{' }} lookup('file', '{{ files_dir }}/{{ rest_config.fn }}') {{ '}}' }}"
+{% endfor %}
+{% endif %}
+{% if early_rest_configs %}
+    early_rest_configs:
+{% for early_rest_config in early_rest_configs %}
+       - endpoint: {{ early_rest_config.endpoint }}
+         body: "{{ '{{' }} lookup('file', '{{ files_dir }}/{{ early_rest_config.fn }}') {{ '}}' }}"
+{% endfor %}
+{% endif %}
+
+  tasks:
+
+  - name: Get Docker IP
+    script: /opt/xos/observers/onos/scripts/dockerip.sh {{ ONOS_container }}
+    register: onosaddr
+
+  - name: Wait for ONOS to come up
+    wait_for:
+      host={{ '{{' }} onosaddr.stdout {{ '}}' }}
+      port={{ '{{' }} item {{ '}}' }}
+      state=present
+    with_items:
+    - 8101
+    - 8181
+    - 9876
+
+  - name: Config file directory
+    file:
+      path=/home/ubuntu/{{ appname }}/
+      state=directory
+
+{% if config_fns %}
+  - name: Copy over configuration files
+    copy:
+      src={{ files_dir }}/{{ '{{' }} item {{ '}}' }}
+      dest=/home/ubuntu/{{ appname }}/{{ '{{' }} item {{ '}}' }}
+    with_items:
+        {% for config_fn in config_fns %}
+        - {{ config_fn }}
+        {% endfor %}
+
+  - name: Make sure config directory exists
+    shell: docker exec {{ ONOS_container }} mkdir -p /root/onos/config/
+    sudo: yes
+
+  - name: Copy config files into container
+    shell: docker cp {{ appname }}/{{ '{{' }} item {{ '}}' }} {{ ONOS_container }}:/root/onos/config/
+    sudo: yes
+    with_items:
+        {% for config_fn in config_fns %}
+        - {{ config_fn }}
+        {% endfor %}
+{% endif %}
+
+  # Don't know how to check for this condition, just wait
+  - name: Wait for ONOS to install the apps
+    wait_for: timeout=15
+
+{% if early_rest_configs %}
+  - name: Add ONOS early configuration values
+    uri:
+      url: http://{{ '{{' }} onosaddr.stdout {{ '}}' }}:8181/{{ '{{' }} item.endpoint {{ '}}' }}
+      body: "{{ '{{' }} item.body {{ '}}' }}"
+      body_format: raw
+      method: POST
+      user: karaf
+      password: karaf
+    with_items: "early_rest_configs"
+
+  # Don't know how to check for this condition, just wait
+  - name: Wait for ONOS to restart
+    wait_for: timeout=15
+{% endif %}
+
+{% if dependencies %}
+  - name: Add dependencies to ONOS
+    uri:
+      url: http://{{ '{{' }} onosaddr.stdout {{ '}}' }}:8181/onos/v1/applications/{{ '{{' }} item {{ '}}' }}/active
+      method: POST
+      user: karaf
+      password: karaf
+    with_items:
+        {% for dependency in dependencies %}
+        - {{ dependency }}
+        {% endfor %}
+{% endif %}
+
+{% if rest_configs %}
+# Do this after services have been activated, or it will cause an exception.
+# vOLT will re-read its net config; vbng may not.
+  - name: Add ONOS configuration values
+    uri:
+      url: http://{{ '{{' }} onosaddr.stdout {{ '}}' }}:8181/{{ '{{' }} item.endpoint {{ '}}' }} #http://localhost:8181/onos/v1/network/configuration/
+      body: "{{ '{{' }} item.body {{ '}}' }}"
+      body_format: raw
+      method: POST
+      user: karaf
+      password: karaf
+    with_items: "rest_configs"
+{% endif %}
diff --git a/xos/observers/onos/steps/sync_onosservice.py b/xos/observers/onos/steps/sync_onosservice.py
new file mode 100644
index 0000000..efa5d72
--- /dev/null
+++ b/xos/observers/onos/steps/sync_onosservice.py
@@ -0,0 +1,77 @@
+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 synchronizers.base.syncstep import SyncStep
+from synchronizers.base.ansible import run_template_ssh
+from observers.base.SyncInstanceUsingAnsible import SyncInstanceUsingAnsible
+from core.models import Service, Slice
+from services.onos.models import ONOSService, ONOSApp
+from util.logger import Logger, logging
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+logger = Logger(level=logging.INFO)
+
+class SyncONOSService(SyncInstanceUsingAnsible):
+    provides=[ONOSService]
+    observes=ONOSService
+    requested_interval=0
+    template_name = "sync_onosservice.yaml"
+    service_key_name = "/opt/xos/observers/onos/onos_key"
+
+    def __init__(self, *args, **kwargs):
+        super(SyncONOSService, self).__init__(*args, **kwargs)
+
+    def fetch_pending(self, deleted):
+        if (not deleted):
+            objs = ONOSService.get_service_objects().filter(Q(enacted__lt=F('updated')) | Q(enacted=None),Q(lazy_blocked=False))
+        else:
+            objs = ONOSService.get_deleted_service_objects()
+
+        return objs
+
+    def get_instance(self, o):
+        # We assume the ONOS service owns a slice, so pick one of the instances
+        # inside that slice to sync to.
+
+        serv = o
+
+        if serv.use_external_host:
+            return serv.use_external_host
+
+        if serv.slices.exists():
+            slice = serv.slices.all()[0]
+            if slice.instances.exists():
+                return slice.instances.all()[0]
+
+        return None
+
+    def get_extra_attributes(self, o):
+        fields={}
+        fields["instance_hostname"] = self.get_instance(o).instance_name.replace("_","-")
+        fields["appname"] = o.name
+        fields["nat_ip"] = self.get_instance(o).get_ssh_ip()
+        fields["ONOS_container"] = "ONOS"
+        return fields
+
+    def sync_fields(self, o, fields):
+        # the super causes the playbook to be run
+        super(SyncONOSService, self).sync_fields(o, fields)
+
+    def run_playbook(self, o, fields):
+        instance = self.get_instance(o)
+        if (instance.isolation=="container"):
+            # If the instance is already a container, then we don't need to
+            # install ONOS.
+            return
+        super(SyncONOSService, self).run_playbook(o, fields)
+
+    def delete_record(self, m):
+        pass
diff --git a/xos/observers/onos/steps/sync_onosservice.yaml b/xos/observers/onos/steps/sync_onosservice.yaml
new file mode 100644
index 0000000..fd9c3db
--- /dev/null
+++ b/xos/observers/onos/steps/sync_onosservice.yaml
@@ -0,0 +1,66 @@
+---
+- hosts: {{ instance_name }}
+  gather_facts: False
+  connection: ssh
+  user: ubuntu
+  sudo: yes
+
+  tasks:
+
+  - name: Fix /etc/hosts
+    lineinfile:
+      dest=/etc/hosts
+      regexp="127.0.0.1 localhost"
+      line="127.0.0.1 localhost {{ instance_hostname }}"
+
+  - name: Add repo key
+    apt_key:
+      keyserver=hkp://pgp.mit.edu:80
+      id=58118E89F3A912897C070ADBF76221572C52609D
+
+  - name: Install Docker repo
+    apt_repository:
+      repo="deb https://apt.dockerproject.org/repo ubuntu-trusty main"
+      state=present
+
+  - name: Install Docker
+    apt:
+      name={{ '{{' }} item {{ '}}' }}
+      state=latest
+      update_cache=yes
+    with_items:
+    - docker-engine
+    - python-pip
+    - python-httplib2
+
+  - name: Install docker-py
+    pip:
+      name=docker-py
+      state=latest
+
+  - name: Start ONOS container
+    docker:
+      docker_api_version: "1.18"
+      name: {{ ONOS_container }}
+      # was: reloaded
+      state: running
+      image: onosproject/onos
+      ports:
+      - "6653:6653"
+      - "8101:8101"
+      - "8181:8181"
+      - "9876:9876"
+
+  - name: Get Docker IP
+    script: /opt/xos/observers/onos/scripts/dockerip.sh {{ ONOS_container }}
+    register: dockerip
+
+  - name: Wait for ONOS to come up
+    wait_for:
+      host={{ '{{' }} dockerip.stdout {{ '}}' }}
+      port={{ '{{' }} item {{ '}}' }}
+      state=present
+    with_items:
+    - 8101
+    - 8181
+    - 9876
diff --git a/xos/observers/onos/stop.sh b/xos/observers/onos/stop.sh
new file mode 100755
index 0000000..17d6eb7
--- /dev/null
+++ b/xos/observers/onos/stop.sh
@@ -0,0 +1 @@
+pkill -9 -f onos-observer.py
diff --git a/xos/observers/onos/supervisor/onos-observer.conf b/xos/observers/onos/supervisor/onos-observer.conf
new file mode 100644
index 0000000..36e00d9
--- /dev/null
+++ b/xos/observers/onos/supervisor/onos-observer.conf
@@ -0,0 +1,9 @@
+[supervisord]
+logfile=/var/log/supervisord.log ; (main log file;default $CWD/supervisord.log)
+pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
+nodaemon=true
+
+[program:synchronizer]
+command=python /opt/xos/observers/onos/onos-observer.py -C /opt/xos/observers/onos/onos_observer_config
+stderr_logfile=/var/log/supervisor/synchronizer.err.log
+stdout_logfile=/var/log/supervisor/synchronizer.out.log
diff --git a/xos/observers/requestrouter/configurationPush.py b/xos/observers/requestrouter/configurationPush.py
new file mode 100644
index 0000000..857de8b
--- /dev/null
+++ b/xos/observers/requestrouter/configurationPush.py
@@ -0,0 +1,30 @@
+import ansible.playbook
+import ansible.constants as C
+import ansible.utils.template
+from ansible import errors
+from ansible import callbacks
+from ansible import utils
+from subprocess import call
+
+class ConfigurationPush:
+	def __init__(self):
+		pass
+
+	def config_push(self, service_name, user, playbook_name,hostfile):
+		'''stats = callbacks.AggregateStats()
+		playbook_cb = callbacks.PlaybookCallbacks(verbose=utils.VERBOSITY)
+		runner_cb = callbacks.PlaybookRunnerCallbacks(stats, verbose=utils.VERBOSITY)
+		pb = ansible.playbook.PlayBook(playbook="playbook/site.yml",
+					callbacks=playbook_cb,
+            				runner_callbacks=runner_cb,
+            				stats=stats
+					)
+		result = pb.run()
+		print result
+		'''
+
+		call("ansible-playbook --private-key=planetw "+playbook_name+" -i "+hostfile+" -u "+user+"  --extra-vars \"name="+service_name+"\"", shell=True)
+	
+
+if __name__ == "__main__":
+        main()
diff --git a/xos/observers/requestrouter/deleters/servicemap_deleter.py b/xos/observers/requestrouter/deleters/servicemap_deleter.py
new file mode 100644
index 0000000..427242f
--- /dev/null
+++ b/xos/observers/requestrouter/deleters/servicemap_deleter.py
@@ -0,0 +1,42 @@
+import os
+import sys
+import traceback
+from services.requestrouter.models import ServiceMap
+from synchronizers.base.deleter import Deleter
+from util.logger import Logger, logging
+from xos.config import Config, XOS_DIR
+
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from rrlib import RequestRouterLibrary
+from configurationPush import ConfigurationPush
+import rrlib_config
+
+logger = Logger(level=logging.INFO)
+
+class ServiceMapDeleter(Deleter, RequestRouterLibrary, ConfigurationPush):
+        model='ServiceMap'
+
+        def __init__(self, **args):
+            Deleter.__init__(self, **args)
+            RequestRouterLibrary.__init__(self)
+            ConfigurationPush.__init__(self)
+
+
+        def call(self, pk, model_dict):
+          try:
+              servicemap = ServiceMap.objects.get(pk=pk)
+              service_uid = self.get_servicemap_uid(servicemap)
+              self.config_push(service_uid, rrlib_config.REDIR_USER, XOS_DIR + "/observers/requestrouter/playbook/site_redir_delete.yml", "/etc/ansible/requestrouter/dnsredir/hosts")
+              self.config_push(service_uid, rrlib_config.DEMUX_USER, XOS_DIR + "/observers/requestrouter/playbook/site_demux_delete.yml", "/etc/ansible/requestrouter/dnsdemux/hosts")
+              print "XXX delete ServiceMap %s", servicemap.name
+              return True
+          except Exception, e:
+              traceback.print_exc()
+              logger.exception("Failed to erase map '%s'" % map_name)
+              return False
+
+if __name__ == "__main__":
+  smap = ServiceMapDeleter()
+  smap.call( 6, {'name': 'Service23'} )
diff --git a/xos/observers/requestrouter/model-deps b/xos/observers/requestrouter/model-deps
new file mode 100644
index 0000000..36ef620
--- /dev/null
+++ b/xos/observers/requestrouter/model-deps
@@ -0,0 +1,12 @@
+{
+    "ServiceMap": [
+        "Slice"
+    ], 
+    "Slice": [
+        "Site", 
+        "Service"
+    ], 
+    "User": [
+        "Site"
+    ]
+}
diff --git a/xos/observers/requestrouter/playbook/roles/delete_demux/handlers/main.yml b/xos/observers/requestrouter/playbook/roles/delete_demux/handlers/main.yml
new file mode 100644
index 0000000..a888468
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/delete_demux/handlers/main.yml
@@ -0,0 +1,3 @@
+---
+- name: restart dnsdemux
+  service: name=dnsdemux state=restarted
diff --git a/xos/observers/requestrouter/playbook/roles/delete_demux/tasks/main.yml b/xos/observers/requestrouter/playbook/roles/delete_demux/tasks/main.yml
new file mode 100644
index 0000000..417401a
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/delete_demux/tasks/main.yml
@@ -0,0 +1,5 @@
+---
+# This playbook contains plays to delete configuration files from dnsdemux slice
+- name: delete dnsdemux config files
+  file: path={{dst_dnsdemux_conf}}/{{name}}.conf state=absent
+  notify: restart dnsdemux
diff --git a/xos/observers/requestrouter/playbook/roles/delete_demux/vars/main.yml b/xos/observers/requestrouter/playbook/roles/delete_demux/vars/main.yml
new file mode 100644
index 0000000..b457050
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/delete_demux/vars/main.yml
@@ -0,0 +1,6 @@
+---
+# Variables listed here are applicable to all host groups
+
+src_dnsdemux: ../../../../temp_config/dnsdemux
+dst_dnsdemux_conf: /etc/dnsdemux/default/
+
diff --git a/xos/observers/requestrouter/playbook/roles/delete_redir/handlers/main.yml b/xos/observers/requestrouter/playbook/roles/delete_redir/handlers/main.yml
new file mode 100644
index 0000000..8aa497e
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/delete_redir/handlers/main.yml
@@ -0,0 +1,3 @@
+---
+- name: restart dnsredir
+  service: name=dnsredir state=restarted
diff --git a/xos/observers/requestrouter/playbook/roles/delete_redir/tasks/main.yml b/xos/observers/requestrouter/playbook/roles/delete_redir/tasks/main.yml
new file mode 100644
index 0000000..58833bb
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/delete_redir/tasks/main.yml
@@ -0,0 +1,13 @@
+---
+# This playbook contains plays to delete configuration files from dnsredir slice
+- name: delete  maps.d directory ( and deletes all intermeadiate directories )
+  #copy: src={{src_dnsredir}}/{{name}}.d dest={{dst_dnsredir_confdir}}
+  file: path={{dst_dnsredir_confdir}}/{{name}}.d/maps.d state=absent
+ 
+- name: delete config directory
+  file: path={{dst_dnsredir_confdir}}/{{name}}.d/ state=absent
+ 
+- name: copy dnsredir config file
+  file: path={{dst_dnsredir_conf}}/{{name}}.conf state=absent
+  notify: restart dnsredir
+
diff --git a/xos/observers/requestrouter/playbook/roles/delete_redir/vars/main.yml b/xos/observers/requestrouter/playbook/roles/delete_redir/vars/main.yml
new file mode 100644
index 0000000..dc79d35
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/delete_redir/vars/main.yml
@@ -0,0 +1,6 @@
+---
+# Variables listed here are applicable to all host groups
+
+src_dnsredir: ../../../../temp_config/dnsredir
+dst_dnsredir_conf: /etc/dnsredir/conf.d/
+dst_dnsredir_confdir: /etc/dnsredir
diff --git a/xos/observers/requestrouter/playbook/roles/demux/handlers/main.yml b/xos/observers/requestrouter/playbook/roles/demux/handlers/main.yml
new file mode 100644
index 0000000..a888468
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/demux/handlers/main.yml
@@ -0,0 +1,3 @@
+---
+- name: restart dnsdemux
+  service: name=dnsdemux state=restarted
diff --git a/xos/observers/requestrouter/playbook/roles/demux/tasks/main.yml b/xos/observers/requestrouter/playbook/roles/demux/tasks/main.yml
new file mode 100644
index 0000000..0339aed
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/demux/tasks/main.yml
@@ -0,0 +1,9 @@
+---
+# This playbook contains plays to copy configurations to destination
+- name: copy dnsdemux config files
+  copy: src={{src_dnsdemux}}/{{name}}.conf dest={{dst_dnsdemux_conf}}/{{name}}.conf
+  notify: restart dnsdemux
+
+#- name: Start the dnsdemux service
+  #service: name=dnsdemux state=started enabled=true
+
diff --git a/xos/observers/requestrouter/playbook/roles/demux/vars/main.yml b/xos/observers/requestrouter/playbook/roles/demux/vars/main.yml
new file mode 100644
index 0000000..b457050
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/demux/vars/main.yml
@@ -0,0 +1,6 @@
+---
+# Variables listed here are applicable to all host groups
+
+src_dnsdemux: ../../../../temp_config/dnsdemux
+dst_dnsdemux_conf: /etc/dnsdemux/default/
+
diff --git a/xos/observers/requestrouter/playbook/roles/redir/handlers/main.yml b/xos/observers/requestrouter/playbook/roles/redir/handlers/main.yml
new file mode 100644
index 0000000..8aa497e
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/redir/handlers/main.yml
@@ -0,0 +1,3 @@
+---
+- name: restart dnsredir
+  service: name=dnsredir state=restarted
diff --git a/xos/observers/requestrouter/playbook/roles/redir/tasks/main.yml b/xos/observers/requestrouter/playbook/roles/redir/tasks/main.yml
new file mode 100644
index 0000000..e390ed9
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/redir/tasks/main.yml
@@ -0,0 +1,19 @@
+---
+# This playbook contains plays to copy configurations to destination
+- name: create  maps.d directory ( and creates all intermeadiate directories )
+  #copy: src={{src_dnsredir}}/{{name}}.d dest={{dst_dnsredir_confdir}}
+  file: src={{src_dnsredir}}/{{name}}.d/maps.d dest={{dst_dnsredir_confdir}}/{{name}}.d/maps.d state=directory
+ 
+- name: copy map.conf
+  copy: src={{src_dnsredir}}/{{name}}.d/maps.d/map.conf dest={{dst_dnsredir_confdir}}/{{name}}.d/maps.d/map.conf
+ 
+- name: copy codeen_nodes.conf
+  copy: src={{src_dnsredir}}/{{name}}.d/codeen_nodes.conf dest={{dst_dnsredir_confdir}}/{{name}}.d/codeen_nodes.conf
+  
+- name: copy node-to-ip.txt
+  copy: src={{src_dnsredir}}/{{name}}.d/node-to-ip.txt dest={{dst_dnsredir_confdir}}/{{name}}.d/node-to-ip.txt
+
+- name: copy dnsredir config file
+  copy: src={{src_dnsredir}}/{{name}}.conf dest={{dst_dnsredir_conf}}/{{name}}.conf
+  notify: restart dnsredir
+
diff --git a/xos/observers/requestrouter/playbook/roles/redir/vars/main.yml b/xos/observers/requestrouter/playbook/roles/redir/vars/main.yml
new file mode 100644
index 0000000..dc79d35
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/redir/vars/main.yml
@@ -0,0 +1,6 @@
+---
+# Variables listed here are applicable to all host groups
+
+src_dnsredir: ../../../../temp_config/dnsredir
+dst_dnsredir_conf: /etc/dnsredir/conf.d/
+dst_dnsredir_confdir: /etc/dnsredir
diff --git a/xos/observers/requestrouter/playbook/site_demux.yml b/xos/observers/requestrouter/playbook/site_demux.yml
new file mode 100644
index 0000000..b5261dc
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/site_demux.yml
@@ -0,0 +1,9 @@
+---
+# This playbook copies the dnsdemux configuration files from temp directory.
+
+- name: copies the configuration files from temp directory
+  hosts: all
+  #remote_user: {{r_user}}
+
+  roles:
+    - demux
diff --git a/xos/observers/requestrouter/playbook/site_demux_delete.yml b/xos/observers/requestrouter/playbook/site_demux_delete.yml
new file mode 100644
index 0000000..49a7c87
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/site_demux_delete.yml
@@ -0,0 +1,9 @@
+---
+# This playbook deletes the dnsdemux configuration files from request router instances
+
+- name: deletes the configuration files from request router instances
+  hosts: all
+  #remote_user: {{r_user}}
+
+  roles:
+    - delete_demux
diff --git a/xos/observers/requestrouter/playbook/site_redir.yml b/xos/observers/requestrouter/playbook/site_redir.yml
new file mode 100644
index 0000000..50a7284
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/site_redir.yml
@@ -0,0 +1,9 @@
+---
+# This playbook copies the dnsredir configuration files from temp directory.
+
+- name: copies the configuration files from temp directory
+  hosts: all
+  #remote_user: {{r_user}}
+
+  roles:
+    - redir
diff --git a/xos/observers/requestrouter/playbook/site_redir_delete.yml b/xos/observers/requestrouter/playbook/site_redir_delete.yml
new file mode 100644
index 0000000..9a8611d
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/site_redir_delete.yml
@@ -0,0 +1,9 @@
+---
+# This playbook deletes the dnsredir configuration files from request router instances
+
+- name: deletes the configuration files from request router instances
+  hosts: all
+  #remote_user: {{r_user}}
+
+  roles:
+    - delete_redir
diff --git a/xos/observers/requestrouter/rr_observer_config b/xos/observers/requestrouter/rr_observer_config
new file mode 100644
index 0000000..ec3a1ba
--- /dev/null
+++ b/xos/observers/requestrouter/rr_observer_config
@@ -0,0 +1,36 @@
+
+[plc]
+name=plc
+#deployment=VICCI
+deployment=VINI
+
+[db]
+name=xos
+user=plstackuser
+#password=2uMDYtJK
+password=1HL07C0E
+host=localhost
+port=5432
+
+[api]
+host=128.112.171.237
+port=8000
+ssl_key=None
+ssl_cert=None
+ca_ssl_cert=None
+ratelimit_enabled=0
+omf_enabled=0
+mail_support_address=support@localhost
+nova_enabled=True
+
+[observer]
+dependency_graph=/opt/xos/observers/requestrouter/model-deps
+steps_dir=/opt/xos/observers/requestrouter/steps
+deleters_dir=/opt/xos/observers/requestrouter/deleters
+log_file=console
+#/var/log/hpc.log
+driver=None
+
+#[feefie]
+#client_id='vicci_dev_central'
+#user_id='pl'
diff --git a/xos/observers/requestrouter/rrlib.py b/xos/observers/requestrouter/rrlib.py
new file mode 100644
index 0000000..9faca4a
--- /dev/null
+++ b/xos/observers/requestrouter/rrlib.py
@@ -0,0 +1,203 @@
+import os
+import base64
+import string
+import sys
+import socket
+from sets import Set
+if __name__ == '__main__':
+    sys.path.append("/opt/xos")
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+
+from xos.config import Config
+from core.models import Service
+from services.requestrouter.models import RequestRouterService, ServiceMap
+from util.logger import Logger, logging
+import rrlib_config
+
+logger = Logger(level=logging.INFO)
+
+'''
+Conventions:
+1) All dnsredir backend will listen at port 9000+ servicemap.pk ( where pk is the primary key generated in django model)
+'''
+
+class RequestRouterLibrary:
+
+    def __init__(self):
+        pass
+    
+    def gen_slice_info(self, service=None):   
+        """generates instance information from slice of request router
+        """
+
+        if (service is None ):
+            service = RequestRouterService.objects.get()
+
+        mapping = {}
+        #static mapping for demo purpose 
+        #mapping["node47.princeton.vicci.org"] = "128.112.171.112"
+        mapping["node48.princeton.vicci.org"] = "128.112.171.114"
+    
+        '''for slice in service.service.all():
+            name = slice.name
+            for instance in slice.instances.all():
+                mapping[instance.name] = str(instance.ip)
+        '''
+        return mapping
+
+    def gen_servicemap_slice_info(self, servicemap):
+        """generates instance information from slice of servicemap
+        """
+
+        wzone = Set(['arizona', 'stanford', 'on.lab', 'housten']) # zone=1 in cooden.conf
+        ezone = Set(['princeton', 'atlanta', 'new york', 'georgia tech']) # zone=2 in coodeen.conf
+
+        mapping_zone = {}
+        mapping_ip = {}
+        slice = servicemap.slice
+        name = slice.name
+        for instance in slice.instances.all():
+            mapping_ip[instance.node.name] = socket.gethostbyname(instance.node.name)
+            #print "instance name "+instance.name+str(instance.ip)+"\n"
+            site = instance.node.site.name
+            if(site.lower() in wzone):
+                mapping_zone[instance.node.name] = str(1)
+            else:
+                mapping_zone[instance.node.name] = str(2)
+
+        return mapping_ip, mapping_zone
+
+
+
+    def gen_slice_file(self, service):
+        """ generates host file for the slice information
+            to be used by ansible to push configuration files
+        """
+
+        mapping = self.gen_slice_info(service)
+
+        fn = "/etc/ansible/requestrouter/dnsredir/hosts"
+        f = open(fn, "w")
+        for (k,v) in mapping.items():
+            f.write("%s\n" % k)
+
+        fn = "/etc/ansible/requestrouter/dnsdemux/hosts"
+        f = open(fn, "w")
+        for (k,v) in mapping.items():
+            f.write("%s\n" % k)
+
+
+    def get_servicemap_uid(self, servicemap):
+        seq = ("service_", str(servicemap.pk));
+        return "".join(seq)
+
+    def get_service_port(self, servicemap):
+                return str(9000+servicemap.pk)
+
+    def gen_dnsredir_serviceconf(self, servicemap):
+        objname = self.get_servicemap_uid(servicemap)
+    
+        rr_mapping = self.gen_slice_info(None)
+    
+        #generate dnsredir.conf file parameters to be used in static file.
+        mapping = {}
+        mapping["port_listen"] = self.get_service_port(servicemap)
+        mapping["configdir"] = rrlib_config.DNSREDIR_CONFIGDIR_PREFIX+objname+".d/"
+        mapping["logdir"] = rrlib_config.DNSREDIR_LOGDIR_PREFIX+objname+".d"
+        mapping["pidfile"] = rrlib_config.DNSREDIR_PIDFILE_PREFIX+objname+".pid"
+        mapping["domain_name"] = servicemap.prefix      
+        mapping["heartbeat_port"] = rrlib_config.HEARTBEAT_PORT
+
+        #generate dnsredir.conf file 
+
+        fn = "./temp_config/dnsredir/"+objname+".conf"
+        f = open(fn, "w")
+        for (k,v) in rr_mapping.items():
+                        f.write(mapping["domain_name"]+". NS "+k+". "+v+" 3600 \n" % mapping)
+
+
+        f.write("""
+Default_TTL 30
+
+Port %(port_listen)s
+
+ConfigDir %(configdir)s
+
+MapsDir maps.d
+
+HTTPPort %(heartbeat_port)d
+
+PidFile %(pidfile)s
+
+HttpRequestPort 8081
+
+""" % mapping)
+
+        #generate configdirectory
+        
+        os.mkdir("./temp_config/dnsredir/"+objname+".d")
+        
+        #geenrate codeen_nodes.conf
+        mapping_ip, mapping_zone = self.gen_servicemap_slice_info(servicemap)
+
+        codeen_name = "./temp_config/dnsredir/"+objname+".d/codeen_nodes.conf"
+        f = open(codeen_name, "w")
+        for (k,v) in mapping_zone.items():
+            f.write(k+" zone="+v+" \n")
+
+        iptxt = "./temp_config/dnsredir/"+objname+".d/node-to-ip.txt"
+        f = open(iptxt, "w")
+        for (k,v) in mapping_ip.items():
+            f.write(k+" "+v+" \n")
+
+        #generate maps directory
+        os.mkdir("./temp_config/dnsredir/"+objname+".d/maps.d")
+
+        # redirection map
+        map = "./temp_config/dnsredir/"+objname+".d/maps.d/map.conf"
+        f = open(map, "w")
+		#hardcoded probable public IP masks from arizona and princeton region respectively
+        f.write("prefix "+servicemap.prefix+" \n")
+        f.write("map 150.135.211.252/32 zone 1 || zone 2 \n")
+        f.write("map 128.112.171.112/24 zone 2 || zone 1 \n")
+        f.write("map 0.0.0.0/0 zone 1 || zone 2 \n")
+
+
+    def gen_dnsdemux_serviceconf(self, servicemap):
+        '''
+        generates frontend service*.conf file for each of the service
+        It assumes that there is a dnsdemux frontend running on the RR istallation and will
+        just add a conf file for each service in /etc/dnsdemux/default
+        '''
+        objname = self.get_servicemap_uid(servicemap)
+        #generate dnsdemux.conf file parameters to be used in static file.
+       
+        port_listen = self.get_service_port(servicemap)
+        domain_name = servicemap.prefix  
+        #generate service specific .conf file
+
+        rr_mapping = self.gen_slice_info(None)
+
+        fn = "./temp_config/dnsdemux/"+objname+".conf"
+        f = open(fn, "w")
+
+        for (k,v) in rr_mapping.items():
+            f.write("Forward "+v+" "+port_listen+" 8081 "+domain_name+".\n")
+
+    
+    def teardown_temp_configfiles(self, objname):
+        if os.path.exists("./temp_config/dnsdemux/"+objname+".conf"):
+            os.remove("./temp_config/dnsdemux/"+objname+".conf")
+        if os.path.exists("./temp_config/dnsredir/"+objname+".d/maps.d/map.conf"):
+            os.remove("./temp_config/dnsredir/"+objname+".d/maps.d/map.conf")
+        if os.path.exists("./temp_config/dnsredir/"+objname+".d/maps.d"):
+            os.rmdir("./temp_config/dnsredir/"+objname+".d/maps.d")
+        if os.path.exists("./temp_config/dnsredir/"+objname+".d/node-to-ip.txt"):
+            os.remove("./temp_config/dnsredir/"+objname+".d/node-to-ip.txt")
+        if os.path.exists("./temp_config/dnsredir/"+objname+".d/codeen_nodes.conf"):
+            os.remove("./temp_config/dnsredir/"+objname+".d/codeen_nodes.conf")
+        if os.path.exists("./temp_config/dnsredir/"+objname+".d"):
+            os.rmdir("./temp_config/dnsredir/"+objname+".d")
+        if os.path.exists("./temp_config/dnsredir/"+objname+".conf"):
+            os.remove("./temp_config/dnsredir/"+objname+".conf")
+
diff --git a/xos/observers/requestrouter/rrlib_config.py b/xos/observers/requestrouter/rrlib_config.py
new file mode 100644
index 0000000..d0c00c2
--- /dev/null
+++ b/xos/observers/requestrouter/rrlib_config.py
@@ -0,0 +1,16 @@
+#!/usr/bin/env python
+
+DNSREDIR_CONFIGDIR_PREFIX = "/etc/dnsredir/"
+DNSREDIR_LOGDIR_PREFIX = "/var/log/dnsredir/"
+DNSREDIR_PIDFILE_PREFIX = "/var/run/dnsredir."
+REDIR_USER = "princeton_coredirect"
+#REDIR_USER = "arizona_tools1"
+#DEMUX_USER = "arizona_tools1"
+DEMUX_USER = "princeton_codnsdemux"
+HEARTBEAT_PORT = 9000
+#not required as of now, as there will be only one frontend dnsdemux for all the services
+#DNSDEMUX_CONFIGDIR_PREFIX = "/etc/dnsdemux/"
+#DNSDEMUX_LOGDIR_PREFIX = "/var/log/dnsdemux/"
+#DNSDEMUX_PIDFILE_PREFIX = "/var/run/dnsdemux."
+
+TBD = "TBD"
diff --git a/xos/observers/requestrouter/steps/sync_requestrouterservices.py b/xos/observers/requestrouter/steps/sync_requestrouterservices.py
new file mode 100644
index 0000000..9af3584
--- /dev/null
+++ b/xos/observers/requestrouter/steps/sync_requestrouterservices.py
@@ -0,0 +1,46 @@
+import os
+import sys
+import base64
+import traceback
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+from services.requestrouter.models import RequestRouterService
+from util.logger import Logger, logging
+
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from rrlib import RequestRouterLibrary
+
+logger = Logger(level=logging.INFO)
+
+class SyncRequestRouterService(SyncStep, RequestRouterLibrary):
+    provides=[RequestRouterService]
+    requested_interval=0
+
+    def __init__(self, **args):
+        SyncStep.__init__(self, **args)
+        RequestRouterLibrary.__init__(self)
+
+    def fetch_pending(self):
+	try:
+        	ret = RequestRouterService.objects.filter(Q(enacted__lt=F('updated')) | Q(enacted=None))
+        	return ret
+	except Exception, e:
+        	traceback.print_exc()
+            	return None	
+
+    def sync_record(self, rr_service):
+	try:
+        	print "syncing service!"
+        	logger.info("sync'ing rr_service %s" % str(rr_service))
+        	self.gen_slice_file(rr_service)
+        	rr_service.save()
+		return True
+	except Exception, e:
+                traceback.print_exc()
+                return False
+
+
diff --git a/xos/observers/requestrouter/steps/sync_servicemap.py b/xos/observers/requestrouter/steps/sync_servicemap.py
new file mode 100644
index 0000000..18bcec4
--- /dev/null
+++ b/xos/observers/requestrouter/steps/sync_servicemap.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python
+
+import os
+import sys
+import base64
+import traceback
+from django.db.models import F, Q
+from xos.config import Config, XOS_DIR
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+from services.requestrouter.models import ServiceMap
+from util.logger import Logger, logging
+
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from rrlib import RequestRouterLibrary
+from configurationPush import ConfigurationPush
+import rrlib_config
+
+logger = Logger(level=logging.INFO)
+
+class SyncServiceMap(SyncStep, RequestRouterLibrary, ConfigurationPush):
+    provides=[ServiceMap]
+    requested_interval=0
+
+    def __init__(self, **args):
+        SyncStep.__init__(self, **args)
+	RequestRouterLibrary.__init__(self)
+	ConfigurationPush.__init__(self)
+
+    def fetch_pending(self):
+	try:
+        	ret = ServiceMap.objects.filter(Q(enacted__lt=F('updated')) | Q(enacted=None))
+        	return ret
+	except Exception, e:
+        	traceback.print_exc()
+            	return None
+
+    def sync_record(self, servicemap):
+	try:
+		print "sync! %s " % self.get_servicemap_uid(servicemap)
+		self.gen_dnsredir_serviceconf(servicemap)
+		self.gen_dnsdemux_serviceconf(servicemap)
+        	# push generated files from temp_config
+		service_uid = self.get_servicemap_uid(servicemap)
+		self.config_push(service_uid, rrlib_config.REDIR_USER, XOS_DIR + "/observers/requestrouter/playbook/site_redir.yml", "/etc/ansible/requestrouter/dnsredir/hosts")
+		self.config_push(service_uid, rrlib_config.DEMUX_USER, XOS_DIR + "/observers/requestrouter/playbook/site_demux.yml", "/etc/ansible/requestrouter/dnsdemux/hosts")
+		self.teardown_temp_configfiles(service_uid)
+	except Exception, e:
+                traceback.print_exc()
+                return False
+
+if __name__ == "__main__":
+    sv = SyncServiceMap()
+
+    recs = sv.fetch_pending()
+
+    for rec in recs:
+        sv.sync_record( rec )
diff --git a/xos/observers/syndicate/__init__.py b/xos/observers/syndicate/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xos/observers/syndicate/__init__.py
diff --git a/xos/observers/syndicate/model-deps b/xos/observers/syndicate/model-deps
new file mode 100644
index 0000000..b15c5d1
--- /dev/null
+++ b/xos/observers/syndicate/model-deps
@@ -0,0 +1,16 @@
+{
+    "SlicePrivilege": [
+        "User", 
+        "Slice"
+    ], 
+    "Slice": [
+        "Site", 
+        "Service"
+    ], 
+    "VolumeAccessRight": [
+        "Volume"
+    ], 
+    "User": [
+        "Site"
+    ]
+}
diff --git a/xos/observers/syndicate/requirements.py b/xos/observers/syndicate/requirements.py
new file mode 100644
index 0000000..303fd3d
--- /dev/null
+++ b/xos/observers/syndicate/requirements.py
@@ -0,0 +1,5 @@
+requests
+gevent
+grequests
+setproctitle
+psutil
diff --git a/xos/observers/syndicate/run.sh b/xos/observers/syndicate/run.sh
new file mode 100644
index 0000000..82960a9
--- /dev/null
+++ b/xos/observers/syndicate/run.sh
@@ -0,0 +1,2 @@
+export XOS_DIR=/opt/xos
+python syndicate-backend.py  -C $XOS_DIR/observers/syndicate/syndicate_observer_config
diff --git a/xos/observers/syndicate/start.sh b/xos/observers/syndicate/start.sh
new file mode 100644
index 0000000..1c408a1
--- /dev/null
+++ b/xos/observers/syndicate/start.sh
@@ -0,0 +1,2 @@
+export XOS_DIR=/opt/xos
+nohup python syndicate-backend.py  -C $XOS_DIR/observers/syndicate/syndicate_observer_config > /dev/null 2>&1 &
diff --git a/xos/observers/syndicate/steps/sync_volume.py b/xos/observers/syndicate/steps/sync_volume.py
new file mode 100644
index 0000000..e6dc90b
--- /dev/null
+++ b/xos/observers/syndicate/steps/sync_volume.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python
+
+import os
+import sys
+import traceback
+import base64
+
+if __name__ == "__main__":
+    # for testing 
+    if os.getenv("OPENCLOUD_PYTHONPATH"):
+        sys.path.append( os.getenv("OPENCLOUD_PYTHONPATH") )
+    else:
+        print >> sys.stderr, "No OPENCLOUD_PYTHONPATH variable set.  Assuming that OpenCloud is in PYTHONPATH"
+ 
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+
+
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+from services.syndicate_storage.models import Volume
+
+import logging
+from logging import Logger
+logging.basicConfig( format='[%(levelname)s] [%(module)s:%(lineno)d] %(message)s' )
+logger = logging.getLogger()
+logger.setLevel( logging.INFO )
+
+# point to planetstack
+if __name__ != "__main__": 
+    if os.getenv("OPENCLOUD_PYTHONPATH") is not None:
+        sys.path.insert(0, os.getenv("OPENCLOUD_PYTHONPATH"))
+    else:
+        logger.warning("No OPENCLOUD_PYTHONPATH set; assuming your PYTHONPATH works")
+
+# syndicatelib will be in stes/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+import syndicatelib
+
+
+class SyncVolume(SyncStep):
+    provides=[Volume]
+    requested_interval=0
+
+    def __init__(self, **args):
+        SyncStep.__init__(self, **args)
+
+    def sync_record(self, volume):
+        """
+        Synchronize a Volume record with Syndicate.
+        """
+        
+        logger.info( "Sync Volume = %s\n\n" % volume.name )
+    
+        user_email = volume.owner_id.email
+        config = syndicatelib.get_config()
+        
+        volume_principal_id = syndicatelib.make_volume_principal_id( user_email, volume.name )
+
+        # get the observer secret 
+        try:
+            observer_secret = config.SYNDICATE_OPENCLOUD_SECRET
+        except Exception, e:
+            traceback.print_exc()
+            logger.error("config is missing SYNDICATE_OPENCLOUD_SECRET")
+            raise e
+
+        # volume owner must exist as a Syndicate user...
+        try:
+            rc, user = syndicatelib.ensure_principal_exists( volume_principal_id, observer_secret, is_admin=False, max_UGs=1100, max_RGs=1)
+            assert rc == True, "Failed to create or read volume principal '%s'" % volume_principal_id
+        except Exception, e:
+            traceback.print_exc()
+            logger.error("Failed to ensure principal '%s' exists" % volume_principal_id )
+            raise e
+
+        # volume must exist 
+        
+        # create or update the Volume
+        try:
+            new_volume = syndicatelib.ensure_volume_exists( volume_principal_id, volume, user=user )
+        except Exception, e:
+            traceback.print_exc()
+            logger.error("Failed to ensure volume '%s' exists" % volume.name )
+            raise e
+           
+        # did we create the Volume?
+        if new_volume is not None:
+            # we're good
+            pass 
+             
+        # otherwise, just update it 
+        else:
+            try:
+                rc = syndicatelib.update_volume( volume )
+            except Exception, e:
+                traceback.print_exc()
+                logger.error("Failed to update volume '%s', exception = %s" % (volume.name, e.message))
+                raise e
+                    
+        return True
+    
+    def delete_record(self, volume):
+        try:
+            volume_name = volume.name
+            syndicatelib.ensure_volume_absent( volume_name )
+        except Exception, e:
+            traceback.print_exc()
+            logger.exception("Failed to erase volume '%s'" % volume_name)
+            raise e
+
+
+
+
+
+if __name__ == "__main__":
+    sv = SyncVolume()
+
+
+    # first, set all volumes to not-enacted so we can test 
+    for v in Volume.objects.all():
+       v.enacted = None
+       v.save()
+    
+    # NOTE: for resetting only 
+    if len(sys.argv) > 1 and sys.argv[1] == "reset":
+       sys.exit(0)
+
+    recs = sv.fetch_pending()
+
+    for rec in recs:
+        rc = sv.sync_record( rec )
+        if not rc:
+          print "\n\nFailed to sync %s\n\n" % (rec.name)
+
diff --git a/xos/observers/syndicate/steps/sync_volumeaccessright.py b/xos/observers/syndicate/steps/sync_volumeaccessright.py
new file mode 100644
index 0000000..2889502
--- /dev/null
+++ b/xos/observers/syndicate/steps/sync_volumeaccessright.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+
+import os
+import sys
+import base64
+import traceback
+
+if __name__ == "__main__":
+    # for testing 
+    if os.getenv("OPENCLOUD_PYTHONPATH"):
+        sys.path.append( os.getenv("OPENCLOUD_PYTHONPATH") )
+    else:
+        print >> sys.stderr, "No OPENCLOUD_PYTHONPATH variable set.  Assuming that OpenCloud is in PYTHONPATH"
+ 
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+
+import logging
+from logging import Logger
+logging.basicConfig( format='[%(levelname)s] [%(module)s:%(lineno)d] %(message)s' )
+logger = logging.getLogger()
+logger.setLevel( logging.INFO )
+
+# point to planetstack 
+if __name__ != "__main__":
+    if os.getenv("OPENCLOUD_PYTHONPATH") is not None:
+        sys.path.insert(0, os.getenv("OPENCLOUD_PYTHONPATH"))
+    else:
+        logger.warning("No OPENCLOUD_PYTHONPATH set; assuming your PYTHONPATH works")
+
+from services.syndicate_storage.models import VolumeAccessRight
+
+# syndicatelib will be in stes/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+import syndicatelib
+
+class SyncVolumeAccessRight(SyncStep):
+    provides=[VolumeAccessRight]
+    requested_interval=0
+
+    def __init__(self, **args):
+        SyncStep.__init__(self, **args)
+
+    def sync_record(self, vac):
+        
+        syndicate_caps = "UNKNOWN"  # for exception handling
+        
+        # get arguments
+        config = syndicatelib.get_config()
+        user_email = vac.owner_id.email
+        volume_name = vac.volume.name
+        syndicate_caps = syndicatelib.opencloud_caps_to_syndicate_caps( vac.cap_read_data, vac.cap_write_data, vac.cap_host_data ) 
+        
+        logger.info( "Sync VolumeAccessRight for (%s, %s)" % (user_email, volume_name) )
+        
+        # validate config
+        try:
+           RG_port = config.SYNDICATE_RG_DEFAULT_PORT
+           observer_secret = config.SYNDICATE_OPENCLOUD_SECRET
+        except Exception, e:
+           traceback.print_exc()
+           logger.error("syndicatelib config is missing SYNDICATE_RG_DEFAULT_PORT, SYNDICATE_OPENCLOUD_SECRET")
+           raise e
+            
+        # ensure the user exists and has credentials
+        try:
+            rc, user = syndicatelib.ensure_principal_exists( user_email, observer_secret, is_admin=False, max_UGs=1100, max_RGs=1 )
+            assert rc is True, "Failed to ensure principal %s exists (rc = %s,%s)" % (user_email, rc, user)
+        except Exception, e:
+            traceback.print_exc()
+            logger.error("Failed to ensure user '%s' exists" % user_email )
+            raise e
+ 
+        # make the access right for the user to create their own UGs, and provision an RG for this user that will listen on localhost.
+        # the user will have to supply their own RG closure.
+        try:
+            rc = syndicatelib.setup_volume_access( user_email, volume_name, syndicate_caps, RG_port, observer_secret )
+            assert rc is True, "Failed to setup volume access for %s in %s" % (user_email, volume_name)
+
+        except Exception, e:
+            traceback.print_exc()
+            logger.error("Faoed to ensure user %s can access Volume %s with rights %s" % (user_email, volume_name, syndicate_caps))
+            raise e
+
+        return True
+    
+    # Jude: this will simply go on to purge the object from
+    # OpenCloud. The previous 'deleter' version was a no-op also.
+    def delete_record(self, obj):
+        pass
+
+
+if __name__ == "__main__":
+
+    # first, set all VolumeAccessRights to not-enacted so we can test 
+    for v in VolumeAccessRight.objects.all():
+       v.enacted = None
+       v.save()
+
+    # NOTE: for resetting only 
+    if len(sys.argv) > 1 and sys.argv[1] == "reset":
+       sys.exit(0)
+
+
+    sv = SyncVolumeAccessRight()
+    recs = sv.fetch_pending()
+
+    for rec in recs:
+        sv.sync_record( rec )
+
diff --git a/xos/observers/syndicate/steps/sync_volumeslice.py b/xos/observers/syndicate/steps/sync_volumeslice.py
new file mode 100644
index 0000000..1be61b9
--- /dev/null
+++ b/xos/observers/syndicate/steps/sync_volumeslice.py
@@ -0,0 +1,158 @@
+#!/usr/bin/env python
+
+import os
+import sys
+import base64
+import traceback
+
+if __name__ == "__main__":
+    # for testing 
+    if os.getenv("OPENCLOUD_PYTHONPATH"):
+        sys.path.append( os.getenv("OPENCLOUD_PYTHONPATH") )
+    else:
+        print >> sys.stderr, "No OPENCLOUD_PYTHONPATH variable set.  Assuming that OpenCloud is in PYTHONPATH"
+ 
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service, Slice
+
+import logging
+from logging import Logger
+logging.basicConfig( format='[%(levelname)s] [%(module)s:%(lineno)d] %(message)s' )
+logger = logging.getLogger()
+logger.setLevel( logging.INFO )
+
+# point to planetstack 
+if __name__ != "__main__":
+    if os.getenv("OPENCLOUD_PYTHONPATH") is not None:
+        sys.path.insert(0, os.getenv("OPENCLOUD_PYTHONPATH"))
+    else:
+        logger.warning("No OPENCLOUD_PYTHONPATH set; assuming your PYTHONPATH works") 
+
+from services.syndicate_storage.models import VolumeSlice,VolumeAccessRight,Volume
+
+# syndicatelib will be in stes/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+import syndicatelib
+
+
+class SyncVolumeSlice(SyncStep):
+    provides=[VolumeSlice]
+    requested_interval=0
+
+    def __init__(self, **args):
+        SyncStep.__init__(self, **args)
+
+    def sync_record(self, vs):
+        
+        logger.info("Sync VolumeSlice for (%s, %s)" % (vs.volume_id.name, vs.slice_id.name))
+        
+        # extract arguments...
+        user_email = vs.slice_id.creator.email
+        slice_name = vs.slice_id.name
+        volume_name = vs.volume_id.name
+        syndicate_caps = syndicatelib.opencloud_caps_to_syndicate_caps( vs.cap_read_data, vs.cap_write_data, vs.cap_host_data )
+        RG_port = vs.RG_portnum
+        UG_port = vs.UG_portnum
+        slice_secret = None
+        
+        config = syndicatelib.get_config()
+        try:
+           observer_secret = config.SYNDICATE_OPENCLOUD_SECRET
+           RG_closure = config.SYNDICATE_RG_CLOSURE
+           observer_pkey_path = config.SYNDICATE_PRIVATE_KEY
+           syndicate_url = config.SYNDICATE_SMI_URL
+           
+        except Exception, e:
+           traceback.print_exc()
+           logger.error("syndicatelib config is missing one or more of the following: SYNDICATE_OPENCLOUD_SECRET, SYNDICATE_RG_CLOSURE, SYNDICATE_PRIVATE_KEY, SYNDICATE_SMI_URL")
+           raise e
+            
+        # get secrets...
+        try:
+           observer_pkey_pem = syndicatelib.get_private_key_pem( observer_pkey_path )
+           assert observer_pkey_pem is not None, "Failed to load Observer private key"
+           
+           # get/create the slice secret
+           slice_secret = syndicatelib.get_or_create_slice_secret( observer_pkey_pem, slice_name )    
+           assert slice_secret is not None, "Failed to get or create slice secret for %s" % slice_name
+           
+        except Exception, e:
+           traceback.print_exc()
+           logger.error("Failed to load secret credentials")
+           raise e
+        
+        # make sure there's a slice-controlled Syndicate user account for the slice owner
+        slice_principal_id = syndicatelib.make_slice_principal_id( user_email, slice_name )
+        
+        try:
+            rc, user = syndicatelib.ensure_principal_exists( slice_principal_id, observer_secret, is_admin=False, max_UGs=1100, max_RGs=1 )
+            assert rc is True, "Failed to ensure principal %s exists (rc = %s,%s)" % (slice_principal_id, rc, user)
+        except Exception, e:
+            traceback.print_exc()
+            logger.error('Failed to ensure slice user %s exists' % slice_principal_id)
+            raise e
+            
+        # grant the slice-owning user the ability to provision UGs in this Volume, and also provision for the user the (single) RG the slice will instantiate in each VM.
+        try:
+            rc = syndicatelib.setup_volume_access( slice_principal_id, volume_name, syndicate_caps, RG_port, observer_secret, RG_closure=RG_closure )
+            assert rc is True, "Failed to set up Volume access for slice %s in %s" % (slice_principal_id, volume_name)
+            
+        except Exception, e:
+            traceback.print_exc()
+            logger.error("Failed to set up Volume access for slice %s in %s" % (slice_principal_id, volume_name))
+            raise e
+            
+        # generate and save slice credentials....
+        try:
+            slice_cred = syndicatelib.save_slice_credentials( observer_pkey_pem, syndicate_url, slice_principal_id, volume_name, slice_name, observer_secret, slice_secret, UG_port, existing_user=user )
+            assert slice_cred is not None, "Failed to generate slice credential for %s in %s" % (slice_principal_id, volume_name )
+                
+        except Exception, e:
+            traceback.print_exc()
+            logger.error("Failed to generate slice credential for %s in %s" % (slice_principal_id, volume_name))
+            raise e
+             
+        # ... and push them all out.
+        try:
+            rc = syndicatelib.push_credentials_to_slice( slice_name, slice_cred )
+            assert rc is True, "Failed to push credentials to slice %s for volume %s" % (slice_name, volume_name)
+               
+        except Exception, e:
+            traceback.print_exc()
+            logger.error("Failed to push slice credentials to %s for volume %s" % (slice_name, volume_name))
+            raise e
+        
+        return True
+    
+    # This method will simply cause the object to be purged from OpenCloud
+    def delete_record(self, volume_slice):
+        pass
+
+
+if __name__ == "__main__":
+    sv = SyncVolumeSlice()
+
+    # first, set all VolumeSlice to not-enacted so we can test 
+    for v in VolumeSlice.objects.all():
+       v.enacted = None
+       v.save()
+
+    # NOTE: for resetting only 
+    if len(sys.argv) > 1 and sys.argv[1] == "reset":
+       sys.exit(0)
+
+    recs = sv.fetch_pending()
+
+    for rec in recs:
+        if rec.slice_id.creator is None:
+           print "Ignoring slice %s, since it has no creator" % (rec.slice_id)
+           continue
+
+        sv.sync_record( rec )
+
diff --git a/xos/observers/syndicate/stop.sh b/xos/observers/syndicate/stop.sh
new file mode 100644
index 0000000..f4a8e28
--- /dev/null
+++ b/xos/observers/syndicate/stop.sh
@@ -0,0 +1 @@
+pkill -9 -f syndicate-backend.py
diff --git a/xos/observers/syndicate/syndicate-backend.py b/xos/observers/syndicate/syndicate-backend.py
new file mode 100644
index 0000000..9b53c77
--- /dev/null
+++ b/xos/observers/syndicate/syndicate-backend.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+import os
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+from synchronizers.base.backend import Backend 
+
+if __name__ == '__main__':
+
+    backend = Backend()
+    backend.run()
+ 
diff --git a/xos/observers/syndicate/syndicate_observer_config b/xos/observers/syndicate/syndicate_observer_config
new file mode 100644
index 0000000..7e6d78a
--- /dev/null
+++ b/xos/observers/syndicate/syndicate_observer_config
@@ -0,0 +1,35 @@
+
+[plc]
+name=plc
+deployment=VICCI
+
+[db]
+name=xos
+#user=plstackuser
+#password=2uMDYtJK
+user=postgres
+password=password
+host=localhost
+port=5432
+
+[api]
+host=128.112.171.237
+port=8000
+ssl_key=None
+ssl_cert=None
+ca_ssl_cert=None
+ratelimit_enabled=0
+omf_enabled=0
+mail_support_address=support@localhost
+nova_enabled=True
+
+[observer]
+dependency_graph=/opt/xos/observers/syndicate/model-deps
+steps_dir=/opt/xos/observers/syndicate/steps
+deleters_dir=/opt/xos/observers/syndicate/deleters
+log_file=console
+driver=None
+
+[feefie]
+client_id='vicci_dev_central'
+user_id='pl'
diff --git a/xos/observers/syndicate/syndicatelib.py b/xos/observers/syndicate/syndicatelib.py
new file mode 100644
index 0000000..56bd120
--- /dev/null
+++ b/xos/observers/syndicate/syndicatelib.py
@@ -0,0 +1,1353 @@
+#!/usr/bin/env python
+
+"""
+Define some common methods for the Syndicate observer.
+"""
+import os
+import sys
+import random
+import json
+import time
+import requests
+import traceback
+import base64
+import BaseHTTPServer
+import setproctitle
+import threading
+import urllib
+
+from Crypto.Hash import SHA256 as HashAlg
+from Crypto.PublicKey import RSA as CryptoKey
+from Crypto import Random
+from Crypto.Signature import PKCS1_PSS as CryptoSigner
+
+import logging
+from logging import Logger
+logging.basicConfig( format='[%(levelname)s] [%(module)s:%(lineno)d] %(message)s' )
+logger = logging.getLogger()
+logger.setLevel( logging.INFO )
+
+# get config package 
+import syndicatelib_config.config as CONFIG
+
+# get the Syndicate modules
+import syndicate
+
+import syndicate.client.bin.syntool as syntool
+import syndicate.client.common.msconfig as msconfig
+import syndicate.client.common.api as api
+import syndicate.util.storage as syndicate_storage
+import syndicate.util.watchdog as syndicate_watchdog
+import syndicate.util.daemonize as syndicate_daemon
+import syndicate.util.crypto as syndicate_crypto
+import syndicate.util.provisioning as syndicate_provisioning
+import syndicate.syndicate as c_syndicate
+
+# for testing 
+TESTING = False
+class FakeObject(object):
+   def __init__(self):
+       pass
+
+if os.getenv("OPENCLOUD_PYTHONPATH") is not None:
+   sys.path.insert(0, os.getenv("OPENCLOUD_PYTHONPATH"))
+else:
+   logger.warning("No OPENCLOUD_PYTHONPATH set.  Assuming Syndicate models are in your PYTHONPATH")
+
+try:
+   os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+
+   # get our models
+   import services.syndicate_storage.models as models
+
+   # get OpenCloud models 
+   from core.models import Slice,Instance
+   
+   from django.core.exceptions import ObjectDoesNotExist
+   from django.db import IntegrityError
+
+except ImportError, ie:
+   logger.warning("Failed to import models; some tests will not work")
+
+   # create a fake "models" package that has exactly the members we need for testing.
+   models = FakeObject()
+   models.Volume = FakeObject()
+   models.Volume.CAP_READ_DATA = 1
+   models.Volume.CAP_WRITE_DATA = 2
+   models.Volume.CAP_HOST_DATA = 4
+   
+   TESTING = True
+
+
+#-------------------------------
+class SyndicateObserverError( Exception ):
+    pass
+
+#-------------------------------
+def get_config():
+    """
+    Return the imported config
+    """
+    return CONFIG
+
+
+#-------------------------------
+def make_openid_url( email ):
+    """
+    Generate an OpenID identity URL from an email address.
+    """
+    return os.path.join( CONFIG.SYNDICATE_OPENID_TRUSTROOT, "id", email )
+
+
+#-------------------------------
+def connect_syndicate( username=CONFIG.SYNDICATE_OPENCLOUD_USER, password=CONFIG.SYNDICATE_OPENCLOUD_PASSWORD, user_pkey_pem=CONFIG.SYNDICATE_OPENCLOUD_PKEY ):
+    """
+    Connect to the OpenCloud Syndicate SMI, using the OpenCloud user credentials.
+    """
+    debug = True 
+    if hasattr(CONFIG, "DEBUG"):
+       debug = CONFIG.DEBUG
+       
+    client = syntool.Client( username, CONFIG.SYNDICATE_SMI_URL,
+                             password=password,
+                             user_pkey_pem=user_pkey_pem,
+                             debug=debug )
+
+    return client
+
+
+#-------------------------------
+def opencloud_caps_to_syndicate_caps( cap_read, cap_write, cap_host ):
+    """
+    Convert OpenCloud capability bits from the UI into Syndicate's capability bits.
+    """
+    syn_caps = 0
+    
+    if cap_read:
+        syn_caps |= (msconfig.GATEWAY_CAP_READ_DATA | msconfig.GATEWAY_CAP_READ_METADATA)
+    if cap_write:
+        syn_caps |= (msconfig.GATEWAY_CAP_WRITE_DATA | msconfig.GATEWAY_CAP_WRITE_METADATA)
+    if cap_host:
+        syn_caps |= (msconfig.GATEWAY_CAP_COORDINATE)
+
+    return syn_caps
+
+#-------------------------------
+def ensure_user_exists( user_email, **user_kw ):
+    """
+    Given an OpenCloud user, ensure that the corresponding
+    Syndicate user exists on the MS.  This method does NOT 
+    create any OpenCloud-specific data.
+
+    Return the (created, user), where created==True if the user 
+    was created and created==False if the user was read.
+    Raise an exception on error.
+    """
+    
+    client = connect_syndicate()
+    user_openid_url = make_openid_url( user_email )
+    
+    return syndicate_provisioning.ensure_user_exists( client, user_email, user_openid_url, **user_kw )
+
+
+#-------------------------------
+def ensure_user_absent( user_email ):
+    """
+    Ensure that a given OpenCloud user's associated Syndicate user record
+    has been deleted.  This method does NOT delete any OpenCloud-specific data.
+
+    Returns True on success
+    Raises an exception on error
+    """
+
+    client = connect_syndicate()
+
+    return client.delete_user( user_email )
+ 
+
+#-------------------------------
+def make_volume_principal_id( user_email, volume_name ):
+    """
+    Create a principal id for a Volume owner.
+    """
+    
+    volume_name_safe = urllib.quote( volume_name )
+    
+    return "volume_%s.%s" % (volume_name_safe, user_email)
+ 
+ 
+#-------------------------------
+def make_slice_principal_id( user_email, slice_name ):
+    """
+    Create a principal id for a slice owner.
+    """
+    
+    slice_name_safe = urllib.quote( slice_name )
+    
+    return "slice_%s.%s" % (slice_name, user_email)
+ 
+
+#-------------------------------
+def ensure_principal_exists( user_email, observer_secret, **user_kw ):
+    """ 
+    Ensure that a Syndicate user exists, as well as its OpenCloud-specific data.
+    
+    Return (True, (None OR user)) on success.  Returns a user if the user was created.
+    Return (False, None) on error
+    """
+    
+    try:
+         created, new_user = ensure_user_exists( user_email, **user_kw )
+    except Exception, e:
+         traceback.print_exc()
+         logger.error("Failed to ensure user '%s' exists" % user_email )
+         return (False, None)
+      
+    # if we created a new user, then save its (sealed) credentials to the Django DB
+    if created:
+         try:
+            rc = put_principal_data( user_email, observer_secret, new_user['signing_public_key'], new_user['signing_private_key'] )
+            assert rc == True, "Failed to save SyndicatePrincipal"
+         except Exception, e:
+            traceback.print_exc()
+            logger.error("Failed to save private key for principal %s" % (user_email))
+            return (False, None)
+
+    return (True, new_user)
+
+
+
+#-------------------------------
+def ensure_principal_absent( user_email ):
+    """
+    Ensure that a Syndicate user does not exists, and remove the OpenCloud-specific data.
+    
+    Return True on success.
+    """
+    
+    ensure_user_absent( user_email )
+    delete_principal_data( user_email )
+    return True
+
+#-------------------------------
+def ensure_volume_exists( user_email, opencloud_volume, user=None ):
+    """
+    Given the email address of a user, ensure that the given
+    Volume exists and is owned by that user.
+    Do not try to ensure that the user exists.
+
+    Return the Volume if we created it, or return None if we did not.
+    Raise an exception on error.
+    """
+    client = connect_syndicate()
+
+    try:
+        volume = client.read_volume( opencloud_volume.name )
+    except Exception, e:
+        # transport error 
+        logger.exception(e)
+        raise e
+
+    if volume is None:
+        # the volume does not exist....try to create it 
+        vol_name = opencloud_volume.name
+        vol_blocksize = opencloud_volume.blocksize
+        vol_description = opencloud_volume.description
+        vol_private = opencloud_volume.private
+        vol_archive = opencloud_volume.archive 
+        vol_default_gateway_caps = opencloud_caps_to_syndicate_caps( opencloud_volume.cap_read_data, opencloud_volume.cap_write_data, opencloud_volume.cap_host_data )
+
+        try:
+            vol_info = client.create_volume( user_email, vol_name, vol_description, vol_blocksize,
+                                             private=vol_private,
+                                             archive=vol_archive,
+                                             active=True,
+                                             default_gateway_caps=vol_default_gateway_caps,
+                                             store_private_key=False,
+                                             metadata_private_key="MAKE_METADATA_KEY" )
+
+        except Exception, e:
+            # transport error
+            logger.exception(e)
+            raise e
+
+        else:
+            # successfully created the volume!
+            return vol_info
+
+    else:
+        
+        # volume already exists.  Verify its owned by this user.
+        if user is None:
+           try:
+               user = client.read_user( volume['owner_id'] )
+           except Exception, e:
+               # transport error, or user doesn't exist (either is unacceptable)
+               logger.exception(e)
+               raise e
+
+        if user is None or user['email'] != user_email:
+            raise Exception("Volume '%s' already exists, but is NOT owned by '%s'" % (opencloud_volume.name, user_email) )
+
+        # we're good!
+        return None
+
+
+#-------------------------------
+def ensure_volume_absent( volume_name ):
+    """
+    Given an OpenCloud volume, ensure that the corresponding Syndicate
+    Volume does not exist.
+    """
+
+    client = connect_syndicate()
+
+    # this is idempotent, and returns True even if the Volume doesn't exist
+    return client.delete_volume( volume_name )
+    
+    
+#-------------------------------
+def update_volume( opencloud_volume ):
+    """
+    Update a Syndicate Volume from an OpenCloud Volume model.
+    Fails if the Volume does not exist in Syndicate.
+    """
+
+    client = connect_syndicate()
+
+    vol_name = opencloud_volume.name
+    vol_description = opencloud_volume.description
+    vol_private = opencloud_volume.private
+    vol_archive = opencloud_volume.archive
+    vol_default_gateway_caps = opencloud_caps_to_syndicate_caps( opencloud_volume.cap_read_data, opencloud_volume.cap_write_data, opencloud_volume.cap_host_data )
+
+    try:
+        rc = client.update_volume( vol_name,
+                                   description=vol_description,
+                                   private=vol_private,
+                                   archive=vol_archive,
+                                   default_gateway_caps=vol_default_gateway_caps )
+
+        if not rc:
+            raise Exception("update_volume(%s) failed!" % vol_name )
+
+    except Exception, e:
+        # transort or method error 
+        logger.exception(e)
+        return False
+
+    else:
+        return True
+
+
+#-------------------------------
+def ensure_volume_access_right_exists( user_email, volume_name, caps, allowed_gateways=[msconfig.GATEWAY_TYPE_UG] ):
+    """
+    Ensure that a particular user has particular access to a particular volume.
+    Do not try to ensure that the user or volume exist, however!
+    """
+    client = connect_syndicate()
+    return syndicate_provisioning.ensure_volume_access_right_exists( client, user_email, volume_name, caps, allowed_gateways )
+
+#-------------------------------
+def ensure_volume_access_right_absent( user_email, volume_name ):
+    """
+    Ensure that acess to a particular volume is revoked.
+    """
+    client = connect_syndicate()
+    return syndicate_provisioning.ensure_volume_access_right_absent( client, user_email, volume_name )
+    
+
+#-------------------------------
+def setup_volume_access( user_email, volume_name, caps, RG_port, slice_secret, RG_closure=None ):
+    """
+    Set up the Volume to allow the slice to provision UGs in it, and to fire up RGs.
+       * create the Volume Access Right for the user, so (s)he can create Gateways.
+       * provision a single Replica Gateway, serving on localhost.
+    """
+    client = connect_syndicate()
+    
+    try:
+       rc = ensure_volume_access_right_exists( user_email, volume_name, caps )
+       assert rc is True, "Failed to create access right for %s in %s" % (user_email, volume_name)
+       
+    except Exception, e:
+       logger.exception(e)
+       return False
+    
+    RG_name = syndicate_provisioning.make_gateway_name( "OpenCloud", "RG", volume_name, "localhost" )
+    RG_key_password = syndicate_provisioning.make_gateway_private_key_password( RG_name, slice_secret )
+    
+    try:
+       rc = syndicate_provisioning.ensure_RG_exists( client, user_email, volume_name, RG_name, "localhost", RG_port, RG_key_password, closure=RG_closure )
+    except Exception, e:
+       logger.exception(e)
+       return False
+    
+    return True
+       
+
+#-------------------------------
+def teardown_volume_access( user_email, volume_name ):
+    """
+    Revoke access to a Volume for a User.
+      * remove the user's Volume Access Right
+      * remove the use'rs gateways
+    """
+    client = connect_syndicate()
+    
+    # block the user from creating more gateways, and delete the gateways
+    try:
+       rc = client.remove_user_from_volume( user_email, volume_name )
+       assert rc is True, "Failed to remove access right for %s in %s" % (user_email, volume_name)
+       
+    except Exception, e:
+       logger.exception(e)
+       return False
+    
+    return True
+    
+
+#-------------------------------
+def create_sealed_and_signed_blob( private_key_pem, secret, data ):
+    """
+    Create a sealed and signed message.
+    """
+    
+    # seal it with the password 
+    logger.info("Sealing credential data")
+    
+    rc, sealed_data = c_syndicate.password_seal( data, secret )
+    if rc != 0:
+       logger.error("Failed to seal data with the secret, rc = %s" % rc)
+       return None
+    
+    msg = syndicate_crypto.sign_and_serialize_json( private_key_pem, sealed_data )
+    if msg is None:
+       logger.error("Failed to sign credential")
+       return None 
+    
+    return msg 
+
+
+#-------------------------------
+def verify_and_unseal_blob( public_key_pem, secret, blob_data ):
+    """
+    verify and unseal a serialized string of JSON
+    """
+
+    # verify it 
+    rc, sealed_data = syndicate_crypto.verify_and_parse_json( public_key_pem, blob_data )
+    if rc != 0:
+        logger.error("Failed to verify and parse blob, rc = %s" % rc)
+        return None
+
+    logger.info("Unsealing credential data")
+
+    rc, data = c_syndicate.password_unseal( sealed_data, secret )
+    if rc != 0:
+        logger.error("Failed to unseal blob, rc = %s" % rc )
+        return None
+
+    return data
+
+
+#-------------------------------
+def create_volume_list_blob( private_key_pem, slice_secret, volume_list ):
+    """
+    Create a sealed volume list, signed with the private key.
+    """
+    list_data = {
+       "volumes": volume_list
+    }
+    
+    list_data_str = json.dumps( list_data )
+    
+    msg = create_sealed_and_signed_blob( private_key_pem, slice_secret, list_data_str )
+    if msg is None:
+       logger.error("Failed to seal volume list")
+       return None 
+    
+    return msg
+ 
+
+#-------------------------------
+def create_slice_credential_blob( private_key_pem, slice_name, slice_secret, syndicate_url, volume_name, volume_owner, UG_port, user_pkey_pem ):
+    """
+    Create a sealed, signed, encoded slice credentials blob.
+    """
+    
+    # create and serialize the data 
+    cred_data = {
+       "syndicate_url":   syndicate_url,
+       "volume_name":     volume_name,
+       "volume_owner":    volume_owner,
+       "slice_name":      slice_name,
+       "slice_UG_port":   UG_port,
+       "principal_pkey_pem": user_pkey_pem,
+    }
+    
+    cred_data_str = json.dumps( cred_data )
+    
+    msg = create_sealed_and_signed_blob( private_key_pem, slice_secret, cred_data_str )
+    if msg is None:
+       logger.error("Failed to seal volume list")
+       return None 
+    
+    return msg 
+
+
+#-------------------------------
+def put_principal_data( user_email, observer_secret, public_key_pem, private_key_pem ):
+    """
+    Seal and store the principal's private key into the database, in a SyndicatePrincipal object,
+    so the instance-side Syndicate daemon syndicated.py can get them later.
+    Overwrite an existing principal if one exists.
+    """
+    
+    sealed_private_key = create_sealed_and_signed_blob( private_key_pem, observer_secret, private_key_pem )
+    if sealed_private_key is None:
+        return False
+
+    try:
+       sp = models.SyndicatePrincipal( sealed_private_key=sealed_private_key, public_key_pem=public_key_pem, principal_id=user_email )
+       sp.save()
+    except IntegrityError, e:
+       logger.error("WARN: overwriting existing principal %s" % user_email)
+       sp.delete()
+       sp.save()
+    
+    return True
+
+
+#-------------------------------
+def delete_principal_data( user_email ):
+    """
+    Delete an OpenCloud SyndicatePrincipal object.
+    """
+    
+    sp = get_principal_data( user_email )
+    if sp is not None:
+      sp.delete()
+    
+    return True
+
+
+#-------------------------------
+def get_principal_data( user_email ):
+    """
+    Get a SyndicatePrincipal record from the database 
+    """
+    
+    try:
+        sp = models.SyndicatePrincipal.objects.get( principal_id=user_email )
+        return sp
+    except ObjectDoesNotExist:
+        logger.error("No SyndicatePrincipal record for %s" % user_email)
+        return None
+    
+
+
+#-------------------------------
+def get_principal_pkey( user_email, observer_secret ):
+    """
+    Fetch and unseal the private key of a SyndicatePrincipal.
+    """
+    
+    sp = get_principal_data( user_email )
+    if sp is None:
+        logger.error("Failed to find private key for principal %s" % user_email )
+        return None 
+     
+    public_key_pem = sp.public_key_pem
+    sealed_private_key_pem = sp.sealed_private_key
+
+    # unseal
+    private_key_pem = verify_and_unseal_blob(public_key_pem, observer_secret, sealed_private_key_pem)
+    if private_key_pem is None:
+        logger.error("Failed to unseal private key")
+
+    return private_key_pem
+
+
+#-------------------------------
+def get_private_key_pem( pkey_path ):
+    """
+    Get a private key from storage, PEM-encoded.
+    """
+    
+    # get the OpenCloud private key 
+    observer_pkey = syndicate_storage.read_private_key( pkey_path )
+    if observer_pkey is None:
+       logger.error("Failed to load Observer private key")
+       return None
+    
+    observer_pkey_pem = observer_pkey.exportKey()
+    
+    return observer_pkey_pem
+
+
+#-------------------------------
+def encrypt_slice_secret( observer_pkey_pem, slice_secret ):
+    """
+    Encrypt and serialize the slice secret with the Observer private key
+    """
+    
+    # get the public key
+    try:
+       observer_pubkey_pem = CryptoKey.importKey( observer_pkey_pem ).publickey().exportKey()
+    except Exception, e:
+       logger.exception(e)
+       logger.error("Failed to derive public key from private key")
+       return None 
+    
+    # encrypt the data 
+    rc, sealed_slice_secret = c_syndicate.encrypt_data( observer_pkey_pem, observer_pubkey_pem, slice_secret )
+    
+    if rc != 0:
+       logger.error("Failed to encrypt slice secret")
+       return None 
+    
+    sealed_slice_secret_b64 = base64.b64encode( sealed_slice_secret )
+    
+    return sealed_slice_secret_b64
+    
+
+#-------------------------------
+def decrypt_slice_secret( observer_pkey_pem, sealed_slice_secret_b64 ):
+    """
+    Unserialize and decrypt a slice secret
+    """
+        
+    # get the public key
+    try:
+       observer_pubkey_pem = CryptoKey.importKey( observer_pkey_pem ).publickey().exportKey()
+    except Exception, e:
+       logger.exception(e)
+       logger.error("Failed to derive public key from private key")
+       return None 
+    
+    sealed_slice_secret = base64.b64decode( sealed_slice_secret_b64 )
+    
+    # decrypt it 
+    rc, slice_secret = c_syndicate.decrypt_data( observer_pubkey_pem, observer_pkey_pem, sealed_slice_secret )
+    
+    if rc != 0:
+       logger.error("Failed to decrypt '%s', rc = %d" % (sealed_slice_secret_b64, rc))
+       return None
+    
+    return slice_secret
+ 
+
+#--------------------------------
+def get_slice_secret( observer_pkey_pem, slice_name, slice_fk=None ):
+    """
+    Get the shared secret for a slice.
+    """
+    
+    ss = None 
+    
+    # get the sealed slice secret from Django
+    try:
+       if slice_fk is not None:
+          ss = models.SliceSecret.objects.get( slice_id=slice_fk )
+       else:
+          ss = models.SliceSecret.objects.get( slice_id__name=slice_name )
+    except ObjectDoesNotExist, e:
+       logger.error("Failed to load slice secret for (%s, %s)" % (slice_fk, slice_name) )
+       return None 
+
+    return ss.secret 
+ 
+
+#-------------------------------
+def put_slice_secret( observer_pkey_pem, slice_name, slice_secret, slice_fk=None, opencloud_slice=None ):
+    """
+    Put the shared secret for a slice, encrypting it first.
+    """
+    
+    ss = None 
+    
+    if opencloud_slice is None:
+       # look up the slice 
+       try:
+          if slice_fk is None:
+             opencloud_slice = models.Slice.objects.get( name=slice_name )
+          else:
+             opencloud_slice = models.Slice.objects.get( id=slice_fk.id )
+       except Exception, e:
+          logger.exception(e)
+          logger.error("Failed to load slice (%s, %s)" % (slice_fk, slice_name) )
+          return False 
+    
+    ss = models.SliceSecret( slice_id=opencloud_slice, secret=slice_secret )
+    
+    ss.save()
+    
+    return True
+
+
+#-------------------------------
+def get_or_create_slice_secret( observer_pkey_pem, slice_name, slice_fk=None ):
+   """
+   Get a slice secret if it already exists, or generate a slice secret if one does not.
+   """
+   
+   slice_secret = get_slice_secret( observer_pkey_pem, slice_name, slice_fk=slice_fk )
+   if slice_secret is None or len(slice_secret) == 0:
+      
+      # generate a slice secret 
+      slice_secret = "".join( random.sample("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 32) )
+      
+      # store it 
+      rc = put_slice_secret( observer_pkey_pem, slice_name, slice_secret, slice_fk=slice_fk )
+      
+      if not rc:
+         raise SyndicateObserverError("Failed to create slice secret for (%s, %s)" % (slice_fk, slice_name))
+      
+   return slice_secret
+
+
+#-------------------------------
+def generate_slice_credentials( observer_pkey_pem, syndicate_url, user_email, volume_name, slice_name, observer_secret, slice_secret, UG_port, existing_user=None ):
+    """
+    Generate and return the set of credentials to be sent off to the slice VMs.
+    exisitng_user is a Syndicate user, as a dictionary.
+    
+    Return None on failure
+    """
+    
+    # get the user's private key 
+    logger.info("Obtaining private key for %s" % user_email)
+    
+    # it might be in the existing_user...
+    user_pkey_pem = None
+    if existing_user is not None:
+       user_pkey_pem = existing_user.get('signing_private_key', None)
+       
+    # no luck?
+    if user_pkey_pem is None:
+      try:
+         # get it from Django DB
+         user_pkey_pem = get_principal_pkey( user_email, observer_secret )
+         assert user_pkey_pem is not None, "No private key for %s" % user_email
+         
+      except:
+         traceback.print_exc()
+         logger.error("Failed to get private key; cannot generate credentials for %s in %s" % (user_email, volume_name) )
+         return None
+    
+    # generate a credetials blob 
+    logger.info("Generating credentials for %s's slice" % (user_email))
+    try:
+       creds = create_slice_credential_blob( observer_pkey_pem, slice_name, slice_secret, syndicate_url, volume_name, user_email, UG_port, user_pkey_pem )
+       assert creds is not None, "Failed to create credentials for %s" % user_email 
+    
+    except:
+       traceback.print_exc()
+       logger.error("Failed to generate credentials for %s in %s" % (user_email, volume_name))
+       return None
+    
+    return creds
+
+
+#-------------------------------
+def save_slice_credentials( observer_pkey_pem, syndicate_url, user_email, volume_name, slice_name, observer_secret, slice_secret, UG_port, existing_user=None ): 
+    """
+    Create and save a credentials blob to a VolumeSlice.
+    Return the creds on success.
+    Return None on failure
+    """
+    
+    creds = generate_slice_credentials( observer_pkey_pem, syndicate_url, user_email, volume_name, slice_name, observer_secret, slice_secret, UG_port, existing_user=existing_user )
+    ret = None
+    
+    if creds is not None:
+       # save it 
+       vs = get_volumeslice( volume_name, slice_name )
+       
+       if vs is not None:
+          vs.credentials_blob = creds
+          vs.save()
+          
+          # success!
+          ret = creds
+       else:
+          logger.error("Failed to look up VolumeSlice(%s, %s)" % (volume_name, slice_name))
+       
+    else:
+       logger.error("Failed to generate credentials for %s, %s" % (volume_name, slice_name))
+       
+    return ret
+
+
+#-------------------------------
+def get_volumeslice_volume_names( slice_name ):
+    """
+    Get the list of Volume names from the datastore.
+    """
+    try:
+        all_vs = models.VolumeSlice.objects.filter( slice_id__name = slice_name )
+        volume_names = []
+        for vs in all_vs:
+           volume_names.append( vs.volume_id.name )
+           
+        return volume_names
+    except Exception, e:
+        logger.exception(e)
+        logger.error("Failed to query datastore for volumes mounted in %s" % slice_name)
+        return None 
+ 
+
+#-------------------------------
+def get_volumeslice( volume_name, slice_name ):
+    """
+    Get a volumeslice record from the datastore.
+    """
+    try:
+        vs = models.VolumeSlice.objects.get( volume_id__name = volume_name, slice_id__name = slice_name )
+        return vs
+    except Exception, e:
+        logger.exception(e)
+        logger.error("Failed to query datastore for volumes (mounted in %s)" % (slice_name if (slice_name is not None or len(slice_name) > 0) else "UNKNOWN"))
+        return None 
+
+
+#-------------------------------
+def do_push( instance_hosts, portnum, payload ):
+    """
+    Push a payload to a list of instances.
+    NOTE: this has to be done in one go, since we can't import grequests
+    into the global namespace (without wrecking havoc on the credential server),
+    but it has to stick around for the push to work.
+    """
+    
+    global TESTING, CONFIG
+    
+    from gevent import monkey
+    
+    if TESTING:
+       monkey.patch_all()
+    
+    else:
+       # make gevents runnabale from multiple threads (or Django will complain)
+       monkey.patch_all(socket=True, dns=True, time=True, select=True, thread=False, os=True, ssl=True, httplib=False, aggressive=True)
+    
+    import grequests
+    
+    # fan-out 
+    requests = []
+    for sh in instance_hosts:
+      rs = grequests.post( "http://" + sh + ":" + str(portnum), data={"observer_message": payload}, timeout=getattr(CONFIG, "SYNDICATE_HTTP_PUSH_TIMEOUT", 60) )
+      requests.append( rs )
+      
+    # fan-in
+    responses = grequests.map( requests )
+    
+    assert len(responses) == len(requests), "grequests error: len(responses) != len(requests)"
+    
+    for i in xrange(0,len(requests)):
+       resp = responses[i]
+       req = requests[i]
+       
+       if resp is None:
+          logger.error("Failed to connect to %s" % (req.url))
+          continue 
+       
+       # verify they all worked 
+       if resp.status_code != 200:
+          logger.error("Failed to POST to %s, status code = %s" % (resp.url, resp.status_code))
+          continue
+          
+    return True
+   
+
+#-------------------------------
+def get_slice_hostnames( slice_name ):
+   """
+   Query the Django DB and get the list of hosts running in a slice.
+   """
+
+   openstack_slice = Slice.objects.get( name=slice_name )
+   if openstack_slice is None:
+       logger.error("No such slice '%s'" % slice_name)
+       return None
+
+   hostnames = [s.node.name for s in openstack_slice.instances.all()]
+
+   return hostnames
+
+   
+#-------------------------------
+def push_credentials_to_slice( slice_name, payload ):
+   """
+   Push a credentials payload to the VMs in a slice.
+   """
+   hostnames = get_slice_hostnames( slice_name )
+   return do_push( hostnames, CONFIG.SYNDICATE_SLIVER_PORT, payload )
+
+   
+#-------------------------------
+class CredentialServerHandler( BaseHTTPServer.BaseHTTPRequestHandler ):
+   """
+   HTTP server handler that allows syndicated.py instances to poll
+   for volume state.
+   
+   NOTE: this is a fall-back mechanism.  The observer should push new 
+   volume state to the slices' instances.  However, if that fails, the 
+   instances are configured to poll for volume state periodically.  This 
+   server allows them to do just that.
+   
+   Responses:
+      GET /<slicename>              -- Reply with the signed sealed list of volume names, encrypted by the slice secret
+      GET /<slicename>/<volumename> -- Reply with the signed sealed volume access credentials, encrypted by the slice secret
+      
+      !!! TEMPORARY !!!
+      GET /<slicename>/SYNDICATE_SLICE_SECRET    -- Reply with the slice secret (TEMPORARY)
+   
+   
+   NOTE: We want to limit who can learn which Volumes a slice can access, so we'll seal its instances'
+   credentials with the SliceSecret secret.  The instances (which have the slice-wide secret) can then decrypt it.
+   However, sealing the listing is a time-consuming process (on the order of 10s), so we only want 
+   to do it when we have to.  Since *anyone* can ask for the ciphertext of the volume list,
+   we will cache the list ciphertext for each slice for a long-ish amount of time, so we don't
+   accidentally DDoS this server.  This necessarily means that the instance might see a stale
+   volume listing, but that's okay, since the Observer is eventually consistent anyway.
+   """
+   
+   cached_volumes_json = {}             # map slice_name --> (volume name, timeout)
+   cached_volumes_json_lock = threading.Lock()
+   
+   CACHED_VOLUMES_JSON_LIFETIME = 3600          # one hour
+   
+   SLICE_SECRET_NAME = "SYNDICATE_SLICE_SECRET"
+   
+   def parse_request_path( self, path ):
+      """
+      Parse the URL path into a slice name and (possibly) a volume name or SLICE_SECRET_NAME
+      """
+      path_parts = path.strip("/").split("/")
+      
+      if len(path_parts) == 0:
+         # invalid 
+         return (None, None)
+      
+      if len(path_parts) > 2:
+         # invalid
+         return (None, None)
+      
+      slice_name = path_parts[0]
+      if len(slice_name) == 0:
+         # empty string is invalid 
+         return (None, None)
+      
+      volume_name = None
+      
+      if len(path_parts) > 1:
+         volume_name = path_parts[1]
+         
+      return slice_name, volume_name
+   
+   
+   def reply_data( self, data, datatype="application/json" ):
+      """
+      Give back a 200 response with data.
+      """
+      self.send_response( 200 )
+      self.send_header( "Content-Type", datatype )
+      self.send_header( "Content-Length", len(data) )
+      self.end_headers()
+      
+      self.wfile.write( data )
+      return 
+   
+   
+   def get_volumes_message( self, private_key_pem, observer_secret, slice_name ):
+      """
+      Get the json-ized list of volumes this slice is attached to.
+      Check the cache, evict stale data if necessary, and on miss, 
+      regenerate the slice volume list.
+      """
+      
+      # block the cache.
+      # NOTE: don't release the lock until we've generated credentials.
+      # Chances are, there's a thundering herd of instances coming online.
+      # Block them all until we've generated their slice's credentials,
+      # and then serve them the cached one.
+      
+      self.cached_volumes_json_lock.acquire()
+      
+      ret = None
+      volume_list_json, cache_timeout = self.cached_volumes_json.get( slice_name, (None, None) )
+      
+      if (cache_timeout is not None) and cache_timeout < time.time():
+         # expired
+         volume_list_json = None
+      
+      if volume_list_json is None:
+         # generate a new list and cache it.
+         
+         volume_names = get_volumeslice_volume_names( slice_name )
+         if volume_names is None:
+            # nothing to do...
+            ret = None
+         
+         else:
+            # get the slice secret 
+            slice_secret = get_slice_secret( private_key_pem, slice_name )
+            
+            if slice_secret is None:
+               # no such slice 
+               logger.error("No slice secret for %s" % slice_name)
+               ret = None
+            
+            else:
+               # seal and sign 
+               ret = create_volume_list_blob( private_key_pem, slice_secret, volume_names )
+         
+         # cache this 
+         if ret is not None:
+            self.cached_volumes_json[ slice_name ] = (ret, time.time() + self.CACHED_VOLUMES_JSON_LIFETIME )
+      
+      else:
+         # hit the cache
+         ret = volume_list_json
+      
+      self.cached_volumes_json_lock.release()
+      
+      return ret
+      
+   
+   def do_GET( self ):
+      """
+      Handle one GET
+      """
+      slice_name, volume_name = self.parse_request_path( self.path )
+      
+      # valid request?
+      if volume_name is None and slice_name is None:
+         self.send_error( 400 )
+      
+      # slice secret request?
+      elif volume_name == self.SLICE_SECRET_NAME and slice_name is not None:
+         
+         # get the slice secret 
+         ret = get_slice_secret( self.server.private_key_pem, slice_name )
+         
+         if ret is not None:
+            self.reply_data( ret )
+            return 
+         else:
+            self.send_error( 404 )
+      
+      # volume list request?
+      elif volume_name is None and slice_name is not None:
+         
+         # get the list of volumes for this slice
+         ret = self.get_volumes_message( self.server.private_key_pem, self.server.observer_secret, slice_name )
+         
+         if ret is not None:
+            self.reply_data( ret )
+            return
+         else:
+            self.send_error( 404 )
+      
+      # volume credential request?
+      elif volume_name is not None and slice_name is not None:
+         
+         # get the VolumeSlice record
+         vs = get_volumeslice( volume_name, slice_name )
+         if vs is None:
+            # not found
+            self.send_error( 404 )
+            return
+         
+         else:
+            ret = vs.credentials_blob 
+            if ret is not None:
+               self.reply_data( vs.credentials_blob )
+            else:
+               # not generated???
+               print ""
+               print vs
+               print ""
+               self.send_error( 503 )
+            return
+         
+      else:
+         # shouldn't get here...
+         self.send_error( 500 )
+         return 
+   
+   
+#-------------------------------
+class CredentialServer( BaseHTTPServer.HTTPServer ):
+   
+   def __init__(self, private_key_pem, observer_secret, server, req_handler ):
+      self.private_key_pem = private_key_pem
+      self.observer_secret = observer_secret
+      BaseHTTPServer.HTTPServer.__init__( self, server, req_handler )
+
+
+#-------------------------------
+def credential_server_spawn( old_exit_status ):
+   """
+   Start our credential server (i.e. in a separate process, started by the watchdog)
+   """
+   
+   setproctitle.setproctitle( "syndicate-credential-server" )
+   
+   private_key = syndicate_storage.read_private_key( CONFIG.SYNDICATE_PRIVATE_KEY )
+   if private_key is None:
+      # exit code 255 will be ignored...
+      logger.error("Cannot load private key.  Exiting...")
+      sys.exit(255)
+   
+   logger.info("Starting Syndicate Observer credential server on port %s" % CONFIG.SYNDICATE_HTTP_PORT)
+               
+   srv = CredentialServer( private_key.exportKey(), CONFIG.SYNDICATE_OPENCLOUD_SECRET, ('', CONFIG.SYNDICATE_HTTP_PORT), CredentialServerHandler)
+   srv.serve_forever()
+
+
+#-------------------------------
+def ensure_credential_server_running( foreground=False, run_once=False ):
+   """
+   Instantiate our credential server and keep it running.
+   """
+   
+   # is the watchdog running?
+   pids = syndicate_watchdog.find_by_attrs( "syndicate-credential-server-watchdog", {} )
+   if len(pids) > 0:
+      # it's running
+      return True
+   
+   if foreground:
+      # run in foreground 
+      
+      if run_once:
+         return credential_server_spawn( 0 )
+      
+      else:
+         return syndicate_watchdog.main( credential_server_spawn, respawn_exit_statuses=range(1,254) )
+      
+   
+   # not running, and not foregrounding.  fork a new one
+   try:
+      watchdog_pid = os.fork()
+   except OSError, oe:
+      logger.error("Failed to fork, errno = %s" % oe.errno)
+      return False
+   
+   if watchdog_pid != 0:
+      
+      # child--become watchdog
+      setproctitle.setproctitle( "syndicate-credential-server-watchdog" )
+      
+      if run_once:
+         syndicate_daemon.daemonize( lambda: credential_server_spawn(0), logfile_path=getattr(CONFIG, "SYNDICATE_HTTP_LOGFILE", None) )
+      
+      else:
+         syndicate_daemon.daemonize( lambda: syndicate_watchdog.main( credential_server_spawn, respawn_exit_statuses=range(1,254) ), logfile_path=getattr(CONFIG, "SYNDICATE_HTTP_LOGFILE", None) )
+
+
+#-------------------------------
+# Begin functional tests.
+# Any method starting with ft_ is a functional test.
+#-------------------------------
+  
+#-------------------------------
+def ft_syndicate_access():
+    """
+    Functional tests for ensuring objects exist and don't exist in Syndicate.
+    """
+    
+    fake_user = FakeObject()
+    fake_user.email = "fakeuser@opencloud.us"
+
+    print "\nensure_user_exists(%s)\n" % fake_user.email
+    ensure_user_exists( fake_user.email, is_admin=False, max_UGs=1100, max_RGs=1 )
+
+    print "\nensure_user_exists(%s)\n" % fake_user.email
+    ensure_user_exists( fake_user.email, is_admin=False, max_UGs=1100, max_RGs=1 )
+
+    fake_volume = FakeObject()
+    fake_volume.name = "fakevolume"
+    fake_volume.description = "This is a fake volume, created for funtional testing"
+    fake_volume.blocksize = 1024
+    fake_volume.cap_read_data = True 
+    fake_volume.cap_write_data = True 
+    fake_volume.cap_host_data = False
+    fake_volume.archive = False
+    fake_volume.private = True
+    
+    # test idempotency
+    print "\nensure_volume_exists(%s)\n" % fake_volume.name
+    ensure_volume_exists( fake_user.email, fake_volume )
+
+    print "\nensure_volume_exists(%s)\n" % fake_volume.name
+    ensure_volume_exists( fake_user.email, fake_volume )
+    
+    print "\nensure_volume_access_right_exists(%s,%s)\n" % (fake_user.email, fake_volume.name)
+    ensure_volume_access_right_exists( fake_user.email, fake_volume.name, 31 )
+    
+    print "\nensure_volume_access_right_exists(%s,%s)\n" % (fake_user.email, fake_volume.name)
+    ensure_volume_access_right_exists( fake_user.email, fake_volume.name, 31 )
+    
+    print "\nensure_volume_access_right_absent(%s,%s)\n" % (fake_user.email, fake_volume.name)
+    ensure_volume_access_right_absent( fake_user.email, fake_volume.name )
+    
+    print "\nensure_volume_access_right_absent(%s,%s)\n" % (fake_user.email, fake_volume.name)
+    ensure_volume_access_right_absent( fake_user.email, fake_volume.name )
+ 
+    print "\nensure_volume_absent(%s)\n" % fake_volume.name
+    ensure_volume_absent( fake_volume.name )
+
+    print "\nensure_volume_absent(%s)\n" % fake_volume.name
+    ensure_volume_absent( fake_volume.name )
+
+    print "\nensure_user_absent(%s)\n" % fake_user.email
+    ensure_user_absent( fake_user.email )
+
+    print "\nensure_user_absent(%s)\n" % fake_user.email
+    ensure_user_absent( fake_user.email )
+    
+    
+    
+    
+    print "\nensure_principal_exists(%s)\n" % fake_user.email
+    ensure_principal_exists( fake_user.email, "asdf", is_admin=False, max_UGs=1100, max_RGs=1 )
+    
+    print "\nensure_principal_exists(%s)\n" % fake_user.email
+    ensure_principal_exists( fake_user.email, "asdf", is_admin=False, max_UGs=1100, max_RGs=1 )
+
+    print "\nensure_volume_exists(%s)\n" % fake_volume.name
+    ensure_volume_exists( fake_user.email, fake_volume )
+
+    print "\nsetup_volume_access(%s, %s)\n" % (fake_user.email, fake_volume.name)
+    setup_volume_access( fake_user.email, fake_volume.name, 31, 38800, "abcdef" )
+    
+    print "\nsetup_volume_access(%s, %s)\n" % (fake_user.email, fake_volume.name)
+    setup_volume_access( fake_user.email, fake_volume.name, 31, 38800, "abcdef" )
+    
+    print "\nteardown_volume_access(%s, %s)\n" % (fake_user.email, fake_volume.name )
+    teardown_volume_access( fake_user.email, fake_volume.name )
+    
+    print "\nteardown_volume_access(%s, %s)\n" % (fake_user.email, fake_volume.name )
+    teardown_volume_access( fake_user.email, fake_volume.name )
+    
+    print "\nensure_volume_absent(%s)\n" % fake_volume.name
+    ensure_volume_absent( fake_volume.name )
+
+    print "\nensure_principal_absent(%s)\n" % fake_user.email
+    ensure_principal_absent( fake_user.email )
+    
+
+
+#-------------------------------
+def ft_volumeslice( slice_name ):
+    """
+    Functional tests for reading VolumeSlice information
+    """
+    print "slice: %s" % slice_name
+    
+    volumes = get_volumeslice_volume_names( slice_name )
+    
+    print "volumes mounted in slice %s:" % slice_name
+    for v in volumes:
+       print "   %s:" % v
+      
+       vs = get_volumeslice( v, slice_name )
+       
+       print "      %s" % dir(vs)
+          
+
+#-------------------------------
+def ft_get_slice_hostnames( slice_name ):
+   """
+   Functional tests for getting slice hostnames
+   """
+   
+   print "Get slice hostnames for %s" % slice_name
+   
+   hostnames = get_slice_hostnames( slice_name )
+   import pprint 
+   
+   pp = pprint.PrettyPrinter()
+   
+   pp.pprint( hostnames )
+
+
+#-------------------------------
+def ft_syndicate_principal():
+   """
+   Functional tests for creating, reading, and deleting SyndicatePrincipals.
+   """
+   print "generating key pair"
+   pubkey_pem, privkey_pem = api.generate_key_pair( 4096 )
+   
+   user_email = "fakeuser@opencloud.us"
+   
+   print "saving principal"
+   put_principal_data( user_email, "asdf", pubkey_pem, privkey_pem )
+   
+   print "fetching principal private key"
+   saved_privkey_pem = get_principal_pkey( user_email, "asdf" )
+   
+   assert saved_privkey_pem is not None, "Could not fetch saved private key"
+   assert saved_privkey_pem == privkey_pem, "Saved private key does not match actual private key"
+   
+   print "delete principal"
+   
+   delete_principal_data( user_email )
+   
+   print "make sure its deleted..."
+   
+   saved_privkey_pem = get_principal_pkey( user_email, "asdf" )
+   
+   assert saved_privkey_pem is None, "Principal key not deleted"
+   
+
+#-------------------------------
+def ft_credential_server():
+   """
+   Functional test for the credential server
+   """
+   ensure_credential_server_running( run_once=True, foreground=True )
+
+
+#-------------------------------
+def ft_seal_and_unseal():
+    """
+    Functional test for sealing/unsealing data
+    """
+    print "generating key pair"
+    pubkey_pem, privkey_pem = api.generate_key_pair( 4096 )
+    
+    sealed_buf = create_sealed_and_signed_blob( privkey_pem, "foo", "hello world")
+    print "sealed data is:\n\n%s\n\n" % sealed_buf
+
+    buf = verify_and_unseal_blob( pubkey_pem, "foo", sealed_buf )
+    print "unsealed data is: \n\n%s\n\n" % buf
+    
+
+# run functional tests
+if __name__ == "__main__":
+    sys.path.append("/opt/xos")
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+
+    if len(sys.argv) < 2:
+      print "Usage: %s testname [args]" % sys.argv[0]
+      
+    # call a method starting with ft_, and then pass the rest of argv as its arguments
+    testname = sys.argv[1]
+    ft_testname = "ft_%s" % testname
+    
+    test_call = "%s(%s)" % (ft_testname, ",".join(sys.argv[2:]))
+   
+    print "calling %s" % test_call
+   
+    rc = eval( test_call )
+   
+    print "result = %s" % rc
+      
+    
diff --git a/xos/observers/syndicate/syndicatelib_config/__init__.py b/xos/observers/syndicate/syndicatelib_config/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xos/observers/syndicate/syndicatelib_config/__init__.py
diff --git a/xos/observers/syndicate/syndicatelib_config/config-jude.py b/xos/observers/syndicate/syndicatelib_config/config-jude.py
new file mode 100644
index 0000000..9e0f1fd
--- /dev/null
+++ b/xos/observers/syndicate/syndicatelib_config/config-jude.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+
+# configuration for syndicatelib
+SYNDICATE_SMI_URL="http://localhost:8080"
+
+SYNDICATE_OPENID_TRUSTROOT="http://localhost:8081"
+
+SYNDICATE_OPENCLOUD_USER="judecn@gmail.com"
+SYNDICATE_OPENCLOUD_PASSWORD="nya"
+
+SYNDICATE_PYTHONPATH="/home/jude/Desktop/research/git/syndicate/build/out/python"
+
+SYNDICATE_PRIVATE_KEY="/home/jude/Desktop/research/git/syndicate/opencloud/observers/syndicate/syndicatelib_config/pollserver.pem"
+SYNDICATE_OPENCLOUD_SECRET="e4988309a5005edb8ea185f16f607938c0fb7657e4d7609853bcb7c4884d1c92"
+
+SYNDICATE_HTTP_PORT=65321
+
+SYNDICATE_RG_CLOSURE="/home/jude/Desktop/research/git/syndicate/build/out/python/syndicate/rg/drivers/disk"
+SYNDICATE_RG_DEFAULT_PORT=38800
+
+DEBUG=True
diff --git a/xos/observers/syndicate/syndicatelib_config/config-opencloud.py b/xos/observers/syndicate/syndicatelib_config/config-opencloud.py
new file mode 100644
index 0000000..b3add16
--- /dev/null
+++ b/xos/observers/syndicate/syndicatelib_config/config-opencloud.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+
+# ---------------------------------
+# This is the configuration file used by the Syndicate observer.
+# It is a well-formed Python file, and will be imported into the
+# observer as a Python module.  This means you can run any config-
+# generation code here you like, but all of the following global 
+# variables must be defined.
+# ---------------------------------
+
+# URL to the Syndicate SMI.  For example, https://syndicate-metadata.appspot.com
+SYNDICATE_SMI_URL="http://localhost:8080"
+
+# If you are going to use OpenID to authenticate the Syndicate instance daemon,
+# this is the OpenID provider URL.  It is currently used only to generate 
+# identity pages for users, so you can put whatever you want here for now.
+SYNDICATE_OPENID_TRUSTROOT="http://localhost:8081"
+
+# This is the observer's user account on Syndicate.  You must create it out-of-band
+# prior to using the observer, and it must be an admin user since it will
+# create other users (i.e. for slices).
+SYNDICATE_OPENCLOUD_USER="jcnelson@cs.princeton.edu"
+
+# This is the password for the observer to authenticate itself to Syndicate.
+SYNDICATE_OPENCLOUD_PASSWORD="nya"
+
+# If the observer uses public-key authentication with Syndicate, you will 
+# need to identify the absolute path to its private key here.  It must be 
+# a 4096-bit PEM-encoded RSA key, and the Syndicate observer's user account
+# must have been given the public key on activation.
+SYNDICATE_OPENCLOUD_PKEY=None
+
+# This is the location on disk where Syndicate observer code can be found, 
+# if it is not already in the Python path.  This is optional.
+SYNDICATE_PYTHONPATH="/root/syndicate/build/out/python"
+
+# This is the location of the observer's private key.  It must be an absolute
+# path, and refer to a 4096-bit PEM-encoded RSA key.
+SYNDICATE_PRIVATE_KEY="/opt/xos/observers/syndicate/syndicatelib_config/pollserver.pem"
+
+# This is the master secret used to generate secrets to seal sensitive information sent to the 
+# Syndicate instance mount daemons.  It is also used to seal sensitive information
+# stored to the Django database.  
+# TODO: think of a way to not have to store this on disk.  Maybe we feed into the
+# observer when it starts up?
+SYNDICATE_OPENCLOUD_SECRET="e4988309a5005edb8ea185f16f607938c0fb7657e4d7609853bcb7c4884d1c92"
+
+# This is the default port number on which a Syndicate Replica Gateway
+# will be provisioned.  It's a well-known port, and can be the same across
+# instances, since in OpenCloud, an RG instance only listens to localhost.
+SYNDICATE_RG_DEFAULT_PORT=38800
+
+# This is the absolute path to the RG's storage driver (which will be automatically
+# pushed to instances by Syndicate).  See https://github.com/jcnelson/syndicate/wiki/Replica-Gateways
+SYNDICATE_RG_CLOSURE=None
+
+# This is the port number the observer listens on for GETs from the Syndicate instance mount 
+# daemons.  Normally, the oserver pushes (encrypted) commands to the daemons, but if the 
+# daemons are NAT'ed or temporarily partitioned, they will pull commands instead.
+SYNDICATE_HTTP_PORT=65321
+
+# This is the path to the logfile for the observer's HTTP server.
+SYNDICATE_HTTP_LOGFILE="/tmp/syndicate-observer.log"
+
+# This is the number of seconds to wait for pushing a slice credential before timing out.
+SYNDICATE_HTTP_PUSH_TIMEOUT=60
+
+# This is the port number the Syndicate instance mount daemons listen on.  The observer will 
+# push commands to them on this port.
+SYNDICATE_SLIVER_PORT=65322
+
+# If true, print verbose debug messages.
+DEBUG=True
diff --git a/xos/observers/syndicate/syndicatelib_config/config.py b/xos/observers/syndicate/syndicatelib_config/config.py
new file mode 100644
index 0000000..b3add16
--- /dev/null
+++ b/xos/observers/syndicate/syndicatelib_config/config.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+
+# ---------------------------------
+# This is the configuration file used by the Syndicate observer.
+# It is a well-formed Python file, and will be imported into the
+# observer as a Python module.  This means you can run any config-
+# generation code here you like, but all of the following global 
+# variables must be defined.
+# ---------------------------------
+
+# URL to the Syndicate SMI.  For example, https://syndicate-metadata.appspot.com
+SYNDICATE_SMI_URL="http://localhost:8080"
+
+# If you are going to use OpenID to authenticate the Syndicate instance daemon,
+# this is the OpenID provider URL.  It is currently used only to generate 
+# identity pages for users, so you can put whatever you want here for now.
+SYNDICATE_OPENID_TRUSTROOT="http://localhost:8081"
+
+# This is the observer's user account on Syndicate.  You must create it out-of-band
+# prior to using the observer, and it must be an admin user since it will
+# create other users (i.e. for slices).
+SYNDICATE_OPENCLOUD_USER="jcnelson@cs.princeton.edu"
+
+# This is the password for the observer to authenticate itself to Syndicate.
+SYNDICATE_OPENCLOUD_PASSWORD="nya"
+
+# If the observer uses public-key authentication with Syndicate, you will 
+# need to identify the absolute path to its private key here.  It must be 
+# a 4096-bit PEM-encoded RSA key, and the Syndicate observer's user account
+# must have been given the public key on activation.
+SYNDICATE_OPENCLOUD_PKEY=None
+
+# This is the location on disk where Syndicate observer code can be found, 
+# if it is not already in the Python path.  This is optional.
+SYNDICATE_PYTHONPATH="/root/syndicate/build/out/python"
+
+# This is the location of the observer's private key.  It must be an absolute
+# path, and refer to a 4096-bit PEM-encoded RSA key.
+SYNDICATE_PRIVATE_KEY="/opt/xos/observers/syndicate/syndicatelib_config/pollserver.pem"
+
+# This is the master secret used to generate secrets to seal sensitive information sent to the 
+# Syndicate instance mount daemons.  It is also used to seal sensitive information
+# stored to the Django database.  
+# TODO: think of a way to not have to store this on disk.  Maybe we feed into the
+# observer when it starts up?
+SYNDICATE_OPENCLOUD_SECRET="e4988309a5005edb8ea185f16f607938c0fb7657e4d7609853bcb7c4884d1c92"
+
+# This is the default port number on which a Syndicate Replica Gateway
+# will be provisioned.  It's a well-known port, and can be the same across
+# instances, since in OpenCloud, an RG instance only listens to localhost.
+SYNDICATE_RG_DEFAULT_PORT=38800
+
+# This is the absolute path to the RG's storage driver (which will be automatically
+# pushed to instances by Syndicate).  See https://github.com/jcnelson/syndicate/wiki/Replica-Gateways
+SYNDICATE_RG_CLOSURE=None
+
+# This is the port number the observer listens on for GETs from the Syndicate instance mount 
+# daemons.  Normally, the oserver pushes (encrypted) commands to the daemons, but if the 
+# daemons are NAT'ed or temporarily partitioned, they will pull commands instead.
+SYNDICATE_HTTP_PORT=65321
+
+# This is the path to the logfile for the observer's HTTP server.
+SYNDICATE_HTTP_LOGFILE="/tmp/syndicate-observer.log"
+
+# This is the number of seconds to wait for pushing a slice credential before timing out.
+SYNDICATE_HTTP_PUSH_TIMEOUT=60
+
+# This is the port number the Syndicate instance mount daemons listen on.  The observer will 
+# push commands to them on this port.
+SYNDICATE_SLIVER_PORT=65322
+
+# If true, print verbose debug messages.
+DEBUG=True
diff --git a/xos/observers/syndicate/syndicatelib_config/pollserver.pem b/xos/observers/syndicate/syndicatelib_config/pollserver.pem
new file mode 100644
index 0000000..cb50de7
--- /dev/null
+++ b/xos/observers/syndicate/syndicatelib_config/pollserver.pem
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIJKwIBAAKCAgEA6yeoVsm+Yf4sgW/9kzk2NfwkbHloIKXUOpi5x5LkEbnohNRC
+RIGjiMJJp/OefduU3c14h/K6Qefi9j4dw4pyvh2QP36K4lJObIpKAdohHjVjxHqK
+12bVXvCpCAbJHkiX8VK5HGJPDr6sJke1vPTrP6RSWxT7ZRawIBInKuT/OVshskzh
+kOwVXb5ct0AXjRZ6FBdXvNcJnNONKRCIuFHOx2roWLsdgTlPI3+bJim2dQ0JKyHh
+uaTPguZ4s23sCuSKyXCh/X9yVloxIraY6KdaAKQZLyANLfXQdsyyH69kQvvBEZ2R
+EXD0c1qIZwuIm68UH+60BwTPdXGWSL73C0Zsq36vZOadFPb0pmz/o4CuguILNA3i
+560MKcVvQ8HVqA56z+v8pE0TRp0ajTDtpW2ee+t1cXE8VzCwthkIxmneFk+ityoD
+o1N+fBUu4lXJ3kl2qGk+0KECqJ6sc/QN+Ft97JTTRshSzn1kqIlKQoZo3u0Jeo/G
+PFZ0b13/DxYA7nvjt2az48h0VL4mNf5tzDr8GxOK4lYoWIGzKjZLDeJRyxLOCK5j
+F/AvbbSnegT0O/vamn5EoN8HfooH5qiJdPDerriPsgN3HlcY6QjrY5phmAFiGa5T
+X1j1VNb5oamRslgPv3rwTvioTaY/wUmrHLBuU6Sqg/WGrLO2Bocg0USMbG8CAwEA
+AQKCAgEArxuO7WG5lXsSZSih6Rm3Vqf175jQg085JJFJ9mVZ1CFeFluBJUZsIpCb
+DKgLI6l5x1kUIhgLvrwQdFF5FH1qSEv3eHCgtzuXDphD1/E4rCgRrOObtB7tUI9h
+L4ruBNEF5Dw3f/1s5Yvy4WaQ3K58551TfmO3eGVWresWo4h2zZ0hEIbTiXljx7TT
+kdn2L6fHLGLdgM+YZuHZwfR/+tFga3sencRoiivE1KhXPindpngYlbfbQMSLiexZ
+gTOfi9T3zF1FI2HeIJN092aFounLyaJo5oC1j732iCCRm6qdvIuAD8AHoLc+MQ//
+dsxN47CSCd1Uzc01Nz1oLa+WgxzkGbsGNO2eVkRj/xzB0Rld/+Q1YQvn1TnWZFuG
+nXXKi+VwSX8htpDaZL5+hWVy39YXUQcBkTzBIS69tdfd7HCZS0JnKeOMYQFgvANH
+0/J529l8m0oHpex4DdW1scHXgcBOq6oD6KVkiNurfWZu/mWwdEnxAKtRFZc/xFfh
+a4kbTgNk3unGL+gZzeWL1YuIM843Ia4J8V0PYH7GueeZBaXyiT7r4x5hgQ57ObkX
+K9wlgrvSHBNq4OhzygTNs37vJwu38ry2AGmA8LuiFBeVCsVhMk3yVz4A6fXjwWH8
+266gNuODIiHelahvz/IBGLdrjnbA4SYaYQh1SYKfCRdlA2yNlBECggEBAPcqiWP/
+XUCsCy9vMbCvZNCUyr4nqTSvWkGsie6t+yIC0pYCynKdrL9WNF5zee9SVmtcBO0Q
+z+aff8TZAax3tWvD5GzlQOQh1l4QBj30uatklQ0nvwbw+gf5EFG2QerPakwyfC59
+dSagxchzpjloBGniq7jqc6vL3xlZ62vnOLHf+nOQXzDcZ7QK/uLRKj2r01D5+9lh
+08Ah42QID5VQL/NMyg2lylXaPXx6TnSMjJVjzNmLRCIRlOstAOt3isBJ21sT0LOk
+lCGvuF//cwS7VABRMa0TspSEkuMbgFw0XEZStkh68eEUVqax+HHfa1rlxobSIwib
+1Oa9s7KbQNaozgUCggEBAPOPOSLazItJ4OOFu8/69M33Ag8hpPZNkMw1fw99p2fD
+KnZYlEWHgF4Z76gkozHh+nk8HhohvYfIhmFYEHQOXfmgljsz3jFJKwTEnfN7IsZA
+C3TVl6OVuY2rjhBOe3137CYHn9d8KRaJAKdyd038LK29Yy+FvUVw6LD4QUoRiA21
+9sOrhO/Wgcohnqk5yVnXtBkp7j7qGN9d+GLZVAVOqKjniQqy9ir3IdLYmB801t9P
+TcbucmgEzs/cmx7d/jv1kx9/O0HHIm959Ox66hPkcG3bssJk41F6PDMOVEWiC/hc
+E5a7Mlr6M4YhuDjO1zoyQpy4Sj/MKpasnotNSL51JuMCggEBALhYkYBzximmJ/GJ
+DZaqOpcXYt/Q1PLmlnrFJVtPiC8ly8r26efykhVjRkvr9NX6o1oPl9z43Rc1fyZi
+dE0eO8HUqVpO4sdENY6ShRVQoeqjakgVjPSwZsvrh7BqL1/is3WBcf16tRXKc7m+
+CAxo+GHBHjMdKojH1e4ikuQ34KFKXJI068qVmQM/8DtbphW5QjLzQFQyEq0KmX7S
+RE0pMZpVe54SOYcu7w0Ya8uhyHjjprXamUaPtnJxbm4xCtvAOksDzHUwGwvE888l
+x7OPxGc4J8TfHCKJfsTEjkg3BVut9Sa6DA3EDZzmwFauPHPfTOLheB/Dmlc+xfhA
+s2tnG8ECggEBAKiLbFaaYwHg1iec3CNI3y/IxzwBZE6tzo4CVzM5GSfM/w12ruSO
+qF52REpvUB+s6dALsikTQD0+nv+uGXS2nIGqh0vg0Nn6cDKUfVmI1L+sgkEPrigd
+7JIFLgJKzVo+KsUGca6E1Uoq9LDrnXPyFlkEviacviXXxK7ynPvMtgIG8gTmJNBz
++M0QBuPEgXoSsybWxW/0P9ITDVgaXPJvRHfeAg/NWFzTOCzYhizSO/+8uW34hGNH
+MHbXiuEJbm2/u1gIi9ExJLtQAhXD2Uh6xPLBHis39bbkh9QtDlRBl1b/IO8mC+q5
+Sf6ARyPIv1gef8pEHd2YQ8CRJAXyLWzfVVECggEBANrvnE2hQaYqe/BM9QGN9Cam
+CUTTBhvQDTBnpBYv8iQCgy0nmmVZ07j0yjR/I5wipcWAB7Bskv1rfh/3VpWUGCcR
+2MnPobZnvL1Dl22G7P8HBUiIA+NGWNdR5FyIL/yLy2BVEs7dNeK5WolD8IQP+fTw
+E9Mvd6ns2TIveXMZFtqRja3H426iv38QqWg0RmmhcmnSkD7SqAZWGI+OoRsUJ2Et
+bg4N9Cb46Gjqdh8SQF+rXYfL1AWnjMM7/AhJLMoWWb0sBzqA7UeJxLlAt1Lgnnsl
+P2nszH+Ia9V0dSlr79haGo80FALM8TiKBAQ/bTktqP5vOWSlCzHj7K30Bil36TQ=
+-----END RSA PRIVATE KEY-----
diff --git a/xos/observers/vbng/model-deps b/xos/observers/vbng/model-deps
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/xos/observers/vbng/model-deps
@@ -0,0 +1 @@
+{}
diff --git a/xos/observers/vbng/run.sh b/xos/observers/vbng/run.sh
new file mode 100755
index 0000000..efb586f
--- /dev/null
+++ b/xos/observers/vbng/run.sh
@@ -0,0 +1,6 @@
+#if [[ ! -e ./vbng-observer.py ]]; then
+#    ln -s ../../xos-observer.py vbng-observer.py
+#fi
+
+export XOS_DIR=/opt/xos
+python vbng-observer.py  -C $XOS_DIR/observers/vbng/vbng_observer_config
diff --git a/xos/observers/vbng/start.sh b/xos/observers/vbng/start.sh
new file mode 100755
index 0000000..98008f4
--- /dev/null
+++ b/xos/observers/vbng/start.sh
@@ -0,0 +1,6 @@
+#if [[ ! -e ./vbng-observer.py ]]; then
+#    ln -s ../../xos-observer.py vbng-observer.py
+#fi
+
+export XOS_DIR=/opt/xos
+nohup python vbng-observer.py  -C $XOS_DIR/observers/vbng/vbng_observer_config > /dev/null 2>&1 &
diff --git a/xos/observers/vbng/steps/sync_vbngtenant.py b/xos/observers/vbng/steps/sync_vbngtenant.py
new file mode 100644
index 0000000..c997f4f
--- /dev/null
+++ b/xos/observers/vbng/steps/sync_vbngtenant.py
@@ -0,0 +1,142 @@
+import os
+import requests
+import socket
+import sys
+import base64
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from synchronizers.base.ansible import run_template_ssh
+from core.models import Service
+from services.cord.models import VCPEService, VCPETenant, VBNGTenant, VBNGService
+from services.hpc.models import HpcService, CDNPrefix
+from util.logger import Logger, logging
+
+# VBNG_API = "http://10.0.3.136:8181/onos/virtualbng/privateip/"
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+logger = Logger(level=logging.INFO)
+
+class SyncVBNGTenant(SyncStep):
+    provides=[VCPETenant]
+    observes=VCPETenant
+    requested_interval=0
+
+    def __init__(self, **args):
+        SyncStep.__init__(self, **args)
+
+    def fetch_pending(self, deleted):
+        if (not deleted):
+            objs = VBNGTenant.get_tenant_objects().filter(Q(enacted__lt=F('updated')) | Q(enacted=None),Q(lazy_blocked=False))
+        else:
+            objs = VBNGTenant.get_deleted_tenant_objects()
+
+        return objs
+
+    def defer_sync(self, o, reason):
+        logger.info("defer object %s due to %s" % (str(o), reason))
+        raise Exception("defer object %s due to %s" % (str(o), reason))
+
+    def get_vbng_service(self, o):
+        if not o.provider_service:
+             raise Exception("vBNG tenant %s has no provider_service" % str(o.id))
+        services = VBNGService.get_service_objects().filter(id = o.provider_service.id)
+        if not services:
+             raise Exception("vBNG tenant %s is associated with the wrong kind of provider_service" % str(o.id))
+        return services[0]
+
+    def get_vbng_url(self, o):
+        service = self.get_vbng_service(o)
+
+        # if the service object specifies a vbng_url, then use it
+        if service.vbng_url:
+            return service.vbng_url
+
+        # otherwise, see if the service has tenancy in ONOS
+        for tenant in service.subscribed_tenants.all():
+            if tenant.provider_service and tenant.provider_service.kind == "onos":
+                onos_service = tenant.provider_service
+                if not onos_service.slices.exists():
+                    raise Exception("vBNG service is linked to an ONOSApp, but the App's Service has no slices")
+                onos_slice = onos_service.slices.all()[0]
+                if not onos_slice.instances.exists():
+                    raise Exception("vBNG service is linked to an ONOSApp, but the App's Service's Slice has no instances")
+                instance = onos_slice.instances.all()[0]
+
+                #onos_app = ONOSApp.objects.filter(id = tenant.id)
+                #instance = onos_app.instance
+                #if not instance:
+                #    raise Exception("ONOSApp has no instance")
+
+                if not instance.instance_name:
+                    raise Exception("vBNG service is linked to an ONOSApp, but the App's Service's Slice's first instance is not instantiated")
+                ip = instance.get_network_ip("nat")
+                if not ip:
+                    raise Exception("vBNG service is linked to an ONOSApp, but the App's Service's Slice's first instance does not have an ip")
+
+                logger.info("Using ip %s from ONOS Instance %s" % (ip, instance))
+
+                return "http://%s:8181/onos/virtualbng/" % ip
+
+        raise Exception("vBNG service does not have vbng_url set, and is not linked to an ONOSApp")
+
+    def get_private_interface(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:
+            raise Exception("No vCPE tenant is associated with vBNG %s" % str(o.id))
+        if len(vcpes)>1:
+            raise Exception("More than one vCPE tenant is associated with vBNG %s" % str(o.id))
+
+        vcpe = vcpes[0]
+        instance = vcpe.instance
+
+        if not instance:
+            raise Exception("No instance associated with vBNG %s" % str(o.id))
+
+        if not vcpe.wan_ip:
+            self.defer_sync(o, "does not have a WAN IP yet")
+
+        if not vcpe.wan_container_mac:
+            # this should never happen; container MAC is computed from WAN IP
+            self.defer_sync(o, "does not have a WAN container MAC yet")
+
+        return (vcpe.wan_ip, vcpe.wan_container_mac, vcpe.instance.node.name)
+
+    def sync_record(self, o):
+        logger.info("sync'ing VBNGTenant %s" % str(o))
+
+        if not o.routeable_subnet:
+            (private_ip, private_mac, private_hostname) = self.get_private_interface(o)
+            logger.info("contacting vBNG service to request mapping for private ip %s mac %s host %s" % (private_ip, private_mac, private_hostname) )
+
+            url = self.get_vbng_url(o) + "privateip/%s/%s/%s" % (private_ip, private_mac, private_hostname)
+            logger.info( "vbng url: %s" % url )
+            r = requests.post(url )
+            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))
+
+            if r.text == "0":
+                raise Exception("VBNG service failed to return a routeable_subnet (probably ran out)")
+
+            o.routeable_subnet = r.text
+            o.mapped_ip = private_ip
+            o.mapped_mac = private_mac
+            o.mapped_hostname = private_hostname
+
+        o.save()
+
+    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(self.get_vbng_url(o) + "privateip/%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/stop.sh b/xos/observers/vbng/stop.sh
new file mode 100755
index 0000000..d49591e
--- /dev/null
+++ b/xos/observers/vbng/stop.sh
@@ -0,0 +1 @@
+pkill -9 -f vbng-observer.py
diff --git a/xos/observers/vbng/supervisor/vbng-observer.conf b/xos/observers/vbng/supervisor/vbng-observer.conf
new file mode 100644
index 0000000..cff77b8
--- /dev/null
+++ b/xos/observers/vbng/supervisor/vbng-observer.conf
@@ -0,0 +1,2 @@
+[program:vbng-observer]
+command=python /opt/xos/observers/vbng/vbng-observer.py -C /opt/xos/observers/vbng/vbng_observer_config
diff --git a/xos/observers/vbng/vbng-observer.py b/xos/observers/vbng/vbng-observer.py
new file mode 100755
index 0000000..d6a71ff
--- /dev/null
+++ b/xos/observers/vbng/vbng-observer.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+
+# This imports and runs ../../xos-observer.py
+
+import importlib
+import os
+import sys
+observer_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"../..")
+sys.path.append(observer_path)
+mod = importlib.import_module("xos-observer")
+mod.main()
diff --git a/xos/observers/vbng/vbng_observer_config b/xos/observers/vbng/vbng_observer_config
new file mode 100644
index 0000000..b75d498
--- /dev/null
+++ b/xos/observers/vbng/vbng_observer_config
@@ -0,0 +1,38 @@
+
+[plc]
+name=plc
+deployment=VICCI
+
+[db]
+name=xos
+user=postgres
+password=password
+host=localhost
+port=5432
+
+[api]
+host=128.112.171.237
+port=8000
+ssl_key=None
+ssl_cert=None
+ca_ssl_cert=None
+ratelimit_enabled=0
+omf_enabled=0
+mail_support_address=support@localhost
+nova_enabled=True
+
+[observer]
+name=vbng
+dependency_graph=/opt/xos/observers/vbng/model-deps
+steps_dir=/opt/xos/observers/vbng/steps
+sys_dir=/opt/xos/observers/vbng/sys
+deleters_dir=/opt/xos/observers/vbng/deleters
+log_file=console
+#/var/log/hpc.log
+driver=None
+pretend=False
+backoff_disabled=True
+
+[feefie]
+client_id='vicci_dev_central'
+user_id='pl'
diff --git a/xos/observers/vcpe/broadbandshield.py b/xos/observers/vcpe/broadbandshield.py
new file mode 100644
index 0000000..dd2f00b
--- /dev/null
+++ b/xos/observers/vcpe/broadbandshield.py
@@ -0,0 +1,396 @@
+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": "PG13",
+                 "NONE": "OFF",
+                 "ALL": "NONE",
+                 None: "NONE"}
+
+    def __init__(self, username, password, bbs_hostname=None, bbs_port=None):
+        self.username = username
+        self.password = password
+
+        # XXX not tested on port 80
+        #self.bbs_hostname = "www.broadbandshield.com"
+        #self.bbs_port = 80
+
+        if not bbs_hostname:
+            bbs_hostname = "cordcompute01.onlab.us"
+        if not bbs_port:
+            bbs_port = 8018
+
+        self.bbs_hostname = bbs_hostname
+        self.bbs_port = int(bbs_port)
+
+        self.api = "http://%s:%d/api" % (self.bbs_hostname, self.bbs_port)
+        self.nic_update = "http://%s:%d/nic/update" % (self.bbs_hostname, self.bbs_port)
+
+        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 associate(self, ip):
+        bbs_hostname = "cordcompute01.onlab.us"
+        r = requests.get(self.nic_update, params={"hostname": "onlab.us"}, headers={"X-Forwarded-For": ip}, auth=requests.auth.HTTPBasicAuth(self.username,self.password))
+        if (r.status_code != 200):
+            raise BBS_Failure("Failed to associate account with ip (%d)" % r.status_code)
+
+def dump():
+    bbs = BBS(sys.argv[2], sys.argv[3])
+    bbs.dump()
+
+def associate():
+    if len(sys.argv)<5:
+        print "you need to specify IP address"
+        sys.exit(-1)
+
+    bbs = BBS(sys.argv[2], sys.argv[3])
+    bbs.associate(sys.argv[4])
+
+def self_test():
+    bbs = BBS(sys.argv[2], sys.argv[3])
+
+    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():
+    if len(sys.argv)<4:
+        print "syntax: broadbandshield.py <operation> <email> <password>"
+        print "        operation = [dump | selftest | assocate"
+        sys.exit(-1)
+
+    operation = sys.argv[1]
+
+    if operation=="dump":
+        dump()
+    elif operation=="selftest":
+        self_test()
+    elif operation=="associate":
+        associate()
+
+if __name__ == "__main__":
+    main()
+
+
diff --git a/xos/observers/vcpe/files/docker.list b/xos/observers/vcpe/files/docker.list
new file mode 100644
index 0000000..0ee9ae0
--- /dev/null
+++ b/xos/observers/vcpe/files/docker.list
@@ -0,0 +1 @@
+deb https://get.docker.com/ubuntu docker main
diff --git a/xos/observers/vcpe/files/etc/rc.local b/xos/observers/vcpe/files/etc/rc.local
new file mode 100755
index 0000000..24c8f8d
--- /dev/null
+++ b/xos/observers/vcpe/files/etc/rc.local
@@ -0,0 +1,22 @@
+#!/bin/sh -e
+#
+# rc.local
+#
+# This script is executed at the end of each multiuser runlevel.
+# Make sure that the script will "exit 0" on success or any other
+# value on error.
+#
+# In order to enable or disable this script just change the execution
+# bits.
+#
+# By default this script does nothing.
+
+ufw enable
+ufw allow bootps
+ufw allow from 192.168.0.0/24
+ufw route allow in on eth1 out on eth0
+ufw route allow in on eth1 out on eth2
+
+# service dnsmasq start
+
+exit 0
diff --git a/xos/observers/vcpe/files/etc/ufw/after.init b/xos/observers/vcpe/files/etc/ufw/after.init
new file mode 100644
index 0000000..e89217d
--- /dev/null
+++ b/xos/observers/vcpe/files/etc/ufw/after.init
@@ -0,0 +1,40 @@
+#!/bin/sh
+#
+# after.init: if executable, called by ufw-init. See 'man ufw-framework' for
+#             details. Note that output from these scripts is not seen via the
+#             the ufw command, but instead via ufw-init.
+#
+# Copyright 2013 Canonical Ltd.
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License version 3,
+#    as published by the Free Software Foundation.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+set -e
+
+case "$1" in
+start)
+    # typically required
+    ;;
+stop)
+    # typically required
+    ;;
+status)
+    # optional
+    ;;
+flush-all)
+    # optional
+    ;;
+*)
+    echo "'$1' not supported"
+    echo "Usage: after.init {start|stop|flush-all|status}"
+    ;;
+esac
diff --git a/xos/observers/vcpe/files/etc/ufw/after.rules b/xos/observers/vcpe/files/etc/ufw/after.rules
new file mode 100644
index 0000000..0d6c646
--- /dev/null
+++ b/xos/observers/vcpe/files/etc/ufw/after.rules
@@ -0,0 +1,30 @@
+#
+# rules.input-after
+#
+# Rules that should be run after the ufw command line added rules. Custom
+# rules should be added to one of these chains:
+#   ufw-after-input
+#   ufw-after-output
+#   ufw-after-forward
+#
+
+# Don't delete these required lines, otherwise there will be errors
+*filter
+:ufw-after-input - [0:0]
+:ufw-after-output - [0:0]
+:ufw-after-forward - [0:0]
+# End required lines
+
+# don't log noisy services by default
+-A ufw-after-input -p udp --dport 137 -j ufw-skip-to-policy-input
+-A ufw-after-input -p udp --dport 138 -j ufw-skip-to-policy-input
+-A ufw-after-input -p tcp --dport 139 -j ufw-skip-to-policy-input
+-A ufw-after-input -p tcp --dport 445 -j ufw-skip-to-policy-input
+-A ufw-after-input -p udp --dport 67 -j ufw-skip-to-policy-input
+-A ufw-after-input -p udp --dport 68 -j ufw-skip-to-policy-input
+
+# don't log noisy broadcast
+-A ufw-after-input -m addrtype --dst-type BROADCAST -j ufw-skip-to-policy-input
+
+# don't delete the 'COMMIT' line or these rules won't be processed
+COMMIT
diff --git a/xos/observers/vcpe/files/etc/ufw/after6.rules b/xos/observers/vcpe/files/etc/ufw/after6.rules
new file mode 100644
index 0000000..0d99672
--- /dev/null
+++ b/xos/observers/vcpe/files/etc/ufw/after6.rules
@@ -0,0 +1,27 @@
+#
+# rules.input-after
+#
+# Rules that should be run after the ufw command line added rules. Custom
+# rules should be added to one of these chains:
+#   ufw6-after-input
+#   ufw6-after-output
+#   ufw6-after-forward
+#
+
+# Don't delete these required lines, otherwise there will be errors
+*filter
+:ufw6-after-input - [0:0]
+:ufw6-after-output - [0:0]
+:ufw6-after-forward - [0:0]
+# End required lines
+
+# don't log noisy services by default
+-A ufw6-after-input -p udp --dport 137 -j ufw6-skip-to-policy-input
+-A ufw6-after-input -p udp --dport 138 -j ufw6-skip-to-policy-input
+-A ufw6-after-input -p tcp --dport 139 -j ufw6-skip-to-policy-input
+-A ufw6-after-input -p tcp --dport 445 -j ufw6-skip-to-policy-input
+-A ufw6-after-input -p udp --dport 546 -j ufw6-skip-to-policy-input
+-A ufw6-after-input -p udp --dport 547 -j ufw6-skip-to-policy-input
+
+# don't delete the 'COMMIT' line or these rules won't be processed
+COMMIT
diff --git a/xos/observers/vcpe/files/etc/ufw/applications.d/openssh-server b/xos/observers/vcpe/files/etc/ufw/applications.d/openssh-server
new file mode 100644
index 0000000..9bbe906
--- /dev/null
+++ b/xos/observers/vcpe/files/etc/ufw/applications.d/openssh-server
@@ -0,0 +1,4 @@
+[OpenSSH]
+title=Secure shell server, an rshd replacement
+description=OpenSSH is a free implementation of the Secure Shell protocol.
+ports=22/tcp
diff --git a/xos/observers/vcpe/files/etc/ufw/before.init b/xos/observers/vcpe/files/etc/ufw/before.init
new file mode 100644
index 0000000..1348cb1
--- /dev/null
+++ b/xos/observers/vcpe/files/etc/ufw/before.init
@@ -0,0 +1,40 @@
+#!/bin/sh
+#
+# before.init: if executable, called by ufw-init. See 'man ufw-framework' for
+#              details. Note that output from these scripts is not seen via the
+#              the ufw command, but instead via ufw-init.
+#
+# Copyright 2013 Canonical Ltd.
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License version 3,
+#    as published by the Free Software Foundation.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+set -e
+
+case "$1" in
+start)
+    # typically required
+    ;;
+stop)
+    # typically required
+    ;;
+status)
+    # optional
+    ;;
+flush-all)
+    # optional
+    ;;
+*)
+    echo "'$1' not supported"
+    echo "Usage: before.init {start|stop|flush-all|status}"
+    ;;
+esac
diff --git a/xos/observers/vcpe/files/etc/ufw/before6.rules b/xos/observers/vcpe/files/etc/ufw/before6.rules
new file mode 100644
index 0000000..0b26ed8
--- /dev/null
+++ b/xos/observers/vcpe/files/etc/ufw/before6.rules
@@ -0,0 +1,73 @@
+#
+# rules.before
+#
+# Rules that should be run before the ufw command line added rules. Custom
+# rules should be added to one of these chains:
+#   ufw6-before-input
+#   ufw6-before-output
+#   ufw6-before-forward
+#
+
+# Don't delete these required lines, otherwise there will be errors
+*filter
+:ufw6-before-input - [0:0]
+:ufw6-before-output - [0:0]
+:ufw6-before-forward - [0:0]
+# End required lines
+
+
+# allow all on loopback
+-A ufw6-before-input -i lo -j ACCEPT
+-A ufw6-before-output -o lo -j ACCEPT
+
+# drop packets with RH0 headers
+-A ufw6-before-input -m rt --rt-type 0 -j DROP
+-A ufw6-before-forward -m rt --rt-type 0 -j DROP
+-A ufw6-before-output -m rt --rt-type 0 -j DROP
+
+# for stateless autoconfiguration (restrict NDP messages to hop limit of 255)
+-A ufw6-before-input -p icmpv6 --icmpv6-type neighbor-solicitation -m hl --hl-eq 255 -j ACCEPT
+-A ufw6-before-output -p icmpv6 --icmpv6-type neighbor-solicitation -m hl --hl-eq 255 -j ACCEPT
+-A ufw6-before-input -p icmpv6 --icmpv6-type neighbor-advertisement -m hl --hl-eq 255 -j ACCEPT
+-A ufw6-before-output -p icmpv6 --icmpv6-type neighbor-advertisement -m hl --hl-eq 255 -j ACCEPT
+-A ufw6-before-input -p icmpv6 --icmpv6-type router-solicitation -m hl --hl-eq 255 -j ACCEPT
+-A ufw6-before-input -p icmpv6 --icmpv6-type router-advertisement -m hl --hl-eq 255 -j ACCEPT
+
+# quickly process packets for which we already have a connection
+-A ufw6-before-input -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
+-A ufw6-before-output -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
+-A ufw6-before-forward -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
+
+# for multicast ping replies from link-local addresses (these don't have an
+# associated connection and would otherwise be marked INVALID)
+-A ufw6-before-input -p icmpv6 --icmpv6-type echo-reply -s fe80::/10 -j ACCEPT
+
+# drop INVALID packets (logs these in loglevel medium and higher)
+-A ufw6-before-input -m conntrack --ctstate INVALID -j ufw6-logging-deny
+-A ufw6-before-input -m conntrack --ctstate INVALID -j DROP
+
+# ok icmp codes for INPUT
+-A ufw6-before-input -p icmpv6 --icmpv6-type destination-unreachable -j ACCEPT
+-A ufw6-before-input -p icmpv6 --icmpv6-type packet-too-big -j ACCEPT
+-A ufw6-before-input -p icmpv6 --icmpv6-type time-exceeded -j ACCEPT
+-A ufw6-before-input -p icmpv6 --icmpv6-type parameter-problem -j ACCEPT
+-A ufw6-before-input -p icmpv6 --icmpv6-type echo-request -j ACCEPT
+
+# ok icmp code for FORWARD
+-A ufw6-before-forward -p icmpv6 --icmpv6-type destination-unreachable -j ACCEPT
+-A ufw6-before-forward -p icmpv6 --icmpv6-type packet-too-big -j ACCEPT
+-A ufw6-before-forward -p icmpv6 --icmpv6-type time-exceeded -j ACCEPT
+-A ufw6-before-forward -p icmpv6 --icmpv6-type parameter-problem -j ACCEPT
+-A ufw6-before-forward -p icmpv6 --icmpv6-type echo-request -j ACCEPT
+
+# allow dhcp client to work
+-A ufw6-before-input -p udp -s fe80::/10 --sport 547 -d fe80::/10 --dport 546 -j ACCEPT
+
+# allow MULTICAST mDNS for service discovery
+-A ufw6-before-input -p udp -d ff02::fb --dport 5353 -j ACCEPT
+
+# allow MULTICAST UPnP for service discovery
+-A ufw6-before-input -p udp -d ff02::f --dport 1900 -j ACCEPT
+
+# don't delete the 'COMMIT' line or these rules won't be processed
+COMMIT
diff --git a/xos/observers/vcpe/files/etc/ufw/sysctl.conf b/xos/observers/vcpe/files/etc/ufw/sysctl.conf
new file mode 100644
index 0000000..8707032
--- /dev/null
+++ b/xos/observers/vcpe/files/etc/ufw/sysctl.conf
@@ -0,0 +1,57 @@
+#
+# Configuration file for setting network variables. Please note these settings
+# override /etc/sysctl.conf. If you prefer to use /etc/sysctl.conf, please
+# adjust IPT_SYSCTL in /etc/default/ufw.
+#
+
+# Uncomment this to allow this host to route packets between interfaces
+#net/ipv4/ip_forward=1
+#net/ipv6/conf/default/forwarding=1
+#net/ipv6/conf/all/forwarding=1
+
+# Turn on Source Address Verification in all interfaces to prevent some
+# spoofing attacks
+net/ipv4/conf/default/rp_filter=1
+net/ipv4/conf/all/rp_filter=1
+
+# Do not accept IP source route packets (we are not a router)
+net/ipv4/conf/default/accept_source_route=0
+net/ipv4/conf/all/accept_source_route=0
+net/ipv6/conf/default/accept_source_route=0
+net/ipv6/conf/all/accept_source_route=0
+
+# Disable ICMP redirects. ICMP redirects are rarely used but can be used in
+# MITM (man-in-the-middle) attacks. Disabling ICMP may disrupt legitimate
+# traffic to those sites.
+net/ipv4/conf/default/accept_redirects=0
+net/ipv4/conf/all/accept_redirects=0
+net/ipv6/conf/default/accept_redirects=0
+net/ipv6/conf/all/accept_redirects=0
+
+# Ignore bogus ICMP errors
+net/ipv4/icmp_echo_ignore_broadcasts=1
+net/ipv4/icmp_ignore_bogus_error_responses=1
+net/ipv4/icmp_echo_ignore_all=0
+
+# Don't log Martian Packets (impossible packets)
+net/ipv4/conf/default/log_martians=0
+net/ipv4/conf/all/log_martians=0
+
+# Change to '1' to enable TCP/IP SYN cookies This disables TCP Window Scaling
+# (http://lkml.org/lkml/2008/2/5/167)
+net/ipv4/tcp_syncookies=0
+
+#net/ipv4/tcp_fin_timeout=30
+#net/ipv4/tcp_keepalive_intvl=1800
+
+# normally allowing tcp_sack is ok, but if going through OpenBSD 3.8 RELEASE or
+# earlier pf firewall, should set this to 0
+net/ipv4/tcp_sack=1
+
+# Uncomment this to turn off ipv6 autoconfiguration
+#net/ipv6/conf/default/autoconf=0
+#net/ipv6/conf/all/autoconf=0
+
+# Uncomment this to enable ipv6 privacy addressing
+#net/ipv6/conf/default/use_tempaddr=2
+#net/ipv6/conf/all/use_tempaddr=2
diff --git a/xos/observers/vcpe/files/etc/ufw/ufw.conf b/xos/observers/vcpe/files/etc/ufw/ufw.conf
new file mode 100644
index 0000000..28fe534
--- /dev/null
+++ b/xos/observers/vcpe/files/etc/ufw/ufw.conf
@@ -0,0 +1,10 @@
+# /etc/ufw/ufw.conf
+#
+
+# Set to yes to start on boot. If setting this remotely, be sure to add a rule
+# to allow your remote connection before starting ufw. Eg: 'ufw allow 22/tcp'
+ENABLED=yes
+
+# Please use the 'ufw' command to set the loglevel. Eg: 'ufw logging medium'.
+# See 'man ufw' for details.
+LOGLEVEL=low
diff --git a/xos/observers/vcpe/files/vcpe.conf b/xos/observers/vcpe/files/vcpe.conf
new file mode 100644
index 0000000..752c57a
--- /dev/null
+++ b/xos/observers/vcpe/files/vcpe.conf
@@ -0,0 +1,10 @@
+# Upstart script for vCPE 
+description "vCPE container"
+author "andy@onlab.us"
+start on filesystem and started docker
+stop on runlevel [!2345]
+respawn
+
+script
+  /usr/local/sbin/start-vcpe.sh
+end script
diff --git a/xos/observers/vcpe/files/vcpe.dnsmasq b/xos/observers/vcpe/files/vcpe.dnsmasq
new file mode 100644
index 0000000..2b2687b
--- /dev/null
+++ b/xos/observers/vcpe/files/vcpe.dnsmasq
@@ -0,0 +1,2 @@
+listen-address=192.168.0.1
+dhcp-range=192.168.0.2,192.168.0.254,6
diff --git a/xos/observers/vcpe/files/vm-resolv.conf b/xos/observers/vcpe/files/vm-resolv.conf
new file mode 100644
index 0000000..cae093a
--- /dev/null
+++ b/xos/observers/vcpe/files/vm-resolv.conf
@@ -0,0 +1 @@
+nameserver 8.8.8.8
diff --git a/xos/observers/vcpe/model-deps b/xos/observers/vcpe/model-deps
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/xos/observers/vcpe/model-deps
@@ -0,0 +1 @@
+{}
diff --git a/xos/observers/vcpe/observer_ansible_test.py b/xos/observers/vcpe/observer_ansible_test.py
new file mode 100644
index 0000000..b28da63
--- /dev/null
+++ b/xos/observers/vcpe/observer_ansible_test.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+import os
+import sys
+
+sys.path.append("../..")
+import synchronizers.base.ansible
+
+print sys.argv
+
+private_key="""-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAtJiuarud5S4Y2quDeWyaS0UCQGQtfuSzzNhplFwujYnJGL65
+e14REtv+UuHGymyr/SfkTrBd8vH5NI2UZ/4sZW13ieI/1d97OeVe2+ct0Y4BaFEI
+3Hja6DIpsY3Q2cBQsWUwcQzbMIF9jIq8SzwR1zk8UtZi09fNxqjCchRPlBvbiLKX
+g0/yirN237WbaKzK++8EPy3nuv83216MXHFFSjuxfICe/RhjaqMzVp7isSbv1suU
+geyvNNzU71c/K13gTggdcIXeRQBiJYio2Sn3h2nsV6AFqFH4fjERxWG55Q4e3jeE
+tWM/Dw+hqYKg/25UcmM862a6sUmuDCmM5S3VEQIDAQABAoIBACH88iZoNOki6c6N
+pOq/Q7KSxD+2GtHc3PHacNRZHPHKUqxziJjtNS1lddHHaBBEr4GvdkpQ6v2ECLCZ
+TKrdrSFRnsO2bukjbB+TSWz9byQXI7CsP4yuuhQlDK+7zuiMRyN7tcgw8TeJx0Uh
+/xnxrjHhNbcpXeQcoz+WFzI9HFT1MEGmMS4Lyp/zLB/pmfY9h7V9d+EeRZDi78jq
+Vir6MI6iCTa0T02dvHUFOg+wXLb0nb8V1xKDL+6cAJla7LzwoG8lTnvp5DSYCojI
+5JrILYafeO8RbBV2GWmaE5mkHgeBkFZ+qZQ7K0MjR30Yh6tajB7P3+F/Max8FUgW
+xLHr8AECgYEA2+o0ge3HtZcepEFBKKYnLTwoEpPCfLElWZHzUJYDz259s4JLsfak
+tROANFEdsJUjpmWG52MCL+bgKFFOedDkt4p1jgcIneaHk0jvoU11wG7W3jZZVy1q
+WjQNH5vDU+hg5tm/CREwm7lbUxR9Xuj9K63CNAAGp8KO7h2tOH8woIECgYEA0jrb
+LUg30RxO3+vrq9dUYohrDRisk5zKXuRLfxRA+E+ruvZ7CctG2OpM+658/qZM/w95
+7pOj6zz3//w7tAvH9erY+JOISnzaYKx04sYC1MfbFiFkq5j0gpuYm/MULDYNvFqr
+NU2Buj4dW+ZB+SeficsQOqm5QeNxh1kgiDCs7JECgYEAjSLGCAzeesA9vhTTCI95
+3SIaZbHGw9e8rLtqeHGOiHXU3nvksJYmJsAZK3pTn5xXgNbvuVhlcvCtM7LatntG
+DjUiNMB22z+0CuZoRBE+XP3FkF84/yX6d2Goenyw4wzkA8QDQoJxu789yRgBTgQh
+VwLw/AZ4PvoyWMdbAENApgECgYEAvFikosYP09XTyIPaKaOKY5iqqBoSC1GucSOB
+jAG+T3k5dxB6nQS0nYQUomvqak7drqnT6O33Lrr5ySrW5nCjnmvgJZwvv+Rp1bDM
+K5uRT8caPpJ+Wcp4TUdPi3BVA2MOHVDyEJg3AH/D1+DL/IgGQ/JcwOHsKt61iLhO
+EBXj5zECgYEAk+HuwksUPkSxg/AiJGbapGDK6XGymEUzo2duWlnofRqGcZ3NT3bB
+/kDI1KxQdlpODXSi4/BuTpbQiFOrzcEq5e5ytoMxlCHh3Fl3Jxl+JlgO21vAUvP6
+4SET7Q/6LxmfBlCVRg0dXDwcfJLgbnWxyvprIcz4e0FSFVZTBs/6tFk=
+-----END RSA PRIVATE KEY-----
+"""
+
+observer.ansible.run_template_ssh("test.yaml",
+                                  {"instance_name": "onlab_test405-378",
+                                   "instance_id": "instance-0000004d",
+                                   "hostname": "node67.washington.vicci.org",
+                                   "private_key": private_key})
+
diff --git a/xos/observers/vcpe/run.sh b/xos/observers/vcpe/run.sh
new file mode 100755
index 0000000..f180e66
--- /dev/null
+++ b/xos/observers/vcpe/run.sh
@@ -0,0 +1,6 @@
+#if [[ ! -e ./vcpe-observer.py ]]; then
+#    ln -s ../../xos-observer.py vcpe-observer.py
+#fi
+
+export XOS_DIR=/opt/xos
+python vcpe-observer.py  -C $XOS_DIR/observers/vcpe/vcpe_observer_config
diff --git a/xos/observers/vcpe/start-bbs.sh b/xos/observers/vcpe/start-bbs.sh
new file mode 100755
index 0000000..c8ee147
--- /dev/null
+++ b/xos/observers/vcpe/start-bbs.sh
@@ -0,0 +1,14 @@
+#! /bin/bash
+
+# put this in /opt/xerocole/start-bbs.sh
+# make sure it's executable
+# set it up in crontab
+#   @reboot /opt/xerocole/start-bbs.sh
+
+ulimit -n 200000
+cd /opt/xerocole/answerx
+/opt/xerocole/answerx/startStop checkconfig answerx
+/opt/xerocole/answerx/startStop start answerx
+cd /opt/xerocole/namecontrols
+nohup /opt/xerocole/namecontrols/broadbandshield &
+nohup socat TCP-LISTEN:80,bind=0.0.0.0,fork TCP4:127.0.0.1:8018 &  
diff --git a/xos/observers/vcpe/start.sh b/xos/observers/vcpe/start.sh
new file mode 100755
index 0000000..b402e5d
--- /dev/null
+++ b/xos/observers/vcpe/start.sh
@@ -0,0 +1,6 @@
+#if [[ ! -e ./vcpe-observer.py ]]; then
+#    ln -s ../../xos-observer.py vcpe-observer.py
+#fi
+
+export XOS_DIR=/opt/xos
+nohup python vcpe-observer.py  -C $XOS_DIR/observers/vcpe/vcpe_observer_config > /dev/null 2>&1 &
diff --git a/xos/observers/vcpe/steps/ansible_test/README b/xos/observers/vcpe/steps/ansible_test/README
new file mode 100644
index 0000000..d3b2c54
--- /dev/null
+++ b/xos/observers/vcpe/steps/ansible_test/README
@@ -0,0 +1,4 @@
+Some scripts used while testing the Ansible instance configuraiton observer
+
+xos.py was probably the prototype of an XOS SSH module for Ansible, that understood how to SSH into the instances
+without needing to play config file and environment tricks. 
diff --git a/xos/observers/vcpe/steps/ansible_test/inventory.txt b/xos/observers/vcpe/steps/ansible_test/inventory.txt
new file mode 100644
index 0000000..bd5b542
--- /dev/null
+++ b/xos/observers/vcpe/steps/ansible_test/inventory.txt
@@ -0,0 +1,16 @@
+[onlab_hpc-355]
+node67.washington.vicci.org instance_id=instance-00000045 instance_name=onlab_hpc-355
+
+[onlab_test405-372]
+node67.washington.vicci.org instance_id=instance-0000004c instance_name=onlab_test405-372
+
+[onlab_test405-376]
+node1.cs.arizona.edu
+
+[onlab_test405-378]
+node67.washington.vicci.org ansible_ssh_private_key_file=/home/smbaker/.ssh/id_rsa
+#/home/smbaker/projects/vicci/keys/test_service_key_rsa
+
+[mysite_test2-48]
+cordcompute02.onlab.us ansible_ssh_private_key_file=/home/smbaker/projects/vicci/keys/demo_admin.rsa
+
diff --git a/xos/observers/vcpe/steps/ansible_test/test.sh b/xos/observers/vcpe/steps/ansible_test/test.sh
new file mode 100755
index 0000000..157ba9c
--- /dev/null
+++ b/xos/observers/vcpe/steps/ansible_test/test.sh
@@ -0,0 +1,2 @@
+#! /bin/bash
+ansible-playbook --private-key /home/smbaker/.ssh/id_rsa -i ./inventory.txt test.yaml
diff --git a/xos/observers/vcpe/steps/ansible_test/test.yaml b/xos/observers/vcpe/steps/ansible_test/test.yaml
new file mode 100644
index 0000000..6a29d56
--- /dev/null
+++ b/xos/observers/vcpe/steps/ansible_test/test.yaml
@@ -0,0 +1,12 @@
+---
+- hosts: onlab_test405-372
+  connection: xos
+  user: ubuntu
+  vars:
+     foo: 25
+#  instance_name: instance-00000045
+#  slice_name: onlab_hpc-355
+
+  tasks:
+    - name: foobar
+      shell: echo foo > /tmp/foobar
diff --git a/xos/observers/vcpe/steps/ansible_test/xos.py b/xos/observers/vcpe/steps/ansible_test/xos.py
new file mode 100755
index 0000000..eb4f3eb
--- /dev/null
+++ b/xos/observers/vcpe/steps/ansible_test/xos.py
@@ -0,0 +1,444 @@
+# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import os
+import re
+import subprocess
+import shlex
+import pipes
+import random
+import select
+import fcntl
+import hmac
+import pwd
+import gettext
+import pty
+from hashlib import sha1
+import ansible.constants as C
+from ansible.callbacks import vvv
+from ansible import errors
+from ansible import utils
+
+class Connection(object):
+    ''' ssh based connections '''
+
+    def __init__(self, runner, host, port, user, password, private_key_file, *args, **kwargs):
+        self.runner = runner
+        self.host = host
+        self.ipv6 = ':' in self.host
+        self.port = port
+        self.user = str(user)
+        self.password = password
+        self.private_key_file = private_key_file
+        self.HASHED_KEY_MAGIC = "|1|"
+        self.has_pipelining = True
+        #self.instance_id = "instance-00000045" # C.get_config(C.p, "xos", "instance_id", "INSTANCE_ID", None)
+        #self.instance_name = "onlab_hpc-355" # C.get_config(C.p, "xos", "instance_name", "SLIVER_NAME", None)
+
+        inject={}
+        inject= utils.combine_vars(inject, self.runner.inventory.get_variables(self.host))
+
+        self.instance_id = inject["instance_id"]
+        self.instance_name = inject["instance_name"]
+
+        fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_EX)
+        self.cp_dir = utils.prepare_writeable_dir('$HOME/.ansible/cp',mode=0700)
+        fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_UN)
+
+    def connect(self):
+        ''' connect to the remote host '''
+
+        vvv("ESTABLISH CONNECTION FOR USER: %s" % self.user, host=self.host)
+
+        self.common_args = []
+        extra_args = C.ANSIBLE_SSH_ARGS
+        if extra_args is not None:
+            # make sure there is no empty string added as this can produce weird errors
+            self.common_args += [x.strip() for x in shlex.split(extra_args) if x.strip()]
+        else:
+            self.common_args += ["-o", "ControlMaster=auto",
+                                 "-o", "ControlPersist=60s",
+                                 "-o", "ControlPath=%s" % (C.ANSIBLE_SSH_CONTROL_PATH % dict(directory=self.cp_dir))]
+
+        self.common_args += ["-o", "ProxyCommand ssh -q -i %s %s@%s" % (self.private_key_file, self.instance_id, self.host)]
+
+        cp_in_use = False
+        cp_path_set = False
+        for arg in self.common_args:
+            if "ControlPersist" in arg:
+                cp_in_use = True
+            if "ControlPath" in arg:
+                cp_path_set = True
+
+        if cp_in_use and not cp_path_set:
+            self.common_args += ["-o", "ControlPath=%s" % (C.ANSIBLE_SSH_CONTROL_PATH % dict(directory=self.cp_dir))]
+
+        if not C.HOST_KEY_CHECKING:
+            self.common_args += ["-o", "StrictHostKeyChecking=no"]
+
+        if self.port is not None:
+            self.common_args += ["-o", "Port=%d" % (self.port)]
+        if self.private_key_file is not None:
+            self.common_args += ["-o", "IdentityFile=\"%s\"" % os.path.expanduser(self.private_key_file)]
+        elif self.runner.private_key_file is not None:
+            self.common_args += ["-o", "IdentityFile=\"%s\"" % os.path.expanduser(self.runner.private_key_file)]
+        if self.password:
+            self.common_args += ["-o", "GSSAPIAuthentication=no",
+                                 "-o", "PubkeyAuthentication=no"]
+        else:
+            self.common_args += ["-o", "KbdInteractiveAuthentication=no",
+                                 "-o", "PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey",
+                                 "-o", "PasswordAuthentication=no"]
+        if self.user != pwd.getpwuid(os.geteuid())[0]:
+            self.common_args += ["-o", "User="+self.user]
+        self.common_args += ["-o", "ConnectTimeout=%d" % self.runner.timeout]
+
+        return self
+
+    def _run(self, cmd, indata):
+        if indata:
+            # do not use pseudo-pty
+            p = subprocess.Popen(cmd, stdin=subprocess.PIPE,
+                                     stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+            stdin = p.stdin
+        else:
+            # try to use upseudo-pty
+            try:
+                # Make sure stdin is a proper (pseudo) pty to avoid: tcgetattr errors
+                master, slave = pty.openpty()
+                p = subprocess.Popen(cmd, stdin=slave,
+                                     stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+                stdin = os.fdopen(master, 'w', 0)
+                os.close(slave)
+            except:
+                p = subprocess.Popen(cmd, stdin=subprocess.PIPE,
+                                     stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+                stdin = p.stdin
+
+        return (p, stdin)
+
+    def _password_cmd(self):
+        if self.password:
+            try:
+                p = subprocess.Popen(["sshpass"], stdin=subprocess.PIPE,
+                    stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+                p.communicate()
+            except OSError:
+                raise errors.AnsibleError("to use the 'ssh' connection type with passwords, you must install the sshpass program")
+            (self.rfd, self.wfd) = os.pipe()
+            return ["sshpass", "-d%d" % self.rfd]
+        return []
+
+    def _send_password(self):
+        if self.password:
+            os.close(self.rfd)
+            os.write(self.wfd, "%s\n" % self.password)
+            os.close(self.wfd)
+
+    def _communicate(self, p, stdin, indata, su=False, sudoable=False, prompt=None):
+        fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) & ~os.O_NONBLOCK)
+        fcntl.fcntl(p.stderr, fcntl.F_SETFL, fcntl.fcntl(p.stderr, fcntl.F_GETFL) & ~os.O_NONBLOCK)
+        # We can't use p.communicate here because the ControlMaster may have stdout open as well
+        stdout = ''
+        stderr = ''
+        rpipes = [p.stdout, p.stderr]
+        if indata:
+            try:
+                stdin.write(indata)
+                stdin.close()
+            except:
+                raise errors.AnsibleError('SSH Error: data could not be sent to the remote host. Make sure this host can be reached over ssh')
+        # Read stdout/stderr from process
+        while True:
+            rfd, wfd, efd = select.select(rpipes, [], rpipes, 1)
+
+            # fail early if the sudo/su password is wrong
+            if self.runner.sudo and sudoable:
+                if self.runner.sudo_pass:
+                    incorrect_password = gettext.dgettext(
+                        "sudo", "Sorry, try again.")
+                    if stdout.endswith("%s\r\n%s" % (incorrect_password,
+                                                     prompt)):
+                        raise errors.AnsibleError('Incorrect sudo password')
+
+                if stdout.endswith(prompt):
+                    raise errors.AnsibleError('Missing sudo password')
+
+            if self.runner.su and su and self.runner.su_pass:
+                incorrect_password = gettext.dgettext(
+                    "su", "Sorry")
+                if stdout.endswith("%s\r\n%s" % (incorrect_password, prompt)):
+                    raise errors.AnsibleError('Incorrect su password')
+
+            if p.stdout in rfd:
+                dat = os.read(p.stdout.fileno(), 9000)
+                stdout += dat
+                if dat == '':
+                    rpipes.remove(p.stdout)
+            if p.stderr in rfd:
+                dat = os.read(p.stderr.fileno(), 9000)
+                stderr += dat
+                if dat == '':
+                    rpipes.remove(p.stderr)
+            # only break out if no pipes are left to read or
+            # the pipes are completely read and
+            # the process is terminated
+            if (not rpipes or not rfd) and p.poll() is not None:
+                break
+            # No pipes are left to read but process is not yet terminated
+            # Only then it is safe to wait for the process to be finished
+            # NOTE: Actually p.poll() is always None here if rpipes is empty
+            elif not rpipes and p.poll() == None:
+                p.wait()
+                # The process is terminated. Since no pipes to read from are
+                # left, there is no need to call select() again.
+                break
+        # close stdin after process is terminated and stdout/stderr are read
+        # completely (see also issue #848)
+        stdin.close()
+        return (p.returncode, stdout, stderr)
+
+    def not_in_host_file(self, host):
+        if 'USER' in os.environ:
+            user_host_file = os.path.expandvars("~${USER}/.ssh/known_hosts")
+        else:
+            user_host_file = "~/.ssh/known_hosts"
+        user_host_file = os.path.expanduser(user_host_file)
+        
+        host_file_list = []
+        host_file_list.append(user_host_file)
+        host_file_list.append("/etc/ssh/ssh_known_hosts")
+        host_file_list.append("/etc/ssh/ssh_known_hosts2")
+        
+        hfiles_not_found = 0
+        for hf in host_file_list:
+            if not os.path.exists(hf):
+                hfiles_not_found += 1
+                continue
+            try:
+                host_fh = open(hf)
+            except IOError, e:
+                hfiles_not_found += 1
+                continue
+            else:
+                data = host_fh.read()
+                host_fh.close()
+                
+            for line in data.split("\n"):
+                if line is None or " " not in line:
+                    continue
+                tokens = line.split()
+                if tokens[0].find(self.HASHED_KEY_MAGIC) == 0:
+                    # this is a hashed known host entry
+                    try:
+                        (kn_salt,kn_host) = tokens[0][len(self.HASHED_KEY_MAGIC):].split("|",2)
+                        hash = hmac.new(kn_salt.decode('base64'), digestmod=sha1)
+                        hash.update(host)
+                        if hash.digest() == kn_host.decode('base64'):
+                            return False
+                    except:
+                        # invalid hashed host key, skip it
+                        continue
+                else:
+                    # standard host file entry
+                    if host in tokens[0]:
+                        return False
+
+        if (hfiles_not_found == len(host_file_list)):
+            vvv("EXEC previous known host file not found for %s" % host)
+        return True
+
+    def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False, executable='/bin/sh', in_data=None, su_user=None, su=False):
+        ''' run a command on the remote host '''
+
+        ssh_cmd = self._password_cmd()
+        ssh_cmd += ["ssh", "-C"]
+        if not in_data:
+            # we can only use tty when we are not pipelining the modules. piping data into /usr/bin/python
+            # inside a tty automatically invokes the python interactive-mode but the modules are not
+            # compatible with the interactive-mode ("unexpected indent" mainly because of empty lines)
+            ssh_cmd += ["-tt"]
+        if utils.VERBOSITY > 3:
+            ssh_cmd += ["-vvv"]
+        else:
+            ssh_cmd += ["-q"]
+        ssh_cmd += self.common_args
+
+        if self.ipv6:
+            ssh_cmd += ['-6']
+        #ssh_cmd += [self.host]
+        ssh_cmd += [self.instance_name]
+
+        if su and su_user:
+            sudocmd, prompt, success_key = utils.make_su_cmd(su_user, executable, cmd)
+            prompt_re = re.compile(prompt)
+            ssh_cmd.append(sudocmd)
+        elif not self.runner.sudo or not sudoable:
+            prompt = None
+            if executable:
+                ssh_cmd.append(executable + ' -c ' + pipes.quote(cmd))
+            else:
+                ssh_cmd.append(cmd)
+        else:
+            sudocmd, prompt, success_key = utils.make_sudo_cmd(sudo_user, executable, cmd)
+            ssh_cmd.append(sudocmd)
+
+        vvv("EXEC %s" % ssh_cmd, host=self.host)
+
+        not_in_host_file = self.not_in_host_file(self.host)
+
+        if C.HOST_KEY_CHECKING and not_in_host_file:
+            # lock around the initial SSH connectivity so the user prompt about whether to add 
+            # the host to known hosts is not intermingled with multiprocess output.
+            fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_EX)
+            fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_EX)
+
+        # create process
+        (p, stdin) = self._run(ssh_cmd, in_data)
+
+        self._send_password()
+
+        if (self.runner.sudo and sudoable and self.runner.sudo_pass) or \
+                (self.runner.su and su and self.runner.su_pass):
+            # several cases are handled for sudo privileges with password
+            # * NOPASSWD (tty & no-tty): detect success_key on stdout
+            # * without NOPASSWD:
+            #   * detect prompt on stdout (tty)
+            #   * detect prompt on stderr (no-tty)
+            fcntl.fcntl(p.stdout, fcntl.F_SETFL,
+                        fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK)
+            fcntl.fcntl(p.stderr, fcntl.F_SETFL,
+                        fcntl.fcntl(p.stderr, fcntl.F_GETFL) | os.O_NONBLOCK)
+            sudo_output = ''
+            sudo_errput = ''
+
+            while True:
+                if success_key in sudo_output or \
+                    (self.runner.sudo_pass and sudo_output.endswith(prompt)) or \
+                    (self.runner.su_pass and prompt_re.match(sudo_output)):
+                    break
+
+                rfd, wfd, efd = select.select([p.stdout, p.stderr], [],
+                                              [p.stdout], self.runner.timeout)
+                if p.stderr in rfd:
+                    chunk = p.stderr.read()
+                    if not chunk:
+                        raise errors.AnsibleError('ssh connection closed waiting for sudo or su password prompt')
+                    sudo_errput += chunk
+                    incorrect_password = gettext.dgettext(
+                        "sudo", "Sorry, try again.")
+                    if sudo_errput.strip().endswith("%s%s" % (prompt, incorrect_password)):
+                        raise errors.AnsibleError('Incorrect sudo password')
+                    elif sudo_errput.endswith(prompt):
+                        stdin.write(self.runner.sudo_pass + '\n')
+
+                if p.stdout in rfd:
+                    chunk = p.stdout.read()
+                    if not chunk:
+                        raise errors.AnsibleError('ssh connection closed waiting for sudo or su password prompt')
+                    sudo_output += chunk
+
+                if not rfd:
+                    # timeout. wrap up process communication
+                    stdout = p.communicate()
+                    raise errors.AnsibleError('ssh connection error waiting for sudo or su password prompt')
+
+            if success_key not in sudo_output:
+                if sudoable:
+                    stdin.write(self.runner.sudo_pass + '\n')
+                elif su:
+                    stdin.write(self.runner.su_pass + '\n')
+
+        (returncode, stdout, stderr) = self._communicate(p, stdin, in_data, su=su, sudoable=sudoable, prompt=prompt)
+
+        if C.HOST_KEY_CHECKING and not_in_host_file:
+            # lock around the initial SSH connectivity so the user prompt about whether to add 
+            # the host to known hosts is not intermingled with multiprocess output.
+            fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_UN)
+            fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_UN)
+        controlpersisterror = 'Bad configuration option: ControlPersist' in stderr or \
+                              'unknown configuration option: ControlPersist' in stderr
+
+        if C.HOST_KEY_CHECKING:
+            if ssh_cmd[0] == "sshpass" and p.returncode == 6:
+                raise errors.AnsibleError('Using a SSH password instead of a key is not possible because Host Key checking is enabled and sshpass does not support this.  Please add this host\'s fingerprint to your known_hosts file to manage this host.')
+
+        if p.returncode != 0 and controlpersisterror:
+            raise errors.AnsibleError('using -c ssh on certain older ssh versions may not support ControlPersist, set ANSIBLE_SSH_ARGS="" (or ssh_args in [ssh_connection] section of the config file) before running again')
+        if p.returncode == 255 and (in_data or self.runner.module_name == 'raw'):
+            raise errors.AnsibleError('SSH Error: data could not be sent to the remote host. Make sure this host can be reached over ssh')
+
+        return (p.returncode, '', stdout, stderr)
+
+    def put_file(self, in_path, out_path):
+        ''' transfer a file from local to remote '''
+        vvv("PUT %s TO %s" % (in_path, out_path), host=self.host)
+        if not os.path.exists(in_path):
+            raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
+        cmd = self._password_cmd()
+
+        host = self.host
+        if self.ipv6:
+            host = '[%s]' % host
+
+        if C.DEFAULT_SCP_IF_SSH:
+            cmd += ["scp"] + self.common_args
+            cmd += [in_path,host + ":" + pipes.quote(out_path)]
+            indata = None
+        else:
+            cmd += ["sftp"] + self.common_args + [host]
+            indata = "put %s %s\n" % (pipes.quote(in_path), pipes.quote(out_path))
+
+        (p, stdin) = self._run(cmd, indata)
+
+        self._send_password()
+
+        (returncode, stdout, stderr) = self._communicate(p, stdin, indata)
+
+        if returncode != 0:
+            raise errors.AnsibleError("failed to transfer file to %s:\n%s\n%s" % (out_path, stdout, stderr))
+
+    def fetch_file(self, in_path, out_path):
+        ''' fetch a file from remote to local '''
+        vvv("FETCH %s TO %s" % (in_path, out_path), host=self.host)
+        cmd = self._password_cmd()
+
+        host = self.host
+        if self.ipv6:
+            host = '[%s]' % host
+
+        if C.DEFAULT_SCP_IF_SSH:
+            cmd += ["scp"] + self.common_args
+            cmd += [host + ":" + in_path, out_path]
+            indata = None
+        else:
+            cmd += ["sftp"] + self.common_args + [host]
+            indata = "get %s %s\n" % (in_path, out_path)
+
+        p = subprocess.Popen(cmd, stdin=subprocess.PIPE,
+                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        self._send_password()
+        stdout, stderr = p.communicate(indata)
+
+        if p.returncode != 0:
+            raise errors.AnsibleError("failed to transfer file from %s:\n%s\n%s" % (in_path, stdout, stderr))
+
+    def close(self):
+        ''' not applicable since we're executing openssh binaries '''
+        pass
+
diff --git a/xos/observers/vcpe/steps/sync_vcpetenant.py b/xos/observers/vcpe/steps/sync_vcpetenant.py
new file mode 100644
index 0000000..d35a628
--- /dev/null
+++ b/xos/observers/vcpe/steps/sync_vcpetenant.py
@@ -0,0 +1,235 @@
+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 synchronizers.base.syncstep import SyncStep
+from synchronizers.base.ansible import run_template_ssh
+from observers.base.SyncInstanceUsingAnsible import SyncInstanceUsingAnsible
+from core.models import Service, Slice
+from services.cord.models import VCPEService, VCPETenant, VOLTTenant
+from services.hpc.models import HpcService, CDNPrefix
+from util.logger import Logger, logging
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from broadbandshield import BBS
+
+logger = Logger(level=logging.INFO)
+
+PARENTAL_MECHANISM="dnsmasq"
+ENABLE_QUICK_UPDATE=False
+
+class SyncVCPETenant(SyncInstanceUsingAnsible):
+    provides=[VCPETenant]
+    observes=VCPETenant
+    requested_interval=0
+    template_name = "sync_vcpetenant.yaml"
+    service_key_name = "/opt/xos/observers/vcpe/vcpe_private_key"
+
+    def __init__(self, *args, **kwargs):
+        super(SyncVCPETenant, self).__init__(*args, **kwargs)
+
+    def fetch_pending(self, deleted):
+        if (not deleted):
+            objs = VCPETenant.get_tenant_objects().filter(Q(enacted__lt=F('updated')) | Q(enacted=None),Q(lazy_blocked=False))
+        else:
+            objs = VCPETenant.get_deleted_tenant_objects()
+
+        return objs
+
+    def get_vcpe_service(self, o):
+        if not o.provider_service:
+            return None
+
+        vcpes = VCPEService.get_service_objects().filter(id=o.provider_service.id)
+        if not vcpes:
+            return None
+
+        return vcpes[0]
+
+    def get_extra_attributes(self, o):
+        # This is a place to include extra attributes that aren't part of the
+        # object itself. In the case of vCPE, we need to know:
+        #   1) the addresses of dnsdemux, to setup dnsmasq in the vCPE
+        #   2) CDN prefixes, so we know what URLs to send to dnsdemux
+        #   3) BroadBandShield server addresses, for parental filtering
+        #   4) vlan_ids, for setting up networking in the vCPE VM
+
+        vcpe_service = self.get_vcpe_service(o)
+
+        dnsdemux_ip = None
+        if vcpe_service.backend_network_label:
+            # Connect to dnsdemux using the network specified by
+            #     vcpe_service.backend_network_label
+            for service in HpcService.objects.all():
+                for slice in service.slices.all():
+                    if "dnsdemux" in slice.name:
+                        for instance in slice.instances.all():
+                            for ns in instance.ports.all():
+                                if ns.ip and ns.network.labels and (vcpe_service.backend_network_label in ns.network.labels):
+                                    dnsdemux_ip = ns.ip
+            if not dnsdemux_ip:
+                logger.info("failed to find a dnsdemux on network %s" % vcpe_service.backend_network_label)
+        else:
+            # Connect to dnsdemux using the instance's public address
+            for service in HpcService.objects.all():
+                for slice in service.slices.all():
+                    if "dnsdemux" in slice.name:
+                        for instance in slice.instances.all():
+                            if dnsdemux_ip=="none":
+                                try:
+                                    dnsdemux_ip = socket.gethostbyname(instance.node.name)
+                                except:
+                                    pass
+            if not dnsdemux_ip:
+                logger.info("failed to find a dnsdemux with a public address")
+
+        dnsdemux_ip = dnsdemux_ip or "none"
+
+        cdn_prefixes = []
+        for prefix in CDNPrefix.objects.all():
+            cdn_prefixes.append(prefix.prefix)
+
+        # Broadbandshield can either be set up internally, using vcpe_service.bbs_slice,
+        # or it can be setup externally using vcpe_service.bbs_server.
+
+        bbs_addrs = []
+        if vcpe_service.bbs_slice:
+            if vcpe_service.backend_network_label:
+                for bbs_instance in vcpe_service.bbs_slice.instances.all():
+                    for ns in bbs_instance.ports.all():
+                        if ns.ip and ns.network.labels and (vcpe_service.backend_network_label in ns.network.labels):
+                            bbs_addrs.append(ns.ip)
+            else:
+                logger.info("unsupported configuration -- bbs_slice is set, but backend_network_label is not")
+            if not bbs_addrs:
+                logger.info("failed to find any usable addresses on bbs_slice")
+        elif vcpe_service.bbs_server:
+            bbs_addrs.append(vcpe_service.bbs_server)
+        else:
+            logger.info("neither bbs_slice nor bbs_server is configured in the vCPE")
+
+        vlan_ids = []
+        s_tags = []
+        c_tags = []
+        if o.volt:
+            vlan_ids.append(o.volt.vlan_id)  # XXX remove this
+            s_tags.append(o.volt.s_tag)
+            c_tags.append(o.volt.c_tag)
+
+        try:
+            full_setup = Config().observer_full_setup
+        except:
+            full_setup = True
+
+        safe_macs=[]
+        if o.volt and o.volt.subscriber:
+            for user in o.volt.subscriber.users:
+                level = user.get("level",None)
+                mac = user.get("mac",None)
+                if level in ["G", "PG"]:
+                    if mac:
+                        safe_macs.append(mac)
+
+        fields = {"vlan_ids": vlan_ids,   # XXX remove this
+                "s_tags": s_tags,
+                "c_tags": c_tags,
+                "dnsdemux_ip": dnsdemux_ip,
+                "cdn_prefixes": cdn_prefixes,
+                "bbs_addrs": bbs_addrs,
+                "full_setup": full_setup,
+                "isolation": o.instance.isolation,
+                "safe_browsing_macs": safe_macs}
+
+        # add in the sync_attributes that come from the SubscriberRoot object
+
+        if o.volt and o.volt.subscriber and hasattr(o.volt.subscriber, "sync_attributes"):
+            for attribute_name in o.volt.subscriber.sync_attributes:
+                fields[attribute_name] = getattr(o.volt.subscriber, attribute_name)
+
+        return fields
+
+    def sync_fields(self, o, fields):
+        # the super causes the playbook to be run
+
+        super(SyncVCPETenant, self).sync_fields(o, fields)
+
+        # now do all of our broadbandshield stuff...
+
+        service = self.get_vcpe_service(o)
+        if not service:
+            # Ansible uses the service's keypair in order to SSH into the
+            # instance. It would be bad if the slice had no service.
+
+            raise Exception("Slice %s is not associated with a service" % instance.slice.name)
+
+        # Make sure the slice is configured properly
+        if (service != o.instance.slice.service):
+            raise Exception("Slice %s is associated with some service that is not %s" % (str(instance.slice), str(service)))
+
+        # only enable filtering if we have a subscriber object (see below)
+        url_filter_enable = False
+
+        # for attributes that come from CordSubscriberRoot
+        if o.volt and o.volt.subscriber:
+            url_filter_enable = o.volt.subscriber.url_filter_enable
+            url_filter_level = o.volt.subscriber.url_filter_level
+            url_filter_users = o.volt.subscriber.users
+
+        if PARENTAL_MECHANISM=="broadbandshield":
+            # disable url_filter if there are no bbs_addrs
+            if url_filter_enable and (not fields.get("bbs_addrs",[])):
+                logger.info("disabling url_filter because there are no bbs_addrs")
+                url_filter_enable = False
+
+            if url_filter_enable:
+                bbs_hostname = None
+                if service.bbs_api_hostname and service.bbs_api_port:
+                    bbs_hostname = service.bbs_api_hostname
+                else:
+                    # TODO: extract from slice
+                    bbs_hostname = "cordcompute01.onlab.us"
+
+                if service.bbs_api_port:
+                    bbs_port = service.bbs_api_port
+                else:
+                    bbs_port = 8018
+
+                if not bbs_hostname:
+                    logger.info("broadbandshield is not configured")
+                else:
+                    tStart = time.time()
+                    bbs = BBS(o.bbs_account, "123", bbs_hostname, bbs_port)
+                    bbs.sync(url_filter_level, url_filter_users)
+
+                    if o.hpc_client_ip:
+                        logger.info("associate account %s with ip %s" % (o.bbs_account, o.hpc_client_ip))
+                        bbs.associate(o.hpc_client_ip)
+                    else:
+                        logger.info("no hpc_client_ip to associate")
+
+                    logger.info("bbs update time %d" % int(time.time()-tStart))
+
+
+    def run_playbook(self, o, fields):
+        ansible_hash = hashlib.md5(repr(sorted(fields.items()))).hexdigest()
+        quick_update = (o.last_ansible_hash == ansible_hash)
+
+        if ENABLE_QUICK_UPDATE and quick_update:
+            logger.info("quick_update triggered; skipping ansible recipe")
+        else:
+            if o.instance.isolation in ["container", "container_vm"]:
+                super(SyncVCPETenant, self).run_playbook(o, fields, "sync_vcpetenant_new.yaml")
+            else:
+                super(SyncVCPETenant, self).run_playbook(o, fields)
+
+        o.last_ansible_hash = ansible_hash
+
+    def delete_record(self, m):
+        pass
diff --git a/xos/observers/vcpe/steps/sync_vcpetenant.yaml b/xos/observers/vcpe/steps/sync_vcpetenant.yaml
new file mode 100644
index 0000000..fdc21da
--- /dev/null
+++ b/xos/observers/vcpe/steps/sync_vcpetenant.yaml
@@ -0,0 +1,174 @@
+---
+- hosts: {{ instance_name }}
+  gather_facts: False
+  connection: ssh
+  user: ubuntu
+  sudo: yes
+  vars:
+      cdn_enable: {{ cdn_enable }}
+      dnsdemux_ip: {{ dnsdemux_ip }}
+      firewall_enable: {{ firewall_enable }}
+      url_filter_enable: {{ url_filter_enable }}
+      vlan_ids:
+        {% for vlan_id in vlan_ids %}
+        - {{ vlan_id }}
+        {% endfor %}
+      c_tags:
+        {% for c_tag in c_tags %}
+        - {{ c_tag }}
+        {% endfor %}
+      s_tags:
+        {% for s_tag in s_tags %}
+        - {{ s_tag }}
+        {% endfor %}
+      firewall_rules:
+        {% for firewall_rule in firewall_rules.split("\n") %}
+        - {{ firewall_rule }}
+        {% endfor %}
+      cdn_prefixes:
+        {% for prefix in cdn_prefixes %}
+        - {{ prefix }}
+        {% endfor %}
+      bbs_addrs:
+        {% for bbs_addr in bbs_addrs %}
+        - {{ bbs_addr }}
+        {% endfor %}
+      nat_ip: {{ nat_ip }}
+      nat_mac: {{ nat_mac }}
+      lan_ip: {{ lan_ip }}
+      lan_mac: {{ lan_mac }}
+      wan_ip: {{ wan_ip }}
+      wan_mac: {{ wan_mac }}
+      wan_container_mac: {{ wan_container_mac }}
+      wan_next_hop: 10.0.1.253   # FIX ME
+      private_ip: {{ private_ip }}
+      private_mac: {{ private_mac }}
+      hpc_client_ip: {{ hpc_client_ip }}
+      hpc_client_mac: {{ hpc_client_mac }}
+      keystone_tenant_id: {{ keystone_tenant_id }}
+      keystone_user_id: {{ keystone_user_id }}
+      rabbit_user: {{ rabbit_user }}
+      rabbit_password: {{ rabbit_password }}
+      rabbit_host: {{ rabbit_host }}
+      safe_browsing:
+        {% for mac in safe_browsing_macs %}
+        - {{ mac }}
+        {% endfor %}
+
+  tasks:
+{% if full_setup %}
+  - name: Docker repository
+    copy: src=/opt/xos/observers/vcpe/files/docker.list
+      dest=/etc/apt/sources.list.d/docker.list
+
+  - name: Import the repository key
+    apt_key: keyserver=keyserver.ubuntu.com id=36A1D7869245C8950F966E92D8576A8BA88D21E9
+
+  - name: install Docker
+    apt: name=lxc-docker state=present update_cache=yes
+
+  - name: install python-setuptools
+    apt: name=python-setuptools state=present
+
+  - name: install pip
+    easy_install: name=pip
+
+  - name: install docker-py
+    pip: name=docker-py version=0.5.3
+
+  - name: install Pipework
+    get_url: url=https://raw.githubusercontent.com/jpetazzo/pipework/master/pipework
+       dest=/usr/local/bin/pipework
+       mode=0755
+
+  - name: make sure /etc/dnsmasq.d exists
+    file: path=/etc/dnsmasq.d state=directory owner=root group=root
+
+  - name: Disable resolvconf service
+    shell: service resolvconf stop
+    shell: echo manual > /etc/init/resolvconf.override
+    shell: rm -f /etc/resolv.conf
+
+  - name: Install resolv.conf
+    copy: src=/opt/xos/observers/vcpe/files/vm-resolv.conf
+      dest=/etc/resolv.conf
+
+  - name: Verify if vcpe_stats_notifier ([] is to avoid capturing the shell process) cron job is already running
+    shell: pgrep -f [v]cpe_stats_notifier | wc -l
+    register: cron_job_pids_count
+
+#  - name: DEBUG
+#    debug: var=cron_job_pids_count.stdout
+
+  - name: make sure ~/bin exists
+    file: path=~/bin state=directory owner=root group=root
+    when: cron_job_pids_count.stdout == "0"
+
+  - name: Copy cron job to destination
+    copy: src=/opt/xos/observers/vcpe/vcpe_stats_notifier.py
+      dest=~/bin/vcpe_stats_notifier.py
+    when: cron_job_pids_count.stdout == "0"
+
+  - name: install python-kombu
+    apt: name=python-kombu state=present
+    when: cron_job_pids_count.stdout == "0"
+
+  - name: Initiate vcpe_stats_notifier cron job
+    command: python ~/bin/vcpe_stats_notifier.py --keystone_tenant_id={{ keystone_tenant_id }} --keystone_user_id={{ keystone_user_id }} --rabbit_user={{ rabbit_user }} --rabbit_password={{ rabbit_password }} --rabbit_host={{ rabbit_host }} --vcpeservice_rabbit_exchange='vcpeservice'
+    async: 9999999999999999
+    poll: 0
+    when: cron_job_pids_count.stdout == "0"
+{% endif %}
+
+  - name: vCPE upstart
+    template: src=/opt/xos/observers/vcpe/templates/vcpe.conf.j2 dest=/etc/init/vcpe-{{ s_tags[0] }}-{{ c_tags[0] }}.conf
+
+  - name: vCPE startup script
+    template: src=/opt/xos/observers/vcpe/templates/start-vcpe.sh.j2 dest=/usr/local/sbin/start-vcpe-{{ s_tags[0] }}-{{ c_tags[0] }}.sh mode=0755
+    notify:
+#    - restart vcpe
+     - stop vcpe
+     - remove container
+     - start vcpe
+
+  - name: create /etc/vcpe-{{ s_tags[0] }}-{{ c_tags[0] }}/dnsmasq.d
+    file: path=/etc/vcpe-{{ s_tags[0] }}-{{ c_tags[0] }}/dnsmasq.d state=directory owner=root group=root
+
+  - name: vCPE basic dnsmasq config
+    copy: src=/opt/xos/observers/vcpe/files/vcpe.dnsmasq dest=/etc/vcpe-{{ s_tags[0] }}-{{ c_tags[0] }}/dnsmasq.d/vcpe.conf owner=root group=root
+    notify:
+    - restart dnsmasq
+
+  - name: dnsmasq config
+    template: src=/opt/xos/observers/vcpe/templates/dnsmasq_servers.j2 dest=/etc/vcpe-{{ s_tags[0] }}-{{ c_tags[0] }}/dnsmasq.d/servers.conf owner=root group=root
+    notify:
+    - restart dnsmasq
+
+# 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
+
+  - name: Make sure vCPE service is running
+    service: name=vcpe-{{ s_tags[0] }}-{{ c_tags[0] }} state=started
+
+  handlers:
+  # Dnsmasq is automatically restarted in the container
+  - name: restart dnsmasq
+    shell: docker exec vcpe-{{ s_tags[0] }}-{{ c_tags[0] }} killall dnsmasq
+
+  - name: restart vcpe
+    shell: service vcpe-{{ s_tags[0] }}-{{ c_tags[0] }} stop; sleep 1; service vcpe-{{ s_tags[0] }}-{{ c_tags[0] }} start
+
+  - name: stop vcpe
+    service: name=vcpe-{{ s_tags[0] }}-{{ c_tags[0] }} state=stopped
+
+  - name: remove container
+    docker: name=vcpe-{{ s_tags[0] }}-{{ c_tags[0] }} state=absent image=docker-vcpe
+
+  - name: start vcpe
+    service: name=vcpe-{{ s_tags[0] }}-{{ c_tags[0] }} state=started
+
diff --git a/xos/observers/vcpe/steps/sync_vcpetenant_new.yaml b/xos/observers/vcpe/steps/sync_vcpetenant_new.yaml
new file mode 100644
index 0000000..6c7166f
--- /dev/null
+++ b/xos/observers/vcpe/steps/sync_vcpetenant_new.yaml
@@ -0,0 +1,131 @@
+---
+- hosts: {{ instance_name }}
+  gather_facts: False
+  connection: ssh
+  user: {{ username }}
+  sudo: yes
+  vars:
+      container_name: {{ container_name }}
+      cdn_enable: {{ cdn_enable }}
+      dnsdemux_ip: {{ dnsdemux_ip }}
+      firewall_enable: {{ firewall_enable }}
+      url_filter_enable: {{ url_filter_enable }}
+      vlan_ids:
+        {% for vlan_id in vlan_ids %}
+        - {{ vlan_id }}
+        {% endfor %}
+      c_tags:
+        {% for c_tag in c_tags %}
+        - {{ c_tag }}
+        {% endfor %}
+      s_tags:
+        {% for s_tag in s_tags %}
+        - {{ s_tag }}
+        {% endfor %}
+      firewall_rules:
+        {% for firewall_rule in firewall_rules.split("\n") %}
+        - {{ firewall_rule }}
+        {% endfor %}
+      cdn_prefixes:
+        {% for prefix in cdn_prefixes %}
+        - {{ prefix }}
+        {% endfor %}
+      bbs_addrs:
+        {% for bbs_addr in bbs_addrs %}
+        - {{ bbs_addr }}
+        {% endfor %}
+      nat_ip: {{ nat_ip }}
+      nat_mac: {{ nat_mac }}
+      lan_ip: {{ lan_ip }}
+      lan_mac: {{ lan_mac }}
+      wan_ip: {{ wan_ip }}
+      wan_mac: {{ wan_mac }}
+      wan_container_mac: {{ wan_container_mac }}
+      wan_next_hop: 10.0.1.253   # FIX ME
+      private_ip: {{ private_ip }}
+      private_mac: {{ private_mac }}
+      hpc_client_ip: {{ hpc_client_ip }}
+      hpc_client_mac: {{ hpc_client_mac }}
+      keystone_tenant_id: {{ keystone_tenant_id }}
+      keystone_user_id: {{ keystone_user_id }}
+      rabbit_user: {{ rabbit_user }}
+      rabbit_password: {{ rabbit_password }}
+      rabbit_host: {{ rabbit_host }}
+      safe_browsing:
+        {% for mac in safe_browsing_macs %}
+        - {{ mac }}
+        {% endfor %}
+
+  tasks:
+  - name: Verify if vcpe_stats_notifier ([] is to avoid capturing the shell process) cron job is already running
+    shell: pgrep -f [v]cpe_stats_notifier | wc -l
+    register: cron_job_pids_count
+
+#  - name: DEBUG
+#    debug: var=cron_job_pids_count.stdout
+
+  - name: make sure ~/bin exists
+    file: path=~/bin state=directory owner=root group=root
+    when: cron_job_pids_count.stdout == "0"
+
+  - name: Copy cron job to destination
+    copy: src=/opt/xos/observers/vcpe/vcpe_stats_notifier.py
+      dest=~/bin/vcpe_stats_notifier.py
+    when: cron_job_pids_count.stdout == "0"
+
+  - name: install python-kombu
+    apt: name=python-kombu state=present
+    when: cron_job_pids_count.stdout == "0"
+
+  - name: Initiate vcpe_stats_notifier cron job
+    command: python ~/bin/vcpe_stats_notifier.py --keystone_tenant_id={{ keystone_tenant_id }} --keystone_user_id={{ keystone_user_id }} --rabbit_user={{ rabbit_user }} --rabbit_password={{ rabbit_password }} --rabbit_host={{ rabbit_host }} --vcpeservice_rabbit_exchange='vcpeservice'
+    async: 9999999999999999
+    poll: 0
+    when: cron_job_pids_count.stdout == "0"
+
+  - name: vCPE basic dnsmasq config
+    copy: src=/opt/xos/observers/vcpe/files/vcpe.dnsmasq dest=/var/container_volumes/{{ container_name }}/etc/dnsmasq.d/vcpe.conf owner=root group=root
+    notify:
+    - restart dnsmasq
+
+  - name: dnsmasq config
+    template: src=/opt/xos/observers/vcpe/templates/dnsmasq_servers.j2 dest=/var/container_volumes/{{ container_name }}/etc/dnsmasq.d/servers.conf owner=root group=root
+    notify:
+    - restart dnsmasq
+
+  - name: create directory for "safe" config
+    file: path=/var/container_volumes/{{ container_name }}/etc/dnsmasq.d/safe state=directory
+
+  - name: dnsmasq "safe" config
+    template: src=/opt/xos/observers/vcpe/templates/dnsmasq_safe_servers.j2 dest=/var/container_volumes/{{ container_name }}/etc/dnsmasq.d/safe/servers.conf owner=root group=root
+    notify:
+    - restart dnsmasq
+
+  - name: copy base ufw files
+    synchronize: src=/opt/xos/observers/vcpe/files/etc/ufw/ dest=/var/container_volumes/{{ container_name }}/etc/ufw/
+    notify:
+    - reload ufw
+
+  - name: redirection rules for safe DNS
+    template: src=/opt/xos/observers/vcpe/templates/before.rules.j2 dest=/var/container_volumes/{{ container_name }}/etc/ufw/before.rules owner=root group=root
+    notify:
+    - reload ufw
+
+  - name: base ufw setup uses /etc/rc.local
+    copy: src=/opt/xos/observers/vcpe/files/etc/rc.local dest=/var/container_volumes/{{ container_name }}/etc/ owner=root group=root
+    notify:
+    - copy in /etc/rc.local
+
+  handlers:
+  # Dnsmasq is automatically restarted in the container
+  - name: restart dnsmasq
+    shell: docker exec {{ container_name }} /usr/bin/killall dnsmasq
+
+  - name: reload ufw
+    shell: docker exec {{ container_name }} bash -c "/sbin/iptables -t nat -F PREROUTING; /usr/sbin/ufw reload"
+
+  # Use docker cp instead of single-file volume
+  # The reason is that changes to external file volume don't show up inside the container
+  # Probably Ansible deletes and then recreates the external file, and container has old version
+  - name: copy in /etc/rc.local
+    shell: docker cp /var/container_volumes/{{ container_name }}/etc/rc.local {{ container_name }}:/etc/
diff --git a/xos/observers/vcpe/steps/test.yaml b/xos/observers/vcpe/steps/test.yaml
new file mode 100644
index 0000000..fc8251d
--- /dev/null
+++ b/xos/observers/vcpe/steps/test.yaml
@@ -0,0 +1,7 @@
+---
+- hosts: {{ instance_name }}
+  connection: ssh
+  user: ubuntu
+  tasks:
+    - name: foobar
+      shell: echo foo > /tmp/foobar
diff --git a/xos/observers/vcpe/stop.sh b/xos/observers/vcpe/stop.sh
new file mode 100755
index 0000000..e90e16c
--- /dev/null
+++ b/xos/observers/vcpe/stop.sh
@@ -0,0 +1 @@
+pkill -9 -f vcpe-observer.py
diff --git a/xos/observers/vcpe/supervisor/vcpe-observer.conf b/xos/observers/vcpe/supervisor/vcpe-observer.conf
new file mode 100644
index 0000000..27d2796
--- /dev/null
+++ b/xos/observers/vcpe/supervisor/vcpe-observer.conf
@@ -0,0 +1,2 @@
+[program:vcpe-observer]
+command=python /opt/xos/observers/vcpe/vcpe-observer.py -C /opt/xos/observers/vcpe/vcpe_observer_config
diff --git a/xos/observers/vcpe/templates/before.rules.j2 b/xos/observers/vcpe/templates/before.rules.j2
new file mode 100644
index 0000000..e6f7d4a
--- /dev/null
+++ b/xos/observers/vcpe/templates/before.rules.j2
@@ -0,0 +1,97 @@
+#
+# rules.before
+#
+# Rules that should be run before the ufw command line added rules. Custom
+# rules should be added to one of these chains:
+#   ufw-before-input
+#   ufw-before-output
+#   ufw-before-forward
+#
+
+# nat Table rules
+*nat
+:POSTROUTING ACCEPT [0:0]
+
+# Forward traffic from eth1 through eth0.
+-A POSTROUTING -o eth0 -j MASQUERADE
+
+# Set up NAT for CDN services
+-A POSTROUTING -o eth2 -j MASQUERADE
+
+# DNS safe browsing
+{% if safe_browsing %}
+{% for mac in safe_browsing %}
+-A PREROUTING -i eth1 -m mac --mac-source {{ mac }} -p udp --dport 53 -j REDIRECT --to-port 5353
+-A PREROUTING -i eth1 -m mac --mac-source {{ mac }} -p tcp --dport 53 -j REDIRECT --to-port 5353
+{% endfor %}
+{% endif %}
+
+# don't delete the 'COMMIT' line or these nat table rules won't be processed
+COMMIT
+
+# Don't delete these required lines, otherwise there will be errors
+*filter
+:ufw-before-input - [0:0]
+:ufw-before-output - [0:0]
+:ufw-before-forward - [0:0]
+:ufw-not-local - [0:0]
+# End required lines
+
+# allow all on loopback
+-A ufw-before-input -i lo -j ACCEPT
+-A ufw-before-output -o lo -j ACCEPT
+
+# quickly process packets for which we already have a connection
+-A ufw-before-input -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
+-A ufw-before-output -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
+-A ufw-before-forward -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
+
+# drop INVALID packets (logs these in loglevel medium and higher)
+-A ufw-before-input -m conntrack --ctstate INVALID -j ufw-logging-deny
+-A ufw-before-input -m conntrack --ctstate INVALID -j DROP
+
+# ok icmp codes for INPUT
+-A ufw-before-input -p icmp --icmp-type destination-unreachable -j ACCEPT
+-A ufw-before-input -p icmp --icmp-type source-quench -j ACCEPT
+-A ufw-before-input -p icmp --icmp-type time-exceeded -j ACCEPT
+-A ufw-before-input -p icmp --icmp-type parameter-problem -j ACCEPT
+-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT
+
+# ok icmp code for FORWARD
+-A ufw-before-forward -p icmp --icmp-type destination-unreachable -j ACCEPT
+-A ufw-before-forward -p icmp --icmp-type source-quench -j ACCEPT
+-A ufw-before-forward -p icmp --icmp-type time-exceeded -j ACCEPT
+-A ufw-before-forward -p icmp --icmp-type parameter-problem -j ACCEPT
+-A ufw-before-forward -p icmp --icmp-type echo-request -j ACCEPT
+
+# allow dhcp client to work
+-A ufw-before-input -p udp --sport 67 --dport 68 -j ACCEPT
+
+#
+# ufw-not-local
+#
+-A ufw-before-input -j ufw-not-local
+
+# if LOCAL, RETURN
+-A ufw-not-local -m addrtype --dst-type LOCAL -j RETURN
+
+# if MULTICAST, RETURN
+-A ufw-not-local -m addrtype --dst-type MULTICAST -j RETURN
+
+# if BROADCAST, RETURN
+-A ufw-not-local -m addrtype --dst-type BROADCAST -j RETURN
+
+# all other non-local packets are dropped
+-A ufw-not-local -m limit --limit 3/min --limit-burst 10 -j ufw-logging-deny
+-A ufw-not-local -j DROP
+
+# allow MULTICAST mDNS for service discovery (be sure the MULTICAST line above
+# is uncommented)
+-A ufw-before-input -p udp -d 224.0.0.251 --dport 5353 -j ACCEPT
+
+# allow MULTICAST UPnP for service discovery (be sure the MULTICAST line above
+# is uncommented)
+-A ufw-before-input -p udp -d 239.255.255.250 --dport 1900 -j ACCEPT
+
+# don't delete the 'COMMIT' line or these rules won't be processed
+COMMIT
diff --git a/xos/observers/vcpe/templates/dnsmasq_safe_servers.j2 b/xos/observers/vcpe/templates/dnsmasq_safe_servers.j2
new file mode 100644
index 0000000..2f93777
--- /dev/null
+++ b/xos/observers/vcpe/templates/dnsmasq_safe_servers.j2
@@ -0,0 +1,14 @@
+# This file autogenerated by vCPE observer
+# It contains a list of DNS servers for dnsmasq to use.
+no-resolv
+
+{% if cdn_enable %}
+# CDN
+{% for prefix in cdn_prefixes %}
+server=/{{ prefix }}/{{ dnsdemux_ip }}
+{% endfor %}
+{% endif %}
+
+# use OpenDNS service
+server=208.67.222.123
+server=208.67.220.123
diff --git a/xos/observers/vcpe/templates/dnsmasq_servers.j2 b/xos/observers/vcpe/templates/dnsmasq_servers.j2
new file mode 100644
index 0000000..c89c762
--- /dev/null
+++ b/xos/observers/vcpe/templates/dnsmasq_servers.j2
@@ -0,0 +1,14 @@
+# This file autogenerated by vCPE observer
+# It contains a list of DNS servers for dnsmasq to use.
+no-resolv
+
+{% if cdn_enable %}
+# CDN
+{% for prefix in cdn_prefixes %}
+server=/{{ prefix }}/{{ dnsdemux_ip }}
+{% endfor %}
+{% endif %}
+
+# use google's DNS service
+server=8.8.8.8
+server=8.8.4.4
diff --git a/xos/observers/vcpe/templates/firewall_sample.j2 b/xos/observers/vcpe/templates/firewall_sample.j2
new file mode 100644
index 0000000..ce85e68
--- /dev/null
+++ b/xos/observers/vcpe/templates/firewall_sample.j2
@@ -0,0 +1,5 @@
+firewall_enable = {{ firewall_enable }}
+
+{% for firewall_rule in firewall_rules %}
+{{ firewall_rule }}
+{% endfor %}
diff --git a/xos/observers/vcpe/templates/start-vcpe.sh.j2 b/xos/observers/vcpe/templates/start-vcpe.sh.j2
new file mode 100755
index 0000000..c4128f3
--- /dev/null
+++ b/xos/observers/vcpe/templates/start-vcpe.sh.j2
@@ -0,0 +1,50 @@
+#!/bin/bash
+
+function mac_to_iface {
+    MAC=$1
+    ifconfig|grep $MAC| awk '{print $1}'|grep -v '\.'
+}
+
+iptables -L > /dev/null
+ip6tables -L > /dev/null
+
+STAG={{ s_tags[0] }}
+CTAG={{ c_tags[0] }}
+VCPE=vcpe-$STAG-$CTAG
+
+docker inspect $VCPE > /dev/null 2>&1
+if [ "$?" == 1 ]
+then
+    docker pull andybavier/docker-vcpe
+    docker run -d --name=$VCPE --privileged=true --net=none -v /etc/$VCPE/dnsmasq.d:/etc/dnsmasq.d andybavier/docker-vcpe
+else
+    docker start $VCPE
+fi
+
+# Set up networking via pipework
+WAN_IFACE=$( mac_to_iface {{ wan_mac }} )
+docker exec $VCPE ifconfig eth0 >> /dev/null || pipework $WAN_IFACE -i eth0 $VCPE {{ wan_ip }}/24@{{ wan_next_hop }} {{ wan_container_mac }}
+
+# LAN_IFACE=$( mac_to_iface {{ lan_mac }} )
+# Need to encapsulate VLAN traffic so that Neutron doesn't eat it
+# Assumes that br-lan has been set up appropriately by a previous step
+LAN_IFACE=br-lan
+ifconfig $LAN_IFACE >> /dev/null
+if [ "$?" == 0 ]
+then
+    ifconfig $LAN_IFACE.$STAG >> /dev/null || ip link add link $LAN_IFACE name $LAN_IFACE.$STAG type vlan id $STAG
+    ifconfig $LAN_IFACE.$STAG up
+    docker exec $VCPE ifconfig eth1 >> /dev/null || pipework $LAN_IFACE.$STAG -i eth1 $VCPE 192.168.0.1/24 @$CTAG
+fi
+
+#HPC_IFACE=$( mac_to_iface {{ hpc_client_mac }} )
+#docker exec $VCPE ifconfig eth2 >> /dev/null || pipework $HPC_IFACE -i eth2 $VCPE {{ hpc_client_ip }}/24
+
+# Make sure VM's eth0 (hpc_client) has no IP address
+#ifconfig $HPC_IFACE 0.0.0.0
+
+# Now can start up dnsmasq
+docker exec $VCPE service dnsmasq start
+
+# Attach to container
+docker start -a $VCPE
diff --git a/xos/observers/vcpe/templates/vcpe.conf.j2 b/xos/observers/vcpe/templates/vcpe.conf.j2
new file mode 100644
index 0000000..fa7885e
--- /dev/null
+++ b/xos/observers/vcpe/templates/vcpe.conf.j2
@@ -0,0 +1,10 @@
+# Upstart script for vCPE
+description "vCPE container"
+author "andy@onlab.us"
+start on filesystem and started docker
+stop on runlevel [!2345]
+respawn
+
+script
+  /usr/local/sbin/start-vcpe-{{ s_tags[0] }}-{{ c_tags[0] }}.sh
+end script
diff --git a/xos/observers/vcpe/templates/vlan_sample.j2 b/xos/observers/vcpe/templates/vlan_sample.j2
new file mode 100644
index 0000000..a26c840
--- /dev/null
+++ b/xos/observers/vcpe/templates/vlan_sample.j2
@@ -0,0 +1,5 @@
+# below is a list of all vlan_ids associated with this vcpe
+
+{% for vlan_id in vlan_ids %}
+{{ vlan_id }}
+{% endfor %}
diff --git a/xos/observers/vcpe/vcpe-observer.py b/xos/observers/vcpe/vcpe-observer.py
new file mode 100755
index 0000000..d6a71ff
--- /dev/null
+++ b/xos/observers/vcpe/vcpe-observer.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+
+# This imports and runs ../../xos-observer.py
+
+import importlib
+import os
+import sys
+observer_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"../..")
+sys.path.append(observer_path)
+mod = importlib.import_module("xos-observer")
+mod.main()
diff --git a/xos/observers/vcpe/vcpe_observer_config b/xos/observers/vcpe/vcpe_observer_config
new file mode 100644
index 0000000..d2c9239
--- /dev/null
+++ b/xos/observers/vcpe/vcpe_observer_config
@@ -0,0 +1,42 @@
+
+[plc]
+name=plc
+deployment=VICCI
+
+[db]
+name=xos
+user=postgres
+password=password
+host=localhost
+port=5432
+
+[api]
+host=128.112.171.237
+port=8000
+ssl_key=None
+ssl_cert=None
+ca_ssl_cert=None
+ratelimit_enabled=0
+omf_enabled=0
+mail_support_address=support@localhost
+nova_enabled=True
+
+[observer]
+name=vcpe
+dependency_graph=/opt/xos/observers/vcpe/model-deps
+steps_dir=/opt/xos/observers/vcpe/steps
+sys_dir=/opt/xos/observers/vcpe/sys
+deleters_dir=/opt/xos/observers/vcpe/deleters
+log_file=console
+#/var/log/hpc.log
+driver=None
+pretend=False
+backoff_disabled=True
+save_ansible_output=True
+# set proxy_ssh to false on cloudlab
+proxy_ssh=False
+full_setup=True
+
+[feefie]
+client_id='vicci_dev_central'
+user_id='pl'
diff --git a/xos/observers/vcpe/vcpe_stats_notifier.py b/xos/observers/vcpe/vcpe_stats_notifier.py
new file mode 100644
index 0000000..d726e3c
--- /dev/null
+++ b/xos/observers/vcpe/vcpe_stats_notifier.py
@@ -0,0 +1,268 @@
+import six
+import uuid
+import datetime
+from kombu.connection import BrokerConnection
+from kombu.messaging import Exchange, Queue, Consumer, Producer
+import subprocess
+import re
+import time, threading
+import sys, getopt
+import logging
+
+
+logfile = "vcpe_stats_notifier.log"
+level=logging.INFO
+logger=logging.getLogger('vcpe_stats_notifier')
+logger.setLevel(level)
+# create formatter
+formatter = logging.Formatter("%(asctime)s;%(levelname)s;%(message)s")
+handler=logging.handlers.RotatingFileHandler(logfile,maxBytes=1000000, backupCount=1)
+# add formatter to handler
+handler.setFormatter(formatter)
+logger.addHandler(handler)
+
+def extract_dns_stats_from_all_vcpes():
+    p = subprocess.Popen('docker ps', shell=True, stdout=subprocess.PIPE) 
+    firstline = True
+    dockercontainers = {}
+    while True:
+        out = p.stdout.readline()
+        if out == '' and p.poll() != None:
+            break
+        if out != '':
+            if firstline is True:
+                firstline = False
+            else:
+                fields = out.split()
+                container_fields = {}
+                container_fields['id'] = fields[0]
+                dockercontainers[fields[-1]] = container_fields
+    for k,v in dockercontainers.iteritems():
+         cmd = 'docker exec ' + v['id'] + ' killall -10 dnsmasq'
+         p = subprocess.Popen (cmd, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+         (output, error) = p.communicate()
+         if error:
+             logger.error("killall dnsmasq command failed with error = %s",error)
+             continue
+         cmd = 'docker exec ' + v['id'] + ' tail -7 /var/log/syslog'
+         p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
+         (output, error) = p.communicate()
+         if error:
+             logger.error("tail on dnsmasq log command failed with error = %s",error)
+             continue
+         log_list = output.splitlines()
+         i = 0
+         while i < len(log_list):
+             m = re.search('(?<=:\scache size\s)(\S*)(?=,\s),\s(\S*)(?=/)/(\S*)(?=\scache insertions re-used unexpired cache entries)', log_list[i])
+             if m == None:
+                 i = i+1
+                 continue;
+             v['cache_size'] = m.group(1)
+             v['replaced_unexpired_entries'] = m.group(2)
+             v['total_inserted_entries'] = m.group(3)
+             i = i+1
+             m = re.search('(?<=:\squeries forwarded\s)(\S*)(?=,),\squeries answered locally\s(\S*)(?=$)', log_list[i])
+             v['queries_forwarded'] = m.group(1)
+             v['queries_answered_locally'] = m.group(2)
+             break;
+         i = i+2
+         v['server_stats'] = []
+         while i < len(log_list):
+             m = re.search('(?<=:\sserver\s)(\S*)(?=#)#\d*:\squeries sent\s(\S*)(?=,),\sretried or failed\s(\S*)(?=$)', log_list[i])
+             if m == None:
+                 i = i+1
+                 continue
+             dns_server = {}
+             dns_server['id'] = m.group(1)
+             dns_server['queries_sent'] = m.group(2)
+             dns_server['queries_failed'] = m.group(3)
+             v['server_stats'].append(dns_server)
+             i = i+1
+    return dockercontainers
+
+
+keystone_tenant_id='3a397e70f64e4e40b69b6266c634d9d0'
+keystone_user_id='1e3ce043029547f1a61c1996d1a531a2'
+rabbit_user='openstack'
+rabbit_password='80608318c273f348a7c3'
+rabbit_host='10.11.10.1'
+vcpeservice_rabbit_exchange='vcpeservice'
+cpe_publisher_id='vcpe_publisher'
+
+producer = None
+
+def setup_rabbit_mq_channel():
+     global producer
+     global rabbit_user, rabbit_password, rabbit_host, vcpeservice_rabbit_exchange,cpe_publisher_id
+     vcpeservice_exchange = Exchange(vcpeservice_rabbit_exchange, "topic", durable=False)
+     # connections/channels
+     connection = BrokerConnection(rabbit_host, rabbit_user, rabbit_password)
+     logger.info('Connection to RabbitMQ server successful')
+     channel = connection.channel()
+     # produce
+     producer = Producer(channel, exchange=vcpeservice_exchange, routing_key='notifications.info')
+     p = subprocess.Popen('hostname', shell=True, stdout=subprocess.PIPE)
+     (hostname, error) = p.communicate()
+     cpe_publisher_id = cpe_publisher_id + '_on_' + hostname
+     logger.info('cpe_publisher_id=%s',cpe_publisher_id)
+
+def publish_cpe_stats():
+     global producer
+     global keystone_tenant_id, keystone_user_id, cpe_publisher_id
+
+     logger.debug('publish_cpe_stats invoked')
+
+     cpe_container_stats = extract_dns_stats_from_all_vcpes()
+
+     for k,v in cpe_container_stats.iteritems():
+          msg = {'event_type': 'vcpe', 
+                 'message_id':six.text_type(uuid.uuid4()),
+                 'publisher_id': cpe_publisher_id,
+                 'timestamp':datetime.datetime.now().isoformat(),
+                 'priority':'INFO',
+                 'payload': {'vcpe_id':k, 
+                             'user_id':keystone_user_id, 
+                             'tenant_id':keystone_tenant_id 
+                            }
+                }
+          producer.publish(msg)
+          logger.debug('Publishing vcpe event: %s', msg)
+
+          if 'cache_size' in v:
+               msg = {'event_type': 'vcpe.dns.cache.size', 
+                      'message_id':six.text_type(uuid.uuid4()),
+                      'publisher_id': cpe_publisher_id,
+                      'timestamp':datetime.datetime.now().isoformat(),
+                      'priority':'INFO',
+                      'payload': {'vcpe_id':k, 
+                                  'user_id':keystone_user_id,
+                                  'tenant_id':keystone_tenant_id, 
+                                  'cache_size':v['cache_size'] 
+                                 }
+                     }
+               producer.publish(msg)
+               logger.debug('Publishing vcpe.dns.cache.size event: %s', msg)
+
+          if 'total_inserted_entries' in v:
+               msg = {'event_type': 'vcpe.dns.total_inserted_entries', 
+                      'message_id':six.text_type(uuid.uuid4()),
+                      'publisher_id': cpe_publisher_id,
+                      'timestamp':datetime.datetime.now().isoformat(),
+                      'priority':'INFO',
+                      'payload': {'vcpe_id':k, 
+                                  'user_id':keystone_user_id,
+                                  'tenant_id':keystone_tenant_id, 
+                                  'total_inserted_entries':v['total_inserted_entries'] 
+                                 }
+                     }
+               producer.publish(msg)
+               logger.debug('Publishing vcpe.dns.total_inserted_entries event: %s', msg)
+
+          if 'replaced_unexpired_entries' in v:
+               msg = {'event_type': 'vcpe.dns.replaced_unexpired_entries', 
+                      'message_id':six.text_type(uuid.uuid4()),
+                      'publisher_id': cpe_publisher_id,
+                      'timestamp':datetime.datetime.now().isoformat(),
+                      'priority':'INFO',
+                      'payload': {'vcpe_id':k, 
+                                  'user_id':keystone_user_id,
+                                  'tenant_id':keystone_tenant_id, 
+                                  'replaced_unexpired_entries':v['replaced_unexpired_entries'] 
+                                 }
+                     }
+               producer.publish(msg)
+               logger.debug('Publishing vcpe.dns.replaced_unexpired_entries event: %s', msg)
+
+          if 'queries_forwarded' in v:
+               msg = {'event_type': 'vcpe.dns.queries_forwarded', 
+                      'message_id':six.text_type(uuid.uuid4()),
+                      'publisher_id': cpe_publisher_id,
+                      'timestamp':datetime.datetime.now().isoformat(),
+                      'priority':'INFO',
+                      'payload': {'vcpe_id':k, 
+                                  'user_id':keystone_user_id,
+                                  'tenant_id':keystone_tenant_id, 
+                                  'queries_forwarded':v['queries_forwarded'] 
+                                 }
+                     }
+               producer.publish(msg)
+               logger.debug('Publishing vcpe.dns.queries_forwarded event: %s', msg)
+
+          if 'queries_answered_locally' in v:
+               msg = {'event_type': 'vcpe.dns.queries_answered_locally', 
+                      'message_id':six.text_type(uuid.uuid4()),
+                      'publisher_id': cpe_publisher_id,
+                      'timestamp':datetime.datetime.now().isoformat(),
+                      'priority':'INFO',
+                      'payload': {'vcpe_id':k, 
+                                  'user_id':keystone_user_id,
+                                  'tenant_id':keystone_tenant_id, 
+                                  'queries_answered_locally':v['queries_answered_locally'] 
+                                 }
+                     }
+               producer.publish(msg)
+               logger.debug('Publishing vcpe.dns.queries_answered_locally event: %s', msg)
+
+          if 'server_stats' in v:
+               for server in v['server_stats']:
+                   msg = {'event_type': 'vcpe.dns.server.queries_sent', 
+                          'message_id':six.text_type(uuid.uuid4()),
+                          'publisher_id': cpe_publisher_id,
+                          'timestamp':datetime.datetime.now().isoformat(),
+                          'priority':'INFO',
+                          'payload': {'vcpe_id':k, 
+                                      'user_id':keystone_user_id,
+                                      'tenant_id':keystone_tenant_id, 
+                                      'upstream_server':server['id'],
+                                      'queries_sent':server['queries_sent'] 
+                                     }
+                         }
+                   producer.publish(msg)
+                   logger.debug('Publishing vcpe.dns.server.queries_sent event: %s', msg)
+
+                   msg = {'event_type': 'vcpe.dns.server.queries_failed', 
+                          'message_id':six.text_type(uuid.uuid4()),
+                          'publisher_id': cpe_publisher_id,
+                          'timestamp':datetime.datetime.now().isoformat(),
+                          'priority':'INFO',
+                          'payload': {'vcpe_id':k, 
+                                      'user_id':keystone_user_id,
+                                      'tenant_id':keystone_tenant_id, 
+                                      'upstream_server':server['id'],
+                                      'queries_failed':server['queries_failed'] 
+                                     }
+                         }
+                   producer.publish(msg)
+                   logger.debug('Publishing vcpe.dns.server.queries_failed event: %s', msg)
+
+def periodic_publish():
+     publish_cpe_stats()
+     #Publish every 5minutes
+     threading.Timer(300, periodic_publish).start()
+
+def main(argv):
+   global keystone_tenant_id, keystone_user_id, rabbit_user, rabbit_password, rabbit_host, vcpeservice_rabbit_exchange
+   try:
+      opts, args = getopt.getopt(argv,"",["keystone_tenant_id=","keystone_user_id=","rabbit_host=","rabbit_user=","rabbit_password=","vcpeservice_rabbit_exchange="])
+   except getopt.GetoptError:
+      print 'vcpe_stats_notifier.py keystone_tenant_id=<keystone_tenant_id> keystone_user_id=<keystone_user_id> rabbit_host=<IP addr> rabbit_user=<user> rabbit_password=<password> vcpeservice_rabbit_exchange=<exchange name>'
+      sys.exit(2)
+   for opt, arg in opts:
+      if opt in ("--keystone_tenant_id"):
+         keystone_tenant_id = arg
+      elif opt in ("--keystone_user_id"):
+         keystone_user_id = arg
+      elif opt in ("--rabbit_user"):
+         rabbit_user = arg
+      elif opt in ("--rabbit_password"):
+         rabbit_password = arg
+      elif opt in ("--rabbit_host"):
+         rabbit_host = arg
+      elif opt in ("--vcpeservice_rabbit_exchange"):
+         vcpeservice_rabbit_exchange = arg
+   logger.info("vcpe_stats_notifier args:keystone_tenant_id=%s keystone_user_id=%s rabbit_user=%s rabbit_host=%s vcpeservice_rabbit_exchange=%s",keystone_tenant_id,keystone_user_id,rabbit_user,rabbit_host,vcpeservice_rabbit_exchange)
+   setup_rabbit_mq_channel()
+   periodic_publish()
+
+if __name__ == "__main__":
+   main(sys.argv[1:])
diff --git a/xos/observers/vtn/model-deps b/xos/observers/vtn/model-deps
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/xos/observers/vtn/model-deps
@@ -0,0 +1 @@
+{}
diff --git a/xos/observers/vtn/run.sh b/xos/observers/vtn/run.sh
new file mode 100755
index 0000000..d7e5025
--- /dev/null
+++ b/xos/observers/vtn/run.sh
@@ -0,0 +1,2 @@
+export XOS_DIR=/opt/xos
+python vtn-observer.py  -C $XOS_DIR/observers/vtn/vtn_observer_config
diff --git a/xos/observers/vtn/start.sh b/xos/observers/vtn/start.sh
new file mode 100755
index 0000000..168074d
--- /dev/null
+++ b/xos/observers/vtn/start.sh
@@ -0,0 +1,2 @@
+export XOS_DIR=/opt/xos
+nohup python vtn-observer.py  -C $XOS_DIR/observers/vtn/vtn_observer_config > /dev/null 2>&1 &
diff --git a/xos/observers/vtn/steps/sync_tenant.py b/xos/observers/vtn/steps/sync_tenant.py
new file mode 100644
index 0000000..77e6a93
--- /dev/null
+++ b/xos/observers/vtn/steps/sync_tenant.py
@@ -0,0 +1,95 @@
+import os
+import requests
+import socket
+import sys
+import base64
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+from core.models.service import COARSE_KIND
+from services.cord.models import Tenant
+from util.logger import Logger, logging
+from requests.auth import HTTPBasicAuth
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+logger = Logger(level=logging.INFO)
+
+# XXX should save and load this
+glo_saved_vtn_maps = []
+
+class SyncTenant(SyncStep):
+    provides=[Tenant]
+    observes=Tenant
+    requested_interval=0
+
+    def __init__(self, **args):
+        SyncStep.__init__(self, **args)
+
+    def get_vtn_onos_service(self):
+#        vtn_tenant = Tenant.objects.filter(name="VTN_ONOS_app")   # XXX fixme - hardcoded
+#        if not vtn_tenant:
+#            raise "No VTN Onos App found"
+#        vtn_tenant = vtn_tenant[0]
+#
+#        vtn_service = vtn_tenant.provider_service
+        vtn_service = Service.objects.filter(name="service_ONOS_VTN")  # XXX fixme - harcoded
+        if not vtn_service:
+            raise "No VTN Onos Service"
+
+        return vtn_service[0]
+
+    def get_vtn_addr(self):
+        vtn_service = self.get_vtn_onos_service()
+
+        if not vtn_service.slices.exists():
+            raise "VTN Service has no slices"
+
+        vtn_slice = vtn_service.slices.all()[0]
+
+        if not vtn_slice.instances.exists():
+            raise "VTN Slice has no instances"
+
+        vtn_instance = vtn_slice.instances.all()[0]
+
+        return vtn_instance.node.name
+
+    def call(self, **args):
+        global glo_saved_vtn_maps
+
+        logger.info("sync'ing vtn services")
+
+        vtn_maps = []
+        for service in Service.objects.all():
+           for id in service.get_vtn_src_ids():
+               dependencies = service.get_vtn_dependencies_ids()
+               if dependencies:
+                   for dependency in dependencies:
+                       vtn_maps.append( (id, dependency) )
+
+        for vtn_map in vtn_maps:
+            if not (vtn_map in glo_saved_vtn_maps):
+                # call vtn rest api to add map
+                url = "http://" + self.get_vtn_addr() + ":8181/onos/cordvtn/service-dependency/%s/%s" % (vtn_map[0], vtn_map[1])
+
+                print "POST %s" % url
+                r = requests.post(url, auth=HTTPBasicAuth('karaf', 'karaf') )    # XXX fixme - hardcoded auth
+                if (r.status_code != 200):
+                    raise Exception("Received error from vtn service (%d)" % r.status_code)
+
+        for vtn_map in glo_saved_vtn_maps:
+            if not vtn_map in vtn_maps:
+                # call vtn rest api to delete map
+                url = "http://" + self.get_vtn_addr() + ":8181/onos/cordvtn/service-dependency/%s/%s" % (vtn_map[0],vtn_map[1])
+
+                print "DELETE %s" % url
+                r = requests.delete(url, auth=HTTPBasicAuth('karaf', 'karaf') )    # XXX fixme - hardcoded auth
+                if (r.status_code != 200):
+                    raise Exception("Received error from vtn service (%d)" % r.status_code)
+
+        glo_saved_vtn_maps = vtn_maps
+        # TODO: save this
+
diff --git a/xos/observers/vtn/stop.sh b/xos/observers/vtn/stop.sh
new file mode 100755
index 0000000..261f029
--- /dev/null
+++ b/xos/observers/vtn/stop.sh
@@ -0,0 +1 @@
+pkill -9 -f vtn-observer.py
diff --git a/xos/observers/vtn/supervisor/vtn-observer.conf b/xos/observers/vtn/supervisor/vtn-observer.conf
new file mode 100644
index 0000000..714afa7
--- /dev/null
+++ b/xos/observers/vtn/supervisor/vtn-observer.conf
@@ -0,0 +1,2 @@
+[program:vtn-observer]
+command=python /opt/xos/observers/vbng/vtn-observer.py -C /opt/xos/observers/vbng/vtn_observer_config
diff --git a/xos/observers/vtn/vtn-observer.py b/xos/observers/vtn/vtn-observer.py
new file mode 100755
index 0000000..d6a71ff
--- /dev/null
+++ b/xos/observers/vtn/vtn-observer.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+
+# This imports and runs ../../xos-observer.py
+
+import importlib
+import os
+import sys
+observer_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"../..")
+sys.path.append(observer_path)
+mod = importlib.import_module("xos-observer")
+mod.main()
diff --git a/xos/observers/vtn/vtn_observer_config b/xos/observers/vtn/vtn_observer_config
new file mode 100644
index 0000000..19e9a39
--- /dev/null
+++ b/xos/observers/vtn/vtn_observer_config
@@ -0,0 +1,38 @@
+
+[plc]
+name=plc
+deployment=VICCI
+
+[db]
+name=xos
+user=postgres
+password=password
+host=localhost
+port=5432
+
+[api]
+host=128.112.171.237
+port=8000
+ssl_key=None
+ssl_cert=None
+ca_ssl_cert=None
+ratelimit_enabled=0
+omf_enabled=0
+mail_support_address=support@localhost
+nova_enabled=True
+
+[observer]
+name=vtn
+dependency_graph=/opt/xos/observers/vtn/model-deps
+steps_dir=/opt/xos/observers/vtn/steps
+sys_dir=/opt/xos/observers/vtn/sys
+deleters_dir=/opt/xos/observers/vtn/deleters
+log_file=console
+#/var/log/hpc.log
+driver=None
+pretend=False
+backoff_disabled=True
+
+[feefie]
+client_id='vicci_dev_central'
+user_id='pl'