Merge branch 'feature/api-cleanup' of github.com:open-cloud/xos into feature/api-cleanup
diff --git a/xos/api/examples/exampleservice/get_exampletenant_message.sh b/xos/api/examples/exampleservice/get_exampletenant_message.sh
new file mode 100755
index 0000000..96ce65c
--- /dev/null
+++ b/xos/api/examples/exampleservice/get_exampletenant_message.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+# this example illustrates using a custom REST API endpoint
+
+source ./config.sh
+
+if [[ "$#" -ne 1 ]]; then
+    echo "Syntax: get_exampletenant_message.sh <id>"
+    exit -1
+fi
+
+ID=$1
+
+curl -H "Accept: application/json; indent=4" -u $AUTH -X GET $HOST/api/tenant/exampletenant/$ID/message/
diff --git a/xos/api/examples/exampleservice/put_exampletenant_message.sh b/xos/api/examples/exampleservice/put_exampletenant_message.sh
new file mode 100755
index 0000000..bf0810d
--- /dev/null
+++ b/xos/api/examples/exampleservice/put_exampletenant_message.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+# this example illustrates using a custom REST API endpoint  
+
+source ./config.sh
+
+if [[ "$#" -ne 2 ]]; then
+    echo "Syntax: put_exampletenant_message.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/message/
diff --git a/xos/api/examples/util.sh b/xos/api/examples/util.sh
index 8373498..b3d3060 100644
--- a/xos/api/examples/util.sh
+++ b/xos/api/examples/util.sh
@@ -29,4 +29,24 @@
     # echo "(found volt id %1)" >&2
 
     echo $ID
-}
\ No newline at end of file
+}
+
+function lookup_subscriber_vsg {
+    JSON=`curl -f -s -u $AUTH -X GET $HOST/api/tenant/cord/subscriber/$1/`
+    if [[ $? != 0 ]]; then
+        echo "function lookup_subscriber_vsg 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('vsg_id','')"`
+    if [[ $ID == "" ]]; then
+        echo "there is no volt for this subscriber" >&2
+        exit -1
+    fi
+
+    # echo "(found vsg id %1)" >&2
+
+    echo $ID
+}
+
diff --git a/xos/api/examples/vtr/add_truckroll_for_subscriber.sh b/xos/api/examples/vtr/add_truckroll_for_subscriber.sh
new file mode 100755
index 0000000..3a3ec41
--- /dev/null
+++ b/xos/api/examples/vtr/add_truckroll_for_subscriber.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+source ./config.sh
+source ./util.sh
+
+ACCOUNT_NUM=1238
+
+SUBSCRIBER_ID=$(lookup_account_num $ACCOUNT_NUM)
+if [[ $? != 0 ]]; then
+    exit -1
+fi
+
+VSG_ID=$(lookup_subscriber_vsg $SUBSCRIBER_ID)
+if [[ $? != 0 ]]; then
+    exit -1
+fi
+
+DATA=$(cat <<EOF
+{"target_id": $SUBSCRIBER_ID,
+ "scope": "container",
+ "test": "ping",
+ "argument": "8.8.8.8"}
+EOF
+)
+
+curl -H "Accept: application/json; indent=4" -H "Content-Type: application/json" -u $AUTH -X POST -d "$DATA" $HOST/api/tenant/truckroll/
diff --git a/xos/api/examples/vtr/config.sh b/xos/api/examples/vtr/config.sh
new file mode 100644
index 0000000..92d703c
--- /dev/null
+++ b/xos/api/examples/vtr/config.sh
@@ -0,0 +1,2 @@
+# see config.sh in the parent directory
+source ../config.sh
diff --git a/xos/api/examples/vtr/delete_truckroll.sh b/xos/api/examples/vtr/delete_truckroll.sh
new file mode 100755
index 0000000..ee31aeb
--- /dev/null
+++ b/xos/api/examples/vtr/delete_truckroll.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+source ./config.sh
+
+if [[ "$#" -ne 1 ]]; then
+    echo "Syntax: delete_truckroll.sh <id>"
+    exit -1
+fi
+
+ID=$1
+
+curl -H "Accept: application/json; indent=4" -u $AUTH -X DELETE $HOST/api/tenant/truckroll/$ID/
diff --git a/xos/api/examples/vtr/list_truckrolls.sh b/xos/api/examples/vtr/list_truckrolls.sh
new file mode 100755
index 0000000..f1d7e87
--- /dev/null
+++ b/xos/api/examples/vtr/list_truckrolls.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+source ./config.sh
+
+curl -H "Accept: application/json; indent=4" -u $AUTH -X GET $HOST/api/tenant/truckroll/
diff --git a/xos/api/examples/vtr/util.sh b/xos/api/examples/vtr/util.sh
new file mode 100644
index 0000000..7b66903
--- /dev/null
+++ b/xos/api/examples/vtr/util.sh
@@ -0,0 +1 @@
+source ../util.sh
diff --git a/xos/api/import_methods.py b/xos/api/import_methods.py
index d53556c..1b5e3ca 100644
--- a/xos/api/import_methods.py
+++ b/xos/api/import_methods.py
@@ -1,6 +1,7 @@
 from django.views.generic import View
 from django.conf.urls import patterns, url, include
 from rest_framework.routers import DefaultRouter
+from xosapi_helpers import XOSIndexViewSet
 import os, sys
 import inspect
 import importlib
@@ -35,6 +36,7 @@
     return module
 
 def import_api_methods(dirname=None, api_path="api", api_module="api"):
+    has_index_view = False
     subdirs=[]
     urlpatterns=[]
 
@@ -61,10 +63,12 @@
                             method_name = os.path.join(api_path, method_name)
                         else:
                             method_name = api_path
+                            has_index_view = True
                         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))
+            subdirs.append(fn)
 
     for view_url in view_urls:
         if view_url[0] == "list":
@@ -75,6 +79,9 @@
            viewset = view_url[3]
            urlpatterns.extend(viewset.get_urlpatterns(api_path="^"+api_path+"/"))
 
+    if not has_index_view:
+        urlpatterns.append(url('^' + api_path + '/$', XOSIndexViewSet.as_view({'get': 'list'}, view_urls=view_urls, subdirs=subdirs), name="api_path"+"_index"))
+
     return urlpatterns
 
 urlpatterns = import_api_methods()
diff --git a/xos/api/service/onos.py b/xos/api/service/onos.py
new file mode 100644
index 0000000..a143b3d
--- /dev/null
+++ b/xos/api/service/onos.py
@@ -0,0 +1,87 @@
+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.onos.models import ONOSService
+from xos.apibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView, XOSPermissionDenied
+from api.xosapi_helpers import PlusModelSerializer, XOSViewSet, ReadOnlyField
+
+class ONOSServiceSerializer(PlusModelSerializer):
+    id = ReadOnlyField()
+    rest_hostname = serializers.CharField(required=False)
+    rest_port = serializers.CharField(default="8181")
+    no_container = serializers.BooleanField(default=False)
+    node_key = serializers.CharField(required=False)
+
+    humanReadableName = serializers.SerializerMethodField("getHumanReadableName")
+    class Meta:
+        model = ONOSService
+        fields = ('humanReadableName', 'id', 'rest_hostname', 'rest_port', 'no_container', 'node_key')
+
+    def getHumanReadableName(self, obj):
+        return obj.__unicode__()
+
+class ServiceAttributeSerializer(serializers.Serializer):
+    id = ReadOnlyField()
+    name = serializers.CharField(required=False)
+    value = serializers.CharField(required=False)
+
+class ONOSServiceViewSet(XOSViewSet):
+    base_name = "onos"
+    method_name = "onos"
+    method_kind = "viewset"
+    queryset = ONOSService.get_service_objects().all()
+    serializer_class = ONOSServiceSerializer
+
+    custom_serializers = {"set_attribute": ServiceAttributeSerializer}
+
+    @classmethod
+    def get_urlpatterns(self, api_path="^"):
+        patterns = super(ONOSServiceViewSet, self).get_urlpatterns(api_path=api_path)
+
+        patterns.append( self.detail_url("attributes/$", {"get": "get_attributes", "post": "add_attribute"}, "attributes") )
+        patterns.append( self.detail_url("attributes/(?P<attribute>[0-9]+)/$", {"get": "get_attribute", "put": "set_attribute", "delete": "delete_attribute"}, "attribute") )
+
+        return patterns
+
+    def get_attributes(self, request, pk=None):
+        svc = self.get_object()
+        return Response(ServiceAttributeSerializer(svc.serviceattributes.all(), many=True).data)
+
+    def add_attribute(self, request, pk=None):
+        svc = self.get_object()
+        ser = ServiceAttributeSerializer(data=request.data)
+        ser.is_valid(raise_exception = True)
+        att = ServiceAttribute(service=svc, **ser.validated_data)
+        att.save()
+        return Response(ServiceAttributeSerializer(att).data)
+
+    def get_attribute(self, request, pk=None, attribute=None):
+        svc = self.get_object()
+        att = ServiceAttribute.objects.get(pk=attribute)
+        return Response(ServiceAttributeSerializer(att).data)
+
+    def set_attribute(self, request, pk=None, attribute=None):
+        svc = self.get_object()
+        att = ServiceAttribute.objects.get(pk=attribute)
+        ser = ServicettributeSerializer(att, data=request.data)
+        ser.is_valid(raise_exception = True)
+        att.name = ser.validated_data.get("name", att.name)
+        att.value = ser.validated_data.get("value", att.value)
+        att.save()
+        return Response(ServiceAttributeSerializer(att).data)
+
+    def delete_attribute(self, request, pk=None, attribute=None):
+        att = ServiceAttribute.objects.get(pk=attribute)
+        att.delete()
+        return Response(status=status.HTTP_204_NO_CONTENT)
+
+
+
+
+
+
diff --git a/xos/api/tenant/cord/subscriber.py b/xos/api/tenant/cord/subscriber.py
index 89f42b9..b33c7ad 100644
--- a/xos/api/tenant/cord/subscriber.py
+++ b/xos/api/tenant/cord/subscriber.py
@@ -129,6 +129,11 @@
     queryset = CordSubscriberNew.get_tenant_objects().select_related().all()
     serializer_class = CordSubscriberSerializer
 
+    custom_serializers = {"set_features": FeatureSerializer,
+                          "set_feature": FeatureSerializer,
+                          "set_identities": IdentitySerializer,
+                          "set_identity": IdentitySerializer}
+
     @classmethod
     def get_urlpatterns(self, api_path="^"):
         patterns = super(CordSubscriberViewSet, self).get_urlpatterns(api_path=api_path)
diff --git a/xos/api/tenant/exampletenant.py b/xos/api/tenant/exampletenant.py
index b046a88..c50680f 100644
--- a/xos/api/tenant/exampletenant.py
+++ b/xos/api/tenant/exampletenant.py
@@ -44,6 +44,9 @@
     def get_urlpatterns(self, api_path="^"):
         patterns = super(ExampleTenantViewSet, self).get_urlpatterns(api_path=api_path)
 
+        # example to demonstrate adding a custom endpoint
+        patterns.append( self.detail_url("message/$", {"get": "get_message", "put": "set_message"}, "message") )
+
         return patterns
 
     def list(self, request):
@@ -53,3 +56,13 @@
 
         return Response(serializer.data)
 
+    def get_message(self, request, pk=None):
+        example_tenant = self.get_object()
+        return Response({"tenant_message": example_tenant.tenant_message})
+
+    def set_message(self, request, pk=None):
+        example_tenant = self.get_object()
+        example_tenant.tenant_message = request.data["tenant_message"]
+        example_tenant.save()
+        return Response({"tenant_message": example_tenant.tenant_message})
+
diff --git a/xos/api/tenant/onos/__init__.py b/xos/api/tenant/onos/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/xos/api/tenant/onos/__init__.py
@@ -0,0 +1 @@
+
diff --git a/xos/api/tenant/onos/app.py b/xos/api/tenant/onos/app.py
new file mode 100644
index 0000000..481057d
--- /dev/null
+++ b/xos/api/tenant/onos/app.py
@@ -0,0 +1,91 @@
+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.onos.models import ONOSService, ONOSApp
+from xos.apibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView, XOSPermissionDenied
+from api.xosapi_helpers import PlusModelSerializer, XOSViewSet, ReadOnlyField
+
+def get_default_onos_service():
+    onos_services = ONOSService.get_service_objects().all()
+    if onos_services:
+        return onos_services[0].id
+    return None
+
+class ONOSAppSerializer(PlusModelSerializer):
+    id = ReadOnlyField()
+    name = serializers.CharField()
+    dependencies = serializers.CharField()
+
+    humanReadableName = serializers.SerializerMethodField("getHumanReadableName")
+    class Meta:
+        model = ONOSApp
+        fields = ('humanReadableName', 'id', 'name', 'dependencies')
+
+    def getHumanReadableName(self, obj):
+        return obj.__unicode__()
+
+class TenantAttributeSerializer(serializers.Serializer):
+    id = ReadOnlyField()
+    name = serializers.CharField(required=False)
+    value = serializers.CharField(required=False)
+
+class ONOSAppViewSet(XOSViewSet):
+    base_name = "app"
+    method_name = "app"
+    method_kind = "viewset"
+    queryset = ONOSApp.get_tenant_objects().all()
+    serializer_class = ONOSAppSerializer
+
+    custom_serializers = {"set_attribute": TenantAttributeSerializer}
+
+    @classmethod
+    def get_urlpatterns(self, api_path="^"):
+        patterns = super(ONOSAppViewSet, self).get_urlpatterns(api_path=api_path)
+
+        patterns.append( self.detail_url("attributes/$", {"get": "get_attributes", "post": "add_attribute"}, "attributes") )
+        patterns.append( self.detail_url("attributes/(?P<attribute>[0-9]+)/$", {"get": "get_attribute", "put": "set_attribute", "delete": "delete_attribute"}, "attribute") )
+
+        return patterns
+
+    def get_attributes(self, request, pk=None):
+        app = self.get_object()
+        return Response(TenantAttributeSerializer(app.tenantattributes.all(), many=True).data)
+
+    def add_attribute(self, request, pk=None):
+        app = self.get_object()
+        ser = TenantAttributeSerializer(data=request.data)
+        ser.is_valid(raise_exception = True)
+        att = TenantAttribute(tenant=app, **ser.validated_data)
+        att.save()
+        return Response(TenantAttributeSerializer(att).data)
+
+    def get_attribute(self, request, pk=None, attribute=None):
+        app = self.get_object()
+        att = TenantAttribute.objects.get(pk=attribute)
+        return Response(TenantAttributeSerializer(att).data)
+
+    def set_attribute(self, request, pk=None, attribute=None):
+        app = self.get_object()
+        att = TenantAttribute.objects.get(pk=attribute)
+        ser = TenantAttributeSerializer(att, data=request.data)
+        ser.is_valid(raise_exception = True)
+        att.name = ser.validated_data.get("name", att.name)
+        att.value = ser.validated_data.get("value", att.value)
+        att.save()
+        return Response(TenantAttributeSerializer(att).data)
+
+    def delete_attribute(self, request, pk=None, attribute=None):
+        att = TenantAttribute.objects.get(pk=attribute)
+        att.delete()
+        return Response(status=status.HTTP_204_NO_CONTENT)
+
+
+
+
+
+
diff --git a/xos/api/tenant/truckroll.py b/xos/api/tenant/truckroll.py
index ea0200d..b5e9e3f 100644
--- a/xos/api/tenant/truckroll.py
+++ b/xos/api/tenant/truckroll.py
@@ -14,7 +14,7 @@
 def get_default_vtr_service():
     vtr_services = VTRService.get_service_objects().all()
     if vtr_services:
-        return vtr_services[0].id
+        return vtr_services[0]
     return None
 
 class VTRTenantForAPI(VTRTenant):
diff --git a/xos/api/xosapi_helpers.py b/xos/api/xosapi_helpers.py
index ee3ed00..6f5665c 100644
--- a/xos/api/xosapi_helpers.py
+++ b/xos/api/xosapi_helpers.py
@@ -97,3 +97,30 @@
         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
+
+    def get_serializer_class(self):
+        if hasattr(self, "custom_serializers") and hasattr(self, "action") and (self.action in self.custom_serializers):
+            return self.custom_serializers[self.action]
+        else:
+            return super(XOSViewSet, self).get_serializer_class()
+
+class XOSIndexViewSet(viewsets.ViewSet):
+    view_urls=[]
+    subdirs=[]
+
+    def __init__(self, view_urls, subdirs):
+        self.view_urls = view_urls
+        self.subdirs = subdirs
+        super(XOSIndexViewSet, self).__init__()
+
+    def list(self, request):
+        endpoints = []
+        for view_url in self.view_urls:
+            method_name = view_url[1].split("/")[-1]
+            endpoints.append(method_name)
+
+        for subdir in self.subdirs:
+            endpoints.append(subdir)
+
+        return Response({"endpoints": endpoints})
+
diff --git a/xos/core/admin.py b/xos/core/admin.py
index 0608e4e..3ace086 100644
--- a/xos/core/admin.py
+++ b/xos/core/admin.py
@@ -1266,7 +1266,7 @@
     list_display_links = ('backend_status_icon', 'name', )
 
 class NodeForm(forms.ModelForm):
-    labels = forms.ModelMultipleChoiceField(
+    nodelabels = forms.ModelMultipleChoiceField(
         queryset=NodeLabel.objects.all(),
         required=False,
         help_text="Select which labels apply to this node",
@@ -1286,12 +1286,12 @@
       super(NodeForm, self).__init__(*args, **kwargs)
 
       if self.instance and self.instance.pk:
-        self.fields['labels'].initial = self.instance.labels.all()
+        self.fields['nodelabels'].initial = self.instance.nodelabels.all()
 
     def save(self, commit=True):
       node = super(NodeForm, self).save(commit=False)
 
-      node.labels = self.cleaned_data['labels']
+      node.nodelabels = self.cleaned_data['nodelabels']
 
       if commit:
         node.save()
@@ -1314,7 +1314,7 @@
 
     inlines = [TagInline,InstanceInline]
     fieldsets = [('Node Details', {'fields': ['backend_status_text', 'name', 'site_deployment'], 'classes':['suit-tab suit-tab-details']}),
-                 ('Labels', {'fields': ['labels'], 'classes':['suit-tab suit-tab-labels']})]
+                 ('Labels', {'fields': ['nodelabels'], 'classes':['suit-tab suit-tab-labels']})]
     readonly_fields = ('backend_status_text', )
 
     user_readonly_fields = ['name','site_deployment']
diff --git a/xos/core/models/node.py b/xos/core/models/node.py
index b825787..d464532 100644
--- a/xos/core/models/node.py
+++ b/xos/core/models/node.py
@@ -31,6 +31,6 @@
 
 class NodeLabel(PlCoreBase):
     name = StrippedCharField(max_length=200, help_text="label name", unique=True)
-    node = models.ManyToManyField(Node, related_name="labels", blank=True)
+    node = models.ManyToManyField(Node, related_name="nodelabels", blank=True)
 
     def __unicode__(self): return u'%s' % (self.name)
diff --git a/xos/core/models/service.py b/xos/core/models/service.py
index 124feb5..aca4bb0 100644
--- a/xos/core/models/service.py
+++ b/xos/core/models/service.py
@@ -458,7 +458,7 @@
         nodes = Node.objects.all()
 
         if self.label:
-           nodes = nodes.filter(labels__name=self.label)
+           nodes = nodes.filter(nodelabels__name=self.label)
 
         nodes = list(nodes)
 
@@ -778,6 +778,8 @@
     value = models.TextField(help_text="Attribute Value")
     tenant = models.ForeignKey(Tenant, related_name='tenantattributes', help_text="The Tenant this attribute is associated with")
 
+    def __unicode__(self): return u'%s-%s' % (self.name, self.id)
+
 class TenantRootRole(PlCoreBase):
     ROLE_CHOICES = (('admin','Admin'), ('access','Access'))