Merge branch 'master' into feature/ceilometerDashboard
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7d279c5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,14 @@
+## Getting Started with XOS and CORD
+
+For a general introduction to XOS and how it is used in CORD, see
+http://guide.xosproject.org. The "Developer Guide" at that URL is
+especially helpful, although it is sync'ed with the previous
+release (currently Burwell), which likely lags what's in master.
+Additional design notes, presentations, and other collateral are 
+also available at http://xosproject.org.
+
+The quickest way to get started is to look at the collection of
+"stock" configurations in *xos/configurations*. The *cord* 
+configuration in that directory corresponds to our current 
+CORD development environment, and the README you'll find there
+will help you get started.
diff --git a/containers/postgresql/Makefile b/containers/postgresql/Makefile
index 38f159c..c50923e 100644
--- a/containers/postgresql/Makefile
+++ b/containers/postgresql/Makefile
@@ -1,5 +1,5 @@
-IMAGE_NAME:=xosproject/xos-postgress
-CONTAINER_NAME:=xos-db-postgress
+IMAGE_NAME:=xosproject/xos-postgres
+CONTAINER_NAME:=xos-db-postgres
 NO_DOCKER_CACHE?=false
 
 .PHONY: build
diff --git a/containers/xos-compose.yml b/containers/xos-compose.yml
index 27a3b5a..464b560 100644
--- a/containers/xos-compose.yml
+++ b/containers/xos-compose.yml
@@ -1,5 +1,5 @@
 xos_db:
-    image: xosproject/xos-postgress
+    image: xosproject/xos-postgres
     expose:
         - "5432"
 
diff --git a/containers/xos/Dockerfile b/containers/xos/Dockerfile
index 37bd55b..38e08a1 100644
--- a/containers/xos/Dockerfile
+++ b/containers/xos/Dockerfile
@@ -47,7 +47,7 @@
     django-geoposition \
     django-ipware \
     django_rest_swagger \
-    django-suit \
+    django-suit==0.3a1 \
     django-timezones \
     djangorestframework==2.4.4 \
     dnslib \
diff --git a/containers/xos/Dockerfile.templ b/containers/xos/Dockerfile.templ
index e669692..5071f68 100644
--- a/containers/xos/Dockerfile.templ
+++ b/containers/xos/Dockerfile.templ
@@ -47,7 +47,7 @@
     django-geoposition \
     django-ipware \
     django_rest_swagger \
-    django-suit \
+    django-suit==0.3a1 \
     django-timezones \
     djangorestframework==2.4.4 \
     dnslib \
diff --git a/xos/configurations/common/Makefile.devstack b/xos/configurations/common/Makefile.devstack
new file mode 100644
index 0000000..2f31696
--- /dev/null
+++ b/xos/configurations/common/Makefile.devstack
@@ -0,0 +1,34 @@
+# This shouldn't be hardcoded
+DEVSTACK_ROOT:=~/devstack
+
+all: prereqs admin-openrc flat_name nodes_yaml public_key private_key ceilometer_url node_key
+
+prereqs:
+	make -f Makefile.prereqs
+
+admin-openrc:
+	bash ./devstack-creds.sh $(DEVSTACK_ROOT) > admin-openrc.sh
+	touch controller_settings
+
+flat_name:
+	echo private|tr -d '\n' > flat_net_name
+	bash -c "source admin-openrc.sh; openstack network set --share private"
+
+nodes_yaml:
+	bash ./make-cloudlab-nodes-yaml.sh
+
+ceilometer_url:
+	echo http://`hostname -i`/xosmetering/ > ceilometer_url
+
+public_key: ~/.ssh/id_rsa.pub
+	cp ~/.ssh/id_rsa.pub .
+
+private_key: ~/.ssh/id_rsa
+	cp ~/.ssh/id_rsa .
+
+~/.ssh/id_rsa.pub:
+	cat /dev/zero | ssh-keygen -q -N ""
+
+node_key:
+	sudo cat ~/.ssh/id_rsa > node_key
+	sudo cat ~/.ssh/id_rsa.pub > node_key.pub
diff --git a/xos/configurations/common/devstack-creds.sh b/xos/configurations/common/devstack-creds.sh
new file mode 100644
index 0000000..b90e6ec
--- /dev/null
+++ b/xos/configurations/common/devstack-creds.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+DEVSTACK_ROOT=$1
+
+source $DEVSTACK_ROOT/openrc admin admin
+echo export OS_TENANT_NAME=$OS_TENANT_NAME
+echo export OS_USERNAME=$OS_USERNAME
+echo export OS_PASSWORD=$OS_PASSWORD
+echo export OS_AUTH_URL=$OS_AUTH_URL
diff --git a/xos/configurations/common/devstack.yaml b/xos/configurations/common/devstack.yaml
new file mode 100644
index 0000000..cd17b1c
--- /dev/null
+++ b/xos/configurations/common/devstack.yaml
@@ -0,0 +1,87 @@
+tosca_definitions_version: tosca_simple_yaml_1_0
+
+# Note:
+#   assumes the following have been created and filled with appropriate data:
+#       /root/setup/admin_openrc
+#       /root/setup/flat_net_name
+#       /root/setup/padmin_public_key
+
+description: >
+    * Adds OpenCloud Sites, Deployments, and Controllers.
+
+imports:
+   - custom_types/xos.yaml
+
+topology_template:
+  node_templates:
+    cirros-0.3.4-x86_64-uec:
+      type: tosca.nodes.Image
+      properties:
+         disk_format: RAW
+         container_format: BARE
+
+    MyDeployment:
+      type: tosca.nodes.Deployment
+      properties:
+          flavors: m1.large, m1.medium, m1.small
+      requirements:
+          - image:
+              node: cirros-0.3.4-x86_64-uec
+              relationship: tosca.relationships.SupportsImage
+
+    CloudLab:
+      type: tosca.nodes.Controller
+      requirements:
+          - deployment:
+              node: MyDeployment
+              relationship: tosca.relationships.ControllerDeployment
+      properties:
+          backend_type: OpenStack
+          version: Kilo
+          auth_url: { get_script_env: [ SELF, adminrc, OS_AUTH_URL, LOCAL_FILE] }
+          admin_user: { get_script_env: [ SELF, adminrc, OS_USERNAME, LOCAL_FILE] }
+          admin_password: { get_script_env: [ SELF, adminrc, OS_PASSWORD, LOCAL_FILE] }
+          admin_tenant: { get_script_env: [ SELF, adminrc, OS_TENANT_NAME, LOCAL_FILE] }
+          rabbit_user: { get_script_env: [ SELF, controller_settings, RABBIT_USER, LOCAL_FILE] }
+          rabbit_password: { get_script_env: [ SELF, controller_settings, RABBIT_PASS, LOCAL_FILE] }
+          rabbit_host: { get_script_env: [ SELF, controller_settings, CONTROLLER_IP, LOCAL_FILE] }
+          domain: Default
+      artifacts:
+          adminrc: /root/setup/admin-openrc.sh
+          controller_settings: /root/setup/controller_settings
+
+    mysite:
+      type: tosca.nodes.Site
+      properties:
+          display_name: MySite
+          site_url: http://opencloud.us/
+      requirements:
+          - deployment:
+               node: MyDeployment
+               relationship: tosca.relationships.SiteDeployment
+               requirements:
+                   - controller:
+                       node: CloudLab
+                       relationship: tosca.relationships.UsesController
+
+    Public shared IPv4:
+      type: tosca.nodes.NetworkTemplate
+      properties:
+          visibility: private
+          translation: NAT
+          shared_network_name: private
+
+    padmin@vicci.org:
+      type: tosca.nodes.User
+      requirements:
+          - site:
+              node: mysite
+              relationship: tosca.relationships.MemberOfSite
+      properties:
+          public_key: { get_artifact: [ SELF, pubkey, LOCAL_FILE ] }
+          is_admin: true
+          is_active: true
+          firstname: XOS
+          lastname: admin
+      artifacts:
+          pubkey: /root/setup/padmin_public_key
diff --git a/xos/configurations/common/make-cloudlab-nodes-yaml.sh b/xos/configurations/common/make-cloudlab-nodes-yaml.sh
index e68adf1..f2555a4 100644
--- a/xos/configurations/common/make-cloudlab-nodes-yaml.sh
+++ b/xos/configurations/common/make-cloudlab-nodes-yaml.sh
@@ -19,7 +19,7 @@
         type: tosca.nodes.Site
 EOF
 
-NODES=$( sudo bash -c "source /root/setup/admin-openrc.sh ; nova hypervisor-list" |grep cloudlab|awk '{print $4}' )
+NODES=$( bash -c "source ./admin-openrc.sh ; nova hypervisor-list" |grep enabled|awk '{print $4}' )
 I=0
 for NODE in $NODES; do
     echo $NODE
diff --git a/xos/configurations/cord/cord.yaml b/xos/configurations/cord/cord.yaml
index 02137ec..a35454b 100644
--- a/xos/configurations/cord/cord.yaml
+++ b/xos/configurations/cord/cord.yaml
@@ -108,6 +108,21 @@
           kind: onos
           view_url: /admin/onos/onosservice/$id$/
           public_key: { get_artifact: [ SELF, pubkey, LOCAL_FILE] }
+          rest_onos/v1/network/configuration/: >
+            {
+              "devices" : {
+                "of:0000000000000001" : {
+                  "accessDevice" : {
+                    "uplink" : "2",
+                    "vlan"   : "222",
+                    "defaultVlan" : "1"
+                  },
+                  "basic" : {
+                    "driver" : "default"
+                  }
+                }
+              }
+            }
       artifacts:
           pubkey: /opt/xos/observers/onos/onos_key.pub
 
@@ -123,21 +138,21 @@
               relationship: tosca.relationships.UsedByService
       properties:
           dependencies: org.onosproject.openflow-base, org.onosproject.olt
-          config_network-cfg.json: >
-            {
-              "devices" : {
-                "of:0000000000000001" : {
-                  "accessDevice" : {
-                    "uplink" : "2",
-                    "vlan"   : "222",
-                    "defaultVlan" : "1"
-                  },
-                  "basic" : {
-                    "driver" : "default"
-                  }
-                }
-              }
-            }
+#          config_network-cfg.json: >
+#            {
+#              "devices" : {
+#                "of:0000000000000001" : {
+#                  "accessDevice" : {
+#                    "uplink" : "2",
+#                    "vlan"   : "222",
+#                    "defaultVlan" : "1"
+#                  },
+#                  "basic" : {
+#                    "driver" : "default"
+#                  }
+#                }
+#              }
+#            }
 
     # Network templates
     Private:
diff --git a/xos/configurations/devel/Makefile b/xos/configurations/devel/Makefile
index 3ce188b..24b7be7 100644
--- a/xos/configurations/devel/Makefile
+++ b/xos/configurations/devel/Makefile
@@ -2,16 +2,23 @@
 RUNNING_CONTAINER:=$(shell sudo docker ps|grep "xos"|awk '{print $$NF}')
 LAST_CONTAINER=$(shell sudo docker ps -l -q)
 
-test: common_cloudlab
+cloudlab: common_cloudlab xos
+
+devstack: common_devstack xos
+
+xos:
 	echo "# Autogenerated -- do not edit" > Dockerfile
 	cat ../common/Dockerfile.common Dockerfile.devel >> Dockerfile
-	cd ../../..; sudo docker build -t xos -f xos/configurations/devel/Dockerfile .
-	sudo docker run -d --add-host="ctl:$(MYIP)" -p 9999:8000 xos
+	cd ../../..; sudo docker build -t xosproject/xos-devel -f xos/configurations/devel/Dockerfile .
+	sudo docker run -d --add-host="ctl:$(MYIP)" -p 9999:8000 xosproject/xos-devel
 	bash ../common/wait_for_xos.sh
 
 common_cloudlab:
 	make -C ../common -f Makefile.cloudlab
 
+common_devstack:
+	make -C ../common -f Makefile.devstack
+
 stop:
 	sudo docker stop $(RUNNING_CONTAINER)
 
diff --git a/xos/configurations/devel/README b/xos/configurations/devel/README
deleted file mode 100644
index b69f24f..0000000
--- a/xos/configurations/devel/README
+++ /dev/null
@@ -1,3 +0,0 @@
-This configuration launches an XOS container on Cloudlab that runs the XOS develserver. The container is left running
-in the backgorund.
-
diff --git a/xos/configurations/devel/README.md b/xos/configurations/devel/README.md
new file mode 100644
index 0000000..47cc6e7
--- /dev/null
+++ b/xos/configurations/devel/README.md
@@ -0,0 +1,58 @@
+# XOS development environment
+
+This configuration can be used to do basic end-to-end development of XOS.  It launches
+an XOS container, runs the XOS develserver, and configures XOS to talk to an OpenStack
+backend.  
+
+## How to run it
+
+The configuration can be either run on [CloudLab](http://cloudlab.us) (controlling
+an OpenStack backend set up by a CloudLab profile) or used with a basic
+[DevStack](http://docs.openstack.org/developer/devstack/) configuration.
+
+### CloudLab
+
+To get started on CloudLab:
+* Create an experiment using the *OpenStack* profile.  Choose *Kilo* and
+disable security groups.
+* Wait until you get an email from CloudLab with title "OpenStack Instance Finished Setting Up".
+* Login to the *ctl* node of your experiment and run:
+```
+ctl:~$ git clone https://github.com/open-cloud/xos.git
+ctl:~$ cd xos/xos/configurations/devel/
+ctl:~/xos/xos/configurations/devel$ make cloudlab
+```
+
+### DevStack
+
+The following instructions can be used to install DevStack and XOS together
+on a single node.  This setup has been run successfully in a VirtualBox VM
+with 2 CPUs and 4096 GB RAM.
+
+First, if you happen to be installing DevStack on a CloudLab node, you can
+configure about 1TB of unallocated disk space for DevStack as follows:
+```
+~$ sudo mkdir -p /opt/stack
+~$ sudo /usr/testbed/bin/mkextrafs /opt/stack
+```
+
+To install DevStack and XOS:
+
+```
+~$ git clone https://github.com/open-cloud/xos.git
+~$ git clone https://git.openstack.org/openstack-dev/devstack
+~$ cd devstack
+~/devstack$ cp ../xos/xos/configurations/devstack/local.conf .
+~/devstack$ ./stack.sh
+~/devstack$ cd ../xos/xos/configurations/devel/
+~/xos/xos/configurations/devel$ make devstack
+```
+
+Note that there are some issues with the networking setup in this configuration;
+you will be able to create VMs but they are not accessible on the network.  However it is
+possible to log into a VM by first entering the appropriate network namespace.
+
+## What you get
+
+XOS will be set up with a single Deployment and Site.  It should be in a state where
+you can create slices and associate instances with them.
diff --git a/xos/configurations/devstack/local.conf b/xos/configurations/devstack/local.conf
new file mode 100644
index 0000000..946d16e
--- /dev/null
+++ b/xos/configurations/devstack/local.conf
@@ -0,0 +1,17 @@
+# A single node devstack configuration for use with XOS
+[[local|localrc]]
+
+DOWNLOAD_DEFAULT_IMAGES=false
+IMAGE_URLS="http://www.planet-lab.org/cord/trusty-server-multi-nic.img"
+
+disable_service n-net
+enable_service q-svc
+enable_service q-agt
+enable_service q-dhcp
+enable_service q-l3
+enable_service q-meta
+# Optional, to enable tempest configuration as part of devstack
+enable_service tempest
+
+## Neutron options
+Q_USE_SECGROUP=False
diff --git a/xos/core/models/service.py b/xos/core/models/service.py
index ba54a33..2b450e0 100644
--- a/xos/core/models/service.py
+++ b/xos/core/models/service.py
@@ -83,6 +83,13 @@
             service_ids = [sp.slice.id for sp in ServicePrivilege.objects.filter(user=user)]
             return cls.objects.filter(id__in=service_ids)
 
+    @property
+    def serviceattribute_dict(self):
+        attrs = {}
+        for attr in self.serviceattributes.all():
+            attrs[attr.name] = attr.value
+        return attrs
+
     def __unicode__(self): return u'%s' % (self.name)
 
     def can_update(self, user):
@@ -161,8 +168,42 @@
 
                 # print "add instance", s
 
+    def get_vtn_nets(self):
+        nets=[]
+        for slice in self.slices.all():
+            for ns in slice.networkslices.all():
+                if not ns.network:
+                    continue
+                for cn in ns.network.controllernetworks.all():
+                    if cn.net_id:
+                        net = {"name": ns.network.name, "net_id": cn.net_id}
+                        nets.append(net)
+        return nets
+
+    def get_vtn_dependencies_nets(self):
+        provider_nets = []
+        for tenant in self.subscribed_tenants.all():
+            if tenant.provider_service:
+                for net in tenant.provider_service.get_vtn_nets():
+                    if not net in provider_nets:
+                        provider_nets.append(net)
+        return provider_nets
+
+    def get_vtn_dependencies_ids(self):
+        return [x["net_id"] for x in self.get_vtn_dependencies_nets()]
+
+    def get_vtn_dependencies_names(self):
+        return [x["name"]+"_"+x["net_id"] for x in self.get_vtn_dependencies_nets()]
+
+    def get_vtn_ids(self):
+        return [x["net_id"] for x in self.get_vtn_nets()]
+
+    def get_vtn_names(self):
+        return [x["name"]+"_"+x["net_id"] for x in self.get_vtn_nets()]
+
+
 class ServiceAttribute(PlCoreBase):
-    name = models.SlugField(help_text="Attribute Name", max_length=128)
+    name = models.CharField(help_text="Attribute Name", max_length=128)
     value = StrippedCharField(help_text="Attribute Value", max_length=1024)
     service = models.ForeignKey(Service, related_name='serviceattributes', help_text="The Service this attribute is associated with")
 
@@ -310,6 +351,13 @@
     def get_deleted_tenant_objects(cls):
         return cls.deleted_objects.filter(kind = cls.KIND)
 
+    @property
+    def tenantattribute_dict(self):
+        attrs = {}
+        for attr in self.tenantattributes.all():
+            attrs[attr.name] = attr.value
+        return attrs
+
     # helper function to be used in subclasses that want to ensure service_specific_id is unique
     def validate_unique_service_specific_id(self):
         if self.pk is None:
@@ -361,6 +409,7 @@
     def pick(self):
         from core.models import Node
         nodes = list(Node.objects.all())
+
         # TODO: logic to filter nodes by which nodes are up, and which
         #   nodes the slice can instantiate on.
         nodes = sorted(nodes, key=lambda node: node.instances.all().count())
diff --git a/xos/core/xoslib/methods/ceilometerview.py b/xos/core/xoslib/methods/ceilometerview.py
index a416110..d02e716 100644
--- a/xos/core/xoslib/methods/ceilometerview.py
+++ b/xos/core/xoslib/methods/ceilometerview.py
@@ -1,4 +1,3 @@
-import logging
 import requests
 from six.moves import urllib
 import urllib2
@@ -18,9 +17,9 @@
 from django.utils import timezone
 from syndicate_storage.models import Volume
 from django.core.exceptions import PermissionDenied
+from util.logger import observer_logger as logger
 
 # This REST API endpoint provides information that the ceilometer view needs to display
-LOG = logging.getLogger(__name__)
 
 def getTenantCeilometerProxyURL(user):
     monitoring_channel = None
@@ -36,11 +35,12 @@
             response = urllib2.urlopen(monitoring_channel.ceilometer_url,timeout=1)
             break
         except urllib2.HTTPError, e:
-            LOG.info('SRIKANTH: HTTP error %(reason)s' % {'reason':e.reason})
+            logger.info('SRIKANTH: HTTP error %(reason)s' % {'reason':e.reason})
             break
         except urllib2.URLError, e:
-            LOG.info('SRIKANTH: URL error %(reason)s' % {'reason':e.reason})
+            logger.info('SRIKANTH: URL error %(reason)s' % {'reason':e.reason})
             pass
+    logger.info("SRIKANTH: Ceilometer proxy URL for user %(user)s is %(url)s" % {'user':user.username,'url':monitoring_channel.ceilometer_url})
     return monitoring_channel.ceilometer_url
 
 def getTenantControllerTenantMap(user):
@@ -48,7 +48,12 @@
     for slice in Slice.objects.filter(creator=user):
         for cs in slice.controllerslices.all():
             if cs.tenant_id:
-                tenantmap[cs.tenant_id] = cs.slice.name
+                tenantmap[cs.tenant_id] = {"slice": cs.slice.name}
+                if cs.slice.service:
+                    tenantmap[cs.tenant_id]["service"] = slice.service.name
+                else:
+                    logger.warn("SRIKANTH: Slice %(slice)s is not associated with any service" % {'slice':cs.slice.name})
+                    tenantmap[cs.tenant_id]["service"] = "Other"
     return tenantmap
 
 def build_url(path, q, params=None):
@@ -171,7 +176,6 @@
             try:
                 query=[]
                 self._ceilometer_meter_list = meter_list(request, self.ceilometer_url, query)
-                #LOG.info('SRIKANTH: meters=%(meters)s'%{'meters':[m.project_id for m in self._ceilometer_meter_list]})
             except requests.exceptions.RequestException as e:
                 self._ceilometer_meter_list = []
                 raise e
@@ -354,9 +358,9 @@
 
         meters = []
         for meter_name in meter_names:
-            meter = self._get_meter(meter_name)
-            if meter:
-                meters.append(meter)
+            meter_candidates = self._get_meter(meter_name)
+            if meter_candidates:
+                meters.extend(meter_candidates)
         return meters
 
     def _get_meter(self, meter_name):
@@ -368,8 +372,8 @@
         :Parameters:
           - `meter_name`: A meter name we want to fetch.
         """
-        meter = self._cached_meters.get(meter_name, None)
-        if not meter:
+        meter_candidates = self._cached_meters.get(meter_name, None)
+        if not meter_candidates:
             meter_candidates = [m for m in self._ceilometer_meter_list
                                 if m["name"] == meter_name]
 
@@ -378,20 +382,25 @@
                 if meter_info:
                     label = meter_info["label"]
                     description = meter_info["description"]
+                    meter_type = meter_info["type"]
                 else:
                     label = ""
                     description = ""
-                meter = meter_candidates[0]
-                meter["label"] = label
-                meter["description"] = description
-                if meter["project_id"] in self.tenant_map.keys():
-                    meter["project_name"] = self.tenant_map[meter["project_id"]]
-                else:
-                    meter["project_name"] = meter["project_id"]
+                    meter_type = "Other"
+                for meter in meter_candidates:
+                    meter["label"] = label
+                    meter["description"] = description
+                    meter["type"] = meter_type
+                    if meter["project_id"] in self.tenant_map.keys():
+                        meter["slice"] = self.tenant_map[meter["project_id"]]["slice"]
+                        meter["service"] = self.tenant_map[meter["project_id"]]["service"]
+                    else:
+                        meter["slice"] = meter["project_id"]
+                        meter["service"] = "Other"
 
-                self._cached_meters[meter_name] = meter
+                self._cached_meters[meter_name] = meter_candidates
 
-        return meter
+        return meter_candidates
 
     def _get_nova_meters_info(self):
         """Returns additional info for each meter.
@@ -405,40 +414,49 @@
         # some day it will be supported all.
         meters_info = datastructures.SortedDict([
             ("instance", {
+                'type': _("Nova"),
                 'label': '',
                 'description': _("Existence of instance"),
             }),
             ("instance:<type>", {
+                'type': _("Nova"),
                 'label': '',
                 'description': _("Existence of instance <type> "
                                  "(openstack types)"),
             }),
             ("memory", {
+                'type': _("Nova"),
                 'label': '',
                 'description': _("Volume of RAM"),
             }),
             ("memory.usage", {
+                'type': _("Nova"),
                 'label': '',
                 'description': _("Volume of RAM used"),
             }),
             ("cpu", {
+                'type': _("Nova"),
                 'label': '',
                 'description': _("CPU time used"),
             }),
             ("cpu_util", {
+                'type': _("Nova"),
                 'label': '',
                 'description': _("Average CPU utilization"),
             }),
             ("vcpus", {
+                'type': _("Nova"),
                 'label': '',
                 'description': _("Number of VCPUs"),
             }),
             ("network.incoming.bytes.rate", {
+                'type': _("Nova"),
                 'label': '',
                 'description': _("Average rate per sec of incoming "
                                  "bytes on a VM network interface"),
             }),
             ("network.outgoing.bytes.rate", {
+                'type': _("Nova"),
                 'label': '',
                 'description': _("Average rate per sec of outgoing "
                                  "bytes on a VM network interface"),
@@ -471,18 +489,22 @@
         # some day it will be supported all.
         return datastructures.SortedDict([
             ('network', {
+                'type': _("Neutron"),
                 'label': '',
                 'description': _("Existence of network"),
             }),
             ('subnet', {
+                'type': _("Neutron"),
                 'label': '',
                 'description': _("Existence of subnet"),
             }),
             ('port', {
+                'type': _("Neutron"),
                 'label': '',
                 'description': _("Existence of port"),
             }),
             ('ip.floating', {
+                'type': _("Neutron"),
                 'label': '',
                 'description': _("Existence of floating ip"),
             }),
@@ -500,30 +522,37 @@
         # some day it will be supported all.
         return datastructures.SortedDict([
             ('image', {
+                'type': _("Glance"),
                 'label': '',
                 'description': _("Image existence check"),
             }),
             ('image.size', {
+                'type': _("Glance"),
                 'label': '',
                 'description': _("Uploaded image size"),
             }),
             ('image.update', {
+                'type': _("Glance"),
                 'label': '',
                 'description': _("Number of image updates"),
             }),
             ('image.upload', {
+                'type': _("Glance"),
                 'label': '',
                 'description': _("Number of image uploads"),
             }),
             ('image.delete', {
+                'type': _("Glance"),
                 'label': '',
                 'description': _("Number of image deletions"),
             }),
             ('image.download', {
+                'type': _("Glance"),
                 'label': '',
                 'description': _("Image is downloaded"),
             }),
             ('image.serve', {
+                'type': _("Glance"),
                 'label': '',
                 'description': _("Image is served out"),
             }),
@@ -541,10 +570,12 @@
         # some day it will be supported all.
         return datastructures.SortedDict([
             ('volume', {
+                'type': _("Cinder"),
                 'label': '',
                 'description': _("Existence of volume"),
             }),
             ('volume.size', {
+                'type': _("Cinder"),
                 'label': '',
                 'description': _("Size of volume"),
             }),
@@ -562,26 +593,32 @@
         # some day it will be supported all.
         return datastructures.SortedDict([
             ('storage.objects', {
+                'type': _("Swift"),
                 'label': '',
                 'description': _("Number of objects"),
             }),
             ('storage.objects.size', {
+                'type': _("Swift"),
                 'label': '',
                 'description': _("Total size of stored objects"),
             }),
             ('storage.objects.containers', {
+                'type': _("Swift"),
                 'label': '',
                 'description': _("Number of containers"),
             }),
             ('storage.objects.incoming.bytes', {
+                'type': _("Swift"),
                 'label': '',
                 'description': _("Number of incoming bytes"),
             }),
             ('storage.objects.outgoing.bytes', {
+                'type': _("Swift"),
                 'label': '',
                 'description': _("Number of outgoing bytes"),
             }),
             ('storage.api.request', {
+                'type': _("Swift"),
                 'label': '',
                 'description': _("Number of API requests against swift"),
             }),
@@ -599,10 +636,12 @@
         # some day it will be supported all.
         return datastructures.SortedDict([
             ('energy', {
+                'type': _("Kwapi"),
                 'label': '',
                 'description': _("Amount of energy"),
             }),
             ('power', {
+                'type': _("Kwapi"),
                 'label': '',
                 'description': _("Power consumption"),
             }),
@@ -620,50 +659,62 @@
         # some day it will be supported all.
         return datastructures.SortedDict([
             ('hardware.ipmi.node.power', {
+                'type': _("IPMI"),
                 'label': '',
                 'description': _("System Current Power"),
             }),
             ('hardware.ipmi.fan', {
+                'type': _("IPMI"),
                 'label': '',
                 'description': _("Fan RPM"),
             }),
             ('hardware.ipmi.temperature', {
+                'type': _("IPMI"),
                 'label': '',
                 'description': _("Sensor Temperature Reading"),
             }),
             ('hardware.ipmi.current', {
+                'type': _("IPMI"),
                 'label': '',
                 'description': _("Sensor Current Reading"),
             }),
             ('hardware.ipmi.voltage', {
+                'type': _("IPMI"),
                 'label': '',
                 'description': _("Sensor Voltage Reading"),
             }),
             ('hardware.ipmi.node.inlet_temperature', {
+                'type': _("IPMI"),
                 'label': '',
                 'description': _("System Inlet Temperature Reading"),
             }),
             ('hardware.ipmi.node.outlet_temperature', {
+                'type': _("IPMI"),
                 'label': '',
                 'description': _("System Outlet Temperature Reading"),
             }),
             ('hardware.ipmi.node.airflow', {
+                'type': _("IPMI"),
                 'label': '',
                 'description': _("System Airflow Reading"),
             }),
             ('hardware.ipmi.node.cups', {
+                'type': _("IPMI"),
                 'label': '',
                 'description': _("System CUPS Reading"),
             }),
             ('hardware.ipmi.node.cpu_util', {
+                'type': _("IPMI"),
                 'label': '',
                 'description': _("System CPU Utility Reading"),
             }),
             ('hardware.ipmi.node.mem_util', {
+                'type': _("IPMI"),
                 'label': '',
                 'description': _("System Memory Utility Reading"),
             }),
             ('hardware.ipmi.node.io_util', {
+                'type': _("IPMI"),
                 'label': '',
                 'description': _("System IO Utility Reading"),
             }),
@@ -681,34 +732,42 @@
         # some day it will be supported all.
         return datastructures.SortedDict([
             ('vcpe', {
+                'type': _("VCPE"),
                 'label': '',
                 'description': _("Existence of vcpe instance"),
             }),
             ('vcpe.dns.cache.size', {
+                'type': _("VCPE"),
                 'label': '',
                 'description': _("Number of entries in DNS cache"),
             }),
             ('vcpe.dns.total_instered_entries', {
+                'type': _("VCPE"),
                 'label': '',
                 'description': _("Total number of inserted entries into the cache"),
             }),
             ('vcpe.dns.replaced_unexpired_entries', {
+                'type': _("VCPE"),
                 'label': '',
                 'description': _("Unexpired entries that were thrown out of cache"),
             }),
             ('vcpe.dns.queries_answered_locally', {
+                'type': _("VCPE"),
                 'label': '',
                 'description': _("Number of cache hits"),
             }),
             ('vcpe.dns.queries_forwarded', {
+                'type': _("VCPE"),
                 'label': '',
                 'description': _("Number of cache misses"),
             }),
             ('vcpe.dns.server.queries_sent', {
+                'type': _("VCPE"),
                 'label': '',
                 'description': _("For each upstream server, the number of queries sent"),
             }),
             ('vcpe.dns.server.queries_failed', {
+                'type': _("VCPE"),
                 'label': '',
                 'description': _("For each upstream server, the number of queries failed"),
             }),
@@ -726,50 +785,62 @@
         # some day it will be supported all.
         return datastructures.SortedDict([
             ('switch', {
+                'type': _("SDN"),
                 'label': '',
                 'description': _("Existence of switch"),
             }),
             ('switch.port', {
+                'type': _("SDN"),
                 'label': '',
                 'description': _("Existence of port"),
             }),
             ('switch.port.receive.packets', {
+                'type': _("SDN"),
                 'label': '',
                 'description': _("Packets received on port"),
             }),
             ('switch.port.transmit.packets', {
+                'type': _("SDN"),
                 'label': '',
                 'description': _("Packets transmitted on port"),
             }),
             ('switch.port.receive.drops', {
+                'type': _("SDN"),
                 'label': '',
                 'description': _("Drops received on port"),
             }),
             ('switch.port.transmit.drops', {
+                'type': _("SDN"),
                 'label': '',
                 'description': _("Drops transmitted on port"),
             }),
             ('switch.port.receive.errors', {
+                'type': _("SDN"),
                 'label': '',
                 'description': _("Errors received on port"),
             }),
             ('switch.port.transmit.errors', {
+                'type': _("SDN"),
                 'label': '',
                 'description': _("Errors transmitted on port"),
             }),
             ('switch.flow', {
+                'type': _("SDN"),
                 'label': '',
                 'description': _("Duration of flow"),
             }),
             ('switch.flow.packets', {
+                'type': _("SDN"),
                 'label': '',
                 'description': _("Packets received"),
             }),
             ('switch.table', {
+                'type': _("SDN"),
                 'label': '',
                 'description': _("Existence of table"),
             }),
             ('switch.table.active.entries', {
+                'type': _("SDN"),
                 'label': '',
                 'description': _("Active entries in table"),
             }),
@@ -865,7 +936,10 @@
             _('VCPE'): meters.list_vcpe(),
             _('SDN'): meters.list_sdn(),
         }
-        return Response(meters._cached_meters.values())
+        meters = []
+        for service,smeters in services.iteritems():
+             meters.extend(smeters)
+        return Response(meters)
 
 class MeterStatisticsList(APIView):
     method_kind = "list"
@@ -890,6 +964,39 @@
         except Exception as e:
            raise e 
 
+        additional_query = []
+        if date_from:
+            additional_query.append({'field': 'timestamp',
+                                     'op': 'ge',
+                                     'value': date_from})
+        if date_to:
+            additional_query.append({'field': 'timestamp',
+                                     'op': 'le',
+                                     'value': date_to})
+
+        meter_name = request.QUERY_PARAMS.get('meter', None)
+        tenant_id = request.QUERY_PARAMS.get('tenant', None)
+        resource_id = request.QUERY_PARAMS.get('resource', None)
+
+        if meter_name:
+            #Statistics query for one meter
+            query = []
+            if tenant_id:
+                query.extend(make_query(tenant_id=tenant_id))
+            if resource_id:
+                query.extend(make_query(resource_id=resource_id))
+            if additional_query:
+                query = query + additional_query
+            statistics = statistic_list(request, meter_name,
+                                        ceilometer_url=tenant_ceilometer_url, query=query, period=3600*24)
+            statistic = statistics[-1]
+            row = {"name": 'none',
+                   "meter": meter_name,
+                   "time": statistic["period_end"],
+                   "value": statistic["avg"]}
+            return Response(row)
+
+        #Statistics query for all meter
         meters = Meters(request, ceilometer_url=tenant_ceilometer_url, tenant_map=tenant_map)
         services = {
             _('Nova'): meters.list_nova(),
@@ -900,17 +1007,21 @@
         report_rows = []
         for service,meters in services.items():
             for meter in meters:
-                query = make_query(tenant_id=meter["project_id"])
+                query = make_query(tenant_id=meter["project_id"],resource_id=meter["resource_id"])
+                if additional_query:
+                    query = query + additional_query
                 statistics = statistic_list(request, meter["name"],
                                         ceilometer_url=tenant_ceilometer_url, query=query, period=3600*24)
                 if not statistics:
                     continue
-                statistic = statistics[0]
+                statistic = statistics[-1]
                 row = {"name": 'none',
-                       "project": meter["project_name"],
+                       "slice": meter["slice"],
+                       "service": meter["service"],
+                       "resource_id": meter["resource_id"],
                        "meter": meter["name"],
                        "description": meter["description"],
-                       "service": service,
+                       "type": service,
                        "time": statistic["period_end"],
                        "value": statistic["avg"],
                        "unit": meter["unit"]}
diff --git a/xos/core/xoslib/methods/cordsubscriber.py b/xos/core/xoslib/methods/cordsubscriber.py
index 297ac4a..3892014 100644
--- a/xos/core/xoslib/methods/cordsubscriber.py
+++ b/xos/core/xoslib/methods/cordsubscriber.py
@@ -11,7 +11,7 @@
 from django.conf.urls import patterns, url
 from cord.models import VOLTTenant, VBNGTenant, CordSubscriberRoot
 from core.xoslib.objects.cordsubscriber import CordSubscriber
-from plus import PlusSerializerMixin
+from plus import PlusSerializerMixin, XOSViewSet
 from django.shortcuts import get_object_or_404
 from xos.apibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView, XOSPermissionDenied
 from xos.exceptions import *
@@ -158,30 +158,6 @@
         subscriber.save()
         return Response(serialize_user(subscriber,user))
 
-# this may be moved into plus.py...
-
-class XOSViewSet(viewsets.ModelViewSet):
-    @classmethod
-    def detail_url(self, pattern, viewdict, name):
-        return url(r'^' + self.method_name + r'/(?P<pk>[a-zA-Z0-9\-]+)/' + pattern,
-                   self.as_view(viewdict),
-                   name=self.base_name+"_"+name)
-
-    @classmethod
-    def list_url(self, pattern, viewdict, name):
-        return url(r'^' + self.method_name + r'/' + pattern,
-                   self.as_view(viewdict),
-                   name=self.base_name+"_"+name)
-
-    @classmethod
-    def get_urlpatterns(self):
-        patterns = []
-
-        patterns.append(url(r'^' + self.method_name + '/$', self.as_view({'get': 'list'}), name=self.base_name+'_list'))
-        patterns.append(url(r'^' + self.method_name + '/(?P<pk>[a-zA-Z0-9\-]+)/$', self.as_view({'get': 'retrieve', 'put': 'update', 'post': 'update', 'delete': 'destroy', 'patch': 'partial_update'}), name=self.base_name+'_detail'))
-
-        return patterns
-
 #------------------------------------------------------------------------------
 # The "new" API with many more REST endpoints.
 # This is for integration with with the subscriber GUI
diff --git a/xos/core/xoslib/methods/plus.py b/xos/core/xoslib/methods/plus.py
index 280c468..294fba8 100644
--- a/xos/core/xoslib/methods/plus.py
+++ b/xos/core/xoslib/methods/plus.py
@@ -3,6 +3,8 @@
 from rest_framework.response import Response
 from rest_framework import status
 from xos.apibase import XOSRetrieveUpdateDestroyAPIView, XOSListCreateAPIView
+from rest_framework import viewsets
+from django.conf.urls import patterns, url
 
 """ PlusSerializerMixin
 
@@ -26,7 +28,24 @@
     def getBackendHtml(self, obj):
         return obj.getBackendHtml()
 
+class XOSViewSet(viewsets.ModelViewSet):
+    @classmethod
+    def detail_url(self, pattern, viewdict, name):
+        return url(r'^' + self.method_name + r'/(?P<pk>[a-zA-Z0-9\-]+)/' + pattern,
+                   self.as_view(viewdict),
+                   name=self.base_name+"_"+name)
 
+    @classmethod
+    def list_url(self, pattern, viewdict, name):
+        return url(r'^' + self.method_name + r'/' + pattern,
+                   self.as_view(viewdict),
+                   name=self.base_name+"_"+name)
 
+    @classmethod
+    def get_urlpatterns(self):
+        patterns = []
 
+        patterns.append(url(r'^' + self.method_name + '/$', self.as_view({'get': 'list'}), name=self.base_name+'_list'))
+        patterns.append(url(r'^' + self.method_name + '/(?P<pk>[a-zA-Z0-9\-]+)/$', self.as_view({'get': 'retrieve', 'put': 'update', 'post': 'update', 'delete': 'destroy', 'patch': 'partial_update'}), name=self.base_name+'_detail'))
 
+        return patterns
diff --git a/xos/core/xoslib/methods/vtn.py b/xos/core/xoslib/methods/vtn.py
new file mode 100644
index 0000000..a912613
--- /dev/null
+++ b/xos/core/xoslib/methods/vtn.py
@@ -0,0 +1,64 @@
+from rest_framework.decorators import api_view
+from rest_framework.response import Response
+from rest_framework.reverse import reverse
+from rest_framework import serializers
+from rest_framework import generics
+from rest_framework import viewsets
+from rest_framework.decorators import detail_route, list_route
+from rest_framework.views import APIView
+from core.models import *
+from django.forms import widgets
+from django.conf.urls import patterns, url
+from plus import PlusSerializerMixin, XOSViewSet
+from django.shortcuts import get_object_or_404
+from xos.apibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView, XOSPermissionDenied
+from xos.exceptions import *
+import json
+import subprocess
+
+class VTNViewSet(XOSViewSet):
+    base_name = "vtn"
+    method_name = "rs/vtn"
+    method_kind = "viewset"
+
+    # these are just because ViewSet needs some queryset and model, even if we don't use the
+    # default endpoints
+    queryset = Service.objects.none() # CordSubscriber.get_tenant_objects().select_related().all()
+    model = Service
+
+    @classmethod
+    def get_urlpatterns(self):
+        patterns = []
+        patterns.append( self.list_url("services/$", {"get": "get_services"}, "services") )
+        patterns.append( self.list_url("services_names/$", {"get": "get_services_names"}, "services") )
+        patterns.append( self.list_url("services/(?P<service>[a-zA-Z0-9\-_]+)/$", {"get": "get_service"}, "get_service") )
+
+        return patterns
+
+    def get_services_names(self, request, pk=None):
+        result = {}
+        for service in Service.objects.all():
+           for id in service.get_vtn_names():
+               dependencies = service.get_vtn_dependencies_names()
+               if dependencies:
+                   result[id] = dependencies
+        return Response(result)
+
+    def get_services(self, request, pk=None):
+        result = {}
+        for service in Service.objects.all():
+           for id in service.get_vtn_ids():
+               dependencies = service.get_vtn_dependencies_ids()
+               if dependencies:
+                   result[id] = dependencies
+        return Response(result)
+
+    def get_service(self, request, pk=None, service=None):
+        for xos_service in Service.objects.all():
+            if service in xos_service.get_vtn_ids():
+                return Response(xos_service.get_vtn_dependencies_ids())
+        raise DoesNotExist()
+
+    def list(self, request):
+        raise Exception("Not Implemented")
+
diff --git a/xos/observers/monitoring_channel/templates/ceilometer_proxy_server.py b/xos/observers/monitoring_channel/templates/ceilometer_proxy_server.py
index 249c965..9329cfa 100644
--- a/xos/observers/monitoring_channel/templates/ceilometer_proxy_server.py
+++ b/xos/observers/monitoring_channel/templates/ceilometer_proxy_server.py
@@ -223,11 +223,15 @@
              "q.op": [],
              "q.type": [],
              "q.value": [],
+             "limit": None,
+             "links": None,
         }
         query_params = web.input(**keyword_args)
         new_query, user_specified_tenants = filter_query_params(query_params)
 
         client = ceilometerclient()
+        limit=query_params.limit
+        links=query_params.links
         resources=[]
         for (k,v) in config.items('allowed_tenants'):
               if user_specified_tenants and (k not in user_specified_tenants):
@@ -237,7 +241,7 @@
               query = make_query(tenant_id=k)
               final_query.extend(query)
               logger.debug('final query=%s',final_query)
-              results = client.resources.list(q=final_query, links=1)
+              results = client.resources.list(q=final_query, limit=limit, links=links)
               resources.extend(results)
         return json.dumps([ob._info for ob in resources])
 
diff --git a/xos/observers/onos/steps/sync_onosapp.py b/xos/observers/onos/steps/sync_onosapp.py
index 9b32298..97bb8a6 100644
--- a/xos/observers/onos/steps/sync_onosapp.py
+++ b/xos/observers/onos/steps/sync_onosapp.py
@@ -81,20 +81,25 @@
         if not os.path.exists(o.files_dir):
             os.makedirs(o.files_dir)
 
-        for attr in o.tenantattributes.all():
-            if attr.name.startswith("config_"):
-                fn = attr.name[7:] # .replace("_json",".json")
+        # Combine the service attributes with the tenant attributes. Tenant
+        # attribute can override service attributes.
+        attrs = o.provider_service.serviceattribute_dict
+        attrs.update(o.tenantattribute_dict)
+
+        for (name, value) in attrs.items():
+            if name.startswith("config_"):
+                fn = name[7:] # .replace("_json",".json")
                 o.config_fns.append(fn)
-                file(os.path.join(o.files_dir, fn),"w").write(attr.value)
-            if attr.name.startswith("rest_"):
-                fn = attr.name[5:].replace("/","_")
-                endpoint = attr.name[5:]
+                file(os.path.join(o.files_dir, fn),"w").write(value)
+            if name.startswith("rest_"):
+                fn = name[5:].replace("/","_")
+                endpoint = name[5:]
                 # Ansible goes out of it's way to make our life difficult. If
                 # 'lookup' sees a file that it thinks contains json, then it'll
                 # insist on parsing and return a json object. We just want
                 # a string, so prepend a space and then strip the space off
                 # later.
-                file(os.path.join(o.files_dir, fn),"w").write(" " +attr.value)
+                file(os.path.join(o.files_dir, fn),"w").write(" " +value)
                 o.rest_configs.append( {"endpoint": endpoint, "fn": fn} )
 
     def prepare_record(self, o):
diff --git a/xos/observers/onos/steps/sync_onosapp.yaml b/xos/observers/onos/steps/sync_onosapp.yaml
index 9ee2513..9105a2e 100644
--- a/xos/observers/onos/steps/sync_onosapp.yaml
+++ b/xos/observers/onos/steps/sync_onosapp.yaml
@@ -45,18 +45,6 @@
         {% endfor %}
 {% endif %}
 
-{% if rest_configs %}
-  - name: Add ONOS configuration values
-    uri:
-      url: http://localhost:8181/{{ '{{' }} item.endpoint {{ '}}' }} #http://localhost:8181/onos/v1/network/configuration/
-      body: "{{ '{{' }} item.body {{ '}}' }}"
-      body_format: raw
-      method: POST
-      user: karaf
-      password: karaf
-    with_items: "rest_configs"
-{% endif %}
-
   # Don't know how to check for this condition, just wait
   - name: Wait for ONOS to install the apps
     wait_for: timeout=15
@@ -71,3 +59,17 @@
         {% for dependency in dependencies %}
         - {{ dependency }}
         {% endfor %}
+
+{% if rest_configs %}
+# Do this after services have been activated, or it will cause an exception.
+# vOLT will re-read its net config; vbng may not.
+  - name: Add ONOS configuration values
+    uri:
+      url: http://localhost:8181/{{ '{{' }} item.endpoint {{ '}}' }} #http://localhost:8181/onos/v1/network/configuration/
+      body: "{{ '{{' }} item.body {{ '}}' }}"
+      body_format: raw
+      method: POST
+      user: karaf
+      password: karaf
+    with_items: "rest_configs"
+{% endif %}
diff --git a/xos/tosca/custom_types/xos.m4 b/xos/tosca/custom_types/xos.m4
index d4e96ce..a806327 100644
--- a/xos/tosca/custom_types/xos.m4
+++ b/xos/tosca/custom_types/xos.m4
@@ -101,6 +101,10 @@
             xos_base_service_caps
         properties:
             xos_base_service_props
+            rest_onos/v1/network/configuration/:
+                type: string
+                required: false
+
 
     tosca.nodes.ONOSApp:
         derived_from: tosca.nodes.Root
diff --git a/xos/tosca/custom_types/xos.yaml b/xos/tosca/custom_types/xos.yaml
index 0c20211..3339fdf 100644
--- a/xos/tosca/custom_types/xos.yaml
+++ b/xos/tosca/custom_types/xos.yaml
@@ -102,6 +102,10 @@
                 type: string
                 required: false
                 description: Version number of Service.
+            rest_onos/v1/network/configuration/:
+                type: string
+                required: false
+
 
     tosca.nodes.ONOSApp:
         derived_from: tosca.nodes.Root
diff --git a/xos/tosca/resources/onosservice.py b/xos/tosca/resources/onosservice.py
index 1275fab..b742ebb 100644
--- a/xos/tosca/resources/onosservice.py
+++ b/xos/tosca/resources/onosservice.py
@@ -5,6 +5,7 @@
 sys.path.append("/opt/tosca")
 from translator.toscalib.tosca_template import ToscaTemplate
 
+from core.models import ServiceAttribute
 from services.onos.models import ONOSService
 
 from service import XOSService
@@ -14,3 +15,27 @@
     xos_model = ONOSService
     copyin_props = ["view_url", "icon_url", "enabled", "published", "public_key", "versionNumber"]
 
+    def set_service_attr(self, obj, prop_name, value):
+        value = self.try_intrinsic_function(value)
+        if value:
+            attrs = ServiceAttribute.objects.filter(service=obj, name=prop_name)
+            if attrs:
+                attr = attrs[0]
+                if attr.value != value:
+                    self.info("updating attribute %s" % prop_name)
+                    attr.value = value
+                    attr.save()
+            else:
+                self.info("adding attribute %s" % prop_name)
+                ta = ServiceAttribute(service=obj, name=prop_name, value=value)
+                ta.save()
+
+    def postprocess(self, obj):
+        props = self.nodetemplate.get_properties()
+        for (k,d) in props.items():
+            v = d.value
+            if k.startswith("config_"):
+                self.set_service_attr(obj, k, v)
+            elif k.startswith("rest_"):
+                self.set_service_attr(obj, k, v)
+