Merge branch 'master' of github.com:open-cloud/xos
diff --git a/containers/xos/Dockerfile b/containers/xos/Dockerfile
index a558018..b5064ae 100644
--- a/containers/xos/Dockerfile
+++ b/containers/xos/Dockerfile
@@ -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.devel b/containers/xos/Dockerfile.devel
index 818b463..7bed082 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 c9a577a..cfcf9ac 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/__init__.py b/xos/api/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/xos/api/__init__.py
@@ -0,0 +1 @@
+
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
new file mode 100644
index 0000000..d53556c
--- /dev/null
+++ b/xos/api/import_methods.py
@@ -0,0 +1,81 @@
+from django.views.generic import View
+from django.conf.urls import patterns, url, include
+from rest_framework.routers import DefaultRouter
+import os, sys
+import inspect
+import importlib
+
+try:
+    from rest_framework.serializers import DictField
+except:
+    raise Exception("Failed check for django-rest-framework >= 3.3.3")
+
+urlpatterns = []
+
+def import_module_from_filename(dirname, fn):
+    print "importing", dirname, fn
+    sys_path_save = sys.path
+    try:
+        # __import__() and importlib.import_module() both import modules from
+        # sys.path. So we make sure that the path where we can find the views is
+        # the first thing in sys.path.
+        sys.path = [dirname] + sys.path
+
+        module = __import__(fn[:-3])
+    finally:
+        sys.path = sys_path_save
+
+    return module
+
+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=[]
+
+    if not dirname:
+        dirname = os.path.dirname(os.path.abspath(__file__))
+
+    view_urls = []
+    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_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()):
+                    globals()[classname] = c
+
+                    method_kind = getattr(c, "method_kind", None)
+                    method_name = getattr(c, "method_name", None)
+                    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), api_module+"." + fn))
+
+    for view_url in view_urls:
+        if view_url[0] == "list":
+           urlpatterns.append(url(r'^' + view_url[1] + '/$',  view_url[3].as_view(), name=view_url[1]+'list'))
+        elif view_url[0] == "detail":
+           urlpatterns.append(url(r'^' + view_url[1] + '/(?P<pk>[a-zA-Z0-9\-]+)/$',  view_url[3].as_view(), name=view_url[1]+'detail'))
+        elif view_url[0] == "viewset":
+           viewset = view_url[3]
+           urlpatterns.extend(viewset.get_urlpatterns(api_path="^"+api_path+"/"))
+
+    return urlpatterns
+
+urlpatterns = import_api_methods()
+
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
new file mode 100644
index 0000000..8ecec0f
--- /dev/null
+++ b/xos/api/service/vbng/debug.py
@@ -0,0 +1,61 @@
+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 services.cord.models import VOLTTenant, VBNGTenant, CordSubscriberRoot
+from core.xoslib.objects.cordsubscriber import CordSubscriber
+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 *
+import json
+import subprocess
+
+if hasattr(serializers, "ReadOnlyField"):
+    # rest_framework 3.x
+    ReadOnlyField = serializers.ReadOnlyField
+else:
+    # rest_framework 2.x
+    ReadOnlyField = serializers.Field
+
+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()
+    class Meta:
+        model = CordSubscriber
+
+class CordDebugViewSet(XOSViewSet):
+    base_name = "debug"
+    method_name = "debug"
+    method_kind = "viewset"
+    serializer_class = CordDebugIdSerializer
+
+    @classmethod
+    def get_urlpatterns(self, api_path="^"):
+        patterns = []
+        patterns.append( url(api_path + "debug/vbng_dump/$", self.as_view({"get": "get_vbng_dump"}), name="vbng_dump"))
+        return patterns
+
+    # contact vBNG service and dump current list of mappings
+    def get_vbng_dump(self, request, pk=None):
+        result=subprocess.check_output(["curl", "http://10.0.3.136:8181/onos/virtualbng/privateip/map"])
+        if request.GET.get("theformat",None)=="text":
+            from django.http import HttpResponse
+            result = json.loads(result)["map"]
+
+            lines = []
+            for row in result:
+                for k in row.keys():
+                     lines.append( "%s %s" % (k, row[k]) )
+
+            return HttpResponse("\n".join(lines), content_type="text/plain")
+        else:
+            return Response( {"vbng_dump": json.loads(result)["map"] } )
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
new file mode 100644
index 0000000..89f42b9
--- /dev/null
+++ b/xos/api/tenant/cord/subscriber.py
@@ -0,0 +1,230 @@
+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 VOLTTenant, VBNGTenant, CordSubscriberRoot
+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 CordSubscriberNew(CordSubscriberRoot):
+    class Meta:
+        proxy = True
+        app_label = "cord"
+
+    def __init__(self, *args, **kwargs):
+        super(CordSubscriberNew, self).__init__(*args, **kwargs)
+
+    def __unicode__(self):
+        return u"cordSubscriber-%s" % str(self.id)
+
+    @property
+    def features(self):
+        return {"cdn": self.cdn_enable,
+                "uplink_speed": self.uplink_speed,
+                "downlink_speed": self.downlink_speed,
+                "uverse": self.enable_uverse,
+                "status": self.status}
+
+    @features.setter
+    def features(self, value):
+        self.cdn_enable = value.get("cdn", self.get_default_attribute("cdn_enable"))
+        self.uplink_speed = value.get("uplink_speed", self.get_default_attribute("uplink_speed"))
+        self.downlink_speed = value.get("downlink_speed", self.get_default_attribute("downlink_speed"))
+        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)
+        self.features = d
+
+    @property
+    def identity(self):
+        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)
+        self.name = value.get("name", self.name)
+
+    def update_identity(self, value):
+        d=self.identity
+        d.update(value)
+        self.identity = d
+
+    @property
+    def related(self):
+        related = {}
+        if self.volt:
+            related["volt_id"] = self.volt.id
+            related["s_tag"] = self.volt.s_tag
+            related["c_tag"] = self.volt.c_tag
+            if self.volt.vcpe:
+                related["vsg_id"] = self.volt.vcpe.id
+                if self.volt.vcpe.instance:
+                    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)
+    downlink_speed = serializers.IntegerField(required=False)
+    uverse = serializers.BooleanField(required=False)
+    status = serializers.CharField(required=False)
+
+class IdentitySerializer(serializers.Serializer):
+    account_num = serializers.CharField(required=False)
+    name = serializers.CharField(required=False)
+
+class CordSubscriberSerializer(PlusModelSerializer):
+        id = ReadOnlyField()
+        humanReadableName = serializers.SerializerMethodField("getHumanReadableName")
+        features = FeatureSerializer(required=False)
+        identity = IdentitySerializer(required=False)
+        related = serializers.DictField(required=False)
+
+        nested_fields = ["features", "identity"]
+
+        class Meta:
+            model = CordSubscriberNew
+            fields = ('humanReadableName',
+                      'id',
+                      'features',
+                      'identity',
+                      'related')
+
+        def getHumanReadableName(self, obj):
+            return obj.__unicode__()
+
+# @ensure_csrf_cookie
+class CordSubscriberViewSet(XOSViewSet):
+    base_name = "subscriber"
+    method_name = "subscriber"
+    method_kind = "viewset"
+    queryset = CordSubscriberNew.get_tenant_objects().select_related().all()
+    serializer_class = CordSubscriberSerializer
+
+    @classmethod
+    def get_urlpatterns(self, api_path="^"):
+        patterns = super(CordSubscriberViewSet, self).get_urlpatterns(api_path=api_path)
+        patterns.append( self.detail_url("features/$", {"get": "get_features", "put": "set_features"}, "features") )
+        patterns.append( self.detail_url("features/(?P<feature>[a-zA-Z0-9\-_]+)/$", {"get": "get_feature", "put": "set_feature"}, "get_feature") )
+        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 + "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
+
+    def list(self, request):
+        object_list = self.filter_queryset(self.get_queryset())
+
+        serializer = self.get_serializer(object_list, many=True)
+
+        return Response(serializer.data)
+
+    def get_features(self, request, pk=None):
+        subscriber = self.get_object()
+        return Response(FeatureSerializer(subscriber.features).data)
+
+    def set_features(self, request, pk=None):
+        subscriber = self.get_object()
+        ser = FeatureSerializer(subscriber.features, data=request.data)
+        ser.is_valid(raise_exception = True)
+        subscriber.update_features(ser.validated_data)
+        subscriber.save()
+        return Response(FeatureSerializer(subscriber.features).data)
+
+    def get_feature(self, request, pk=None, feature=None):
+        subscriber = self.get_object()
+        return Response({feature: FeatureSerializer(subscriber.features).data[feature]})
+
+    def set_feature(self, request, pk=None, feature=None):
+        subscriber = self.get_object()
+        if [feature] != request.data.keys():
+             raise serializers.ValidationError("feature %s does not match keys in request body (%s)" % (feature, ",".join(request.data.keys())))
+        ser = FeatureSerializer(subscriber.features, data=request.data)
+        ser.is_valid(raise_exception = True)
+        subscriber.update_features(ser.validated_data)
+        subscriber.save()
+        return Response({feature: FeatureSerializer(subscriber.features).data[feature]})
+
+    def get_identities(self, request, pk=None):
+        subscriber = self.get_object()
+        return Response(IdentitySerializer(subscriber.identity).data)
+
+    def set_identities(self, request, pk=None):
+        subscriber = self.get_object()
+        ser = IdentitySerializer(subscriber.identity, data=request.data)
+        ser.is_valid(raise_exception = True)
+        subscriber.update_identity(ser.validated_data)
+        subscriber.save()
+        return Response(IdentitySerializer(subscriber.identity).data)
+
+    def get_identity(self, request, pk=None, identity=None):
+        subscriber = self.get_object()
+        return Response({identity: IdentitySerializer(subscriber.identity).data[identity]})
+
+    def set_identity(self, request, pk=None, identity=None):
+        subscriber = self.get_object()
+        if [identity] != request.data.keys():
+             raise serializers.ValidationError("identity %s does not match keys in request body (%s)" % (identity, ",".join(request.data.keys())))
+        ser = IdentitySerializer(subscriber.identity, data=request.data)
+        ser.is_valid(raise_exception = True)
+        subscriber.update_identity(ser.validated_data)
+        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()
+
+        ssidmap = [ {"service_specific_id": x.service_specific_id, "subscriber_id": x.id} for x in object_list ]
+
+        return Response({"ssidmap": ssidmap})
+
+    def ssiddetail(self, pk=None, ssid=None):
+        object_list = CordSubscriberNew.get_tenant_objects().all()
+
+        ssidmap = [ {"service_specific_id": x.service_specific_id, "subscriber_id": x.id} for x in object_list if str(x.service_specific_id)==str(ssid) ]
+
+        if len(ssidmap)==0:
+            raise XOSNotFound("didn't find ssid %s" % str(ssid))
+
+        return Response( ssidmap[0] )
+
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
new file mode 100644
index 0000000..ee3ed00
--- /dev/null
+++ b/xos/api/xosapi_helpers.py
@@ -0,0 +1,99 @@
+from rest_framework import generics
+from rest_framework import serializers
+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
+
+if hasattr(serializers, "ReadOnlyField"):
+    # rest_framework 3.x
+    ReadOnlyField = serializers.ReadOnlyField
+else:
+    # rest_framework 2.x
+    ReadOnlyField = serializers.Field
+
+""" PlusSerializerMixin
+
+    Implements Serializer fields that are common to all OpenCloud objects. For
+    example, stuff related to backend fields.
+"""
+
+class PlusModelSerializer(serializers.ModelSerializer):
+    backendIcon = serializers.SerializerMethodField("getBackendIcon")
+    backendHtml = serializers.SerializerMethodField("getBackendHtml")
+
+    # This will cause a descendant class to pull in the methods defined
+    # above. See rest_framework/serializers.py: _get_declared_fields().
+    base_fields = {"backendIcon": backendIcon, "backendHtml": backendHtml}
+    # Rest_framework 3.0 uses _declared_fields instead of base_fields
+    _declared_fields = {"backendIcon": backendIcon, "backendHtml": backendHtml}
+
+    def getBackendIcon(self, obj):
+        return obj.getBackendIcon()
+
+    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.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.get_api_method_path() + pattern,
+                   self.as_view(viewdict),
+                   name=self.base_name+"_"+name)
+
+    @classmethod
+    def get_urlpatterns(self, api_path="^"):
+        self.api_path = api_path
+
+        patterns = []
+
+        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 c339789..9e5b4a1 100644
--- a/xos/core/xoslib/methods/sliceplus.py
+++ b/xos/core/xoslib/methods/sliceplus.py
@@ -8,41 +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(serializers.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(serializers.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(serializers.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)
@@ -78,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)
diff --git a/xos/xos/urls.py b/xos/xos/urls.py
index 4c3f07d..1bc3885 100644
--- a/xos/xos/urls.py
+++ b/xos/xos/urls.py
@@ -10,12 +10,11 @@
 from core.views.legacyapi import LegacyXMLRPC
 from core.views.serviceGraph import ServiceGridView, ServiceGraphView
 from services.helloworld.view import *
-#from core.views.analytics import AnalyticsAjaxView
 from core.models import *
 from rest_framework import generics
 from core.dashboard.sites import SitePlus
 from django.http import HttpResponseRedirect
-#from core.xoslib import XOSLibDataView
+#from api import import_api_methods
 
 admin.site = SitePlus()
 admin.autodiscover()
@@ -43,18 +42,17 @@
     url(r'^', include(admin.site.urls)),
     #url(r'^profile/home', 'core.views.home'),
 
-#    url(r'^admin/xoslib/(?P<name>\w+)/$', XOSLibDataView.as_view(), name="xoslib"),
-
     url(r'^xmlrpc/legacyapi/$', 'core.views.legacyapi.LegacyXMLRPC', name='xmlrpc'),
 
-#    url(r'^analytics/(?P<name>\w+)/$', AnalyticsAjaxView.as_view(), name="analytics"),
-
     url(r'^files/', redirect_to_apache),
 
-    #Adding in rest_framework urls
+    # Adding in rest_framework urls
     url(r'^xos/', include('rest_framework.urls', namespace='rest_framework')),
 
-    # XOSLib rest methods
+    # XOSLib rest methods [deprecated]
     url(r'^xoslib/', include('core.xoslib.methods', namespace='xoslib')),
+
+    url(r'^', include('api.import_methods', namespace='api')),
+
   ) + get_REST_patterns() + get_hpc_REST_patterns()