move vsg service from XOS
diff --git a/xos/synchronizer/broadbandshield.py b/xos/synchronizer/broadbandshield.py
new file mode 100644
index 0000000..dd2f00b
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/files/docker.list b/xos/synchronizer/files/docker.list
new file mode 100644
index 0000000..0ee9ae0
--- /dev/null
+++ b/xos/synchronizer/files/docker.list
@@ -0,0 +1 @@
+deb https://get.docker.com/ubuntu docker main
diff --git a/xos/synchronizer/files/etc/rc.local b/xos/synchronizer/files/etc/rc.local
new file mode 100755
index 0000000..2c7588f
--- /dev/null
+++ b/xos/synchronizer/files/etc/rc.local
@@ -0,0 +1,23 @@
+#!/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
+
+BWLIMIT=/usr/local/sbin/bwlimit.sh
+[ -e $BWLIMIT ] && $BWLIMIT start || true
+
+exit 0
diff --git a/xos/synchronizer/files/etc/service/message/run b/xos/synchronizer/files/etc/service/message/run
new file mode 100755
index 0000000..7b587d8
--- /dev/null
+++ b/xos/synchronizer/files/etc/service/message/run
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+
+import BaseHTTPServer
+
+
+class HTTPHandlerOne(BaseHTTPServer.BaseHTTPRequestHandler):
+    def do_GET(self):
+        with open('./message.html', 'r') as msgfile:
+            message = msgfile.read()
+        self.wfile.write(message)
+
+
+def run(server_class=BaseHTTPServer.HTTPServer,
+        handler_class=BaseHTTPServer.BaseHTTPRequestHandler):
+    server_address = ('192.168.0.1', 8000)
+    httpd = server_class(server_address, handler_class)
+    httpd.serve_forever()
+
+run(handler_class=HTTPHandlerOne)
diff --git a/xos/synchronizer/files/etc/ufw/after.init b/xos/synchronizer/files/etc/ufw/after.init
new file mode 100644
index 0000000..e89217d
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/files/etc/ufw/after.rules b/xos/synchronizer/files/etc/ufw/after.rules
new file mode 100644
index 0000000..0d6c646
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/files/etc/ufw/after6.rules b/xos/synchronizer/files/etc/ufw/after6.rules
new file mode 100644
index 0000000..0d99672
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/files/etc/ufw/applications.d/openssh-server b/xos/synchronizer/files/etc/ufw/applications.d/openssh-server
new file mode 100644
index 0000000..9bbe906
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/files/etc/ufw/before.init b/xos/synchronizer/files/etc/ufw/before.init
new file mode 100644
index 0000000..1348cb1
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/files/etc/ufw/before6.rules b/xos/synchronizer/files/etc/ufw/before6.rules
new file mode 100644
index 0000000..0b26ed8
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/files/etc/ufw/sysctl.conf b/xos/synchronizer/files/etc/ufw/sysctl.conf
new file mode 100644
index 0000000..8707032
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/files/etc/ufw/ufw.conf b/xos/synchronizer/files/etc/ufw/ufw.conf
new file mode 100644
index 0000000..28fe534
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/files/vcpe.conf b/xos/synchronizer/files/vcpe.conf
new file mode 100644
index 0000000..752c57a
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/files/vcpe.dnsmasq b/xos/synchronizer/files/vcpe.dnsmasq
new file mode 100644
index 0000000..2b2687b
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/files/vm-resolv.conf b/xos/synchronizer/files/vm-resolv.conf
new file mode 100644
index 0000000..cae093a
--- /dev/null
+++ b/xos/synchronizer/files/vm-resolv.conf
@@ -0,0 +1 @@
+nameserver 8.8.8.8
diff --git a/xos/synchronizer/manifest b/xos/synchronizer/manifest
new file mode 100644
index 0000000..d13ee05
--- /dev/null
+++ b/xos/synchronizer/manifest
@@ -0,0 +1,49 @@
+templates/bwlimit.sh.j2
+templates/vlan_sample.j2
+templates/before.rules.j2
+templates/start-vcpe.sh.j2
+templates/dnsmasq_safe_servers.j2
+templates/firewall_sample.j2
+templates/rc.local.j2
+templates/vcpe.conf.j2
+templates/message.html.j2
+templates/dnsmasq_servers.j2
+templates/start-vcpe-vtn.sh.j2
+manifest
+broadbandshield.py
+observer_ansible_test.py
+vcpe_synchronizer_config
+start-bbs.sh
+steps/sync_vcpetenant.py
+steps/sync_vcpetenant_new.yaml
+steps/sync_vcpetenant_vtn.yaml
+steps/sync_vcpetenant.yaml
+steps/test.yaml
+steps/ansible_test/README
+steps/ansible_test/test.yaml
+steps/ansible_test/xos.py
+steps/ansible_test/test.sh
+steps/ansible_test/inventory.txt
+start.sh
+files/vcpe.conf
+files/etc/service/message/run
+files/etc/rc.local
+files/etc/ufw/after6.rules
+files/etc/ufw/applications.d/openssh-server
+files/etc/ufw/sysctl.conf
+files/etc/ufw/ufw.conf
+files/etc/ufw/before6.rules
+files/etc/ufw/after.init
+files/etc/ufw/before.init
+files/etc/ufw/after.rules
+files/vm-resolv.conf
+files/docker.list
+files/vcpe.dnsmasq
+run-vtn.sh
+stop.sh
+vcpe-synchronizer.py
+model-deps
+supervisor/vcpe-observer.conf
+run.sh
+vtn_vcpe_synchronizer_config
+vcpe_stats_notifier.py
diff --git a/xos/synchronizer/model-deps b/xos/synchronizer/model-deps
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/xos/synchronizer/model-deps
@@ -0,0 +1 @@
+{}
diff --git a/xos/synchronizer/observer_ansible_test.py b/xos/synchronizer/observer_ansible_test.py
new file mode 100644
index 0000000..b28da63
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/run-vtn.sh b/xos/synchronizer/run-vtn.sh
new file mode 100755
index 0000000..c4c3b00
--- /dev/null
+++ b/xos/synchronizer/run-vtn.sh
@@ -0,0 +1,8 @@
+#if [[ ! -e ./vcpe-observer.py ]]; then
+#    ln -s ../../xos-observer.py vcpe-observer.py
+#fi
+
+export XOS_DIR=/opt/xos
+cp /root/setup/node_key $XOS_DIR/synchronizers/vcpe/node_key
+chmod 0600 $XOS_DIR/synchronizers/vcpe/node_key
+python vcpe-synchronizer.py  -C $XOS_DIR/synchronizers/vcpe/vtn_vcpe_synchronizer_config
diff --git a/xos/synchronizer/run.sh b/xos/synchronizer/run.sh
new file mode 100755
index 0000000..f7c670d
--- /dev/null
+++ b/xos/synchronizer/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-synchronizer.py  -C $XOS_DIR/synchronizers/vcpe/vcpe_synchronizer_config
diff --git a/xos/synchronizer/start-bbs.sh b/xos/synchronizer/start-bbs.sh
new file mode 100755
index 0000000..c8ee147
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/start.sh b/xos/synchronizer/start.sh
new file mode 100755
index 0000000..dc5619b
--- /dev/null
+++ b/xos/synchronizer/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-synchronizer.py  -C $XOS_DIR/synchronizers/vcpe/vcpe_synchronizer_config > /dev/null 2>&1 &
diff --git a/xos/synchronizer/steps/ansible_test/README b/xos/synchronizer/steps/ansible_test/README
new file mode 100644
index 0000000..d3b2c54
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/steps/ansible_test/inventory.txt b/xos/synchronizer/steps/ansible_test/inventory.txt
new file mode 100644
index 0000000..bd5b542
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/steps/ansible_test/test.sh b/xos/synchronizer/steps/ansible_test/test.sh
new file mode 100755
index 0000000..157ba9c
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/steps/ansible_test/test.yaml b/xos/synchronizer/steps/ansible_test/test.yaml
new file mode 100644
index 0000000..6a29d56
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/steps/ansible_test/xos.py b/xos/synchronizer/steps/ansible_test/xos.py
new file mode 100755
index 0000000..eb4f3eb
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/steps/sync_vcpetenant.py b/xos/synchronizer/steps/sync_vcpetenant.py
new file mode 100644
index 0000000..0b777c7
--- /dev/null
+++ b/xos/synchronizer/steps/sync_vcpetenant.py
@@ -0,0 +1,253 @@
+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 synchronizers.base.SyncInstanceUsingAnsible import SyncInstanceUsingAnsible
+from core.models import Service, Slice, Tag
+from services.vsg.models import VSGService, VSGTenant
+from services.hpc.models import HpcService, CDNPrefix
+from xos.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)
+
+ENABLE_QUICK_UPDATE=False
+
+CORD_USE_VTN = getattr(Config(), "networking_use_vtn", False)
+
+class SyncVSGTenant(SyncInstanceUsingAnsible):
+    provides=[VSGTenant]
+    observes=VSGTenant
+    requested_interval=0
+    template_name = "sync_vcpetenant.yaml"
+
+    def __init__(self, *args, **kwargs):
+        super(SyncVSGTenant, self).__init__(*args, **kwargs)
+
+    def fetch_pending(self, deleted):
+        if (not deleted):
+            objs = VSGTenant.get_tenant_objects().filter(Q(enacted__lt=F('updated')) | Q(enacted=None),Q(lazy_blocked=False))
+        else:
+            objs = VSGTenant.get_deleted_tenant_objects()
+
+        return objs
+
+    def get_vcpe_service(self, o):
+        if not o.provider_service:
+            return None
+
+        vcpes = VSGService.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
+        cdn_prefixes = []
+
+        cdn_config_fn = "/opt/xos/synchronizers/vsg/cdn_config"
+        if os.path.exists(cdn_config_fn):
+            # manual CDN configuration
+            #   the first line is the address of dnsredir
+            #   the remaining lines are domain names, one per line
+            lines = file(cdn_config_fn).readlines()
+            if len(lines)>=2:
+                dnsdemux_ip = lines[0].strip()
+                cdn_prefixes = [x.strip() for x in lines[1:] if x.strip()]
+        else:
+            # automatic CDN configuiration
+            #    it learns everything from CDN objects in XOS
+            #    not tested on pod.
+            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,extra=o.tologdict())
+            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",extra=o.tologdict())
+
+            for prefix in CDNPrefix.objects.all():
+                cdn_prefixes.append(prefix.prefix)
+
+        dnsdemux_ip = dnsdemux_ip or "none"
+
+        # 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",extra=o.tologdict())
+            if not bbs_addrs:
+                logger.info("failed to find any usable addresses on bbs_slice",extra=o.tologdict())
+        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",extra=o.tologdict())
+
+        s_tags = []
+        c_tags = []
+        if o.volt:
+            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 vcpe_service.url_filter_kind == "safebrowsing":
+            if o.volt and o.volt.subscriber:
+                for user in o.volt.subscriber.devices:
+                    level = user.get("level",None)
+                    mac = user.get("mac",None)
+                    if level in ["G", "PG"]:
+                        if mac:
+                            safe_macs.append(mac)
+
+        fields = {"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,
+                "container_name": "vcpe-%s-%s" % (s_tags[0], c_tags[0]),
+                "dns_servers": [x.strip() for x in vcpe_service.dns_servers.split(",")],
+                "url_filter_kind": vcpe_service.url_filter_kind }
+
+        # 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(SyncVSGTenant, 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.devices
+
+        if service.url_filter_kind == "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",extra=o.tologdict())
+                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",extra=o.tologdict())
+                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),extra=o.tologdict())
+                        bbs.associate(o.hpc_client_ip)
+                    else:
+                        logger.info("no hpc_client_ip to associate",extra=o.tologdict())
+
+                    logger.info("bbs update time %d" % int(time.time()-tStart),extra=o.tologdict())
+
+
+    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",extra=o.tologdict())
+        else:
+            if o.instance.isolation in ["container", "container_vm"]:
+                super(SyncVSGTenant, self).run_playbook(o, fields, "sync_vcpetenant_new.yaml")
+            else:
+                if CORD_USE_VTN:
+                    super(SyncVSGTenant, self).run_playbook(o, fields, template_name="sync_vcpetenant_vtn.yaml")
+                else:
+                    super(SyncVSGTenant, self).run_playbook(o, fields)
+
+        o.last_ansible_hash = ansible_hash
+
+    def delete_record(self, m):
+        pass
diff --git a/xos/synchronizer/steps/sync_vcpetenant.yaml b/xos/synchronizer/steps/sync_vcpetenant.yaml
new file mode 100644
index 0000000..880895e
--- /dev/null
+++ b/xos/synchronizer/steps/sync_vcpetenant.yaml
@@ -0,0 +1,179 @@
+---
+- 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 }}
+      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 %}
+      dns_servers:
+        {% for dns_server in dns_servers %}
+        - {{ dns_server }}
+        {% 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 %}
+      uplink_speed: {{ uplink_speed }}
+      downlink_speed: {{ downlink_speed }}
+      status: {{ status }}
+      enable_uverse: {{ enable_uverse }}
+      url_filter_kind: {{ url_filter_kind }}
+
+  tasks:
+{% if full_setup %}
+  - name: Docker repository
+    copy: src=/opt/xos/synchronizers/vsg/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/synchronizers/vsg/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/synchronizers/vsg/vcpe_stats_notifier.py
+      dest=/usr/local/sbin/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: sudo python /usr/local/sbin/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/synchronizers/vsg/templates/vcpe.conf.j2 dest=/etc/init/vcpe-{{ s_tags[0] }}-{{ c_tags[0] }}.conf
+
+  - name: vCPE startup script
+    template: src=/opt/xos/synchronizers/vsg/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/synchronizers/vsg/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/synchronizers/vsg/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/synchronizers/vsg/templates/vlan_sample.j2 dest=/etc/vlan_sample owner=root group=root
+
+#  - name: firewall info
+#    template: src=/opt/xos/synchronizers/vsg/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/synchronizer/steps/sync_vcpetenant_new.yaml b/xos/synchronizer/steps/sync_vcpetenant_new.yaml
new file mode 100644
index 0000000..9c59280
--- /dev/null
+++ b/xos/synchronizer/steps/sync_vcpetenant_new.yaml
@@ -0,0 +1,136 @@
+---
+- 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 }}
+      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 %}
+      dns_servers:
+        {% for dns_server in dns_servers %}
+        - {{ dns_server }}
+        {% 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 %}
+      uplink_speed: {{ uplink_speed }}
+      downlink_speed: {{ downlink_speed }}
+      status: {{ status }}
+      enable_uverse: {{ enable_uverse }}
+      url_filter_kind: {{ url_filter_kind }}
+
+  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/synchronizers/vsg/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/synchronizers/vsg/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/synchronizers/vsg/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/synchronizers/vsg/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/synchronizers/vsg/files/etc/ufw/ dest=/var/container_volumes/{{ container_name }}/etc/ufw/
+    notify:
+    - reload ufw
+
+  - name: redirection rules for safe DNS
+    template: src=/opt/xos/synchronizers/vsg/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/synchronizers/vsg/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/synchronizer/steps/sync_vcpetenant_vtn.yaml b/xos/synchronizer/steps/sync_vcpetenant_vtn.yaml
new file mode 100644
index 0000000..0226354
--- /dev/null
+++ b/xos/synchronizer/steps/sync_vcpetenant_vtn.yaml
@@ -0,0 +1,244 @@
+---
+- hosts: {{ instance_name }}
+  #gather_facts: False
+  connection: ssh
+  user: ubuntu
+  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 }}
+      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 %}
+      dns_servers:
+        {% for dns_server in dns_servers %}
+        - {{ dns_server }}
+        {% 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_ip: {{ wan_container_ip }}
+      wan_container_netbits: {{ wan_container_netbits }}
+      wan_container_mac: {{ wan_container_mac }}
+      wan_container_gateway_ip: {{ wan_container_gateway_ip }}
+      wan_vm_ip: {{ wan_vm_ip }}
+      wan_vm_mac: {{ wan_vm_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 %}
+      uplink_speed: {{ uplink_speed }}
+      downlink_speed: {{ downlink_speed }}
+      status: {{ status }}
+      enable_uverse: {{ enable_uverse }}
+      url_filter_kind: {{ url_filter_kind }}
+
+
+  tasks:
+  - name: Add hostname to /etc/hosts
+    lineinfile: dest=/etc/hosts
+      regexp='^127\.0\.0\.1'
+      line="127.0.0.1 localhost {{ '{{' }} ansible_hostname {{ '}}' }}"
+      owner=root group=root mode=0644
+
+  - name: Verify that bridge-utils is installed
+    shell: stat /sbin/brctl
+
+  - name: Verify that docker is installed
+    shell: stat /usr/bin/docker
+
+  - name: Check to see if network is setup
+    stat: path=/root/network_is_setup
+    register: network_is_setup
+
+  - name: set up the network
+    shell: "{{ '{{' }} item {{ '}}' }}"
+    with_items:
+       - ip link del link eth0 eth0.500 || true
+       - ip link add link eth0 eth0.500 type vlan id 500
+       - ip link set eth0.500 up
+       - ifconfig br-wan down || true
+       - brctl delbr br-wan || true
+       - brctl addbr br-wan
+       - brctl addif br-wan eth0.500
+       - ifconfig br-wan hw ether {{ wan_vm_mac }}
+       - ip addr add {{ wan_vm_ip }}/{{ wan_container_netbits }} dev br-wan
+       - ip link set br-wan up
+       - ip route del default || true
+       - ip route add default via {{ wan_container_gateway_ip }}
+       - ip link set dev br-wan promisc on
+    when: network_is_setup.stat.exists == False
+
+  - name: Remember that the network is setup, so we never do the above again
+    shell: touch /root/network_is_setup
+
+{% if full_setup %}
+  - name: Check to see if environment is setup
+    stat: path=/root/environment_is_setup
+    register: environment_is_setup
+
+# Everything here is now baked into the vCPE image
+# Leave this spot in place for future temporary setup stuff
+
+  - name: Remember that the environment is setup, so we never do the above again
+    shell: touch /root/environment_is_setup
+
+  - 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/synchronizers/vsg/vcpe_stats_notifier.py
+#      dest=/usr/local/sbin/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: sudo python /usr/local/sbin/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/synchronizers/vsg/templates/vcpe.conf.j2 dest=/etc/init/{{ container_name }}.conf
+
+  - name: vCPE startup script
+    template: src=/opt/xos/synchronizers/vsg/templates/start-vcpe-vtn.sh.j2 dest=/usr/local/sbin/start-{{ container_name }}.sh mode=0755
+    notify:
+#    - restart vcpe
+     - stop vcpe
+     - remove container
+     - start vcpe
+
+  - name: create /var/container_volumes/{{ container_name }}/etc/dnsmasq.d/safe/
+    file: path=/var/container_volumes/{{ container_name }}/etc/dnsmasq.d/safe state=directory owner=root group=root
+
+  - name: vCPE basic dnsmasq config
+    copy: src=/opt/xos/synchronizers/vsg/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/synchronizers/vsg/templates/dnsmasq_servers.j2 dest=/var/container_volumes/{{ container_name }}/etc/dnsmasq.d/servers.conf owner=root group=root
+    notify:
+    - restart dnsmasq
+
+  - name: dnsmasq "safe" config
+    template: src=/opt/xos/synchronizers/vsg/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: create /var/container_volumes/{{ container_name }}/mount/
+    file: path=/var/container_volumes/{{ container_name }}/mount state=directory owner=root group=root
+
+  - name: redirection rules for safe DNS
+    template: src=/opt/xos/synchronizers/vsg/templates/before.rules.j2 dest=/var/container_volumes/{{ container_name }}/mount/before.rules owner=root group=root mode=0644
+    notify:
+    - reload ufw
+
+  - name: base ufw setup uses /etc/rc.local
+    template: src=/opt/xos/synchronizers/vsg/templates/rc.local.j2 dest=/var/container_volumes/{{ container_name }}/mount/rc.local owner=root group=root mode=0755
+    notify:
+    - rerun /etc/rc.local
+
+  - name: create directory for local programs
+    file: path=/var/container_volumes/{{ container_name }}/usr/local/sbin state=directory
+
+  - name: bandwidth limit script
+    template: src=/opt/xos/synchronizers/vsg/templates/bwlimit.sh.j2 dest=/var/container_volumes/{{ container_name }}/usr/local/sbin/bwlimit.sh owner=root group=root mode=0755
+    notify:
+    - reset bwlimits
+
+  - name: create directory for simple webserver
+    file: path=/var/container_volumes/{{ container_name }}/etc/service/message state=directory
+
+  - name: copy simple webserver
+    copy: src=/opt/xos/synchronizers/vsg/files/etc/service/ dest=/var/container_volumes/{{ container_name }}/etc/service/ owner=root group=root
+    when: status != "enabled"
+
+  - name: make webserver script executable
+    file: path=/var/container_volumes/{{ container_name }}/etc/service/message/run mode=0755
+    when: status != "enabled"
+
+  - name: generate the message page
+    template: src=/opt/xos/synchronizers/vsg/templates/message.html.j2 dest=/var/container_volumes/{{ container_name }}/etc/service/message/message.html owner=root group=root mode=0644
+    when: status != "enabled"
+    #notify: restart vcpe
+
+  - name: remove simple webserver
+    file: path=/var/container_volumes/{{ container_name }}/etc/service/message/run state=absent
+    when: status == "enabled"
+    #notify: restart vcpe
+
+  - name: Make sure vCPE service is running
+    service: name={{ container_name }} state=started
+
+  handlers:
+  # Dnsmasq is automatically restarted in the container
+  - name: restart dnsmasq
+    shell: docker exec {{ container_name }} killall dnsmasq
+
+  - name: stop vcpe
+    service: name={{ container_name }} state=stopped
+
+  - name: remove container
+    docker: name={{ container_name }} state=absent image=docker-vcpe
+
+  - name: start vcpe
+    service: name={{ container_name }} state=started
+
+  - name: reload ufw
+    shell: docker exec {{ container_name }} bash -c "/sbin/iptables -t nat -F PREROUTING; /sbin/iptables -t nat -F POSTROUTING; /usr/sbin/ufw reload"
+
+  - name: rerun /etc/rc.local
+    shell: docker exec {{ container_name }} bash -c "/etc/rc.local"
+
+  - name: reset bwlimits
+    shell: docker exec {{ container_name }} bash -c "/usr/local/sbin/bwlimit.sh restart"
+
+  - name: restart vcpe
+    shell: service {{ container_name }} stop; sleep 1; service {{ container_name }} start
diff --git a/xos/synchronizer/steps/test.yaml b/xos/synchronizer/steps/test.yaml
new file mode 100644
index 0000000..fc8251d
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/stop.sh b/xos/synchronizer/stop.sh
new file mode 100755
index 0000000..e90e16c
--- /dev/null
+++ b/xos/synchronizer/stop.sh
@@ -0,0 +1 @@
+pkill -9 -f vcpe-observer.py
diff --git a/xos/synchronizer/supervisor/vcpe-observer.conf b/xos/synchronizer/supervisor/vcpe-observer.conf
new file mode 100644
index 0000000..2d90293
--- /dev/null
+++ b/xos/synchronizer/supervisor/vcpe-observer.conf
@@ -0,0 +1,2 @@
+[program:vcpe-observer]
+command=python /opt/xos/synchronizers/vsg/vcpe-synchronizer.py -C /opt/xos/synchronizers/vsg/vcpe_synchronizer_config
diff --git a/xos/synchronizer/templates/before.rules.j2 b/xos/synchronizer/templates/before.rules.j2
new file mode 100644
index 0000000..b60aaef
--- /dev/null
+++ b/xos/synchronizer/templates/before.rules.j2
@@ -0,0 +1,101 @@
+#
+# 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 %}
+
+{% if status != "enabled" %}
+-A PREROUTING -i eth1 -p tcp --dport 80 -j REDIRECT --to-port 8000
+{% 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/synchronizer/templates/bwlimit.sh.j2 b/xos/synchronizer/templates/bwlimit.sh.j2
new file mode 100644
index 0000000..b267ada
--- /dev/null
+++ b/xos/synchronizer/templates/bwlimit.sh.j2
@@ -0,0 +1,131 @@
+#!/bin/bash
+#  tc uses the following units when passed as a parameter.
+#  kbps: Kilobytes per second
+#  mbps: Megabytes per second
+#  kbit: Kilobits per second
+#  mbit: Megabits per second
+#  bps: Bytes per second
+#       Amounts of data can be specified in:
+#       kb or k: Kilobytes
+#       mb or m: Megabytes
+#       mbit: Megabits
+#       kbit: Kilobits
+#  To get the byte figure from bits, divide the number by 8 bit
+#
+
+TC=/sbin/tc
+
+WAN=eth0             # External (WAN side) interface
+LAN=eth1             # Customer (LAN side) interface
+
+MAXRATE=10gbit       # Maximum upload/download rate
+DNLD={{ downlink_speed }}          # DOWNLOAD Limit
+UPLD={{ uplink_speed }}            # UPLOAD Limit
+
+[ "$DNLD" == "None" ] && DNLD=$MAXRATE
+[ "$UPLD" == "None" ] && UPLD=$MAXRATE
+
+start() {
+
+# We'll use Hierarchical Token Bucket (HTB) to shape bandwidth.
+# For detailed configuration options, please consult Linux man
+# page.
+
+    #
+    # WAN side (upload limiting)
+    #
+    $TC qdisc add dev $WAN root handle 1: htb default 30
+    $TC class add dev $WAN parent 1: classid 1:1 htb rate $MAXRATE burst 15k
+
+    # The default class
+    $TC class add dev $WAN parent 1:1 classid 1:30 htb rate 1kbit ceil $UPLD burst 15k
+    $TC qdisc add dev $WAN parent 1:30 handle 30: sfq perturb 10
+
+    # This class is exempt from the upload limit
+    $TC class add dev $WAN parent 1:1 classid 1:50 htb rate 1kbit ceil $MAXRATE burst 15k
+    $TC qdisc add dev $WAN parent 1:50 handle 50: sfq perturb 10
+
+    #
+    # LAN side (download limiting)
+    #
+    $TC qdisc add dev $LAN root handle 1: htb default 30
+    $TC class add dev $LAN parent 1: classid 1:1 htb rate $MAXRATE burst 15k
+
+    # The default class
+    $TC class add dev $LAN parent 1:1 classid 1:30 htb rate 1kbit ceil $DNLD burst 15k
+    $TC qdisc add dev $LAN parent 1:30 handle 30: sfq perturb 10
+
+    # This class is exempt from the download limit
+    $TC class add dev $LAN parent 1:1 classid 1:50 htb rate 1kbit ceil $MAXRATE burst 15k
+    $TC qdisc add dev $LAN parent 1:50 handle 50: sfq perturb 10
+
+}
+
+stop() {
+
+# Stop the bandwidth shaping.
+    $TC qdisc del dev $WAN root
+    $TC qdisc del dev $LAN root
+
+}
+
+restart() {
+
+# Self-explanatory.
+    stop
+    sleep 1
+    start
+
+}
+
+show() {
+
+# Display status of traffic control status.
+    echo "Download ($LAN):"
+    $TC -s class show dev $LAN
+
+    echo ""
+    echo "Upload ($WAN):"
+    $TC -s class show dev $WAN
+
+}
+
+case "$1" in
+
+  start)
+
+    echo -n "Starting bandwidth shaping: "
+    start
+    echo "done"
+    ;;
+
+  stop)
+
+    echo -n "Stopping bandwidth shaping: "
+    stop
+    echo "done"
+    ;;
+
+  restart)
+
+    echo -n "Restarting bandwidth shaping: "
+    restart
+    echo "done"
+    ;;
+
+  show)
+
+    echo "Bandwidth shaping status:"
+    show
+    echo ""
+    ;;
+
+  *)
+
+    pwd=$(pwd)
+    echo "Usage: tc.bash {start|stop|restart|show}"
+    ;;
+
+esac
+
+exit 0
diff --git a/xos/synchronizer/templates/dnsmasq_safe_servers.j2 b/xos/synchronizer/templates/dnsmasq_safe_servers.j2
new file mode 100644
index 0000000..0b3c807
--- /dev/null
+++ b/xos/synchronizer/templates/dnsmasq_safe_servers.j2
@@ -0,0 +1,16 @@
+# This file autogenerated by vCPE observer
+# It contains a list of DNS servers for dnsmasq to use.
+no-resolv
+
+{% if cdn_enable %}
+{% if cdn_prefixes %}
+# CDN
+{% for prefix in cdn_prefixes %}
+server=/{{ prefix }}/{{ dnsdemux_ip }}
+{% endfor %}
+{% endif %}
+{% endif %}
+
+# use OpenDNS service
+server=208.67.222.123
+server=208.67.220.123
diff --git a/xos/synchronizer/templates/dnsmasq_servers.j2 b/xos/synchronizer/templates/dnsmasq_servers.j2
new file mode 100644
index 0000000..7ecb319
--- /dev/null
+++ b/xos/synchronizer/templates/dnsmasq_servers.j2
@@ -0,0 +1,26 @@
+# This file autogenerated by vCPE observer
+# It contains a list of DNS servers for dnsmasq to use.
+no-resolv
+
+{% if cdn_enable %}
+{% if cdn_prefixes %}
+# CDN
+{% for prefix in cdn_prefixes %}
+server=/{{ prefix }}/{{ dnsdemux_ip }}
+{% endfor %}
+{% endif %}
+{% endif %}
+
+{% if url_filter_kind=="answerx" %}
+cache-size=0
+add-mac
+{% endif %}
+
+# temporary for ONS demo
+address=/z.cdn.turner.com/207.141.192.134
+
+# use google's DNS service
+{% for dns_server in dns_servers %}
+server={{ dns_server }}
+{% endfor %}
+
diff --git a/xos/synchronizer/templates/firewall_sample.j2 b/xos/synchronizer/templates/firewall_sample.j2
new file mode 100644
index 0000000..ce85e68
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/templates/message.html.j2 b/xos/synchronizer/templates/message.html.j2
new file mode 100644
index 0000000..eb4497a
--- /dev/null
+++ b/xos/synchronizer/templates/message.html.j2
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <title>Service Suspended</title>
+  <style>
+    
+    .row {
+      width: 100%;
+    }
+
+    .col-sm-offset-2 {
+      margin-left: 16.66666667%;
+    }
+
+    .col-sm-8 {
+      width: 66.66666667%;
+    }
+
+    .alert-danger {
+        color: #a94442;
+        background-color: #f2dede;
+        border-color: #a94442;
+    }
+    .alert {
+        padding: 15px;
+        margin-bottom: 20px;
+        border: 1px solid transparent;
+        border-radius: 4px;
+    }
+
+    body {
+      background-size: cover;
+      background-color: #00BFEC;
+      font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
+      font-size: 18px;
+      line-height: 1.42857143;
+    }
+
+    .vertical-center {
+      min-height: 100%;  /* Fallback for browsers do NOT support vh unit */
+      min-height: 100vh; /* These two lines are counted as one :-)       */
+
+      display: flex;
+      align-items: center;
+    }
+
+    .jumbotron {
+      padding: 60px;
+      border-radius: 6px;
+      background-color: #eee;
+      box-shadow: 4px 4px 18px black;
+    }
+
+    .cord-logo-title{
+      font-size: 150px;
+      display: inline-block;
+      color: #007EC4;
+    }
+
+    path {
+      fill: #B2181E;
+    }
+
+    #cord-logo {
+      transform: scale(1.3)
+    }
+
+    @media only screen 
+    and (min-device-width : 768px) 
+    and (max-device-width : 1024px)  {
+      #cord-logo {
+        transform: scale(1.2)
+      }
+      .cord-logo-title{
+        font-size: 100px;
+      }
+    }
+  </style>
+</head>
+<body>
+  
+  <div class="container vertical-center">
+    <div class="row">
+      <div class="col-sm-8 col-sm-offset-2">
+        <div class="jumbotron">
+          <div class="cord-logo-title">
+            <svg height="150" width="150">
+              <path id="cord-logo" d="M92.5,62.3l-33,33,2.5,2.5c4.1,4.1,7.4,3.6,11.2-.1L95.9,75l-4.5-4.5,4.7-4.7-3.6-3.6Zm2.6,7L98.4,66l3.3,3.3-3.3,3.3-3.3-3.3ZM94.5,60l4.9-4.9,4.9,4.9-4.9,4.9ZM36.2,36.1L18.6,53.8c-7.8,7.8-5.8,17.4-2.4,22l-2.2-2.2c-10.6-10.6-11.2-20,0-31.2L28.2,28.1L31.3,25l8,8-3.1,3.1ZM55.5,55.4l3.6-3.6L66.9,44l-8-8l-2.5,2.5-5.2,5.2l-3.6,3.6L33.2,61.6C22,72.7,22.5,82.2,33.2,92.8L35.4,95c-3.4-4.5-5.4-14.1,2.4-22L55.5,55.4ZM50.7,21.7l-8-8L35,21.2l8,8,7.6-7.6ZM62.8,9.6L55.4,17l-8-8,7.4-7.4,8,8Zm0.7,18.3-7.6,7.6-8-8,7.6-7.6,8,8Zm26.1-6.6-8.1,8.1-8-8,8.1-8.1,8,8ZM79.3,31.5l-7.4,7.4-8-8,7.4-7.4,8,8ZM45.7,45.6L54.3,37l-8-8-8.6,8.6L23.4,51.8C12.2,63,12.8,72.4,23.4,83l2.2,2.2c-3.4-4.5-5.4-14.1,2.4-22ZM34.9,80.7l20.6,20.5c2,2,4.6,4.1,7.9,3.2-2.9,2.9-8.9,1.7-11.9-1.3L35.1,86.8,35,86.6H34.9l-0.8-.8a15,15,0,0,1,.1-1.9,14.7,14.7,0,0,1,.7-3.2Zm-0.6,7.4a21.3,21.3,0,0,0,5.9,11.7l5.7,5.7c3,3,9,4.1,11.9,1.3-3.3.9-5.9-1.2-7.9-3.2L34.3,88.1Zm3.5-12.4a16.6,16.6,0,0,0-2.3,3.6L57,100.8c3,3,9,4.1,11.9,1.3-3.3.9-5.9-1.2-7.9-3.2Z" />
+              Sorry, your browser does not support inline SVG.
+            </svg>
+            CORD
+          </div>
+          <div class="alert alert-danger">
+            {% if status == "delinquent" %}
+            Your account is delinquent.  Please visit the customer portal to pay your bill.
+            {% elif status == "copyrightviolation" %}
+            Someone in your home has been illegally downloading copyrighted material.
+            Please visit the customer portal and perform the Copyright Training course.
+            {% else %}
+            Your service has been suspended.  Please visit the customer portal to resume.
+            {% endif %}
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+
+</body>
+</html>
\ No newline at end of file
diff --git a/xos/synchronizer/templates/rc.local.j2 b/xos/synchronizer/templates/rc.local.j2
new file mode 100755
index 0000000..4226a48
--- /dev/null
+++ b/xos/synchronizer/templates/rc.local.j2
@@ -0,0 +1,28 @@
+#!/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
+{% if status == "enabled" %}
+ufw route allow in on eth1 out on eth0
+ufw route allow in on eth1 out on eth2
+{% else %}
+ufw route deny in on eth1 out on eth0
+ufw route deny in on eth1 out on eth2
+{% endif %}
+
+BWLIMIT=/usr/local/sbin/bwlimit.sh
+[ -e $BWLIMIT ] && $BWLIMIT restart || true
+
+exit 0
diff --git a/xos/synchronizer/templates/start-vcpe-vtn.sh.j2 b/xos/synchronizer/templates/start-vcpe-vtn.sh.j2
new file mode 100644
index 0000000..dfdce0a
--- /dev/null
+++ b/xos/synchronizer/templates/start-vcpe-vtn.sh.j2
@@ -0,0 +1,49 @@
+#!/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 /var/container_volumes/$VCPE/mount:/mount:ro \
+    -v /var/container_volumes/$VCPE/etc/dnsmasq.d:/etc/dnsmasq.d:ro \
+    -v /var/container_volumes/$VCPE/etc/service/message:/etc/service/message \
+    -v /var/container_volumes/$VCPE/usr/local/sbin:/usr/local/sbin:ro \
+    andybavier/docker-vcpe
+else
+    docker start $VCPE
+fi
+
+# Set up networking via pipework
+WAN_IFACE=br-wan
+docker exec $VCPE ifconfig eth0 >> /dev/null || pipework $WAN_IFACE -i eth0 $VCPE {{ wan_container_ip }}/{{ wan_container_netbits }}@{{ wan_container_gateway_ip }} {{ wan_container_mac }}
+
+LAN_IFACE=eth0
+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
+
+# Attach to container
+docker start -a $VCPE
diff --git a/xos/synchronizer/templates/start-vcpe.sh.j2 b/xos/synchronizer/templates/start-vcpe.sh.j2
new file mode 100755
index 0000000..c4128f3
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/templates/vcpe.conf.j2 b/xos/synchronizer/templates/vcpe.conf.j2
new file mode 100644
index 0000000..fa7885e
--- /dev/null
+++ b/xos/synchronizer/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/synchronizer/templates/vlan_sample.j2 b/xos/synchronizer/templates/vlan_sample.j2
new file mode 100644
index 0000000..b73954b
--- /dev/null
+++ b/xos/synchronizer/templates/vlan_sample.j2
@@ -0,0 +1,5 @@
+# below is a list of all vlan_ids associated with this vcpe
+
+{% for vlan_id in c_tags %}
+{{ vlan_id }}
+{% endfor %}
diff --git a/xos/synchronizer/vcpe-synchronizer.py b/xos/synchronizer/vcpe-synchronizer.py
new file mode 100755
index 0000000..84bec4f
--- /dev/null
+++ b/xos/synchronizer/vcpe-synchronizer.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__)),"../../synchronizers/base")
+sys.path.append(observer_path)
+mod = importlib.import_module("xos-synchronizer")
+mod.main()
diff --git a/xos/synchronizer/vcpe_stats_notifier.py b/xos/synchronizer/vcpe_stats_notifier.py
new file mode 100644
index 0000000..4d2cc76
--- /dev/null
+++ b/xos/synchronizer/vcpe_stats_notifier.py
@@ -0,0 +1,344 @@
+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
+import os
+
+
+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 get_all_docker_containers():
+    p = subprocess.Popen('docker ps --no-trunc', 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
+    return dockercontainers
+
+def extract_compute_stats_from_all_vcpes(dockercontainers):
+    for k,v in dockercontainers.iteritems():
+        cmd = 'sudo docker stats --no-stream=true ' + v['id'] 
+        p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) 
+        firstline = True
+        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_ID', 'CPU%', 'MEMUSE', 'UNITS', '/', 'MEMLIMIT', 'UNITS', 'MEM%', 'NET I/O', 'UNITS', '/', 'NET I/O LIMIT', 'UNITS', 'BLOCK I/O', 'UNITS', '/', 'BLOCK I/O LIMIT', 'UNITS']
+                    v['cpu_util'] = fields[1][:-1]
+                    if fields[6] == 'GB':
+                       v['memory'] = str(float(fields[5]) * 1000)
+                    else:
+                       v['memory'] = fields[5]
+                    if fields[3] == 'GB':
+                       v['memory_usage'] = str(float(fields[2]) * 1000)
+                    else:
+                       v['memory_usage'] = fields[2]
+        v['network_stats'] = []
+        for intf in ['eth0', 'eth1']:
+            cmd = 'sudo docker exec ' + v['id'] + ' ifconfig ' + intf
+            p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
+            out,err = p.communicate()
+            if out:
+                intf_stats = {}
+                m = re.search("RX bytes:(\d+)", str(out))
+                if m:
+                    intf_stats['rx_bytes'] = m.group(1)
+                m = re.search("TX bytes:(\d+)", str(out))
+                if m:
+                    intf_stats['tx_bytes'] = m.group(1)
+                m = re.search("RX packets:(\d+)", str(out))
+                if m:
+                    intf_stats['rx_packets'] = m.group(1)
+                m = re.search("TX packets:(\d+)", str(out))
+                if m:
+                    intf_stats['tx_packets'] = m.group(1)
+                if intf_stats:
+                    intf_stats['intf'] = intf
+                    v['network_stats'].append(intf_stats)
+
+def extract_dns_stats_from_all_vcpes(dockercontainers):
+    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')
+
+     dockercontainers = get_all_docker_containers()
+     cpe_container_compute_stats = extract_compute_stats_from_all_vcpes(dockercontainers)
+     cpe_container_dns_stats = extract_dns_stats_from_all_vcpes(dockercontainers)
+
+     for k,v in cpe_container_dns_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)
+
+          compute_payload = {}
+          if 'cpu_util' in v:
+               compute_payload['cpu_util']= v['cpu_util']
+          if 'memory' in v:
+               compute_payload['memory']= v['memory']
+          if 'memory_usage' in v:
+               compute_payload['memory_usage']= v['memory_usage']
+          if ('network_stats' in v) and (v['network_stats']):
+               compute_payload['network_stats']= v['network_stats']
+          if compute_payload:
+               compute_payload['vcpe_id'] = k
+               compute_payload['user_id'] = keystone_user_id
+               compute_payload['tenant_id'] = keystone_tenant_id
+               msg = {'event_type': 'vcpe.compute.stats', 
+                      'message_id':six.text_type(uuid.uuid4()),
+                      'publisher_id': cpe_publisher_id,
+                      'timestamp':datetime.datetime.now().isoformat(),
+                      'priority':'INFO',
+                      'payload': compute_payload 
+                     }
+               producer.publish(msg)
+               logger.debug('Publishing vcpe.dns.cache.size 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/synchronizer/vcpe_synchronizer_config b/xos/synchronizer/vcpe_synchronizer_config
new file mode 100644
index 0000000..e41dc9a
--- /dev/null
+++ b/xos/synchronizer/vcpe_synchronizer_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/synchronizers/vsg/model-deps
+steps_dir=/opt/xos/synchronizers/vsg/steps
+sys_dir=/opt/xos/synchronizers/vsg/sys
+deleters_dir=/opt/xos/synchronizers/vsg/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/synchronizer/vtn_vcpe_synchronizer_config b/xos/synchronizer/vtn_vcpe_synchronizer_config
new file mode 100644
index 0000000..b43d831
--- /dev/null
+++ b/xos/synchronizer/vtn_vcpe_synchronizer_config
@@ -0,0 +1,47 @@
+
+[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/synchronizers/vsg/model-deps
+steps_dir=/opt/xos/synchronizers/vsg/steps
+sys_dir=/opt/xos/synchronizers/vsg/sys
+deleters_dir=/opt/xos/synchronizers/vsg/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
+full_setup=True
+proxy_ssh=True
+proxy_ssh_key=/opt/xos/synchronizers/vsg/node_key
+proxy_ssh_user=root
+
+[networking]
+use_vtn=True
+
+[feefie]
+client_id='vicci_dev_central'
+user_id='pl'