API refactor WIP
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/cord/subscriber.py b/xos/api/cord/subscriber.py
new file mode 100644
index 0000000..f0bee26
--- /dev/null
+++ b/xos/api/cord/subscriber.py
@@ -0,0 +1,414 @@
+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 plus import PlusSerializerMixin, XOSViewSet
+from django.shortcuts import get_object_or_404
+from xos.apibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView, XOSPermissionDenied
+from xos.exceptions import *
+import json
+import subprocess
+
+if hasattr(serializers, "ReadOnlyField"):
+    # rest_framework 3.x
+    ReadOnlyField = serializers.ReadOnlyField
+else:
+    # rest_framework 2.x
+    ReadOnlyField = serializers.Field
+
+class CordSubscriberIdSerializer(serializers.ModelSerializer, PlusSerializerMixin):
+        id = ReadOnlyField()
+        service_specific_id = ReadOnlyField()
+        vlan_id = ReadOnlyField()      # XXX remove this
+        c_tag = ReadOnlyField()
+        s_tag = ReadOnlyField()
+        vcpe_id = ReadOnlyField()
+        instance = ReadOnlyField()
+        image = ReadOnlyField()
+        vbng_id = ReadOnlyField()
+        firewall_enable = serializers.BooleanField()
+        firewall_rules = serializers.CharField()
+        url_filter_enable = serializers.BooleanField()
+        url_filter_rules = serializers.CharField()
+        url_filter_level = serializers.CharField(required=False)
+        cdn_enable = serializers.BooleanField()
+        instance_name = ReadOnlyField()
+        image_name = ReadOnlyField()
+        routeable_subnet = serializers.CharField(required=False)
+        ssh_command = ReadOnlyField()
+        bbs_account = ReadOnlyField()
+
+        wan_container_ip = ReadOnlyField()
+        uplink_speed = serializers.CharField(required=False)
+        downlink_speed = serializers.CharField(required=False)
+        status = serializers.CharField()
+        enable_uverse = serializers.BooleanField()
+
+        lan_ip = ReadOnlyField()
+        wan_ip = ReadOnlyField()
+        nat_ip = ReadOnlyField()
+        private_ip = ReadOnlyField()
+
+        wan_mac = ReadOnlyField()
+
+        vcpe_synced = serializers.BooleanField()
+
+        humanReadableName = serializers.SerializerMethodField("getHumanReadableName")
+
+        class Meta:
+            model = CordSubscriber
+            fields = ('humanReadableName', 'id',
+                      'service_specific_id', 'vlan_id', 's_tag', 'c_tag',
+                      'vcpe_id', 'instance', 'instance_name', 'image', 'image_name',
+                      'firewall_enable', 'firewall_rules',
+                      'url_filter_enable', 'url_filter_rules', 'url_filter_level',
+                      'bbs_account',
+                      'ssh_command',
+                      'vcpe_synced',
+                      'cdn_enable', 'vbng_id', 'routeable_subnet', 'nat_ip', 'lan_ip', 'wan_ip', 'private_ip', 'wan_mac',
+                      'wan_container_ip',
+                      'uplink_speed', 'downlink_speed', 'status', 'enable_uverse')
+
+
+        def getHumanReadableName(self, obj):
+            return obj.__unicode__()
+
+#------------------------------------------------------------------------------
+# The "old" API
+# This is used by the xoslib-based GUI
+#------------------------------------------------------------------------------
+
+class CordSubscriberList(XOSListCreateAPIView):
+    queryset = CordSubscriber.get_tenant_objects().select_related().all()
+    serializer_class = CordSubscriberIdSerializer
+
+    method_kind = "list"
+    method_name = "cordsubscriber"
+
+class CordSubscriberDetail(XOSRetrieveUpdateDestroyAPIView):
+    queryset = CordSubscriber.get_tenant_objects().select_related().all()
+    serializer_class = CordSubscriberIdSerializer
+
+    method_kind = "detail"
+    method_name = "cordsubscriber"
+
+# We fake a user object by pulling the user data struct out of the
+# subscriber object...
+
+def serialize_user(subscriber, user):
+    return {"id": "%d-%d" % (subscriber.id, user["id"]),
+            "name": user["name"],
+            "level": user.get("level",""),
+            "mac": user.get("mac", ""),
+            "subscriber": subscriber.id }
+
+class CordUserList(APIView):
+    method_kind = "list"
+    method_name = "corduser"
+
+    def get(self, request, format=None):
+        instances=[]
+        for subscriber in CordSubscriber.get_tenant_objects().all():
+            for user in subscriber.users:
+                instances.append( serialize_user(subscriber, user) )
+
+        return Response(instances)
+
+    def post(self, request, format=None):
+        data = request.DATA
+        subscriber = CordSubscriber.get_tenant_objects().get(id=int(data["subscriber"]))
+        user = subscriber.create_user(name=data["name"],
+                                    level=data["level"],
+                                    mac=data["mac"])
+        subscriber.save()
+
+        return Response(serialize_user(subscriber,user))
+
+class CordUserDetail(APIView):
+    method_kind = "detail"
+    method_name = "corduser"
+
+    def get(self, request, format=None, pk=0):
+        parts = pk.split("-")
+        subscriber = CordSubscriber.get_tenant_objects().filter(id=parts[0])
+        for user in subscriber.users:
+            return Response( [ serialize_user(subscriber, user) ] )
+        raise XOSNotFound("Failed to find user %s" % pk)
+
+    def delete(self, request, pk):
+        parts = pk.split("-")
+        subscriber = CordSubscriber.get_tenant_objects().get(id=int(parts[0]))
+        subscriber.delete_user(parts[1])
+        subscriber.save()
+        return Response("okay")
+
+    def put(self, request, pk):
+        kwargs={}
+        if "name" in request.DATA:
+             kwargs["name"] = request.DATA["name"]
+        if "level" in request.DATA:
+             kwargs["level"] = request.DATA["level"]
+        if "mac" in request.DATA:
+             kwargs["mac"] = request.DATA["mac"]
+
+        parts = pk.split("-")
+        subscriber = CordSubscriber.get_tenant_objects().get(id=int(parts[0]))
+        user = subscriber.update_user(parts[1], **kwargs)
+        subscriber.save()
+        return Response(serialize_user(subscriber,user))
+
+#------------------------------------------------------------------------------
+# The "new" API with many more REST endpoints.
+# This is for integration with with the subscriber GUI
+#------------------------------------------------------------------------------
+
+class CordSubscriberViewSet(XOSViewSet):
+    base_name = "subscriber"
+    method_name = "rs/subscriber"
+    method_kind = "viewset"
+    queryset = CordSubscriber.get_tenant_objects().select_related().all()
+    serializer_class = CordSubscriberIdSerializer
+
+    def get_vcpe(self):
+        subscriber = self.get_object()
+        if not subscriber.vcpe:
+            raise XOSMissingField("vCPE object is not present for subscriber")
+        return subscriber.vcpe
+
+    @classmethod
+    def get_urlpatterns(self):
+        patterns = super(CordSubscriberViewSet, self).get_urlpatterns()
+        patterns.append( self.detail_url("vcpe_synced/$", {"get": "get_vcpe_synced"}, "vcpe_synced") )
+        patterns.append( self.detail_url("url_filter/$", {"get": "get_url_filter"}, "url_filter") )
+        patterns.append( self.detail_url("url_filter/(?P<level>[a-zA-Z0-9\-_]+)/$", {"put": "set_url_filter"}, "url_filter") )
+        patterns.append( self.detail_url("services/$", {"get": "get_services"}, "services") )
+        patterns.append( self.detail_url("services/(?P<service>[a-zA-Z0-9\-_]+)/$", {"get": "get_service"}, "get_service") )
+        patterns.append( self.detail_url("services/(?P<service>[a-zA-Z0-9\-_]+)/true/$", {"put": "enable_service"}, "enable_service") )
+        patterns.append( self.detail_url("services/(?P<service>[a-zA-Z0-9\-_]+)/false/$", {"put": "disable_service"}, "disable_service") )
+
+        patterns.append( self.detail_url("users/$", {"get": "get_users", "post": "create_user"}, "users") )
+        patterns.append( self.detail_url("users/clearusers/$", {"get": "clear_users", "put": "clear_users", "post": "clear_users"}, "clearusers") )
+        patterns.append( self.detail_url("users/newuser/$", {"put": "create_user", "post": "create_user"}, "newuser") )
+        patterns.append( self.detail_url("users/(?P<uid>[0-9\-]+)/$", {"delete": "delete_user"}, "user") )
+        patterns.append( self.detail_url("users/(?P<uid>[0-9\-]+)/url_filter/$", {"get": "get_user_level"}, "user_level") )
+        patterns.append( self.detail_url("users/(?P<uid>[0-9\-]+)/url_filter/(?P<level>[a-zA-Z0-9\-_]+)/$", {"put": "set_user_level"}, "set_user_level") )
+
+        patterns.append( self.detail_url("bbsdump/$", {"get": "get_bbsdump"}, "bbsdump") )
+
+        patterns.append( url("^rs/initdemo/$", self.as_view({"put": "initdemo", "get": "initdemo"}), name="initdemo") )
+
+        patterns.append( url("^rs/subidlookup/(?P<ssid>[0-9\-]+)/$", self.as_view({"get": "ssiddetail"}), name="ssiddetail") )
+        patterns.append( url("^rs/subidlookup/$", self.as_view({"get": "ssidlist"}), name="ssidlist") )
+
+        patterns.append( url("^rs/vbng_mapping/$", self.as_view({"get": "get_vbng_mapping"}), name="vbng_mapping") )
+
+        return patterns
+
+    def list(self, request):
+        object_list = self.filter_queryset(self.get_queryset())
+
+        serializer = self.get_serializer(object_list, many=True)
+
+        return Response({"subscribers": serializer.data})
+
+    def get_vcpe_synced(self, request, pk=None):
+        subscriber = self.get_object()
+        return Response({"vcpe_synced": subscriber.vcpe_synced})
+
+    def get_url_filter(self, request, pk=None):
+        subscriber = self.get_object()
+        return Response({"level": subscriber.url_filter_level})
+
+    def set_url_filter(self, request, pk=None, level=None):
+        subscriber = self.get_object()
+        subscriber.url_filter_level = level
+        subscriber.save()
+        return Response({"level": subscriber.url_filter_level})
+
+    def get_users(self, request, pk=None):
+        subscriber = self.get_object()
+        return Response(subscriber.users)
+
+    def get_user_level(self, request, pk=None, uid=None):
+        subscriber = self.get_object()
+        user = subscriber.find_user(uid)
+        if user and user.get("level", None):
+            level = user["level"]
+        else:
+            level = self.get_object().url_filter_level
+
+        return Response( {"id": uid, "level": level} )
+
+    def set_user_level(self, request, pk=None, uid=None, level=None):
+        subscriber = self.get_object()
+        subscriber.update_user(uid, level=level)
+        subscriber.save()
+        return self.get_user_level(request, pk, uid)
+
+    def create_user(self, request, pk=None):
+        data = request.DATA
+        name = data.get("name",None)
+        mac = data.get("mac",None)
+        if (not name):
+             raise XOSMissingField("name must be specified when creating user")
+        if (not mac):
+             raise XOSMissingField("mac must be specified when creating user")
+
+        subscriber = self.get_object()
+        newuser = subscriber.create_user(name=name, mac=mac)
+        subscriber.save()
+
+        return Response(newuser)
+
+    def delete_user(self, request, pk=None, uid=None):
+        subscriber = self.get_object()
+        subscriber.delete_user(uid)
+        subscriber.save()
+
+        return Response( {"id": uid, "deleted": True} )
+
+    def clear_users(self, request, pk=None):
+        subscriber = self.get_object()
+        subscriber.users = []
+        subscriber.save()
+
+        return Response( "Okay" )
+
+    def get_services(self, request, pk=None):
+        subscriber = self.get_object()
+        return Response(subscriber.services)
+
+    def get_service(self, request, pk=None, service=None):
+        service_attr = service+"_enable"
+        subscriber = self.get_object()
+        return Response({service: getattr(subscriber, service_attr)})
+
+    def enable_service(self, request, pk=None, service=None):
+        service_attr = service+"_enable"
+        subscriber = self.get_object()
+        setattr(subscriber, service_attr, True)
+        subscriber.save()
+        return Response({service: getattr(subscriber, service_attr)})
+
+    def disable_service(self, request, pk=None, service=None):
+        service_attr = service+"_enable"
+        subscriber = self.get_object()
+        setattr(subscriber, service_attr, False)
+        subscriber.save()
+        return Response({service: getattr(subscriber, service_attr)})
+
+    def get_bbsdump(self, request, pk=None):
+        subscriber = self.get_object()
+        if not subsciber.volt or not subscriber.volt.vcpe:
+            raise XOSMissingField("subscriber has no vCPE")
+        if not subscriber.volt.vcpe.bbs_account:
+            raise XOSMissingField("subscriber has no bbs_account")
+
+        result=subprocess.check_output(["python", "/opt/xos/observers/vcpe/broadbandshield.py", "dump", subscriber.volt.vcpe.bbs_account, "123"])
+        if request.GET.get("theformat",None)=="text":
+            from django.http import HttpResponse
+            return HttpResponse(result, content_type="text/plain")
+        else:
+            return Response( {"bbs_dump": result } )
+
+    def setup_demo_subscriber(self, subscriber):
+        # nuke the users and start over
+        subscriber.users = []
+        subscriber.create_user(name="Mom's PC",      mac="010203040506", level="PG_13")
+        subscriber.create_user(name="Dad's PC",      mac="90E2Ba82F975", level="PG_13")
+        subscriber.create_user(name="Jack's Laptop", mac="685B359D91D5", level="PG_13")
+        subscriber.create_user(name="Jill's Laptop", mac="34363BC9B6A6", level="PG_13")
+        subscriber.save()
+
+    def initdemo(self, request):
+        object_list = CordSubscriber.get_tenant_objects().all()
+
+        # reset the parental controls in any existing demo vCPEs
+        for o in object_list:
+            if str(o.service_specific_id) in ["0", "1"]:
+                self.setup_demo_subscriber(o)
+
+        demo_subscribers = [o for o in object_list if o.is_demo_user]
+
+        if demo_subscribers:
+            return Response({"id": demo_subscribers[0].id})
+
+        subscriber = CordSubscriberRoot(service_specific_id=1234,
+                                        name="demo-subscriber",)
+        subscriber.is_demo_user = True
+        subscriber.save()
+
+        self.setup_demo_subscriber(subscriber)
+
+        return Response({"id": subscriber.id})
+
+    def ssidlist(self, request):
+        object_list = CordSubscriber.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 = CordSubscriber.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] )
+
+    def get_vbng_mapping(self, request):
+        object_list = VBNGTenant.get_tenant_objects().all()
+
+        mappings = []
+        for vbng in object_list:
+            if vbng.mapped_ip and vbng.routeable_subnet:
+                mappings.append( {"private_ip": vbng.mapped_ip, "routeable_subnet": vbng.routeable_subnet, "mac": vbng.mapped_mac, "hostname": vbng.mapped_hostname} )
+
+        return Response( {"vbng_mapping": mappings} )
+
+class CordDebugIdSerializer(serializers.ModelSerializer, PlusSerializerMixin):
+    # 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 = "cord_debug"
+    method_name = "rs/cord_debug"
+    method_kind = "viewset"
+    serializer_class = CordDebugIdSerializer
+
+    @classmethod
+    def get_urlpatterns(self):
+        patterns = []
+        patterns.append( url("^rs/cord_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/import_methods.py b/xos/api/import_methods.py
new file mode 100644
index 0000000..abc62f7
--- /dev/null
+++ b/xos/api/import_methods.py
@@ -0,0 +1,57 @@
+from django.views.generic import View
+from django.conf.urls import patterns, url
+from rest_framework.routers import DefaultRouter
+import os, sys
+import inspect
+import importlib
+
+def import_module_from_filename(pathname):
+    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 = [dir] + sys.path
+
+        module = __import__(fn[:-3])
+    finally:
+        sys.path = sys_path_save
+
+    return module
+
+def import_methods(dir=None, api_path="api"):
+    urlpatterns=[]
+    subdirs=[]
+
+    if not dir:
+        dir = os.path.dirname(os.path.abspath(__file__))
+
+    for fn in os.listdir(dir):
+        pathname = os.path.join(dir,fn)
+        if os.path.isfile(pathname) and fn.endswith(".py") and (fn!="__init__.py"):
+            module = import_module_from_filename(fn[:-3])
+            for classname in dir(module):
+                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 = os.path.join(api_path, getattr(c, "method_name", None))
+                    if method_kind and method_name:
+                        view_urls.append( (method_kind, method_name, classname, c) )
+
+        elif os.path.isdir(pathname):
+            urlpatterns.extend(import_methods(os.path.append(dir, fn), os.path.append(api_path, 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())
+
+    return urlpatterns
diff --git a/xos/api/plus.py b/xos/api/plus.py
new file mode 100644
index 0000000..294fba8
--- /dev/null
+++ b/xos/api/plus.py
@@ -0,0 +1,51 @@
+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
+
+""" PlusSerializerMixin
+
+    Implements Serializer fields that are common to all OpenCloud objects. For
+    example, stuff related to backend fields.
+"""
+
+class PlusSerializerMixin():
+    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()
+
+class XOSViewSet(viewsets.ModelViewSet):
+    @classmethod
+    def detail_url(self, pattern, viewdict, name):
+        return url(r'^' + self.method_name + r'/(?P<pk>[a-zA-Z0-9\-]+)/' + pattern,
+                   self.as_view(viewdict),
+                   name=self.base_name+"_"+name)
+
+    @classmethod
+    def list_url(self, pattern, viewdict, name):
+        return url(r'^' + self.method_name + r'/' + pattern,
+                   self.as_view(viewdict),
+                   name=self.base_name+"_"+name)
+
+    @classmethod
+    def get_urlpatterns(self):
+        patterns = []
+
+        patterns.append(url(r'^' + self.method_name + '/$', self.as_view({'get': 'list'}), name=self.base_name+'_list'))
+        patterns.append(url(r'^' + self.method_name + '/(?P<pk>[a-zA-Z0-9\-]+)/$', self.as_view({'get': 'retrieve', 'put': 'update', 'post': 'update', 'delete': 'destroy', 'patch': 'partial_update'}), name=self.base_name+'_detail'))
+
+        return patterns