Merge branch 'acb-develop'
Activate enabling / disabling service
diff --git a/xos/configurations/acord/Makefile b/xos/configurations/acord/Makefile
index 5231e79..5b03692 100644
--- a/xos/configurations/acord/Makefile
+++ b/xos/configurations/acord/Makefile
@@ -1,16 +1,18 @@
 SETUPDIR:=../setup
 MYIP:=$(shell hostname -i)
 
-cloudlab: common_cloudlab acord
+cloudlab: common_cloudlab cloudlab_ceilometer_custom_images acord
 
-devstack: upgrade_pkgs common_devstack devstack_net_fix acord
+devstack: upgrade_pkgs common_devstack devstack_net_fix devstack_images acord
 
-acord: ceilometer_dashboard
+cord: ceilometer_dashboard
 	sudo MYIP=$(MYIP) docker-compose up -d
 	bash ../common/wait_for_xos.sh
 	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/fixtures.yaml
 	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/base.yaml
 	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/nodes.yaml
+
+acord: cord
 	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/acord/ceilometer.yaml
 
 containers:
@@ -57,3 +59,9 @@
 
 rebuild_synchronizer:
 	make -C ../../../containers/synchronizer
+
+devstack_images:
+	bash -c "source ../setup/admin-openrc.sh; glance image-show ceilometer-trusty-server-multi-nic || ! mkdir -p /opt/stack/images || ! wget http://www.vicci.org/cord/ceilometer-trusty-server-multi-nic.qcow2 -P /opt/stack/images || glance image-create --name ceilometer-trusty-server-multi-nic --disk-format qcow2 --file /opt/stack/images/ceilometer-trusty-server-multi-nic.qcow2 --container-format bare"
+
+cloudlab_ceilometer_custom_images:
+	bash -c "source ../setup/admin-openrc.sh; glance image-show ceilometer-trusty-server-multi-nic || ! mkdir -p /tmp/images || ! wget http://www.vicci.org/cord/ceilometer-trusty-server-multi-nic.qcow2 -P /tmp/images || glance image-create --name ceilometer-trusty-server-multi-nic --disk-format qcow2 --file /tmp/images/ceilometer-trusty-server-multi-nic.qcow2 --container-format bare"
diff --git a/xos/configurations/acord/ceilometer.yaml b/xos/configurations/acord/ceilometer.yaml
index 874c577..ff56579 100644
--- a/xos/configurations/acord/ceilometer.yaml
+++ b/xos/configurations/acord/ceilometer.yaml
@@ -123,9 +123,9 @@
           view_url: /admin/ceilometer/ceilometerservice/$id$/
           kind: ceilometer
           ceilometer_pub_sub_url: http://10.11.10.1:4455/
-#          public_key: { get_artifact: [ SELF, pubkey, LOCAL_FILE] }
-#      artifacts:
-#          pubkey: /opt/xos/synchronizers/vcpe/vcpe_public_key
+          public_key: { get_artifact: [ SELF, pubkey, LOCAL_FILE] }
+      artifacts:
+          pubkey: /opt/xos/synchronizers/monitoring_channel/monitoring_channel_public_key
 
 #    service_sflow:
 #      type: tosca.nodes.SFlowService
@@ -161,6 +161,9 @@
     trusty-server-multi-nic:
       type: tosca.nodes.Image
 
+    ceilometer-trusty-server-multi-nic:
+      type: tosca.nodes.Image
+
     mysite_ceilometer:
       description: Ceilometer Proxy Slice
       type: tosca.nodes.Slice
@@ -172,10 +175,10 @@
               node: mysite
               relationship: tosca.relationships.MemberOfSite
           - default_image:
-                node: trusty-server-multi-nic
+                node: ceilometer-trusty-server-multi-nic
                 relationship: tosca.relationships.DefaultImage
       properties:
-          default_flavor: m1.medium
+          default_flavor: m1.small
           max_instances: 2
 
 #    mysite_sflow:
diff --git a/xos/configurations/acord/cleanup.sh b/xos/configurations/acord/cleanup.sh
index 91d821c..dfa1438 100755
--- a/xos/configurations/acord/cleanup.sh
+++ b/xos/configurations/acord/cleanup.sh
@@ -13,7 +13,7 @@
   neutron net-delete $NETWORK
 }
 
-source ../../setup/admin-openrc.sh
+source ../setup/admin-openrc.sh
 
 echo "Deleting VMs"
 # Delete all VMs
diff --git a/xos/configurations/common/devstack/local.conf b/xos/configurations/common/devstack/local.conf
index 38cd20c..632ed1b 100644
--- a/xos/configurations/common/devstack/local.conf
+++ b/xos/configurations/common/devstack/local.conf
@@ -2,7 +2,8 @@
 [[local|localrc]]
 
 DOWNLOAD_DEFAULT_IMAGES=false
-IMAGE_URLS="http://www.planet-lab.org/cord/trusty-server-multi-nic.img"
+IMAGE_URLS="http://www.planet-lab.org/cord/trusty-server-multi-nic.img,"
+IMAGE_URLS+="http://www.vicci.org/cord/ceilometer-trusty-server-multi-nic.qcow2"
 LIBVIRT_FIREWALL_DRIVER=nova.virt.firewall.NoopFirewallDriver
 # Append the git branch name if you wish to download ceilometer from a specific branch
 enable_plugin ceilometer https://git.openstack.org/openstack/ceilometer 
diff --git a/xos/configurations/cord-pod/cord-vtn-vsg.yaml b/xos/configurations/cord-pod/cord-vtn-vsg.yaml
index 89ef49a..c06e237 100644
--- a/xos/configurations/cord-pod/cord-vtn-vsg.yaml
+++ b/xos/configurations/cord-pod/cord-vtn-vsg.yaml
@@ -37,6 +37,7 @@
           wan_container_gateway_ip: 10.168.0.1
           wan_container_gateway_mac: 02:42:0a:a8:00:01
           wan_container_netbits: 24
+#          node_label: label_vsg
       artifacts:
           pubkey: /opt/xos/synchronizers/vcpe/vcpe_public_key
 
@@ -61,6 +62,9 @@
     mysite:
       type: tosca.nodes.Site
 
+    label_vsg:
+      type: tosca.nodes.NodeLabel
+
     # Networks required by the CORD setup
     mysite_vsg-access:
       type: tosca.nodes.network.Network
diff --git a/xos/configurations/cord-pod/make-vtn-networkconfig-json.sh b/xos/configurations/cord-pod/make-vtn-networkconfig-json.sh
index 26e4066..a1e5137 100755
--- a/xos/configurations/cord-pod/make-vtn-networkconfig-json.sh
+++ b/xos/configurations/cord-pod/make-vtn-networkconfig-json.sh
@@ -69,8 +69,8 @@
                 ]
             }
         },
-        "org.onosproject.openstackswitching" : {
-            "openstackswitching" : {
+        "org.onosproject.openstackinterface" : {
+            "openstackinterface" : {
                  "do_not_push_flows" : "true",
                  "neutron_server" : "$NEUTRON_URL/v2.0/",
                  "keystone_server" : "$OS_AUTH_URL/",
diff --git a/xos/configurations/cord/Makefile b/xos/configurations/cord/Makefile
index 41bb37f..56990a2 100644
--- a/xos/configurations/cord/Makefile
+++ b/xos/configurations/cord/Makefile
@@ -1,7 +1,7 @@
 SETUPDIR:=../setup
 MYIP:=$(shell hostname -i)
 
-cloudlab: common_cloudlab cord
+cloudlab: common_cloudlab cord acord
 
 devstack: upgrade_pkgs common_devstack devstack_net_fix cord
 
@@ -12,7 +12,6 @@
 	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/base.yaml
 	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/nodes.yaml
 	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/cord/cord.yaml
-	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/cord/ceilometer.yaml
 
 containers:
 	cd ../../../containers/xos; make devel
@@ -24,6 +23,9 @@
 common_devstack:
 	make -C ../common -f Makefile.devstack
 
+acord: cloudlab_ceilometer_custom_images ceilometer_dashboard
+	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/cord/ceilometer.yaml
+
 ceilometer_dashboard:
 	#NOTE: The below dashboard install scripts assume
 	#clouldlab openstack environment created using "OpenStack" profile
@@ -78,3 +80,6 @@
 
 upgrade_pkgs:
 	sudo pip install httpie --upgrade
+
+cloudlab_ceilometer_custom_images:
+	bash -c "source ../setup/admin-openrc.sh; glance image-show ceilometer-trusty-server-multi-nic || ! mkdir -p /tmp/images || ! wget http://www.vicci.org/cord/ceilometer-trusty-server-multi-nic.qcow2 -P /tmp/images || glance image-create --name ceilometer-trusty-server-multi-nic --disk-format qcow2 --file /tmp/images/ceilometer-trusty-server-multi-nic.qcow2 --container-format bare"
diff --git a/xos/configurations/cord/ceilometer.yaml b/xos/configurations/cord/ceilometer.yaml
index a5655b4..ff282e3 100644
--- a/xos/configurations/cord/ceilometer.yaml
+++ b/xos/configurations/cord/ceilometer.yaml
@@ -123,18 +123,18 @@
           view_url: /admin/ceilometer/ceilometerservice/$id$/
           kind: ceilometer
           ceilometer_pub_sub_url: http://10.11.10.1:4455/
-#          public_key: { get_artifact: [ SELF, pubkey, LOCAL_FILE] }
-#      artifacts:
-#          pubkey: /opt/xos/synchronizers/vcpe/vcpe_public_key
+          public_key: { get_artifact: [ SELF, pubkey, LOCAL_FILE] }
+      artifacts:
+          pubkey: /opt/xos/synchronizers/monitoring_channel/monitoring_channel_public_key
 
-    service_sflow:
-      type: tosca.nodes.SFlowService
-      requirements:
-      properties:
-          view_url: /admin/ceilometer/sflowservice/$id$/
-          kind: sflow
-          sflow_port: 6343
-          sflow_api_port: 33333
+#    service_sflow:
+#      type: tosca.nodes.SFlowService
+#      requirements:
+#      properties:
+#          view_url: /admin/ceilometer/sflowservice/$id$/
+#          kind: sflow
+#          sflow_port: 6343
+#          sflow_api_port: 33333
 
     Private:
       type: tosca.nodes.NetworkTemplate
@@ -161,6 +161,9 @@
     trusty-server-multi-nic:
       type: tosca.nodes.Image
 
+    ceilometer-trusty-server-multi-nic:
+      type: tosca.nodes.Image
+
     mysite_ceilometer:
       description: Ceilometer Proxy Slice
       type: tosca.nodes.Slice
@@ -172,21 +175,21 @@
               node: mysite
               relationship: tosca.relationships.MemberOfSite
           - default_image:
-                node: trusty-server-multi-nic
+                node: ceilometer-trusty-server-multi-nic
                 relationship: tosca.relationships.DefaultImage
       properties:
           default_flavor: m1.small
 
-    mysite_sflow:
-      description: Slice for sFlow service
-      type: tosca.nodes.Slice
-      requirements:
-          - sflow_service:
-              node: service_sflow
-              relationship: tosca.relationships.MemberOfService
-          - site:
-              node: mysite
-              relationship: tosca.relationships.MemberOfSite
+#    mysite_sflow:
+#      description: Slice for sFlow service
+#      type: tosca.nodes.Slice
+#      requirements:
+#          - sflow_service:
+#              node: service_sflow
+#              relationship: tosca.relationships.MemberOfService
+#          - site:
+#              node: mysite
+#              relationship: tosca.relationships.MemberOfSite
 
     my_ceilometer_tenant:
       description: Ceilometer Service default Tenant
@@ -197,27 +200,27 @@
               relationship: tosca.relationships.MemberOfService
        
     # Virtual machines
-    sflow_service_instance:
-      type: tosca.nodes.Compute
-      capabilities:
-        # Host container properties
-        host:
-         properties:
-           num_cpus: 1
-           disk_size: 10 GB
-           mem_size: 4 MB
-        # Guest Operating System properties
-        os:
-          properties:
-            # host Operating System image properties
-            architecture: x86_64
-            type: linux
-            distribution: Ubuntu
-            version: 14.10
-      requirements:
-          - slice:
-                node: mysite_sflow
-                relationship: tosca.relationships.MemberOfSlice
+#    sflow_service_instance:
+#      type: tosca.nodes.Compute
+#      capabilities:
+#        # Host container properties
+#        host:
+#         properties:
+#           num_cpus: 1
+#           disk_size: 10 GB
+#           mem_size: 4 MB
+#        # Guest Operating System properties
+#        os:
+#          properties:
+#            # host Operating System image properties
+#            architecture: x86_64
+#            type: linux
+#            distribution: Ubuntu
+#            version: 14.10
+#      requirements:
+#          - slice:
+#                node: mysite_sflow
+#                relationship: tosca.relationships.MemberOfSlice
 
     Ceilometer:
       type: tosca.nodes.DashboardView
diff --git a/xos/configurations/cord/cord.yaml b/xos/configurations/cord/cord.yaml
index 07d4b68..4dea365 100644
--- a/xos/configurations/cord/cord.yaml
+++ b/xos/configurations/cord/cord.yaml
@@ -41,6 +41,7 @@
           backend_network_label: hpc_client
           public_key: { get_artifact: [ SELF, pubkey, LOCAL_FILE] }
           private_key_fn: /opt/xos/synchronizers/vcpe/vcpe_private_key
+#          node_label: label_vsg
       artifacts:
           pubkey: /opt/xos/synchronizers/vcpe/vcpe_public_key
 
@@ -270,6 +271,8 @@
     mysite:
       type: tosca.nodes.Site
 
+    label_vsg:
+      type: tosca.nodes.NodeLabel
 
     # CORD Slices
     mysite_vsg:
diff --git a/xos/configurations/cord/make-vtn-networkconfig-json.sh b/xos/configurations/cord/make-vtn-networkconfig-json.sh
index 2cccd65..77b855d 100644
--- a/xos/configurations/cord/make-vtn-networkconfig-json.sh
+++ b/xos/configurations/cord/make-vtn-networkconfig-json.sh
@@ -74,8 +74,8 @@
                 ]
             }
         },
-        "org.onosproject.openstackswitching" : {
-            "openstackswitching" : {
+        "org.onosproject.openstackinterface" : {
+            "openstackinterface" : {
                  "do_not_push_flows" : "true",
                  "neutron_server" : "http://$NEUTRONIP:9696/v2.0/",
                  "keystone_server" : "http://$KEYSTONEIP:5000/v2.0/",
diff --git a/xos/configurations/vtn/cord-vtn-vsg.yaml b/xos/configurations/vtn/cord-vtn-vsg.yaml
index 1b26bba..c228a80 100644
--- a/xos/configurations/vtn/cord-vtn-vsg.yaml
+++ b/xos/configurations/vtn/cord-vtn-vsg.yaml
@@ -21,7 +21,7 @@
     public_addresses:
       type: tosca.nodes.AddressPool
       properties:
-          addresses: 10.123.0.0/24 10.124.0.0/24
+          addresses: 10.123.0.128/25
 
     service_vsg:
       type: tosca.nodes.VSGService
@@ -38,6 +38,7 @@
           wan_container_gateway_mac: 00:8c:fa:5b:09:d8
           wan_container_netbits: 24
           dns_servers: 8.8.8.8, 8.8.4.4
+#          node_label: label_vsg
       artifacts:
           pubkey: /opt/xos/synchronizers/vcpe/vcpe_public_key
 
@@ -62,6 +63,9 @@
     mysite:
       type: tosca.nodes.Site
 
+    label_vsg:
+      type: tosca.nodes.NodeLabel
+
     # Networks required by the CORD setup
     mysite_vsg-access:
       type: tosca.nodes.network.Network
diff --git a/xos/configurations/vtn/docker-compose.yml b/xos/configurations/vtn/docker-compose.yml
index 0fa718b..1839f78 100644
--- a/xos/configurations/vtn/docker-compose.yml
+++ b/xos/configurations/vtn/docker-compose.yml
@@ -17,7 +17,7 @@
         - ../setup:/root/setup:ro
         - ../common/xos_common_config:/opt/xos/xos_configuration/xos_common_config:ro
         - ./files/xos_vtn_config:/opt/xos/xos_configuration/xos_vtn_config:ro
-
+        - ./images:/opt/xos/images:ro
 
 xos_synchronizer_onos:
     image: xosproject/xos-synchronizer-openstack
diff --git a/xos/core/admin.py b/xos/core/admin.py
index 28d99fd..910ee97 100644
--- a/xos/core/admin.py
+++ b/xos/core/admin.py
@@ -1266,6 +1266,14 @@
     list_display_links = ('backend_status_icon', 'name', )
 
 class NodeForm(forms.ModelForm):
+    labels = forms.ModelMultipleChoiceField(
+        queryset=NodeLabel.objects.all(),
+        required=False,
+        help_text="Select which labels apply to this node",
+        widget=FilteredSelectMultiple(
+            verbose_name=('Labels'), is_stacked=False
+        )
+    )
     class Meta:
         model = Node
         widgets = {
@@ -1273,6 +1281,31 @@
             'deployment': LinkedSelect
         }
 
+    def __init__(self, *args, **kwargs):
+      request = kwargs.pop('request', None)
+      super(NodeForm, self).__init__(*args, **kwargs)
+
+      if self.instance and self.instance.pk:
+        self.fields['labels'].initial = self.instance.labels.all()
+
+    def save(self, commit=True):
+      node = super(NodeForm, self).save(commit=False)
+
+      node.labels = self.cleaned_data['labels']
+
+      if commit:
+        node.save()
+
+      return node
+
+
+class NodeLabelAdmin(XOSBaseAdmin):
+    list_display = ('name',)
+    list_display_links = ('name', )
+
+    fields = ('name', )
+
+
 class NodeAdmin(XOSBaseAdmin):
     form = NodeForm
     list_display = ('backend_status_icon', 'name', 'site_deployment')
@@ -1280,13 +1313,14 @@
     list_filter = ('site_deployment',)
 
     inlines = [TagInline,InstanceInline]
-    fieldsets = [('Node Details', {'fields': ['backend_status_text', 'name', 'site_deployment'], 'classes':['suit-tab suit-tab-details']})]
+    fieldsets = [('Node Details', {'fields': ['backend_status_text', 'name', 'site_deployment'], 'classes':['suit-tab suit-tab-details']}),
+                 ('Labels', {'fields': ['labels'], 'classes':['suit-tab suit-tab-labels']})]
     readonly_fields = ('backend_status_text', )
 
     user_readonly_fields = ['name','site_deployment']
     user_readonly_inlines = [TagInline,InstanceInline]
 
-    suit_form_tabs =(('details','Node Details'),('instances','Instances'))
+    suit_form_tabs =(('details','Node Details'),('instances','Instances'), ('labels', 'Labels'))
 
     def formfield_for_foreignkey(self, db_field, request, **kwargs):
         if db_field.name == 'site':
@@ -2107,6 +2141,7 @@
     admin.site.register(SiteRole)
     admin.site.register(SliceRole)
     admin.site.register(Node, NodeAdmin)
+    admin.site.register(NodeLabel, NodeLabelAdmin)
     #admin.site.register(SlicePrivilege, SlicePrivilegeAdmin)
     #admin.site.register(SitePrivilege, SitePrivilegeAdmin)
     admin.site.register(Instance, InstanceAdmin)
diff --git a/xos/core/models/__init__.py b/xos/core/models/__init__.py
index 628a3bb..2ee6b94 100644
--- a/xos/core/models/__init__.py
+++ b/xos/core/models/__init__.py
@@ -21,7 +21,7 @@
 from .credential import UserCredential,SiteCredential,SliceCredential
 from .site import SiteRole
 from .site import SitePrivilege
-from .node import Node
+from .node import Node, NodeLabel
 from .slicetag import SliceTag
 from .instance import Instance
 from .reservation import ReservedResource
diff --git a/xos/core/models/node.py b/xos/core/models/node.py
index 5496d6b..b825787 100644
--- a/xos/core/models/node.py
+++ b/xos/core/models/node.py
@@ -28,3 +28,9 @@
 
     def can_update(self, user):
         return user.can_update_site(self.site, allow=['tech'])
+
+class NodeLabel(PlCoreBase):
+    name = StrippedCharField(max_length=200, help_text="label name", unique=True)
+    node = models.ManyToManyField(Node, related_name="labels", blank=True)
+
+    def __unicode__(self): return u'%s' % (self.name)
diff --git a/xos/core/models/service.py b/xos/core/models/service.py
index 51f9b3c..ee28cf6 100644
--- a/xos/core/models/service.py
+++ b/xos/core/models/service.py
@@ -437,12 +437,21 @@
 class LeastLoadedNodeScheduler(Scheduler):
     # This scheduler always return the node with the fewest number of instances.
 
-    def __init__(self, slice):
+    def __init__(self, slice, label=None):
         super(LeastLoadedNodeScheduler, self).__init__(slice)
+        self.label = label
 
     def pick(self):
         from core.models import Node
-        nodes = list(Node.objects.all())
+        nodes = Node.objects.all()
+
+        if self.label:
+           nodes = nodes.filter(labels__name=self.label)
+
+        nodes = list(nodes)
+
+        if not nodes:
+            raise Exception("LeastLoadedNodeScheduler: No suitable nodes to pick from")
 
         # TODO: logic to filter nodes by which nodes are up, and which
         #   nodes the slice can instantiate on.
diff --git a/xos/services/ceilometer/models.py b/xos/services/ceilometer/models.py
index 65bffef..d8fb0fa 100644
--- a/xos/services/ceilometer/models.py
+++ b/xos/services/ceilometer/models.py
@@ -36,7 +36,8 @@
 
     KIND = CEILOMETER_KIND
     LOOK_FOR_IMAGES=[ #"trusty-server-multi-nic-docker", # CloudLab
-                      "trusty-server-multi-nic",
+                      "ceilometer-trusty-server-multi-nic",
+                      #"trusty-server-multi-nic",
                     ]
 
 
diff --git a/xos/services/cord/admin.py b/xos/services/cord/admin.py
index 4ec4c6f..56e46ae 100644
--- a/xos/services/cord/admin.py
+++ b/xos/services/cord/admin.py
@@ -108,6 +108,7 @@
     wan_container_gateway_mac = forms.CharField(required=False)
     wan_container_netbits = forms.CharField(required=False)
     dns_servers = forms.CharField(required=False)
+    node_label = forms.CharField(required=False)
 
     def __init__(self,*args,**kwargs):
         super (VSGServiceForm,self ).__init__(*args,**kwargs)
@@ -121,6 +122,7 @@
             self.fields['wan_container_gateway_mac'].initial = self.instance.wan_container_gateway_mac
             self.fields['wan_container_netbits'].initial = self.instance.wan_container_netbits
             self.fields['dns_servers'].initial = self.instance.dns_servers
+            self.fields['node_label'].initial = self.instance.node_label
 
     def save(self, commit=True):
         self.instance.bbs_api_hostname = self.cleaned_data.get("bbs_api_hostname")
@@ -132,6 +134,7 @@
         self.instance.wan_container_gateway_mac = self.cleaned_data.get("wan_container_gateway_mac")
         self.instance.wan_container_netbits = self.cleaned_data.get("wan_container_netbits")
         self.instance.dns_servers = self.cleaned_data.get("dns_servers")
+        self.instance.node_label = self.cleaned_data.get("node_label")
         return super(VSGServiceForm, self).save(commit=commit)
 
     class Meta:
@@ -143,7 +146,7 @@
     verbose_name_plural = "vSG Service"
     list_display = ("backend_status_icon", "name", "enabled")
     list_display_links = ('backend_status_icon', 'name', )
-    fieldsets = [(None,             {'fields': ['backend_status_text', 'name','enabled','versionNumber', 'description', "view_url", "icon_url", "service_specific_attribute",],
+    fieldsets = [(None,             {'fields': ['backend_status_text', 'name','enabled','versionNumber', 'description', "view_url", "icon_url", "service_specific_attribute", "node_label"],
                                      'classes':['suit-tab suit-tab-general']}),
                  ("backend config", {'fields': [ "backend_network_label", "bbs_api_hostname", "bbs_api_port", "bbs_server", "bbs_slice"],
                                      'classes':['suit-tab suit-tab-backend']}),
diff --git a/xos/services/cord/models.py b/xos/services/cord/models.py
index 9ff2dcb..b7f25af 100644
--- a/xos/services/cord/models.py
+++ b/xos/services/cord/models.py
@@ -369,7 +369,8 @@
                           ("wan_container_gateway_ip", ""),
                           ("wan_container_gateway_mac", ""),
                           ("wan_container_netbits", "24"),
-                          ("dns_servers", "8.8.8.8") )
+                          ("dns_servers", "8.8.8.8"),
+                          ("node_label", None) )
 
     def __init__(self, *args, **kwargs):
         super(VSGService, self).__init__(*args, **kwargs)
@@ -644,6 +645,9 @@
         slice = self.provider_service.slices.all()[0]
         return slice
 
+    def get_vsg_service(self):
+        return VSGService.get_service_objects().get(id=self.provider_service.id)
+
     def find_instance_for_s_tag(self, s_tag):
         #s_tags = STagBlock.objects.find(s_s_tag)
         #if s_tags:
@@ -669,7 +673,7 @@
         if slice.default_isolation == "container_vm":
             (node, parent) = ContainerVmScheduler(slice).pick()
         else:
-            (node, parent) = LeastLoadedNodeScheduler(slice).pick()
+            (node, parent) = LeastLoadedNodeScheduler(slice, label=self.get_vsg_service().node_label).pick()
 
         instance = Instance(slice = slice,
                         node = node,
diff --git a/xos/services/exampleservice/README.md b/xos/services/exampleservice/README.md
new file mode 100644
index 0000000..ce6210d
--- /dev/null
+++ b/xos/services/exampleservice/README.md
@@ -0,0 +1,8 @@
+# ExampleService
+
+This is an example XOS service, specifically the Django Model and Admin. 
+
+The Synchronizer corresponding to this service can be found in `../../synchronizers/exampleservice`.
+
+Documentation for this is located here: [XOS Guide : DevGuide : ExampleService](http://guide.xosproject.org/devguide/exampleservice/).
+
diff --git a/xos/services/exampleservice/__init__.py b/xos/services/exampleservice/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xos/services/exampleservice/__init__.py
diff --git a/xos/services/exampleservice/admin.py b/xos/services/exampleservice/admin.py
new file mode 100644
index 0000000..d4f6248
--- /dev/null
+++ b/xos/services/exampleservice/admin.py
@@ -0,0 +1,101 @@
+# admin.py - ExampleService Django Admin
+
+from core.admin import ReadOnlyAwareAdmin, SliceInline
+from core.middleware import get_request
+from core.models import User
+
+from django import forms
+from django.contrib import admin
+
+from services.exampleservice.models import *
+
+class ExampleServiceAdmin(ReadOnlyAwareAdmin):
+
+    model = ExampleService
+    verbose_name = SERVICE_NAME_VERBOSE
+    verbose_name_plural = SERVICE_NAME_VERBOSE_PLURAL
+
+    list_display = ('backend_status_icon', 'name', 'enabled',)
+    list_display_links = ('backend_status_icon', 'name', )
+
+    fieldsets = [(None, {
+        'fields': ['backend_status_text', 'name', 'enabled', 'versionNumber', 'description',],
+        'classes':['suit-tab suit-tab-general',],
+        })]
+
+    readonly_fields = ('backend_status_text', )
+    user_readonly_fields = ['name', 'enabled', 'versionNumber', 'description',]
+
+    inlines = [SliceInline]
+
+    extracontext_registered_admins = True
+
+    suit_form_tabs = (
+        ('general', 'Example Service Details', ),
+        ('slices', 'Slices',),
+        )
+
+    suit_form_includes = ((
+        'top',
+        'administration'),
+        )
+
+    def queryset(self, request):
+        return ExampleService.get_service_objects_by_user(request.user)
+
+admin.site.register(ExampleService, ExampleServiceAdmin)
+
+class ExampleTenantForm(forms.ModelForm):
+
+    class Meta:
+        model = ExampleTenant
+
+    creator = forms.ModelChoiceField(queryset=User.objects.all())
+
+    def __init__(self, *args, **kwargs):
+        super(ExampleTenantForm, self).__init__(*args, **kwargs)
+
+        self.fields['kind'].widget.attrs['readonly'] = True
+        self.fields['kind'].initial = SERVICE_NAME
+
+        self.fields['provider_service'].queryset = ExampleService.get_service_objects().all()
+
+        if self.instance:
+            self.fields['creator'].initial = self.instance.creator
+            self.fields['tenant_message'].initial = self.instance.tenant_message
+
+        if (not self.instance) or (not self.instance.pk):
+            self.fields['creator'].initial = get_request().user
+            if ExampleService.get_service_objects().exists():
+                self.fields['provider_service'].initial = ExampleService.get_service_objects().all()[0]
+
+    def save(self, commit=True):
+        self.instance.creator = self.cleaned_data.get('creator')
+        self.instance.tenant_message = self.cleaned_data.get('tenant_message')
+        return super(ExampleTenantForm, self).save(commit=commit)
+
+
+class ExampleTenantAdmin(ReadOnlyAwareAdmin):
+
+    verbose_name = TENANT_NAME_VERBOSE
+    verbose_name_plural = TENANT_NAME_VERBOSE_PLURAL
+
+    list_display = ('id', 'backend_status_icon', 'instance', 'tenant_message')
+    list_display_links = ('backend_status_icon', 'instance', 'tenant_message', 'id')
+
+    fieldsets = [(None, {
+        'fields': ['backend_status_text', 'kind', 'provider_service', 'instance', 'creator', 'tenant_message'],
+        'classes': ['suit-tab suit-tab-general'],
+        })]
+
+    readonly_fields = ('backend_status_text', 'instance',)
+
+    form = ExampleTenantForm
+
+    suit_form_tabs = (('general', 'Details'),)
+
+    def queryset(self, request):
+        return ExampleTenant.get_tenant_objects_by_user(request.user)
+
+admin.site.register(ExampleTenant, ExampleTenantAdmin)
+
diff --git a/xos/services/exampleservice/models.py b/xos/services/exampleservice/models.py
new file mode 100644
index 0000000..6305b54
--- /dev/null
+++ b/xos/services/exampleservice/models.py
@@ -0,0 +1,52 @@
+# models.py -  ExampleService Models
+
+from core.models import Service, TenantWithContainer
+from django.db import models, transaction
+
+SERVICE_NAME = 'exampleservice'
+SERVICE_NAME_VERBOSE = 'Example Service'
+SERVICE_NAME_VERBOSE_PLURAL = 'Example Services'
+TENANT_NAME_VERBOSE = 'Example Tenant'
+TENANT_NAME_VERBOSE_PLURAL = 'Example Tenants'
+
+class ExampleService(Service):
+
+    KIND = SERVICE_NAME
+
+    class Meta:
+        app_label = SERVICE_NAME
+        verbose_name = SERVICE_NAME_VERBOSE
+        proxy = True
+
+class ExampleTenant(TenantWithContainer):
+
+    KIND = SERVICE_NAME
+
+    class Meta:
+        verbose_name = TENANT_NAME_VERBOSE
+
+    tenant_message = models.CharField(max_length=254, help_text="Tenant Message to Display")
+
+    def __init__(self, *args, **kwargs):
+        exampleservice = ExampleService.get_service_objects().all()
+        if exampleservice:
+            self._meta.get_field('provider_service').default = exampleservice[0].id
+        super(ExampleTenant, self).__init__(*args, **kwargs)
+
+    def save(self, *args, **kwargs):
+        super(ExampleTenant, self).save(*args, **kwargs)
+        model_policy_exampletenant(self.pk)
+
+    def delete(self, *args, **kwargs):
+        self.cleanup_container()
+        super(ExampleTenant, self).delete(*args, **kwargs)
+
+
+def model_policy_exampletenant(pk):
+    with transaction.atomic():
+        tenant = ExampleTenant.objects.select_for_update().filter(pk=pk)
+        if not tenant:
+            return
+        tenant = tenant[0]
+        tenant.manage_container()
+
diff --git a/xos/synchronizers/exampleservice/exampleservice-synchronizer.py b/xos/synchronizers/exampleservice/exampleservice-synchronizer.py
new file mode 100644
index 0000000..90d2c98
--- /dev/null
+++ b/xos/synchronizers/exampleservice/exampleservice-synchronizer.py
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+
+# Runs the standard XOS synchronizer
+
+import importlib
+import os
+import sys
+
+synchronizer_path = os.path.join(os.path.dirname(
+    os.path.realpath(__file__)), "../../synchronizers/base")
+sys.path.append(synchronizer_path)
+mod = importlib.import_module("xos-synchronizer")
+mod.main()
+
diff --git a/xos/synchronizers/exampleservice/exampleservice_config b/xos/synchronizers/exampleservice/exampleservice_config
new file mode 100644
index 0000000..7e59fdd
--- /dev/null
+++ b/xos/synchronizers/exampleservice/exampleservice_config
@@ -0,0 +1,24 @@
+# Required by XOS
+[db]
+name=xos
+user=postgres
+password=password
+host=localhost
+port=5432
+
+# Required by XOS
+[api]
+nova_enabled=True
+
+# Sets options for the synchronizer
+[observer]
+name=exampleservice
+dependency_graph=/opt/xos/synchronizers/exampleservice/model-deps
+steps_dir=/opt/xos/synchronizers/exampleservice/steps
+sys_dir=/opt/xos/synchronizers/exampleservice/sys
+logfile=/var/log/xos_backend.log
+pretend=False
+backoff_disabled=True
+save_ansible_output=True
+proxy_ssh=False
+
diff --git a/xos/synchronizers/exampleservice/model-deps b/xos/synchronizers/exampleservice/model-deps
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/xos/synchronizers/exampleservice/model-deps
@@ -0,0 +1 @@
+{}
diff --git a/xos/synchronizers/exampleservice/steps/exampletenant_playbook.yaml b/xos/synchronizers/exampleservice/steps/exampletenant_playbook.yaml
new file mode 100644
index 0000000..9da06f5
--- /dev/null
+++ b/xos/synchronizers/exampleservice/steps/exampletenant_playbook.yaml
@@ -0,0 +1,18 @@
+---
+# exampletenant_playbook
+
+- hosts: "{{ instance_name }}"
+  connection: ssh
+  user: ubuntu
+  sudo: yes
+  gather_facts: no
+
+  tasks:
+  - name: install apache
+    apt:
+      name=apache2
+      update_cache=yes
+
+  - name: write message
+    shell: echo "{{ tenant_message }}" > /var/www/html/index.html
+
diff --git a/xos/synchronizers/exampleservice/steps/sync_exampletenant.py b/xos/synchronizers/exampleservice/steps/sync_exampletenant.py
new file mode 100644
index 0000000..bc4169b
--- /dev/null
+++ b/xos/synchronizers/exampleservice/steps/sync_exampletenant.py
@@ -0,0 +1,40 @@
+import os
+import sys
+from django.db.models import Q, F
+from services.exampleservice.models import ExampleService, ExampleTenant
+from synchronizers.base.SyncInstanceUsingAnsible import SyncInstanceUsingAnsible
+
+parentdir = os.path.join(os.path.dirname(__file__), "..")
+sys.path.insert(0, parentdir)
+
+class SyncExampleTenant(SyncInstanceUsingAnsible):
+
+    provides = [ExampleTenant]
+
+    observes = ExampleTenant
+
+    requested_interval = 0
+
+    template_name = "exampletenant_playbook.yaml"
+
+    service_key_name = "/opt/xos/synchronizers/exampleservice/exampleservice_private_key"
+
+    def __init__(self, *args, **kwargs):
+        super(SyncExampleTenant, self).__init__(*args, **kwargs)
+
+    def fetch_pending(self, deleted):
+
+        if (not deleted):
+            objs = ExampleTenant.get_tenant_objects().filter(
+                Q(enacted__lt=F('updated')) | Q(enacted=None), Q(lazy_blocked=False))
+        else:
+            # If this is a deletion we get all of the deleted tenants..
+            objs = ExampleTenant.get_deleted_tenant_objects()
+
+        return objs
+
+    # Gets the attributes that are used by the Ansible template but are not
+    # part of the set of default attributes.
+    def get_extra_attributes(self, o):
+        return {"tenant_message": o.tenant_message}
+
diff --git a/xos/synchronizers/monitoring_channel/steps/sync_monitoringchannel.yaml b/xos/synchronizers/monitoring_channel/steps/sync_monitoringchannel.yaml
index 06403a6..1c4da12 100644
--- a/xos/synchronizers/monitoring_channel/steps/sync_monitoringchannel.yaml
+++ b/xos/synchronizers/monitoring_channel/steps/sync_monitoringchannel.yaml
@@ -32,38 +32,38 @@
      - remove container
 {% else %}
 {% if full_setup %}
-  - name: Docker repository
-    copy: src=/opt/xos/synchronizers/monitoring_channel/files/docker.list
-      dest=/etc/apt/sources.list.d/docker.list
-
-  - name: Import the repository key
-    apt_key: keyserver=keyserver.ubuntu.com id=36A1D7869245C8950F966E92D8576A8BA88D21E9
-
-  - name: install Docker
-    apt: name=lxc-docker state=present update_cache=yes
-
-  - name: install python-setuptools
-    apt: name=python-setuptools state=present
-
-  - name: install pip
-    easy_install: name=pip
-
-  - name: install docker-py
-    pip: name=docker-py version=0.5.3
-
-  - name: install Pipework
-    get_url: url=https://raw.githubusercontent.com/jpetazzo/pipework/master/pipework
-       dest=/usr/local/bin/pipework
-       mode=0755
-
-  - name: Disable resolvconf service
-    shell: service resolvconf stop
-    shell: echo manual > /etc/init/resolvconf.override
-    shell: rm -f /etc/resolv.conf
-
-  - name: Install resolv.conf
-    copy: src=/opt/xos/synchronizers/monitoring_channel/files/vm-resolv.conf
-      dest=/etc/resolv.conf
+#  - name: Docker repository
+#    copy: src=/opt/xos/synchronizers/monitoring_channel/files/docker.list
+#      dest=/etc/apt/sources.list.d/docker.list
+#
+#  - name: Import the repository key
+#    apt_key: keyserver=keyserver.ubuntu.com id=36A1D7869245C8950F966E92D8576A8BA88D21E9
+#
+#  - name: install Docker
+#    apt: name=lxc-docker state=present update_cache=yes
+#
+#  - name: install python-setuptools
+#    apt: name=python-setuptools state=present
+#
+#  - name: install pip
+#    easy_install: name=pip
+#
+#  - name: install docker-py
+#    pip: name=docker-py version=0.5.3
+#
+#  - name: install Pipework
+#    get_url: url=https://raw.githubusercontent.com/jpetazzo/pipework/master/pipework
+#       dest=/usr/local/bin/pipework
+#       mode=0755
+#
+#  - name: Disable resolvconf service
+#    shell: service resolvconf stop
+#    shell: echo manual > /etc/init/resolvconf.override
+#    shell: rm -f /etc/resolv.conf
+#
+#  - name: Install resolv.conf
+#    copy: src=/opt/xos/synchronizers/monitoring_channel/files/vm-resolv.conf
+#      dest=/etc/resolv.conf
 {% endif %}
 
   - name: ceilometer proxy config
diff --git a/xos/synchronizers/monitoring_channel/templates/start-monitoring-channel.sh.j2 b/xos/synchronizers/monitoring_channel/templates/start-monitoring-channel.sh.j2
index a0ebb0c..1685e07 100755
--- a/xos/synchronizers/monitoring_channel/templates/start-monitoring-channel.sh.j2
+++ b/xos/synchronizers/monitoring_channel/templates/start-monitoring-channel.sh.j2
@@ -21,7 +21,7 @@
 if [ "$?" == 1 ]
 then
     #sudo docker build -t monitoring-channel -f Dockerfile.monitoring_channel .
-    sudo docker pull srikanthvavila/monitoring-channel
+    #sudo docker pull srikanthvavila/monitoring-channel
 if [ -z "$HEADNODEFLATLANIP" ] || [ "$HEADNODEFLATLANIP" == "None" ]
 then
     docker run -d --name=$MONITORING_CHANNEL --privileged=true -p $HOST_FORWARDING_PORT_FOR_CEILOMETER:8000 -v /usr/local/share/monitoring-channel-{{ unique_id }}_ceilometer_proxy_config:/usr/local/share/ceilometer_proxy_config srikanthvavila/monitoring-channel
@@ -43,4 +43,4 @@
 #cat /usr/local/share/monitoring-channel-{{ unique_id }}_ceilometer_proxy_config | sudo docker exec -i $MONITORING_CHANNEL bash -c 'cat > /usr/local/share/ceilometer_proxy_config'
 
 # Attach to container
-docker start -a $MONITORING_CHANNEL
+#docker start -a $MONITORING_CHANNEL
diff --git a/xos/synchronizers/openstack/steps/sync_controller_networks.py b/xos/synchronizers/openstack/steps/sync_controller_networks.py
index f8b2292..5375a8d 100644
--- a/xos/synchronizers/openstack/steps/sync_controller_networks.py
+++ b/xos/synchronizers/openstack/steps/sync_controller_networks.py
@@ -1,5 +1,7 @@
 import os
 import base64
+import struct
+import socket
 from collections import defaultdict
 from netaddr import IPAddress, IPNetwork
 from django.db.models import F, Q
@@ -35,10 +37,17 @@
         return cidr
 
     def alloc_gateway(self, subnet):
-        parts = subnet.split(".")
-        if len(parts)!=4:
-            raise Exception("Invalid subnet %s" % subnet)
-        return ".".join(parts[:3]) + ".1"
+        # given a CIDR, allocate a default gateway using the .1 address within
+        # the subnet.
+        #    10.123.0.0/24 --> 10.123.0.1
+        #    207.141.192.128/28 --> 207.141.192.129
+        (network, bits) = subnet.split("/")
+        network=network.strip()
+        bits=int(bits.strip())
+        netmask = (~(pow(2,32-bits)-1) & 0xFFFFFFFF)
+        ip = struct.unpack("!L", socket.inet_aton(network))[0]
+        ip = ip & netmask | 1
+        return socket.inet_ntoa(struct.pack("!L", ip))
 
     def save_controller_network(self, controller_network):
         network_name = controller_network.network.name
diff --git a/xos/tools/apigen/fields.template.txt b/xos/tools/apigen/fields.template.txt
new file mode 100644
index 0000000..ad5a0b4
--- /dev/null
+++ b/xos/tools/apigen/fields.template.txt
@@ -0,0 +1,6 @@
+{% for object in generator.all %}
+Object {{ object }}:
+Fields:
+{% for field in object.fields %}{{ field.name }}:{{ field.type }}
+{% endfor %}
+{% endfor %}
diff --git a/xos/tools/apigen/modelgen b/xos/tools/apigen/modelgen
index 414540d..068b7cb 100644
--- a/xos/tools/apigen/modelgen
+++ b/xos/tools/apigen/modelgen
@@ -11,10 +11,14 @@
 
 # Django set up
 
+import django
 sys.path.append('.')
+sys.path.append('/opt/xos')
 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
 from django.db.models.fields.related import ForeignKey, ManyToManyField
 
+django.setup()
+
 options = None
 
 
@@ -30,7 +34,9 @@
 
 def enum_classes(apps):
     global app_map
+    global class_map
     app_map = {}
+    class_map = {}
     model_classes = []
     for app in apps:
             orig_app=app
@@ -49,6 +55,16 @@
                     if type(c)==type(PlCoreBase) and c.__name__ not in options.blacklist:
                             model_classes.append(c)
                             app_map[c.__name__]=orig_app
+                            c.class_name = c.__name__
+                            file_name = c.__module__.rsplit('.',1)[1]
+                            try:
+                                if (file_name not in class_map[orig_app]):
+                                    class_map[orig_app].append({file_name:[c]})
+                                else:
+                                    class_map[orig_app][file_name].append(c)
+
+                            except KeyError:
+                                class_map[orig_app] = [{file_name:[c]}]
 
 
     return model_classes
@@ -98,6 +114,9 @@
 		return name
 		
 class Generator(dict):
+        def __init__(self):
+            self.apps = {}
+
 	def all(self):
 		return self.values()
 
@@ -117,7 +136,19 @@
                     obj.app = app_map[o.__name__]
                 except KeyError:
                     print "KeyError: %r"%o.__name__
-                    pdb.set_trace()
+                obj.class_name = o.class_name
+
+                file_name = o.__module__.rsplit('.',1)[1]
+
+                try:
+                    if (file_name not in self.apps[obj.app]):
+                        self.apps[obj.app][file_name]=[obj]
+                    else:
+                        self.apps[obj.app][file_name].append(obj)
+
+                except KeyError:
+                    self.apps[obj.app] = {file_name:[obj]}
+
 		self[str(obj).lower()]=obj
 
 	def compute_links(self):
@@ -150,7 +181,8 @@
                                         pass
 				else:
                                         f.type = f.__class__.__name__
-
+                                        if (type(f)==ForeignKey):
+                                            f.related.model.class_name = f.related.model.__name__
                                         if (f.name not in base_props):
                                             obj.fields.append(f) 
                                         obj.props.append(f.name)
@@ -189,6 +221,9 @@
         global options
         parser = OptionParser(usage="modelgen [options] <template_fn>", )
 
+        parser.add_option("-d", "--dict", dest="dict",
+             help="dictionary to replace text in output", metavar="DICT", default=[], action="append")
+
         parser.add_option("-a", "--app", dest="apps",
              help="list of applications to parse", metavar="APP", default=[], action="append")
         parser.add_option("-b", "--blacklist", dest="blacklist",
@@ -228,7 +263,31 @@
 	template_contents = open(template_name).read()
 	template = Template(template_contents)
 	context = Context({'generator':generator})
-	print template.render(context)
+	rendered = template.render(context)
+        lines = rendered.splitlines()
+        current_buffer = []
+        for l in lines:
+            if (l.startswith('+++')):
+                filename = l[4:]
+                fil = open(filename,'w')
+                buf = '\n'.join(current_buffer)
+
+                obuf = buf
+                for d in options.dict:
+                    df = open(d).read()
+                    d = json.loads(df)
+
+                    pattern = re.compile(r'\b(' + '|'.join(d.keys()) + r')\b')
+                    obuf = pattern.sub(lambda x: d[x.group()], buf)
+                fil.write(obuf)
+                fil.close()
+
+                print 'Written to file %s'%filename
+                current_buffer = []
+            else:
+                current_buffer.append(l)
+        if (current_buffer):
+            print '\n'.join(current_buffer)
 
 
 if (__name__=='__main__'):
diff --git a/xos/tools/apigen/modelyaml.fields.template.txt b/xos/tools/apigen/modelyaml.fields.template.txt
new file mode 100644
index 0000000..69129ed
--- /dev/null
+++ b/xos/tools/apigen/modelyaml.fields.template.txt
@@ -0,0 +1,12 @@
+{% for object in generator.all %}
+{{ object.camel }}:
+fields:
+  {% for f in object.fields %}
+  - name: {{ f.name }}
+    type: {{ f.type }}
+    {% if f.help_text %}
+    help_text: {{ f.help_text }}
+    {% endif %}
+    null: {{ f.null }}
+{% endfor %}
+{% endfor %}
diff --git a/xos/tools/apigen/synchronizer.template.txt b/xos/tools/apigen/synchronizer.template.txt
new file mode 100644
index 0000000..c784e21
--- /dev/null
+++ b/xos/tools/apigen/synchronizer.template.txt
@@ -0,0 +1,71 @@
+{% for app,files in generator.apps.items %}
+{% for file,models in files.items %}
+{% for m in models %}
+# This file implements the synchronization of the {{ m.class_name }} model
+# TODO (see below):
+
+import os
+import base64
+import socket
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from {{app}}.models.{{ file }} import {{ m.class_name.title }}
+from synchronizers.base.ansible import *
+from synchronizers.base.syncstep import *
+from xos.logger import observer_logger as logger
+
+class Sync{{m.camel.title}}(SyncStep):
+    # The model synchronized
+    provides=[{{m.class_name.title}}]
+    # How often do you want this to run. 0 means immediately following changes to the model
+    requested_interval=0
+    # The model in which changes trigger this module
+    observes={{m.class_name}}
+    # The Ansible recipe that does the bulk of the synchronization (hopefully)
+    playbook='sync_{{m.class_name}}.yaml'
+
+    # 1. Populate a data structure to pass into your ansible recipe, based on the data model. 
+    def map_sync_inputs(self, {{ m.class_name }}): 
+        fields = {
+            {% for f in m.fields %}'{{f.name}}': {{f.name}},
+            {% endfor %}
+        }
+        return fields
+        
+    # 2. Store the result of the operation in the model
+    def map_sync_outputs(self, {{ m.class_name }}, res):
+        // Store the output of res in the {{m.class_name}} object 
+        return
+    
+    # 3. Populate the data structure that identifies objects to delete
+    def map_delete_inputs(self, {{ m.class_name }}, res):
+        fields = {
+            {% for f in m.fields %}'{{f.name}}': {{f.name}},
+            {% endfor %}
+               'delete':True
+        }
+        return fields
+
+
++++ Sync{{ m.class_name }}.py
+---
+- hosts: 127.0.0.1
+  connection: local
+  tasks:
+  - task_1:
+      {% for f in m.fields %}
+      {{f.name}}: {% templatetag openbrace %}{% templatetag openbrace %}{{f.name}}{% templatetag closebrace %}{% templatetag closebrace %}
+      {% endfor %}
+      {% verbatim %}
+      {% if delete %}
+      state: absent
+      {% else %}
+      state: present
+      {% endif %}
+      {% endif %}
+      {% endverbatim %}
++++ Sync{{ m.class_name }}.yaml
+{% endfor %}
+{% endfor %}
+{% endfor %}
diff --git a/xos/tools/apigen/type_map b/xos/tools/apigen/type_map
new file mode 100644
index 0000000..ac4a8a8
--- /dev/null
+++ b/xos/tools/apigen/type_map
@@ -0,0 +1,10 @@
+{
+    "AutoField":"int64",
+    "ForeignKey":"InstanceIdentifier",
+    "CharField":"string",
+    "TextField":"string",
+    "FloatField":"float64",
+    "BooleanField":"boolean",
+    "StrippedCharField":"string",
+    "DateTimeField":"date-and-time"
+}
diff --git a/xos/tools/apigen/yang.template.txt b/xos/tools/apigen/yang.template.txt
new file mode 100644
index 0000000..ad4b1b1
--- /dev/null
+++ b/xos/tools/apigen/yang.template.txt
@@ -0,0 +1,21 @@
+{% for app,files in generator.apps.items %}
+{% for file,m in files.items %}
+module xos-{{ app }}-{{ file }} {
+    namespace "urn:xos:{{app}}.{{ file }}";
+    prefix xos-cs;
+
+    import complex-types {prefix ct;}
+    revision 2016-2-24 {
+        description "Initial";
+    }   
+
+    complex-type {{ m.class_name }} {
+      {% for f in m.fields %}
+
+      leaf {{ f.name }} { type {{ f.type }}{% ifequal f.type "ForeignKey" %} { ct:instance-type {{f.related.model.class_name}};{% if f.null%}{%else%}require-instance true{% endif %}{% endifequal %};{% if f.max_length %} { length {{ f.max_length }};{% endif %}{% if None %}default "{{ f.default }}";{% endif %}}
+      {% endfor %}
+    }
+}
++++ {{ app }}-{{ file}}.yang
+{% endfor %}
+{% endfor %}
diff --git a/xos/tosca/README.md b/xos/tosca/README.md
new file mode 100644
index 0000000..98f0aaf
--- /dev/null
+++ b/xos/tosca/README.md
@@ -0,0 +1,13 @@
+## TOSCA Interface Definition
+
+This directory implements a TOSCA interface for XOS,
+which can be extended to include specifications for
+service models added to XOS. The directory is organized
+as follows:
+
+ * custom_types -- Defines schema for XOS-specific models.
+   * `.m4` files are source.
+   * `.yaml` files are generated.
+ * definitions -- Defines schema for TOSCA's base models.
+ * resources -- Translates TOSCA specification to Django API.
+ * sample -- Example TOSCA models.
diff --git a/xos/tosca/custom_types/xos.m4 b/xos/tosca/custom_types/xos.m4
index 15e9710..9773822 100644
--- a/xos/tosca/custom_types/xos.m4
+++ b/xos/tosca/custom_types/xos.m4
@@ -230,6 +230,9 @@
             dns_servers:
                 type: string
                 required: false
+            node_label:
+                type: string
+                required: false
 
     tosca.nodes.VBNGService:
         derived_from: tosca.nodes.Root
@@ -715,6 +718,16 @@
             node:
                 type: tosca.capabilities.xos.Node
 
+    tosca.nodes.NodeLabel:
+        derived_from: tosca.nodes.Root
+        description: >
+            An XOS NodeLabel.
+        properties:
+            xos_base_props
+        capabilities:
+            node:
+                type: tosca.capabilities.xos.NodeLabel
+
     tosca.nodes.DashboardView:
         derived_from: tosca.nodes.Root
         description: >
@@ -849,6 +862,10 @@
         derived_from: tosca.relationships.Root
         valid_target_types: [ tosca.capabilities.xos.DashboardView ]
 
+    tosca.relationships.HasLabel:
+        derived_from: tosca.relationships.Root
+        valid_target_types: [ tosca.capabilities.xos.NodeLabel ]
+
     tosca.capabilities.xos.Service:
         derived_from: tosca.capabilities.Root
         description: An XOS Service
@@ -893,6 +910,10 @@
         derived_from: tosca.capabilities.Root
         description: An XOS Node
 
+    tosca.capabilities.xos.NodeLabel:
+        derived_from: tosca.capabilities.Root
+        description: An XOS NodeLabel
+
     tosca.capabilities.xos.Image:
         derived_from: tosca.capabilities.Root
         description: An XOS Image
diff --git a/xos/tosca/custom_types/xos.yaml b/xos/tosca/custom_types/xos.yaml
index 88b3388..21e8b8b 100644
--- a/xos/tosca/custom_types/xos.yaml
+++ b/xos/tosca/custom_types/xos.yaml
@@ -332,6 +332,9 @@
             dns_servers:
                 type: string
                 required: false
+            node_label:
+                type: string
+                required: false
 
     tosca.nodes.VBNGService:
         derived_from: tosca.nodes.Root
@@ -1005,6 +1008,27 @@
             node:
                 type: tosca.capabilities.xos.Node
 
+    tosca.nodes.NodeLabel:
+        derived_from: tosca.nodes.Root
+        description: >
+            An XOS NodeLabel.
+        properties:
+            no-delete:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to delete this object
+            no-create:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to create this object
+            no-update:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to update this object
+        capabilities:
+            node:
+                type: tosca.capabilities.xos.NodeLabel
+
     tosca.nodes.DashboardView:
         derived_from: tosca.nodes.Root
         description: >
@@ -1150,6 +1174,10 @@
         derived_from: tosca.relationships.Root
         valid_target_types: [ tosca.capabilities.xos.DashboardView ]
 
+    tosca.relationships.HasLabel:
+        derived_from: tosca.relationships.Root
+        valid_target_types: [ tosca.capabilities.xos.NodeLabel ]
+
     tosca.capabilities.xos.Service:
         derived_from: tosca.capabilities.Root
         description: An XOS Service
@@ -1194,6 +1222,10 @@
         derived_from: tosca.capabilities.Root
         description: An XOS Node
 
+    tosca.capabilities.xos.NodeLabel:
+        derived_from: tosca.capabilities.Root
+        description: An XOS NodeLabel
+
     tosca.capabilities.xos.Image:
         derived_from: tosca.capabilities.Root
         description: An XOS Image
diff --git a/xos/tosca/resources/node.py b/xos/tosca/resources/node.py
index 59b915b..99e756f 100644
--- a/xos/tosca/resources/node.py
+++ b/xos/tosca/resources/node.py
@@ -5,7 +5,7 @@
 sys.path.append("/opt/tosca")
 from translator.toscalib.tosca_template import ToscaTemplate
 
-from core.models import Node, Site, Deployment, SiteDeployment
+from core.models import Node, NodeLabel, Site, Deployment, SiteDeployment
 
 from xosresource import XOSResource
 
@@ -32,6 +32,17 @@
 
         return args
 
+    def postprocess(self, obj):
+        # We can't set the labels when we create a Node, because they're
+        # ManyToMany related, and the node doesn't exist yet.
+        labels=[]
+        for label_name in self.get_requirements("tosca.relationships.HasLabel"):
+            labels.append(self.get_xos_object(NodeLabel, name=label_name))
+        if labels:
+            self.info("Updated labels for node '%s'" % obj)
+            obj.labels = labels
+            obj.save()
+
     def create(self):
         nodetemplate = self.nodetemplate
         sliceName = nodetemplate.name
diff --git a/xos/tosca/resources/nodelabel.py b/xos/tosca/resources/nodelabel.py
new file mode 100644
index 0000000..9a8df3e
--- /dev/null
+++ b/xos/tosca/resources/nodelabel.py
@@ -0,0 +1,15 @@
+import os
+import pdb
+import sys
+import tempfile
+sys.path.append("/opt/tosca")
+from translator.toscalib.tosca_template import ToscaTemplate
+
+from core.models import NodeLabel
+
+from xosresource import XOSResource
+
+class XOSNodeLabel(XOSResource):
+    provides = "tosca.nodes.NodeLabel"
+    xos_model = NodeLabel
+
diff --git a/xos/tosca/resources/vcpeservice.py b/xos/tosca/resources/vcpeservice.py
index 5c7b2a7..2a6a56d 100644
--- a/xos/tosca/resources/vcpeservice.py
+++ b/xos/tosca/resources/vcpeservice.py
@@ -15,5 +15,5 @@
     copyin_props = ["view_url", "icon_url", "enabled", "published", "public_key",
                     "private_key_fn", "versionNumber", "backend_network_label",
                     "wan_container_gateway_ip", "wan_container_gateway_mac",
-                    "wan_container_netbits", "dns_servers"]
+                    "wan_container_netbits", "dns_servers", "node_label"]