Merge branch 'feature/api-cleanup' of github.com:open-cloud/xos into feature/api-cleanup
diff --git a/containers/xos/Dockerfile.devel b/containers/xos/Dockerfile.devel
index a8a9710..6e2c523 100644
--- a/containers/xos/Dockerfile.devel
+++ b/containers/xos/Dockerfile.devel
@@ -48,7 +48,7 @@
     django_rest_swagger \
     django-suit==0.3a1 \
     django-timezones \
-    djangorestframework==2.4.4 \
+    djangorestframework==3.3.3 \
     dnslib \
     lxml \
     markdown \
diff --git a/containers/xos/Dockerfile.templ b/containers/xos/Dockerfile.templ
index 25270a6..bb0fa4e 100644
--- a/containers/xos/Dockerfile.templ
+++ b/containers/xos/Dockerfile.templ
@@ -49,7 +49,7 @@
     django_rest_swagger \
     django-suit==0.3a1 \
     django-timezones \
-    djangorestframework==2.4.4 \
+    djangorestframework==3.3.3 \
     dnslib \
     google_api_python_client \
     httplib2 \
diff --git a/xos/api/README.md b/xos/api/README.md
new file mode 100644
index 0000000..c0244f0
--- /dev/null
+++ b/xos/api/README.md
@@ -0,0 +1,13 @@
+## XOS REST API
+
+The XOS API importer is automatic and will search this subdirectory and its hierarchy of children for valid API methods. API methods that are descendents of the django View class are discovered automatically. This should include django_rest_framework based Views and Viewsets. This processing is handled by import_methods.py.
+
+A convention is established for locating API methods within the XOS hierarchy. The root of the api will automatically be /api/. Under that are the following paths:
+
+* `/api/service` ... API endpoints that are service-wide
+* `/api/tenant` ... API endpoints that are relative to a tenant within a service
+
+For example, `/api/tenant/cord/subscriber/` contains the Subscriber API for the CORD service. 
+
+The API importer will automatically construct REST paths based on where files are placed within the directory hierarchy. For example, the files in `xos/api/tenant/cord/` will automatically appear at the API endpoint `http://server_name/api/tenant/cord/`. 
+The directory `examples` contains examples that demonstrate using the API from the Linux command line.
diff --git a/xos/api/examples/README.md b/xos/api/examples/README.md
new file mode 100644
index 0000000..0ec7b73
--- /dev/null
+++ b/xos/api/examples/README.md
@@ -0,0 +1,15 @@
+## XOS REST API Examples
+
+This directory contains examples that demonstrate using the XOS REST API using the `curl` command-line tool.
+
+To get started, edit `config.sh` so that it points to a valid XOS server.
+
+We recommend running the following examples in order:
+
+ * `add_subscriber.sh` ... add a cord subscriber using account number 1238
+ * `update_subscriber.sh` ... update the subscriber's upstream_bandwidth feature
+ * `add_volt_to_subscriber.sh` ... add a vOLT to the subscriber with s-tag 33 and c-tag 133
+ * `get_subscriber.sh` ... get an entire subscriber object
+ * `get_subscriber_features.sh` ... get the features of a subscriber
+ * `delete_volt_from_subscriber.sh` ... remove the vOLT from the subscriber
+ * `delete_subscriber.sh` ... delete the subscriber that has account number 1238
diff --git a/xos/api/examples/config.sh b/xos/api/examples/config.sh
new file mode 100644
index 0000000..79baa5f
--- /dev/null
+++ b/xos/api/examples/config.sh
@@ -0,0 +1,4 @@
+#HOST=apt187.apt.emulab.net:9999
+HOST=clnode076.clemson.cloudlab.us:9999
+
+AUTH=padmin@vicci.org:letmein
diff --git a/xos/api/examples/cord/add_subscriber.sh b/xos/api/examples/cord/add_subscriber.sh
new file mode 100755
index 0000000..498a7c6
--- /dev/null
+++ b/xos/api/examples/cord/add_subscriber.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+source ./config.sh
+
+ACCOUNT_NUM=1238
+
+DATA=$(cat <<EOF
+{"identity": {"account_num": "$ACCOUNT_NUM", "name": "test-subscriber"},
+ "features": {"uplink_speed": 2000000000}}
+EOF
+)
+
+curl -H "Accept: application/json; indent=4" -H "Content-Type: application/json" -u $AUTH -X POST -d "$DATA" $HOST/api/tenant/cord/subscriber/   
diff --git a/xos/api/examples/cord/add_volt_to_subscriber.sh b/xos/api/examples/cord/add_volt_to_subscriber.sh
new file mode 100755
index 0000000..377ad65
--- /dev/null
+++ b/xos/api/examples/cord/add_volt_to_subscriber.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+source ./config.sh
+source ./util.sh
+
+ACCOUNT_NUM=1238
+S_TAG=34
+C_TAG=134
+
+SUBSCRIBER_ID=$(lookup_account_num $ACCOUNT_NUM)
+if [[ $? != 0 ]]; then
+    exit -1
+fi
+
+DATA=$(cat <<EOF
+{"s_tag": $S_TAG,
+ "c_tag": $C_TAG,
+ "subscriber": $SUBSCRIBER_ID}
+EOF
+)
+
+curl -H "Accept: application/json; indent=4" -H "Content-Type: application/json" -u $AUTH -X POST -d "$DATA" $HOST/api/tenant/cord/volt/
diff --git a/xos/api/examples/cord/config.sh b/xos/api/examples/cord/config.sh
new file mode 100644
index 0000000..92d703c
--- /dev/null
+++ b/xos/api/examples/cord/config.sh
@@ -0,0 +1,2 @@
+# see config.sh in the parent directory
+source ../config.sh
diff --git a/xos/api/examples/cord/delete_subscriber.sh b/xos/api/examples/cord/delete_subscriber.sh
new file mode 100755
index 0000000..0b897f2
--- /dev/null
+++ b/xos/api/examples/cord/delete_subscriber.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+source ./config.sh
+source ./util.sh
+
+ACCOUNT_NUM=1238
+
+SUBSCRIBER_ID=$(lookup_account_num $ACCOUNT_NUM)
+if [[ $? != 0 ]]; then
+    exit -1
+fi
+
+curl -u $AUTH -X DELETE $HOST/api/tenant/cord/subscriber/$SUBSCRIBER_ID/
diff --git a/xos/api/examples/cord/delete_volt_from_subscriber.sh b/xos/api/examples/cord/delete_volt_from_subscriber.sh
new file mode 100755
index 0000000..c3acd2e
--- /dev/null
+++ b/xos/api/examples/cord/delete_volt_from_subscriber.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+source ./config.sh
+source ./util.sh
+
+ACCOUNT_NUM=1238
+
+SUBSCRIBER_ID=$(lookup_account_num $ACCOUNT_NUM)
+if [[ $? != 0 ]]; then
+    exit -1
+fi
+
+VOLT_ID=$(lookup_subscriber_volt $SUBSCRIBER_ID)
+if [[ $? != 0 ]]; then
+    exit -1
+fi
+
+curl -u $AUTH -X DELETE $HOST/api/tenant/cord/volt/$VOLT_ID/
diff --git a/xos/api/examples/cord/get_subscriber.sh b/xos/api/examples/cord/get_subscriber.sh
new file mode 100755
index 0000000..d1c6c29
--- /dev/null
+++ b/xos/api/examples/cord/get_subscriber.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+source ./config.sh
+source ./util.sh
+
+ACCOUNT_NUM=1238
+
+SUBSCRIBER_ID=$(lookup_account_num $ACCOUNT_NUM)
+if [[ $? != 0 ]]; then
+    exit -1
+fi
+
+curl -H "Accept: application/json; indent=4" -u $AUTH -X GET $HOST/api/tenant/cord/subscriber/$SUBSCRIBER_ID/
diff --git a/xos/api/examples/cord/get_subscriber_features.sh b/xos/api/examples/cord/get_subscriber_features.sh
new file mode 100755
index 0000000..e50f2a7
--- /dev/null
+++ b/xos/api/examples/cord/get_subscriber_features.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+source ./config.sh
+source ./util.sh
+
+ACCOUNT_NUM=1238
+
+SUBSCRIBER_ID=$(lookup_account_num $ACCOUNT_NUM)
+if [[ $? != 0 ]]; then
+    exit -1
+fi
+
+curl -H "Accept: application/json; indent=4" -u $AUTH -X GET $HOST/api/tenant/cord/subscriber/$SUBSCRIBER_ID/features/
diff --git a/xos/api/examples/cord/update_subscriber.sh b/xos/api/examples/cord/update_subscriber.sh
new file mode 100755
index 0000000..9bbe501
--- /dev/null
+++ b/xos/api/examples/cord/update_subscriber.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+source ./config.sh
+source ./util.sh
+
+ACCOUNT_NUM=1238
+
+SUBSCRIBER_ID=$(lookup_account_num $ACCOUNT_NUM)
+if [[ $? != 0 ]]; then
+    exit -1
+fi
+
+DATA=$(cat <<EOF
+{"features": {"uplink_speed": 4000000000}}
+EOF
+)
+
+curl -H "Accept: application/json; indent=4" -H "Content-Type: application/json" -u $AUTH -X PUT -d "$DATA" $HOST/api/tenant/cord/subscriber/$SUBSCRIBER_ID/
diff --git a/xos/api/examples/cord/util.sh b/xos/api/examples/cord/util.sh
new file mode 100644
index 0000000..7b66903
--- /dev/null
+++ b/xos/api/examples/cord/util.sh
@@ -0,0 +1 @@
+source ../util.sh
diff --git a/xos/api/examples/exampleservice/add_exampletenant.sh b/xos/api/examples/exampleservice/add_exampletenant.sh
new file mode 100755
index 0000000..d9ab3e3
--- /dev/null
+++ b/xos/api/examples/exampleservice/add_exampletenant.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+source ./config.sh
+
+DATA=$(cat <<EOF
+{"tenant_message": "This is a test"}
+EOF
+)
+
+curl -H "Accept: application/json; indent=4" -H "Content-Type: application/json" -u $AUTH -X POST -d "$DATA" $HOST/api/tenant/exampletenant/   
diff --git a/xos/api/examples/exampleservice/config.sh b/xos/api/examples/exampleservice/config.sh
new file mode 100644
index 0000000..92d703c
--- /dev/null
+++ b/xos/api/examples/exampleservice/config.sh
@@ -0,0 +1,2 @@
+# see config.sh in the parent directory
+source ../config.sh
diff --git a/xos/api/examples/exampleservice/delete_exampletenant.sh b/xos/api/examples/exampleservice/delete_exampletenant.sh
new file mode 100755
index 0000000..6eaf8b7
--- /dev/null
+++ b/xos/api/examples/exampleservice/delete_exampletenant.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+source ./config.sh
+
+if [[ "$#" -ne 1 ]]; then
+    echo "Syntax: delete_exampletenant.sh <id>"
+    exit -1
+fi
+
+ID=$1
+
+curl -H "Accept: application/json; indent=4" -u $AUTH -X DELETE $HOST/api/tenant/exampletenant/$ID/
diff --git a/xos/api/examples/exampleservice/list_exampleservices.sh b/xos/api/examples/exampleservice/list_exampleservices.sh
new file mode 100755
index 0000000..e369dff
--- /dev/null
+++ b/xos/api/examples/exampleservice/list_exampleservices.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+source ./config.sh
+
+curl -H "Accept: application/json; indent=4" -u $AUTH -X GET $HOST/api/service/exampleservice/
diff --git a/xos/api/examples/exampleservice/list_exampletenants.sh b/xos/api/examples/exampleservice/list_exampletenants.sh
new file mode 100755
index 0000000..9e15968
--- /dev/null
+++ b/xos/api/examples/exampleservice/list_exampletenants.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+source ./config.sh
+
+curl -H "Accept: application/json; indent=4" -u $AUTH -X GET $HOST/api/tenant/exampletenant/   
diff --git a/xos/api/examples/exampleservice/update_exampletenant.sh b/xos/api/examples/exampleservice/update_exampletenant.sh
new file mode 100755
index 0000000..7d5e9ce
--- /dev/null
+++ b/xos/api/examples/exampleservice/update_exampletenant.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+source ./config.sh
+
+if [[ "$#" -ne 2 ]]; then
+    echo "Syntax: delete_exampletenant.sh <id> <message>"
+    exit -1
+fi
+
+ID=$1
+NEW_MESSAGE=$2
+
+DATA=$(cat <<EOF
+{"tenant_message": "$NEW_MESSAGE"}
+EOF
+)
+
+curl -H "Accept: application/json; indent=4" -H "Content-Type: application/json" -u $AUTH -X PUT -d "$DATA" $HOST/api/tenant/exampletenant/$ID/
diff --git a/xos/api/examples/util.sh b/xos/api/examples/util.sh
new file mode 100644
index 0000000..8373498
--- /dev/null
+++ b/xos/api/examples/util.sh
@@ -0,0 +1,32 @@
+source ./config.sh
+
+function lookup_account_num {
+    ID=`curl -f -s -u $AUTH -X GET $HOST/api/tenant/cord/account_num_lookup/$1/`
+    if [[ $? != 0 ]]; then
+        echo "function lookup_account_num with arguments $1 failed" >&2
+        echo "See CURL output below:" >&2
+        curl -s -u $AUTH -X GET $HOST/api/tenant/cord/account_num_lookup/$1/ >&2
+        exit -1
+    fi
+    # echo "(mapped account_num $1 to id $ID)" >&2
+    echo $ID
+}
+
+function lookup_subscriber_volt {
+    JSON=`curl -f -s -u $AUTH -X GET $HOST/api/tenant/cord/subscriber/$1/`
+    if [[ $? != 0 ]]; then
+        echo "function lookup_subscriber_volt failed to read subscriber with arg $1" >&2
+        echo "See CURL output below:" >&2
+        curl -s -u $AUTH -X GET $HOST/api/tenant/cord/account_num_lookup/$1/ >&2
+        exit -1
+    fi
+    ID=`echo $JSON | python -c "import json,sys; print json.load(sys.stdin)['related'].get('volt_id','')"`
+    if [[ $ID == "" ]]; then
+        echo "there is no volt for this subscriber" >&2
+        exit -1
+    fi
+
+    # echo "(found volt id %1)" >&2
+
+    echo $ID
+}
\ No newline at end of file
diff --git a/xos/api/import_methods.py b/xos/api/import_methods.py
index 2a6ca57..d53556c 100644
--- a/xos/api/import_methods.py
+++ b/xos/api/import_methods.py
@@ -27,7 +27,14 @@
 
     return module
 
-def import_api_methods(dirname=None, api_path="api"):
+def import_module_by_dotted_name(name):
+    print "import", name
+    module = __import__(name)
+    for part in name.split(".")[1:]:
+        module = getattr(module, part)
+    return module
+
+def import_api_methods(dirname=None, api_path="api", api_module="api"):
     subdirs=[]
     urlpatterns=[]
 
@@ -38,8 +45,10 @@
     for fn in os.listdir(dirname):
         pathname = os.path.join(dirname,fn)
         if os.path.isfile(pathname) and fn.endswith(".py") and (fn!="__init__.py") and (fn!="import_methods.py"):
-            module = import_module_from_filename(dirname, fn)
+            #module = import_module_from_filename(dirname, fn)
+            module = import_module_by_dotted_name(api_module + "." + fn[:-3])
             for classname in dir(module):
+#                print "  ",classname
                 c = getattr(module, classname, None)
 
                 if inspect.isclass(c) and issubclass(c, View) and (classname not in globals()):
@@ -47,12 +56,15 @@
 
                     method_kind = getattr(c, "method_kind", None)
                     method_name = getattr(c, "method_name", None)
-                    if method_kind and method_name:
-                        method_name = os.path.join(api_path, method_name)
+                    if method_kind:
+                        if method_name:
+                            method_name = os.path.join(api_path, method_name)
+                        else:
+                            method_name = api_path
                         view_urls.append( (method_kind, method_name, classname, c) )
 
         elif os.path.isdir(pathname):
-            urlpatterns.extend(import_api_methods(pathname, os.path.join(api_path, fn)))
+            urlpatterns.extend(import_api_methods(pathname, os.path.join(api_path, fn), api_module+"." + fn))
 
     for view_url in view_urls:
         if view_url[0] == "list":
diff --git a/xos/api/service/__init__.py b/xos/api/service/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/xos/api/service/__init__.py
@@ -0,0 +1 @@
+
diff --git a/xos/api/service/exampleservice.py b/xos/api/service/exampleservice.py
new file mode 100644
index 0000000..d8fe23a
--- /dev/null
+++ b/xos/api/service/exampleservice.py
@@ -0,0 +1,54 @@
+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 import status
+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 api.xosapi_helpers import PlusModelSerializer, XOSViewSet, ReadOnlyField
+from django.shortcuts import get_object_or_404
+from xos.apibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView, XOSPermissionDenied
+from xos.exceptions import *
+import json
+import subprocess
+from services.exampleservice.models import ExampleService
+
+class ExampleServiceSerializer(PlusModelSerializer):
+        id = ReadOnlyField()
+        humanReadableName = serializers.SerializerMethodField("getHumanReadableName")
+        service_message = serializers.CharField(required=False)
+
+        class Meta:
+            model = ExampleService
+            fields = ('humanReadableName',
+                      'id',
+                      'service_message')
+
+        def getHumanReadableName(self, obj):
+            return obj.__unicode__()
+
+class ExampleServiceViewSet(XOSViewSet):
+    base_name = "exampleservice"
+    method_name = "exampleservice"
+    method_kind = "viewset"
+    queryset = ExampleService.get_service_objects().all()
+    serializer_class = ExampleServiceSerializer
+
+    @classmethod
+    def get_urlpatterns(self, api_path="^"):
+        patterns = super(ExampleServiceViewSet, self).get_urlpatterns(api_path=api_path)
+
+        return patterns
+
+    def list(self, request):
+        object_list = self.filter_queryset(self.get_queryset())
+
+        serializer = self.get_serializer(object_list, many=True)
+
+        return Response(serializer.data)
+
diff --git a/xos/api/service/vbng/__init__.py b/xos/api/service/vbng/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/xos/api/service/vbng/__init__.py
@@ -0,0 +1 @@
+
diff --git a/xos/api/service/vbng/debug.py b/xos/api/service/vbng/debug.py
index 8a7f69e..8ecec0f 100644
--- a/xos/api/service/vbng/debug.py
+++ b/xos/api/service/vbng/debug.py
@@ -11,7 +11,7 @@
 from django.conf.urls import patterns, url
 from services.cord.models import VOLTTenant, VBNGTenant, CordSubscriberRoot
 from core.xoslib.objects.cordsubscriber import CordSubscriber
-from api.xosapi_helpers import PlusSerializerMixin, XOSViewSet
+from api.xosapi_helpers import PlusModelSerializer, XOSViewSet
 from django.shortcuts import get_object_or_404
 from xos.apibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView, XOSPermissionDenied
 from xos.exceptions import *
@@ -25,7 +25,7 @@
     # rest_framework 2.x
     ReadOnlyField = serializers.Field
 
-class CordDebugIdSerializer(serializers.ModelSerializer, PlusSerializerMixin):
+class CordDebugIdSerializer(PlusModelSerializer):
     # Swagger is failing because CordDebugViewSet has neither a model nor
     # a serializer_class. Stuck this in here as a placeholder for now.
     id = ReadOnlyField()
diff --git a/xos/api/service/vsg/__init__.py b/xos/api/service/vsg/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/xos/api/service/vsg/__init__.py
@@ -0,0 +1 @@
+
diff --git a/xos/api/service/vsg/vsgservice.py b/xos/api/service/vsg/vsgservice.py
new file mode 100644
index 0000000..9ab4756
--- /dev/null
+++ b/xos/api/service/vsg/vsgservice.py
@@ -0,0 +1,78 @@
+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 import status
+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 services.cord.models import VSGService
+from api.xosapi_helpers import PlusModelSerializer, XOSViewSet, ReadOnlyField
+from django.shortcuts import get_object_or_404
+from xos.apibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView, XOSPermissionDenied
+from xos.exceptions import *
+import json
+import subprocess
+from django.views.decorators.csrf import ensure_csrf_cookie
+
+class VSGServiceForApi(VSGService):
+    class Meta:
+        proxy = True
+        app_label = "cord"
+
+    def __init__(self, *args, **kwargs):
+        super(VSGServiceForApi, self).__init__(*args, **kwargs)
+
+    def save(self, *args, **kwargs):
+        super(VSGServiceForApi, self).save(*args, **kwargs)
+
+    def __init__(self, *args, **kwargs):
+        super(VSGService, self).__init__(*args, **kwargs)
+
+class VSGServiceSerializer(PlusModelSerializer):
+        id = ReadOnlyField()
+        humanReadableName = serializers.SerializerMethodField("getHumanReadableName")
+        wan_container_gateway_ip = serializers.CharField(required=False)
+        wan_container_gateway_mac = serializers.CharField(required=False)
+        dns_servers = serializers.CharField(required=False)
+        url_filter_kind = serializers.CharField(required=False)
+        node_label = serializers.CharField(required=False)
+
+        class Meta:
+            model = VSGServiceForApi
+            fields = ('humanReadableName',
+                      'id',
+                      'wan_container_gateway_ip',
+                      'wan_container_gateway_mac',
+                      'dns_servers',
+                      'url_filter_kind',
+                      'node_label')
+
+        def getHumanReadableName(self, obj):
+            return obj.__unicode__()
+
+# @ensure_csrf_cookie
+class VSGServiceViewSet(XOSViewSet):
+    base_name = "vsgservice"
+    method_name = None # use the api endpoint /api/service/vsg/
+    method_kind = "viewset"
+    queryset = VSGService.get_service_objects().select_related().all()
+    serializer_class = VSGServiceSerializer
+
+    @classmethod
+    def get_urlpatterns(self, api_path="^"):
+        patterns = super(VSGServiceViewSet, self).get_urlpatterns(api_path=api_path)
+
+        return patterns
+
+    def list(self, request):
+        object_list = self.filter_queryset(self.get_queryset())
+
+        serializer = self.get_serializer(object_list, many=True)
+
+        return Response(serializer.data)
+
diff --git a/xos/api/tenant/__init__.py b/xos/api/tenant/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/xos/api/tenant/__init__.py
@@ -0,0 +1 @@
+
diff --git a/xos/api/tenant/cord/__init__.py b/xos/api/tenant/cord/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/xos/api/tenant/cord/__init__.py
@@ -0,0 +1 @@
+
diff --git a/xos/api/tenant/cord/subscriber.py b/xos/api/tenant/cord/subscriber.py
index efb9e39..89f42b9 100644
--- a/xos/api/tenant/cord/subscriber.py
+++ b/xos/api/tenant/cord/subscriber.py
@@ -4,13 +4,14 @@
 from rest_framework import serializers
 from rest_framework import generics
 from rest_framework import viewsets
+from rest_framework import status
 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 services.cord.models import VOLTTenant, VBNGTenant, CordSubscriberRoot
-from api.xosapi_helpers import PlusSerializerMixin, XOSViewSet, ReadOnlyField
+from api.xosapi_helpers import PlusModelSerializer, XOSViewSet, ReadOnlyField
 from django.shortcuts import get_object_or_404
 from xos.apibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView, XOSPermissionDenied
 from xos.exceptions import *
@@ -45,6 +46,7 @@
         self.enable_uverse = value.get("uverse", self.get_default_attribute("enable_uverse"))
         self.status = value.get("status", self.get_default_attribute("status"))
 
+
     def update_features(self, value):
         d=self.features
         d.update(value)
@@ -52,11 +54,13 @@
 
     @property
     def identity(self):
-        return {"account_num": self.service_specific_id}
+        return {"account_num": self.service_specific_id,
+                "name": self.name}
 
     @identity.setter
     def identity(self, value):
-        self.service_specific_id = value.get("account_num", "")
+        self.service_specific_id = value.get("account_num", self.service_specific_id)
+        self.name = value.get("name", self.name)
 
     def update_identity(self, value):
         d=self.identity
@@ -76,11 +80,16 @@
                     related["instance_id"] = self.volt.vcpe.instance.id
                     related["instance_name"] = self.volt.vcpe.instance.name
                     related["wan_container_ip"] = self.volt.vcpe.wan_container_ip
+                    if self.volt.vcpe.instance.node:
+                         related["compute_node_name"] = self.volt.vcpe.instance.node.name
         return related
 
     def save(self, *args, **kwargs):
         super(CordSubscriberNew, self).save(*args, **kwargs)
 
+# Add some structure to the REST API by subdividing the object into
+# features, identity, and related.
+
 class FeatureSerializer(serializers.Serializer):
     cdn = serializers.BooleanField(required=False)
     uplink_speed = serializers.IntegerField(required=False)
@@ -90,14 +99,17 @@
 
 class IdentitySerializer(serializers.Serializer):
     account_num = serializers.CharField(required=False)
+    name = serializers.CharField(required=False)
 
-class CordSubscriberSerializer(serializers.ModelSerializer, PlusSerializerMixin):
+class CordSubscriberSerializer(PlusModelSerializer):
         id = ReadOnlyField()
         humanReadableName = serializers.SerializerMethodField("getHumanReadableName")
-        features = FeatureSerializer()
-        identity = IdentitySerializer()
+        features = FeatureSerializer(required=False)
+        identity = IdentitySerializer(required=False)
         related = serializers.DictField(required=False)
 
+        nested_fields = ["features", "identity"]
+
         class Meta:
             model = CordSubscriberNew
             fields = ('humanReadableName',
@@ -125,8 +137,10 @@
         patterns.append( self.detail_url("identity/$", {"get": "get_identities", "put": "set_identities"}, "identities") )
         patterns.append( self.detail_url("identity/(?P<identity>[a-zA-Z0-9\-_]+)/$", {"get": "get_identity", "put": "set_identity"}, "get_identity") )
 
-        patterns.append( url(self.api_path + "subidlookup/(?P<ssid>[0-9\-]+)/$", self.as_view({"get": "ssiddetail"}), name="ssiddetail") )
-        patterns.append( url(self.api_path + "subidlookup/$", self.as_view({"get": "ssidlist"}), name="ssidlist") )
+        patterns.append( url(self.api_path + "account_num_lookup/(?P<account_num>[0-9\-]+)/$", self.as_view({"get": "account_num_detail"}), name="account_num_detail") )
+
+        patterns.append( url(self.api_path + "ssidmap/(?P<ssid>[0-9\-]+)/$", self.as_view({"get": "ssiddetail"}), name="ssiddetail") )
+        patterns.append( url(self.api_path + "ssidmap/$", self.as_view({"get": "ssidlist"}), name="ssidlist") )
 
         return patterns
 
@@ -189,6 +203,14 @@
         subscriber.save()
         return Response({identity: IdentitySerializer(subscriber.identity).data[identity]})
 
+    def account_num_detail(self, pk=None, account_num=None):
+        object_list = CordSubscriberNew.get_tenant_objects().all()
+        object_list = [x for x in object_list if x.service_specific_id == account_num]
+        if not object_list:
+            return Response("Failed to find account_num %s" % account_num, status=status.HTTP_404_NOT_FOUND)
+
+        return Response( object_list[0].id )
+
     def ssidlist(self, request):
         object_list = CordSubscriberNew.get_tenant_objects().all()
 
diff --git a/xos/api/tenant/cord/volt.py b/xos/api/tenant/cord/volt.py
new file mode 100644
index 0000000..e17cf26
--- /dev/null
+++ b/xos/api/tenant/cord/volt.py
@@ -0,0 +1,96 @@
+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 status
+from core.models import *
+from django.forms import widgets
+from services.cord.models import VOLTTenant, VOLTService, CordSubscriberRoot
+from xos.apibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView, XOSPermissionDenied
+from api.xosapi_helpers import PlusModelSerializer, XOSViewSet, ReadOnlyField
+
+def get_default_volt_service():
+    volt_services = VOLTService.get_service_objects().all()
+    if volt_services:
+        return volt_services[0].id
+    return None
+
+class VOLTTenantForAPI(VOLTTenant):
+    class Meta:
+        proxy = True
+        app_label = "cord"
+
+    @property
+    def subscriber(self):
+        return self.subscriber_root.id
+
+    @subscriber.setter
+    def subscriber(self, value):
+        self.subscriber_root = value # CordSubscriberRoot.get_tenant_objects().get(id=value)
+
+    @property
+    def related(self):
+        related = {}
+        if self.vcpe:
+            related["vsg_id"] = self.vcpe.id
+            if self.vcpe.instance:
+                related["instance_id"] = self.vcpe.instance.id
+                related["instance_name"] = self.vcpe.instance.name
+                related["wan_container_ip"] = self.vcpe.wan_container_ip
+                if self.vcpe.instance.node:
+                    related["compute_node_name"] = self.vcpe.instance.node.name
+        return related
+
+class VOLTTenantSerializer(PlusModelSerializer):
+    id = ReadOnlyField()
+    service_specific_id = serializers.CharField(required=False)
+    s_tag = serializers.CharField()
+    c_tag = serializers.CharField()
+    subscriber = serializers.PrimaryKeyRelatedField(queryset=CordSubscriberRoot.get_tenant_objects().all(), required=False)
+    related = serializers.DictField(required=False)
+
+    property_fields=["subscriber"]
+
+    humanReadableName = serializers.SerializerMethodField("getHumanReadableName")
+    class Meta:
+        model = VOLTTenantForAPI
+        fields = ('humanReadableName', 'id', 'service_specific_id', 's_tag', 'c_tag', 'subscriber', 'related' )
+
+    def getHumanReadableName(self, obj):
+        return obj.__unicode__()
+
+class VOLTTenantViewSet(XOSViewSet):
+    base_name = "volt"
+    method_name = "volt"
+    method_kind = "viewset"
+    queryset = VOLTTenantForAPI.get_tenant_objects().all() # select_related().all()
+    serializer_class = VOLTTenantSerializer
+
+    @classmethod
+    def get_urlpatterns(self, api_path="^"):
+        patterns = super(VOLTTenantViewSet, self).get_urlpatterns(api_path=api_path)
+
+        return patterns
+
+    def list(self, request):
+        queryset = self.filter_queryset(self.get_queryset())
+
+        c_tag = self.request.query_params.get('c_tag', None)
+        if c_tag is not None:
+            ids = [x.id for x in queryset if x.get_attribute("c_tag", None)==c_tag]
+            queryset = queryset.filter(id__in=ids)
+
+        s_tag = self.request.query_params.get('s_tag', None)
+        if s_tag is not None:
+            ids = [x.id for x in queryset if x.get_attribute("s_tag", None)==s_tag]
+            queryset = queryset.filter(id__in=ids)
+
+        serializer = self.get_serializer(queryset, many=True)
+
+        return Response(serializer.data)
+
+
+
+
+
diff --git a/xos/api/tenant/exampletenant.py b/xos/api/tenant/exampletenant.py
new file mode 100644
index 0000000..b046a88
--- /dev/null
+++ b/xos/api/tenant/exampletenant.py
@@ -0,0 +1,55 @@
+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 status
+from core.models import *
+from django.forms import widgets
+from services.cord.models import CordSubscriberRoot
+from xos.apibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView, XOSPermissionDenied
+from api.xosapi_helpers import PlusModelSerializer, XOSViewSet, ReadOnlyField
+
+from services.exampleservice.models import ExampleTenant, ExampleService
+
+def get_default_example_service():
+    example_services = ExampleService.get_service_objects().all()
+    if example_services:
+        return example_services[0]
+    return None
+
+class ExampleTenantSerializer(PlusModelSerializer):
+        id = ReadOnlyField()
+        provider_service = serializers.PrimaryKeyRelatedField(queryset=ExampleService.get_service_objects().all(), default=get_default_example_service)
+        tenant_message = serializers.CharField(required=False)
+        backend_status = ReadOnlyField()
+
+        humanReadableName = serializers.SerializerMethodField("getHumanReadableName")
+
+        class Meta:
+            model = ExampleTenant
+            fields = ('humanReadableName', 'id', 'provider_service', 'tenant_message', 'backend_status')
+
+        def getHumanReadableName(self, obj):
+            return obj.__unicode__()
+
+class ExampleTenantViewSet(XOSViewSet):
+    base_name = "exampletenant"
+    method_name = "exampletenant"
+    method_kind = "viewset"
+    queryset = ExampleTenant.get_tenant_objects().all()
+    serializer_class = ExampleTenantSerializer
+
+    @classmethod
+    def get_urlpatterns(self, api_path="^"):
+        patterns = super(ExampleTenantViewSet, self).get_urlpatterns(api_path=api_path)
+
+        return patterns
+
+    def list(self, request):
+        queryset = self.filter_queryset(self.get_queryset())
+
+        serializer = self.get_serializer(queryset, many=True)
+
+        return Response(serializer.data)
+
diff --git a/xos/api/tenant/truckroll.py b/xos/api/tenant/truckroll.py
new file mode 100644
index 0000000..ea0200d
--- /dev/null
+++ b/xos/api/tenant/truckroll.py
@@ -0,0 +1,68 @@
+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 status
+from core.models import *
+from django.forms import widgets
+from services.cord.models import CordSubscriberRoot
+from services.vtr.models import VTRTenant, VTRService
+from xos.apibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView, XOSPermissionDenied
+from api.xosapi_helpers import PlusModelSerializer, XOSViewSet, ReadOnlyField
+
+def get_default_vtr_service():
+    vtr_services = VTRService.get_service_objects().all()
+    if vtr_services:
+        return vtr_services[0].id
+    return None
+
+class VTRTenantForAPI(VTRTenant):
+    class Meta:
+        proxy = True
+        app_label = "cord"
+
+class VTRTenantSerializer(PlusModelSerializer):
+        id = ReadOnlyField()
+        target_id = serializers.IntegerField()
+        test = serializers.CharField()
+        scope = serializers.CharField()
+        argument = serializers.CharField(required=False)
+        provider_service = serializers.PrimaryKeyRelatedField(queryset=VTRService.get_service_objects().all(), default=get_default_vtr_service)
+        result = serializers.CharField(required=False)
+        result_code = serializers.CharField(required=False)
+        backend_status = ReadOnlyField()
+
+        humanReadableName = serializers.SerializerMethodField("getHumanReadableName")
+        is_synced = serializers.SerializerMethodField("isSynced")
+
+        class Meta:
+            model = VTRTenantForAPI
+            fields = ('humanReadableName', 'id', 'provider_service', 'target_id', 'scope', 'test', 'argument', 'result', 'result_code', 'is_synced', 'backend_status' )
+
+        def getHumanReadableName(self, obj):
+            return obj.__unicode__()
+
+        def isSynced(self, obj):
+            return (obj.enacted is not None) and (obj.enacted >= obj.updated)
+
+class TruckRollViewSet(XOSViewSet):
+    base_name = "truckroll"
+    method_name = "truckroll"
+    method_kind = "viewset"
+    queryset = VTRTenantForAPI.get_tenant_objects().all() # select_related().all()
+    serializer_class = VTRTenantSerializer
+
+    @classmethod
+    def get_urlpatterns(self, api_path="^"):
+        patterns = super(TruckRollViewSet, self).get_urlpatterns(api_path=api_path)
+
+        return patterns
+
+    def list(self, request):
+        queryset = self.filter_queryset(self.get_queryset())
+
+        serializer = self.get_serializer(queryset, many=True)
+
+        return Response(serializer.data)
+
diff --git a/xos/api/xosapi_helpers.py b/xos/api/xosapi_helpers.py
index 98b5a5c..ee3ed00 100644
--- a/xos/api/xosapi_helpers.py
+++ b/xos/api/xosapi_helpers.py
@@ -19,7 +19,7 @@
     example, stuff related to backend fields.
 """
 
-class PlusSerializerMixin():
+class PlusModelSerializer(serializers.ModelSerializer):
     backendIcon = serializers.SerializerMethodField("getBackendIcon")
     backendHtml = serializers.SerializerMethodField("getBackendHtml")
 
@@ -35,18 +35,55 @@
     def getBackendHtml(self, obj):
         return obj.getBackendHtml()
 
+    def create(self, validated_data):
+        property_fields = getattr(self, "property_fields", [])
+        create_fields = {}
+        for k in validated_data:
+            if not k in property_fields:
+                create_fields[k] = validated_data[k]
+        obj = self.Meta.model(**create_fields)
+
+        for k in validated_data:
+            if k in property_fields:
+                setattr(obj, k, validated_data[k])
+
+        obj.caller = self.context['request'].user
+        obj.save()
+        return obj
+
+    def update(self, instance, validated_data):
+        nested_fields = getattr(self, "nested_fields", [])
+        for k in validated_data.keys():
+            v = validated_data[k]
+            if k in nested_fields:
+                d = getattr(instance,k)
+                d.update(v)
+                setattr(instance,k,d)
+            else:
+                setattr(instance, k, v)
+        instance.caller = self.context['request'].user
+        instance.save()
+        return instance
+
 class XOSViewSet(viewsets.ModelViewSet):
     api_path=""
 
     @classmethod
+    def get_api_method_path(self):
+        if self.method_name:
+            return self.api_path + self.method_name + "/"
+        else:
+            return self.api_path
+
+    @classmethod
     def detail_url(self, pattern, viewdict, name):
-        return url(self.api_path + self.method_name + r'/(?P<pk>[a-zA-Z0-9\-]+)/' + pattern,
+        return url(self.get_api_method_path() + 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(self.api_path + self.method_name + r'/' + pattern,
+        return url(self.get_api_method_path() + pattern,
                    self.as_view(viewdict),
                    name=self.base_name+"_"+name)
 
@@ -56,7 +93,7 @@
 
         patterns = []
 
-        patterns.append(url(self.api_path + self.method_name + '/$', self.as_view({'get': 'list'}), name=self.base_name+'_list'))
-        patterns.append(url(self.api_path + 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'))
+        patterns.append(url(self.get_api_method_path() + '$', self.as_view({'get': 'list', 'post': 'create'}), name=self.base_name+'_list'))
+        patterns.append(url(self.get_api_method_path() + '(?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/models/service.py b/xos/core/models/service.py
index ee28cf6..124feb5 100644
--- a/xos/core/models/service.py
+++ b/xos/core/models/service.py
@@ -37,10 +37,10 @@
             if attrname==name:
                 return default
         if hasattr(cls,"default_attributes"):
-            if attrname in cls.default_attributes:
-                return cls.default_attributes[attrname]
-        else:
-            return None
+            if name in cls.default_attributes:
+                return cls.default_attributes[name]
+
+        return None
 
     @classmethod
     def setup_simple_attributes(cls):
@@ -332,6 +332,18 @@
             tr_ids = [trp.tenant_root.id for trp in TenantRootPrivilege.objects.filter(user=user)]
             return cls.objects.filter(id__in=tr_ids)
 
+    # helper function to be used in subclasses that want to ensure service_specific_id is unique
+    def validate_unique_service_specific_id(self, none_okay=False):
+        if not none_okay and (self.service_specific_id is None):
+            raise XOSMissingField("subscriber_specific_id is None, and it's a required field", fields={"service_specific_id": "cannot be none"})
+
+        if self.service_specific_id:
+            conflicts = self.get_tenant_objects().filter(service_specific_id=self.service_specific_id)
+            if self.pk:
+                conflicts = conflicts.exclude(pk=self.pk)
+            if conflicts:
+                raise XOSDuplicateKey("service_specific_id %s already exists" % self.service_specific_id, fields={"service_specific_id": "duplicate key"})
+
 class Tenant(PlCoreBase, AttributeMixin):
     """ A tenant is a relationship between two entities, a subscriber and a
         provider. This object represents an edge.
diff --git a/xos/core/xoslib/methods/sliceplus.py b/xos/core/xoslib/methods/sliceplus.py
index aaf2917..9e5b4a1 100644
--- a/xos/core/xoslib/methods/sliceplus.py
+++ b/xos/core/xoslib/methods/sliceplus.py
@@ -8,43 +8,39 @@
 from core.xoslib.objects.sliceplus import SlicePlus
 from plus import PlusSerializerMixin
 from xos.apibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView, XOSPermissionDenied
+import json
 
 if hasattr(serializers, "ReadOnlyField"):
     # rest_framework 3.x
     IdField = serializers.ReadOnlyField
     WritableField = serializers.Field
+    DictionaryField = serializers.DictField
+    ListField = serializers.ListField
 else:
     # rest_framework 2.x
     IdField = serializers.Field
     WritableField = serializers.WritableField
 
-class NetworkPortsField(WritableField):   # note: maybe just Field in rest_framework 3.x instead of WritableField
-    def to_representation(self, obj):
-        return obj
+    class DictionaryField(WritableField):   # note: maybe just Field in rest_framework 3.x instead of WritableField
+        def to_representation(self, obj):
+            return json.dumps(obj)
 
-    def to_internal_value(self, data):
-        return data
+        def to_internal_value(self, data):
+            return json.loads(data)
 
-class DictionaryField(WritableField):   # note: maybe just Field in rest_framework 3.x instead of WritableField
-    def to_representation(self, obj):
-        return json.dumps(obj)
+    class ListField(WritableField):   # note: maybe just Field in rest_framework 3.x instead of WritableField
+        def to_representation(self, obj):
+            return json.dumps(obj)
 
-    def to_internal_value(self, data):
-        return json.loads(data)
-
-class ListField(WritableField):   # note: maybe just Field in rest_framework 3.x instead of WritableField
-    def to_representation(self, obj):
-        return json.dumps(obj)
-
-    def to_internal_value(self, data):
-        return json.loads(data)
+        def to_internal_value(self, data):
+            return json.loads(data)
 
 class SlicePlusIdSerializer(serializers.ModelSerializer, PlusSerializerMixin):
         id = IdField()
 
         sliceInfo = serializers.SerializerMethodField("getSliceInfo")
         humanReadableName = serializers.SerializerMethodField("getHumanReadableName")
-        network_ports = NetworkPortsField(required=False)
+        network_ports = serializers.CharField(required=False)
         site_allocation = DictionaryField(required=False)
         site_ready = DictionaryField(required=False)
         users = ListField(required=False)
@@ -80,7 +76,7 @@
     method_name = "slicesplus"
 
     def get_queryset(self):
-        current_user_can_see = self.request.QUERY_PARAMS.get('current_user_can_see', False)
+        current_user_can_see = self.request.query_params.get('current_user_can_see', False)
 
         if (not self.request.user.is_authenticated()):
             raise XOSPermissionDenied("You must be authenticated in order to use this API")
diff --git a/xos/core/xoslib/methods/volttenant.py b/xos/core/xoslib/methods/volttenant.py
index 05cd7e8..229e105 100644
--- a/xos/core/xoslib/methods/volttenant.py
+++ b/xos/core/xoslib/methods/volttenant.py
@@ -60,21 +60,21 @@
     def get_queryset(self):
         queryset = VOLTTenant.get_tenant_objects().select_related().all()
 
-        service_specific_id = self.request.QUERY_PARAMS.get('service_specific_id', None)
+        service_specific_id = self.request.query_params.get('service_specific_id', None)
         if service_specific_id is not None:
             queryset = queryset.filter(service_specific_id=service_specific_id)
 
-#        vlan_id = self.request.QUERY_PARAMS.get('vlan_id', None)
+#        vlan_id = self.request.query_params.get('vlan_id', None)
 #        if vlan_id is not None:
 #            ids = [x.id for x in queryset if x.get_attribute("vlan_id", None)==vlan_id]
 #            queryset = queryset.filter(id__in=ids)
 
-        c_tag = self.request.QUERY_PARAMS.get('c_tag', None)
+        c_tag = self.request.query_params.get('c_tag', None)
         if c_tag is not None:
             ids = [x.id for x in queryset if x.get_attribute("c_tag", None)==c_tag]
             queryset = queryset.filter(id__in=ids)
 
-        s_tag = self.request.QUERY_PARAMS.get('s_tag', None)
+        s_tag = self.request.query_params.get('s_tag', None)
         if s_tag is not None:
             ids = [x.id for x in queryset if x.get_attribute("s_tag", None)==s_tag]
             queryset = queryset.filter(id__in=ids)
diff --git a/xos/services/cord/models.py b/xos/services/cord/models.py
index 9b98262..7adc4cc 100644
--- a/xos/services/cord/models.py
+++ b/xos/services/cord/models.py
@@ -158,6 +158,7 @@
         pass
 
     def save(self, *args, **kwargs):
+        self.validate_unique_service_specific_id(none_okay=True)
         if (not hasattr(self, 'caller') or not self.caller.is_admin):
             if (self.has_field_changed("service_specific_id")):
                 raise XOSPermissionDenied("You do not have permission to change service_specific_id")
@@ -319,7 +320,9 @@
                 vcpe.delete()
 
     def save(self, *args, **kwargs):
-        self.validate_unique_service_specific_id()
+        # VOLTTenant probably doesn't need a SSID anymore; that will be handled
+        # by CORDSubscriberRoot...
+        # self.validate_unique_service_specific_id()
 
         if (self.subscriber_root is not None):
             subs = self.subscriber_root.get_subscribed_tenants(VOLTTenant)