Base net-virt CLI files on top of which ONOS specific changes will be done
diff --git a/cli/sdncon/rest/RestApiTestData.py b/cli/sdncon/rest/RestApiTestData.py
new file mode 100755
index 0000000..f907e5d
--- /dev/null
+++ b/cli/sdncon/rest/RestApiTestData.py
@@ -0,0 +1,84 @@
+#
+# Copyright (c) 2013 Big Switch Networks, Inc.
+#
+# Licensed under the Eclipse Public License, Version 1.0 (the
+# "License"); you may not use this file except in compliance with the
+# License. You may obtain a copy of the License at
+#
+#      http://www.eclipse.org/legal/epl-v10.html
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# permissions and limitations under the License.
+#
+
+# This i data file for testing Rest APIs
+# This file is read by RestApiTests.py
+
+test_dpid="11:22:33:44:55:66:77:01"
+test_dpid2="11:22:33:44:55:66:77:01"
+test_mac ="11:22:33:44:55:01"
+test_vns_id   ="test_vns_id_1"
+test_flow_entry_name = "test_flow_entry_name"
+test_vns_acl_name = "test_vns_acl"
+test_syncd_config_id = "test_syncd_config_id"
+test_syncd_transport_config_test_data="test_sync_transport_config_id"
+test_syncd_transport_config_name = "test_syncd_transport_cfg_name"
+test_vns_interface_rule_name = "test_vns_intf_rule_name"
+test_vns_interface_name = "test_vns_intf_name"
+test_vns_interface_acl_direction = "in"
+test_vns_interface_acl_id = "test_vns_interface_acl_id"
+test_vns_acl_name_id = test_vns_id+"|"+test_vns_acl_name
+test_vns_acl_entry_type = "ip"
+test_acl_entry_seq_no = "10"
+test_vns_acl_entry_id = test_vns_acl_name_id+"|"+test_acl_entry_seq_no
+test_vns_acl_entry_src_ip="11.22.33.44"
+test_vns_acl_entry_action="deny"
+test_vns_acl_entry_type="deny"
+test_tag_name = "test_tag_name"
+test_tag_value = "test_tag_value"
+test_tag_id = test_vns_id+"|"+test_tag_name+"|"+test_tag_value
+test_tag_mapping_id = test_tag_id+"|"+test_mac
+
+model_switch_test_data={"path":"model/switch", "data":{"dpid":test_dpid}, "id":"dpid"}
+model_host_test_data  ={"path":"model/host",   "data":{"mac":test_mac}, "id":"mac"}
+model_flow_entry_test_data = {"path":"model/flow-entry", "data":{"switch":test_dpid, "name":test_flow_entry_name}, "id":"name"}
+model_vns_definition_test_data = {"path":"model/vns-definition", "data":{"id":test_vns_id}, "id":"id"}
+model_vns_acl_test_data = {"path":"model/vns-access-list", "data":{"vns":test_vns_id, "name":test_vns_acl_name_id}, "id":"name"}
+model_host_network_address_test_data = {"path":"model/host-network-address", \
+    "data":{"id":"ea:81:27:2a:6b:6b-10.0.0.1"}, "id":"id"}
+model_syncd_config_test_data = {"path":"model/syncd-config", "data":{"id":test_syncd_config_id}, "id":"id"}
+model_tag_test_data = {"path":"model/tag", "data":{"id":test_tag_id, \
+    "name":"tagname", "value":"tagvalue", "namespace":test_vns_id}, "id":"id"}
+model_tag_mapping_test_data = {"path":"model/tag-mapping", "data":{"id": test_tag_mapping_id, \
+                               "tag": test_tag_id, "type": "host", "host": test_mac}, "id":"id"}
+model_syncd_transport_config_test_data = {"path":"model/syncd-transport-config", 
+    "data":{"id":"test_syncd_transport_config_test_data", "type":"random1", "args":"test_args", \
+    "config":test_syncd_config_id, "target-cluster":"Testcluster", "name":test_syncd_transport_config_name}, "id":"id"}
+model_syncd_progress_info_test_data = {"path":"model/syncd-progress-info", "data":{"id":test_syncd_config_id}, "id":"id"}
+model_vns_interface_rule_test_data ={"path":"model/vns-interface-rule", \
+    "data":{"id":test_vns_id+"|"+test_vns_interface_rule_name, "vns":test_vns_id}, "id":"id"}
+model_vns_interface_acl_test_data = {"path":"model/vns-interface-access-list", "data":{"id":test_vns_interface_acl_id, \
+    "in-out":test_vns_interface_acl_direction, "vns-interface":test_vns_interface_name, "vns-access-list":test_vns_acl_name_id}, "id":"id"}
+model_vns_acl_entry_test_data = {"path":"model/vns-access-list-entry", \
+    "data":{"id":test_vns_acl_entry_id, "src-ip":test_vns_acl_entry_src_ip, \
+    "action":test_vns_acl_entry_action, "vns-access-list":test_vns_acl_name_id, "type":test_vns_acl_entry_type}, "id":"id"}
+
+test_index=0
+rest_api_test_data = {}
+rest_api_test_data[test_index] = model_switch_test_data ; test_index += 1
+rest_api_test_data[test_index] = model_host_test_data; test_index += 1
+rest_api_test_data[test_index] = model_flow_entry_test_data; test_index += 1
+rest_api_test_data[test_index] = model_vns_definition_test_data; test_index += 1
+rest_api_test_data[test_index] = model_vns_acl_test_data; test_index += 1
+rest_api_test_data[test_index] = model_host_network_address_test_data; test_index += 1
+rest_api_test_data[test_index] = model_syncd_config_test_data; test_index += 1
+rest_api_test_data[test_index] = model_tag_test_data; test_index += 1
+rest_api_test_data[test_index] = model_tag_mapping_test_data; test_index += 1
+rest_api_test_data[test_index] = model_syncd_transport_config_test_data; test_index += 1
+rest_api_test_data[test_index] = model_syncd_progress_info_test_data; test_index += 1
+rest_api_test_data[test_index] = model_vns_interface_rule_test_data; test_index += 1
+rest_api_test_data[test_index] = model_vns_interface_acl_test_data; test_index += 1
+rest_api_test_data[test_index] = model_vns_acl_entry_test_data; test_index += 1
diff --git a/cli/sdncon/rest/__init__.py b/cli/sdncon/rest/__init__.py
new file mode 100755
index 0000000..580a7bb
--- /dev/null
+++ b/cli/sdncon/rest/__init__.py
@@ -0,0 +1,19 @@
+#
+# Copyright (c) 2013 Big Switch Networks, Inc.
+#
+# Licensed under the Eclipse Public License, Version 1.0 (the
+# "License"); you may not use this file except in compliance with the
+# License. You may obtain a copy of the License at
+#
+#      http://www.eclipse.org/legal/epl-v10.html
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# permissions and limitations under the License.
+#
+
+from sdncon.rest.config import init_config
+
+init_config()
diff --git a/cli/sdncon/rest/config.py b/cli/sdncon/rest/config.py
new file mode 100755
index 0000000..7c0de5c
--- /dev/null
+++ b/cli/sdncon/rest/config.py
@@ -0,0 +1,219 @@
+#
+# Copyright (c) 2013 Big Switch Networks, Inc.
+#
+# Licensed under the Eclipse Public License, Version 1.0 (the
+# "License"); you may not use this file except in compliance with the
+# License. You may obtain a copy of the License at
+#
+#      http://www.eclipse.org/legal/epl-v10.html
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# permissions and limitations under the License.
+#
+
+from django.db.models.signals import post_save, post_delete
+import json
+import os
+import traceback
+from django.core import serializers
+import sdncon
+
+# FIXME: This code is not thread-safe!!!
+# Currently we don't run Django with multiple threads so this isn't a problem,
+# but if we were to try to enable threading this code would need to be fixed.
+
+pre_save_instance = None
+config_handlers = []
+config_models = set()
+
+
+def add_config_handler(dependencies, handler):
+    """
+    The dependencies argument is a dictionary where the key is a model
+    (i.e. the actual model, not the name of the mode) and the value of the
+    key is a tuple (or list) of field names whose modification should
+    trigger the config handler. The value can also be None to indicate
+    that the config handler should be triggered when any field in the
+    model is modified.
+    
+    The calling convention of a config handler is:
+    
+    def my_config_handler(op, old_instance, new_instance, modified_fields)
+    
+    The 'op' argument is one of 'INSERT', 'UPDATE' or 'DELETE', corresponding
+    to an insertion, update or deletion of a row in the model.
+    The 'old_instance' argument is the instance of the model before the changes.
+    For an 'INSERT' op, old_instance and modified_fields are both None.
+    The 'new_instance' argument is the instance of the model after the changes.
+    For a 'DELETE' op, new_instance and modified_fields are both None.
+    the 'modified_fields' is a dictionary containing the fields that were changed.
+    The dictionary key is the name of the field and the value is the new value.
+    """
+    assert dependencies != None
+    assert handler != None
+    
+    for model in dependencies.keys():
+        config_models.add(model)
+            
+    config_handlers.append((dependencies, handler))
+
+last_config_state_path = None
+
+def get_last_config_state_path():
+    global last_config_state_path
+    if not last_config_state_path:
+        last_config_state_dir = "%s/run/" % sdncon.SDN_ROOT
+        if not os.path.exists(last_config_state_dir):
+            last_config_state_dir = '/tmp'
+        last_config_state_path = os.path.join(last_config_state_dir, 'last-config-state')
+    return last_config_state_path
+
+def reset_last_config_state():
+    path = get_last_config_state_path()
+    try:
+        os.unlink(path)
+    except Exception, _e:
+        pass
+    
+
+def config_read_state():
+    last_config_state = None
+    f = None
+    try:
+        f = open(get_last_config_state_path(), 'r')
+        last_config_state_text = f.read()
+        last_config_state = json.loads(last_config_state_text)
+    except Exception, _e:
+        pass
+    finally:
+        if f:
+            f.close()
+    return last_config_state
+
+def config_write_state(config_state):
+    f = None
+    try:
+        config_state_text = json.dumps(config_state)
+        f = open(get_last_config_state_path(), 'w')
+        f.write(config_state_text)
+    except Exception, e:
+        print "Error writing last config state: %s" % str(e)
+    finally:
+        if f:
+            f.close()
+
+def config_do_insert(sender, new_instance):
+    for config_handler in config_handlers:
+        dependencies = config_handler[0]
+        if sender in dependencies:
+            handler = config_handler[1]
+            try:
+                handler(op='INSERT', old_instance=None, new_instance=new_instance, modified_fields=None)
+            except Exception, _e:
+                traceback.print_exc()
+                
+def config_do_update(sender, old_instance, new_instance):
+    for config_handler in config_handlers:
+        dependencies = config_handler[0]
+        if sender in dependencies:
+            handler = config_handler[1]
+            modified_fields = {}
+            fields = dependencies.get(sender)
+            if not fields:
+                # If no fields were specified for the model then check all of the fields.
+                fields = [field.name for field in sender._meta.fields]
+                
+            for field in fields:
+                old_value = getattr(old_instance, field)
+                new_value = getattr(new_instance, field)
+                if new_value != old_value:
+                    modified_fields[field] = new_value
+            if modified_fields:
+                try:
+                    handler(op='UPDATE', old_instance=old_instance, new_instance=new_instance, modified_fields=modified_fields)
+                except Exception, _e:
+                    traceback.print_exc()
+
+
+def config_do_delete(sender, instance):
+    for config_handler in config_handlers:
+        dependencies = config_handler[0]
+        if sender in dependencies:
+            handler = config_handler[1]
+            try:
+                handler(op='DELETE', old_instance=instance, new_instance=None, modified_fields=None)
+            except Exception, _e:
+                traceback.print_exc()
+
+def model_instances_equal(instance1, instance2):
+    if instance1 == None:
+        return instance1 == instance2
+    
+    for field in instance1._meta.fields:
+        value1 = field.value_from_object(instance1)
+        value2 = field.value_from_object(instance2)
+        if value1 != value2:
+            return False
+    return True
+
+def config_check_state():
+    from sdncon.controller.notification import do_modify_notification, do_delete_notification
+    last_config_state = config_read_state()
+    try:
+        last_config_instances = last_config_state.get('instances')
+    except Exception, _e:
+        last_config_instances = {}
+    
+    current_config_instances = {}
+    
+    for config_model in config_models:
+        try:
+            serialized_old_instances = json.dumps(last_config_instances.get(config_model.__name__,[]))
+            old_instance_info = serializers.deserialize('json', serialized_old_instances)
+            old_instances = [info.object for info in old_instance_info]
+        except Exception, _e:
+            old_instances = []
+            
+        new_instances = config_model.objects.all()
+        
+        for new_instance in new_instances:
+            for index, old_instance in enumerate(old_instances):
+                if new_instance.pk == old_instance.pk:
+                    if not model_instances_equal(new_instance, old_instance):
+                        config_do_update(config_model, old_instance, new_instance)
+                        do_modify_notification(config_model, new_instance)
+                    del old_instances[index]
+                    break
+            else:
+                config_do_insert(config_model, new_instance)
+                do_modify_notification(config_model, new_instance)
+        
+        for deleted_instance in old_instances:
+            config_do_delete(config_model, deleted_instance)
+            do_delete_notification(config_model, deleted_instance)
+
+        try:
+            serialized_new_instances = serializers.serialize("json", new_instances)
+            current_config_instances[config_model.__name__] = json.loads(serialized_new_instances)
+        except:
+            print 'Failed to serialize', config_model.__name__
+        
+    config_write_state({'instances': current_config_instances})
+
+def config_post_save_handler(sender, **kwargs):
+    if not kwargs['raw'] and (kwargs['using'] == "default") and sender in config_models:
+        config_check_state()
+    
+def config_post_delete_handler(sender, **kwargs):
+    if kwargs['using'] == 'default' and sender in config_models:
+        config_check_state()
+
+def is_config_model(model):
+    return model in config_models
+ 
+def init_config():
+    post_save.connect(config_post_save_handler)
+    post_delete.connect(config_post_delete_handler)
diff --git a/cli/sdncon/rest/jsonview.py b/cli/sdncon/rest/jsonview.py
new file mode 100755
index 0000000..61c7c39
--- /dev/null
+++ b/cli/sdncon/rest/jsonview.py
@@ -0,0 +1,63 @@
+#
+# Copyright (c) 2013 Big Switch Networks, Inc.
+#
+# Licensed under the Eclipse Public License, Version 1.0 (the
+# "License"); you may not use this file except in compliance with the
+# License. You may obtain a copy of the License at
+#
+#      http://www.eclipse.org/legal/epl-v10.html
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# permissions and limitations under the License.
+#
+
+from django.http import HttpResponseForbidden, HttpResponse
+from django.utils.simplejson import JSONEncoder
+from django.utils.encoding import force_unicode
+from django.db.models.base import ModelBase
+from django.utils.simplejson import dumps
+
+class JsonResponse(HttpResponse):
+    """A HttpResponse subclass that creates properly encoded JSON response"""
+
+    def __init__(self, content='', json_opts={},
+                 mimetype="application/json", *args, **kwargs):
+        
+        if content is None: content = []
+        content = serialize_to_json(content,**json_opts)
+        super(JsonResponse,self).__init__(content,mimetype,*args,**kwargs)
+
+class JsonEncoder(JSONEncoder):
+    """A JSONEncoder subclass that also handles querysets and models objects."""
+
+    def default(self,o):
+        # this handles querysets and other iterable types
+        try: iterable = iter(o)
+        except TypeError: pass
+        else: return list(iterable)
+ 
+        # this handlers Models objects
+        try: isinstance(o.__class__,ModelBase)
+        except Exception: pass
+        else: return force_unicode(o)
+ 
+        # delegate the rest to JSONEncoder
+        return super(JsonEncoder,self).default(obj)
+ 
+def serialize_to_json(obj,*args,**kwargs):
+    """A wrapper for dumps with defaults as:
+        ensure_ascii=False
+        cls=JsonEncoder"""
+ 
+    kwargs['ensure_ascii'] = kwargs.get('ensure_ascii', False)
+    kwargs['cls'] = kwargs.get('cls', JsonEncoder)
+    return dumps(obj,*args,**kwargs)
+
+def as_kwargs(qdict):
+    kwargs = {}
+    for k,v in qdict.items():
+        kwargs[str(k)] = v
+    return kwargs
diff --git a/cli/sdncon/rest/models.py b/cli/sdncon/rest/models.py
new file mode 100755
index 0000000..436a90e
--- /dev/null
+++ b/cli/sdncon/rest/models.py
@@ -0,0 +1,26 @@
+#
+# Copyright (c) 2013 Big Switch Networks, Inc.
+#
+# Licensed under the Eclipse Public License, Version 1.0 (the
+# "License"); you may not use this file except in compliance with the
+# License. You may obtain a copy of the License at
+#
+#      http://www.eclipse.org/legal/epl-v10.html
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# permissions and limitations under the License.
+#
+
+from django.db import models
+from django.contrib.auth.models import User
+# Create your models here.
+
+class UserData(models.Model):
+    user = models.ForeignKey(User, null=True)
+    name = models.CharField(max_length=256)
+    content_type = models.CharField(max_length=128)
+    binary = models.BooleanField()
+    data = models.TextField()
diff --git a/cli/sdncon/rest/poll.py b/cli/sdncon/rest/poll.py
new file mode 100755
index 0000000..bfd0573
--- /dev/null
+++ b/cli/sdncon/rest/poll.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2013 Big Switch Networks, Inc.
+#
+# Licensed under the Eclipse Public License, Version 1.0 (the
+# "License"); you may not use this file except in compliance with the
+# License. You may obtain a copy of the License at
+#
+#      http://www.eclipse.org/legal/epl-v10.html
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# permissions and limitations under the License.
+#
+
+"""Polling interface for the polling services setup by the rest API"""
+
+import sys, os, time, random, json
+
+from packetstreamer import PacketStreamer
+from packetstreamer.ttypes import *
+
+from thrift import Thrift
+from thrift.transport import TSocket
+from thrift.transport import TTransport
+from thrift.protocol import TBinaryProtocol
+
+def sample_func(request, environ):
+    data = {
+        'pid': os.getpid(),
+        'random': int(random.random()*100),
+    }
+
+    return {
+        'status':    '200 OK',
+        'body':      json.dumps(data),
+        'headers':   [('Content-type', 'application/json')], # list of tuples
+    }
+
+def getPackets_func(request, environ, sessionid):
+    retData = {}
+
+    try:
+        # Make socket
+        transport = TSocket.TSocket('localhost', 9090)
+        # Buffering is critical. Raw sockets are very slow
+        transport = TTransport.TFramedTransport(transport)
+        # Wrap in a protocol
+        protocol = TBinaryProtocol.TBinaryProtocol(transport)
+        # Create a client to use the protocol encoder
+        client = PacketStreamer.Client(protocol)
+        # Connect!
+        transport.open()
+
+        packets = client.getPackets(sessionid)
+
+        # Close!
+        transport.close()
+
+        retData = {
+          'status':    '200 OK',
+          'body':       json.dumps(packets),
+          'headers':    [('Content-type', 'application/json')], # list of tuples
+        }
+
+    except Thrift.TException, tx:
+        retData = {
+          'status':    '500 Internal Server Error',
+          'body':      'error: %s'%tx.message,
+          'headers':   [('Content-type', 'application/json')], # list of tuples
+        }
+
+    return retData
+
+# The polling service
+#   The service is driven by the urlpatterns tuples that list the specific function
+#   to use for a matching pattern in URL. The pattern should not include initial '^/poll'.
+#   For Example:
+#     (r'^/sample', sample_func)
+#from sdncon.rest.poll import sample_func
+urlpatterns = [
+    (r'^/packets/(?P<sessionid>[A-Za-z0-9_.\-]+)/?$', getPackets_func),
+    (r'^/sample', sample_func)
+]
+
+import re, wsgiref.util
+
+def compile_patterns(urlpatterns):
+    return map(lambda pat: (re.compile(pat[0]), pat[1]), urlpatterns)
+
+def main(environ, start_response):
+    patterns = compile_patterns(urlpatterns)
+    request = environ['PATH_INFO']
+    response = None
+    status, body = '404 Not Found', 'Sorry, the requested resource "%s" was not found."' % request
+    headers = []
+    for (pat, func) in patterns:
+        m = pat.match(request)
+        if m:
+            args = m.groups()
+            if args:
+                response = func(request, environ, *args)
+            else:
+                response = func(request, environ)
+
+            status, body = response['status'], response['body']
+            if 'headers' in response:
+                headers.extend(response['headers'])
+            break
+    if 'Cache-Control' not in headers:
+        headers.append(('Cache-Control', 'no-cache'))
+    if 'Content-type' not in headers:
+         headers.append(('Content-type', 'text/plain'))
+    if 'Content-Length' not in headers:
+         headers.append(('Content-Length', str(len(body))))
+    start_response(status, headers)
+    return [body]
+
+if __name__ == "__main__":
+    def start_response(x, y):
+        print 'Status:', x
+        print 'Headers:', y
+
+    print 'Sample', sample_func(None, None)
+    print 'Response:', main({'PATH_INFO':'/sample/100'}, start_response)
+    print 'Response:', main({'PATH_INFO':'/xsample/100'}, start_response)
diff --git a/cli/sdncon/rest/tests.py b/cli/sdncon/rest/tests.py
new file mode 100755
index 0000000..7430831
--- /dev/null
+++ b/cli/sdncon/rest/tests.py
@@ -0,0 +1,648 @@
+#
+# Copyright (c) 2013 Big Switch Networks, Inc.
+#
+# Licensed under the Eclipse Public License, Version 1.0 (the
+# "License"); you may not use this file except in compliance with the
+# License. You may obtain a copy of the License at
+#
+#      http://www.eclipse.org/legal/epl-v10.html
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# permissions and limitations under the License.
+#
+
+from django.core.validators import validate_ipv4_address, MinValueValidator
+from django.db import models
+from django.forms import ValidationError
+from django.test import TestCase, Client
+from django.utils import simplejson
+import copy
+import urllib
+import urllib2
+from sdncon.rest.validators import validate_mac_address, RangeValidator, ChoicesValidator
+from sdncon.rest.config import add_config_handler, reset_last_config_state, model_instances_equal
+
+# These are the test models used in the unit tests
+# When running the unit tests, syncdb looks for model definitions
+# in this file (i.e. tests.py), so these will be in the complete
+# list of models used by the REST code
+
+class RestTestModel(models.Model):
+    name = models.CharField(max_length=32, primary_key=True)
+    enabled = models.BooleanField()
+    min = models.IntegerField(validators=[MinValueValidator(100)])
+    range = models.IntegerField(validators=[RangeValidator(10,100)])
+    internal = models.CharField(max_length=24,default='foo')
+    ip = models.IPAddressField(validators=[validate_ipv4_address])
+    mac = models.CharField(max_length=20, validators=[validate_mac_address])
+    ratio = models.FloatField()
+    COLOR_CHOICES = (
+        ('red', 'Dark Red'),
+        ('blue', 'Navy Blue'),
+        ('green', 'Green'),
+        ('white', 'Ivory White')
+    )
+    color = models.CharField(max_length=20, validators=[ChoicesValidator(COLOR_CHOICES)])
+    
+    def clean(self):
+        # This is just a dummy check for testing purposes. Typically you'd only
+        # use the model-level clean function for validating things across multiple fields
+        if self.range == 50:
+            raise ValidationError("Dummy model-level validation failure.")
+    
+    class Rest:
+        NAME = 'rest-test'
+        FIELD_INFO = ({'name':'internal', 'private': True},)
+
+class RestTestTagModel(models.Model):
+    rest_test = models.ForeignKey(RestTestModel)
+    name = models.CharField(max_length=32)
+    value = models.CharField(max_length=256)
+    
+    class Rest:
+        NAME = 'rest-test-tag'
+        FIELD_INFO = ({'name':'rest_test', 'rest_name':'rest-test'},)
+        
+class RestTestRenamedFieldModel(models.Model):
+    test = models.CharField(max_length=16,primary_key=True)
+    
+    class Rest:
+        NAME = 'rest-test-renamed'
+        FIELD_INFO = ({'name':'test', 'rest_name':'renamed'},)
+        
+class RestDisabledModel(models.Model):
+    dummy = models.CharField(max_length=32)
+
+class RestCompoundKeyModel(models.Model):
+    name = models.CharField(max_length=32)
+    index = models.IntegerField()
+    extra = models.CharField(max_length=32, default='dummy')
+    
+    class CassandraSettings:
+        COMPOUND_KEY_FIELDS = ['name', 'index']
+        
+    class Rest:
+        NAME = 'rest-compound-key'
+        
+#############################################################################
+# These are some client-side utility functions for getting, putting, deleting
+# data using the REST API
+#############################################################################
+
+def construct_rest_url(path, query_params=None):
+    url = 'http://localhost:8000/rest/v1/%s/' % path
+    if query_params:
+        url += '?'
+        url += urllib.urlencode(query_params)
+    return url
+
+def get_rest_data(type, query_param_dict=None):
+    url = construct_rest_url(type, query_param_dict)
+    request = urllib2.Request(url)
+    response = urllib2.urlopen(request)
+    response_text = response.read()
+    obj = simplejson.loads(response_text)
+    return obj
+
+def put_rest_data(obj, type, query_param_dict=None):
+    url = construct_rest_url(type, query_param_dict)
+    post_data = simplejson.dumps(obj)
+    request = urllib2.Request(url, post_data, {'Content-Type':'application/json'})
+    request.get_method = lambda: 'PUT'
+    response = urllib2.urlopen(request)
+    return response.read()
+
+def delete_rest_data(type, query_param_dict=None):
+    url = construct_rest_url(type, query_param_dict)
+    request = urllib2.Request(url)
+    request.get_method = lambda: 'DELETE'
+    response = urllib2.urlopen(request)
+    return response.read()
+
+def test_construct_rest_url(path, query_params=None):
+    url = '/rest/v1/%s/' % path
+    if query_params:
+        url += '?'
+        url += urllib.urlencode(query_params)
+    return url
+
+def test_construct_rest_model_url(path, query_params=None):
+    return test_construct_rest_url('model/' + path, query_params)
+    
+def test_get_rest_model_data(type, query_params=None):
+    url = test_construct_rest_model_url(type,query_params)
+    c = Client()
+    response = c.get(url)
+    return response
+
+def test_put_rest_model_data(obj, type, query_params=None):
+    url = test_construct_rest_model_url(type, query_params)
+    data = simplejson.dumps(obj)
+    c = Client()
+    response = c.put(url , data, 'application/json')
+    return response
+
+def test_delete_rest_model_data(type, query_params=None):
+    url = test_construct_rest_model_url(type, query_params)
+    c = Client()
+    response = c.delete(url)
+    return response
+
+def get_sorted_list(data_list, key_name, create_copy=False):
+    if create_copy:
+        data_list = copy.deepcopy(data_list)
+    if key_name:
+        key_func = lambda item: item.get(key_name)
+    else:
+        key_func = lambda item: item[0]
+    data_list.sort(key = key_func)
+    return data_list
+
+#############################################################################
+# The actual unit test classes
+#############################################################################
+
+class BasicFunctionalityTest(TestCase):
+    
+    REST_TEST_DATA = [
+        {'name':'foobar','enabled':True,'min':100,'range':30,'ip':'192.168.1.1','mac':'00:01:22:34:56:af','ratio':4.56,'color':'red'},
+        {'name':'sdnplatform','enabled':False,'min':400,'range':45,'ip':'192.168.1.2','mac':'00:01:22:CC:56:DD','ratio':8.76,'color':'green'},
+        {'name':'foobar2','enabled':True,'min':1000,'range':100,'ip':'192.168.1.3','mac':'00:01:22:34:56:af','ratio':1,'color':'white'},
+    ]
+    
+    REST_TEST_TAG_DATA = [
+        {'id':'1','rest-test':'foobar','name':'one','value':'testit'},
+        {'id':'2','rest-test':'foobar','name':'two','value':'test2'},
+        {'id':'3','rest-test':'sdnplatform','name':'three','value':'three'},
+    ]
+    
+    def setUp(self):
+        # Create the RestTestModel instances
+        rest_test_list = []
+        for rtd in self.REST_TEST_DATA:
+            rt = RestTestModel(**rtd)
+            rt.save()
+            rest_test_list.append(rt)
+        
+        # Create the RestTestTagModel instances
+        for rttd in self.REST_TEST_TAG_DATA:
+            rttd_init = rttd.copy()
+            for rt in rest_test_list:
+                if rt.name == rttd['rest-test']:
+                    del rttd_init['rest-test']
+                    rttd_init['rest_test'] = rt
+                    break
+            else:
+                raise Exception('Invalid initialization data for REST unit tests')
+            rtt = RestTestTagModel(**rttd_init)
+            rtt.save()
+        
+        rtrf = RestTestRenamedFieldModel(test='test')
+        rtrf.save()
+        
+        # Create the RestDisabledModel instance. We're only going to test that
+        # we can't access this model via the REST API, so we don't need to keep
+        # track of the data used for the instance
+        #rdm = RestDisabledModel(dummy='dummy')
+        #rdm.save()
+    
+    def check_rest_test_data_result(self, data_list, expected_data_list, message=None):
+        data_list = get_sorted_list(data_list, 'name', True)
+        expected_data_list = get_sorted_list(expected_data_list, 'name', True)
+        self.assertEqual(len(data_list), len(expected_data_list), message)
+        for i in range(len(data_list)):
+            data = data_list[i]
+            expected_data = expected_data_list[i]
+            self.assertEqual(data['name'], expected_data['name'], message)
+            self.assertEqual(data['enabled'], expected_data['enabled'], message)
+            self.assertEqual(data['min'], expected_data['min'], message)
+            self.assertEqual(data['range'], expected_data['range'], message)
+            self.assertEqual(data['ip'], expected_data['ip'], message)
+            self.assertEqual(data['mac'], expected_data['mac'], message)
+            self.assertAlmostEqual(data['ratio'], expected_data['ratio'], 7, message)
+            self.assertEqual(data['color'], expected_data['color'], message)
+    
+    def check_rest_test_tag_data_result(self, data_list, expected_data_list, message=None):
+        data_list = get_sorted_list(data_list, 'name', True)
+        expected_data_list = get_sorted_list(expected_data_list, 'name', True)
+        self.assertEqual(len(data_list), len(expected_data_list), message)
+        for i in range(len(data_list)):
+            data = data_list[i]
+            expected_data = expected_data_list[i]
+            self.assertEqual(str(data['id']), expected_data['id'], message)
+            self.assertEqual(data['rest-test'], expected_data['rest-test'], message)
+            self.assertEqual(data['name'], expected_data['name'], message)
+            self.assertEqual(data['value'], expected_data['value'], message)
+            
+    def test_put_one(self):
+        test_put_rest_model_data({'min':200,'color':'white'}, 'rest-test/foobar')
+        response = test_get_rest_model_data('rest-test')
+        data = simplejson.loads(response.content)
+        rtd_copy = copy.deepcopy(self.REST_TEST_DATA)
+        rtd_copy[0]['min'] = 200
+        rtd_copy[0]['color'] = 'white'
+        self.check_rest_test_data_result(data, rtd_copy)
+        
+    def test_put_query(self):
+        test_put_rest_model_data({'min':200,'color':'white'}, 'rest-test', {'enabled':True})
+        response = test_get_rest_model_data('rest-test')
+        data = simplejson.loads(response.content)
+        rtd_copy = copy.deepcopy(self.REST_TEST_DATA)
+        for rtd in rtd_copy:
+            if rtd['enabled']:
+                rtd['min'] = 200
+                rtd['color'] = 'white'
+        self.check_rest_test_data_result(data, rtd_copy)
+    
+    def test_create_one(self):
+        rtd_copy = copy.deepcopy(self.REST_TEST_DATA)
+        create_rtd = rtd_copy[0].copy()
+        create_rtd['name'] = 'test-create'
+        rtd_copy.append(create_rtd)
+        test_put_rest_model_data(create_rtd, 'rest-test')
+        response = test_get_rest_model_data('rest-test')
+        data = simplejson.loads(response.content)
+        self.check_rest_test_data_result(data, rtd_copy)
+    
+    def test_create_many(self):
+        rtd_copy = copy.deepcopy(self.REST_TEST_DATA)
+        create_rtd1 = rtd_copy[0].copy()
+        create_rtd1['name'] = 'test-create1'
+        create_rtd1['min'] = 2000
+        rtd_copy.append(create_rtd1)
+        create_rtd2 = rtd_copy[0].copy()
+        create_rtd2['name'] = 'test-create2'
+        create_rtd1['min'] = 4000
+        rtd_copy.append(create_rtd2)
+        test_put_rest_model_data([create_rtd1,create_rtd2], 'rest-test')
+        response = test_get_rest_model_data('rest-test')
+        data = simplejson.loads(response.content)
+        self.check_rest_test_data_result(data, rtd_copy)
+        
+    def test_get(self):
+        RTD = self.REST_TEST_DATA
+        RTTD = self.REST_TEST_TAG_DATA
+        GET_TEST_VECTORS = (
+            #model_name, query_params, expected_data
+            ('rest-test', None, RTD, 'Failure getting all rest-test instances'),
+            ('rest-test-tag', None, RTTD, 'Failure getting all rest-test-tag instances'),
+            ('rest-test/foobar', None, RTD[0], 'Failure getting single rest-test instance by URL'),
+            ('rest-test', {'name':'foobar'}, [RTD[0]], 'Failure querying for single rest-test instance'),
+            ('rest-test', {'name':'foobar','nolist':True}, RTD[0], 'Failure querying for single rest-test instance using nolist'),
+            ('rest-test', {'enabled':True}, [RTD[0],RTD[2]], 'Failure querying for multiple rest-test instances'),
+            ('rest-test', {'name__startswith':'foobar'}, [RTD[0],RTD[2]], 'Failure querying for multiple rest-test instances using startswith'),
+            ('rest-test', {'orderby':'ratio'}, [RTD[2],RTD[0],RTD[1]], 'Failure querying with ascending orderby'),
+            ('rest-test', {'orderby':'-ratio'}, [RTD[1],RTD[0],RTD[2]], 'Failure querying with descending orderby'),
+            ('rest-test', {'orderby':'enabled,-ratio'}, [RTD[1],RTD[0],RTD[2]], 'Failure querying with multi-field orderby'),
+            ('rest-test-tag', {'rest-test':'foobar'}, [RTTD[0],RTTD[1]], 'Failure querying by ForeignKey value'),
+            ('rest-test-tag', {'rest-test__startswith':'foo'}, [RTTD[0],RTTD[1]], 'Failure querying by ForeignKey value'),
+            ('rest-test-tag', {'rest-test__contains':'swi'}, [RTTD[2]], 'Failure querying by ForeignKey value'),
+        )
+        
+        for model_name, query_params, expected_data, message in GET_TEST_VECTORS:
+            response = test_get_rest_model_data(model_name, query_params)
+            data = simplejson.loads(response.content)
+            #print model_name
+            #self.check_rest_test_data_result(data, expected_data, message)
+            if type(data) is not list:
+                data = [data]
+                #data = get_sorted_list(data, 'name', False)
+            if type(expected_data) is not list:
+                expected_data = [expected_data]
+                #expected_data = get_sorted_list(expected_data, 'name', True)
+            if model_name.startswith('rest-test-tag'):
+                self.check_rest_test_tag_data_result(data, expected_data, message)
+            else:
+                self.check_rest_test_data_result(data, expected_data, message)
+            
+    def test_renamed_field(self):
+        response = test_get_rest_model_data('rest-test-renamed/test')
+        self.assertEqual(response.status_code,200)
+        data = simplejson.loads(response.content)
+        self.assertEqual(data['renamed'], 'test')
+        
+    def test_delete_one(self):
+        test_delete_rest_model_data('rest-test/foobar')
+        response = test_get_rest_model_data('rest-test')
+        data = simplejson.loads(response.content)
+        self.check_rest_test_data_result(data, self.REST_TEST_DATA[1:])
+    
+    def test_delete_query(self):
+        test_delete_rest_model_data('rest-test', {'name__startswith':'foobar'})
+        response = test_get_rest_model_data('rest-test')
+        data = simplejson.loads(response.content)
+        self.check_rest_test_data_result(data, [self.REST_TEST_DATA[1]])
+    
+    def test_delete_all(self):
+        test_delete_rest_model_data('rest-test')
+        response = test_get_rest_model_data('rest-test')
+        data = simplejson.loads(response.content)
+        self.assertEqual(len(data),0)
+
+class NegativeTest(TestCase):
+    
+    def test_invalid_model_name(self):
+        response = test_get_rest_model_data('foobar')
+        self.assertEqual(response.status_code,400)
+        response_obj = simplejson.loads(response.content)
+        self.assertEqual(response_obj['error_type'],'RestInvalidDataTypeException')
+        
+    def test_resource_not_found(self):
+        response = test_get_rest_model_data('rest-test/foobar')
+        self.assertEqual(response.status_code,404)
+        response_obj = simplejson.loads(response.content)
+        self.assertEqual(response_obj['error_type'],'RestResourceNotFoundException')
+
+    def test_invalid_orderby(self):
+        response = test_get_rest_model_data('rest-test', {'orderby':'foobar'})
+        self.assertEqual(response.status_code,400)
+        response_obj = simplejson.loads(response.content)
+        self.assertEqual(response_obj['error_type'],'RestInvalidOrderByException')
+    
+    def test_hidden_model(self):
+        response = test_get_rest_model_data('restdisabledmodel')
+        self.assertEqual(response.status_code,400)
+        response_obj = simplejson.loads(response.content)
+        self.assertEqual(response_obj['error_type'],'RestInvalidDataTypeException')
+
+class ValidationTest(TestCase):
+    
+    DEFAULT_DATA = {'name':'foobar','enabled':True,'min':100,'range':30,'ip':'192.168.1.1','mac':'00:01:22:34:56:af','ratio':4.56,'color':'red'}
+    TEST_VECTORS = (
+        #update-dict, error-type, error-messgae
+        ({'min':99}, ['min'],'Min value validation failed'),
+        ({'range':8}, ['range'],'Min range validation failed'),
+        ({'range':2000}, ['range'],'Max range validation failed'),
+        ({'range':50}, None,'Model-level clean validation failed'),
+        ({'mac':'00:01:02:03:05'}, ['mac'],'Too short MAC address validation failed'),
+        ({'mac':'00:01:02:03:05:99:45'}, ['mac'],'Too long MAC address validation failed'),
+        ({'mac':'00,01,02,03,05,99'}, ['mac'],'MAC address separator char validation failed'),
+        ({'mac':'0r:01:02:03:05:99'}, ['mac'],'MAC address character validation failed'),
+        ({'mac':'123:01:02:03:05:99'}, ['mac'],'MAC address byte length validation failed'),
+        ({'color':'purple'}, ['color'],'Choices validation failed'),
+        ({'ip':'foobar'}, ['ip'], 'Invalid IP address char validation failed'),
+        ({'ip':'192.168.1'}, ['ip'], 'Too short IP address char validation failed'),
+        ({'ip':'192.168.1.0.5'}, ['ip'], 'Too long IP address char validation failed'),
+        ({'ip':'192,168,1,0'}, ['ip'], 'IP address separator char validation failed'),
+        ({'ip':'256.168.1.0'}, ['ip'], 'Out of range IP address byte validation failed'),
+        ({'min':99, 'ip':'256.168.1.0'}, ['min', 'ip'], 'Multiple field validation failed'),
+    )
+    
+    def check_response(self, response, invalid_fields, message):
+        self.assertEqual(response.status_code,400)
+        response_obj = simplejson.loads(response.content)
+        self.assertEqual(response_obj['error_type'], 'RestValidationException', message)
+        if invalid_fields:
+            self.assertEqual(response_obj.get('model_error'), None)
+            field_errors = response_obj.get('field_errors')
+            self.assertNotEqual(field_errors, None)
+            self.assertEqual(len(invalid_fields), len(field_errors), message)
+            for field_name in field_errors.keys():
+                self.assertTrue(field_name in invalid_fields, message)
+        else:
+            self.assertEqual(response_obj.get('field_errors'), None)
+            self.assertNotEqual(response_obj.get('model_error'), None)
+        # FIXME: Maybe check the description here too?
+        
+    def test_create_validation(self):
+        for test_data, invalid_fields, message in self.TEST_VECTORS:
+            data = self.DEFAULT_DATA.copy()
+            for name, value in test_data.items():
+                data[name] = value
+            put_response = test_put_rest_model_data(data, 'rest-test')
+            self.check_response(put_response, invalid_fields, message)
+            
+    def test_update_validation(self):
+        # First add an instance with the default data and check that it's OK
+        put_response = test_put_rest_model_data(self.DEFAULT_DATA, 'rest-test')
+        self.assertEqual(put_response.status_code,200)
+        
+        for test_data, invalid_fields, message in self.TEST_VECTORS:
+            put_response = test_put_rest_model_data(test_data, 'rest-test/foobar')
+            self.check_response(put_response, invalid_fields, message)
+
+
+class ConfigHandlerTest(TestCase):
+
+    TEST_DATA = {'name':'foobar','enabled':True,'min':100,'range':30,'ip':'192.168.1.1','mac':'00:01:22:34:56:af','ratio':4.56,'color':'red'}
+    
+    test_op = None
+    test_old_instance = None
+    test_new_instance = None
+    test_modified_fields = None
+    
+    @staticmethod
+    def reset_config_handler():
+        ConfigHandlerTest.test_op = None
+        ConfigHandlerTest.test_old_instance = None
+        ConfigHandlerTest.test_new_instance = None
+        ConfigHandlerTest.test_modified_fields = None
+    
+    @staticmethod
+    def config_handler(op, old_instance, new_instance, modified_fields):
+        ConfigHandlerTest.test_op = op
+        ConfigHandlerTest.test_old_instance = old_instance
+        ConfigHandlerTest.test_new_instance = new_instance
+        ConfigHandlerTest.test_modified_fields = modified_fields
+    
+    def check_config_handler(self, expected_op, expected_old_instance, expected_new_instance, expected_modified_fields):
+        self.assertEqual(ConfigHandlerTest.test_op, expected_op)
+        self.assertTrue(model_instances_equal(ConfigHandlerTest.test_old_instance, expected_old_instance))
+        self.assertTrue(model_instances_equal(ConfigHandlerTest.test_new_instance, expected_new_instance))
+        self.assertEqual(ConfigHandlerTest.test_modified_fields, expected_modified_fields)
+        
+    def test_config(self):
+        reset_last_config_state()
+        # Install the config handler
+        field_list = ['internal', 'min', 'mac']
+        add_config_handler({RestTestModel: field_list}, ConfigHandlerTest.config_handler)
+        
+        # Check that config handler is triggered on an insert
+        ConfigHandlerTest.reset_config_handler()
+        test = RestTestModel(**self.TEST_DATA)
+        test.save()
+        self.check_config_handler('INSERT', None, test, None)
+        
+        # Check that config handler is triggered on an update
+        ConfigHandlerTest.reset_config_handler()
+        expected_old = RestTestModel.objects.get(pk='foobar')
+        test.internal = 'check'
+        test.min = 125
+        test.save()
+        self.check_config_handler('UPDATE', expected_old, test, {'internal': 'check', 'min': 125})
+        
+        # Check that config handler is not triggered on an update to a field that
+        # it's not configured to care about
+        ConfigHandlerTest.reset_config_handler()
+        test.max = 500
+        test.save()
+        self.check_config_handler(None, None, None, None)
+        
+        # Check that config handler is triggered on a delete
+        ConfigHandlerTest.reset_config_handler()
+        test.delete()
+        # delete() clears the pk which messes up the instance
+        # comparison logic in check_config_handler, so we hack
+        # it back to the value it had before the delete
+        test.name = 'foobar'    
+        self.check_config_handler('DELETE', test, None, None)
+
+
+class CompoundKeyModelTest(TestCase):
+    
+    TEST_DATA = [
+        {'name': 'foo', 'index': 3},
+        {'name': 'foo', 'index': 7},
+        {'name': 'bar', 'index': 2, 'extra': 'abc'},
+        {'name': 'bar', 'index': 4, 'extra': 'test'},
+    ]
+    
+    def setUp(self):
+        for data in self.TEST_DATA:
+            test_put_rest_model_data(data, 'rest-compound-key')
+    
+    def check_query_results(self, actual_results, expected_results):
+        self.assertEqual(len(actual_results), len(expected_results))
+        actual_results = get_sorted_list(actual_results, 'id', False)
+        expected_results = copy.deepcopy(expected_results)
+        for expected_result in expected_results:
+            expected_result['id'] = expected_result['name'] + '|' + str(expected_result['index'])
+        expected_results = get_sorted_list(expected_results, 'id')
+        
+        for actual_result, expected_result in zip(actual_results, expected_results):
+            self.assertEqual(actual_result['id'], expected_result['id'])
+            self.assertEqual(actual_result['name'], expected_result['name'])
+            self.assertEqual(actual_result['index'], expected_result['index'])
+            expected_extra = expected_result.get('extra', 'dummy')
+            self.assertEqual(actual_result['extra'], expected_extra)
+                
+    def test_set_up(self):
+        response = test_get_rest_model_data('rest-compound-key')
+        self.assertEqual(response.status_code, 200)
+        actual_results = simplejson.loads(response.content)
+        self.check_query_results(actual_results, self.TEST_DATA)
+        
+    def test_delete_by_id(self):
+        test_delete_rest_model_data('rest-compound-key', {'id': 'foo|3'})
+        test_delete_rest_model_data('rest-compound-key', {'id': 'bar|4'})
+        response = test_get_rest_model_data('rest-compound-key')
+        self.assertEqual(response.status_code, 200)
+        actual_results = simplejson.loads(response.content)
+        self.check_query_results(actual_results, self.TEST_DATA[1:-1])
+    
+    def test_delete_by_fields(self):
+        test_delete_rest_model_data('rest-compound-key', {'name': 'bar', 'index': 4})
+        response = test_get_rest_model_data('rest-compound-key')
+        self.assertEqual(response.status_code, 200)
+        actual_results = simplejson.loads(response.content)
+        self.check_query_results(actual_results, self.TEST_DATA[:-1])
+        
+class UserDataTest(TestCase):
+    
+    TEST_DATA = [
+        # name, data, binary, content-type
+        ('test/foobar', 'hello world\nanother line', False, None),
+        ('test/json-test', '[0,1,2]', False, 'application/json'),
+        ('test/binary-test', '\x01\x02\x03\x04\x55', True, None),
+    ]
+    
+    MORE_TEST_DATA = [
+        ('moretests/foo1', 'moretest1', False, None),
+        ('moretests/foo2', 'moretest2', False, None),
+    ]
+    
+    def add_user_data(self, client, user_data):
+        for name, data, binary, content_type in user_data:
+            if not content_type:
+                if binary:
+                    content_type = 'application/octet-stream'
+                else:
+                    content_type = 'text/plain'
+            url = test_construct_rest_url('data/' + name, {'binary':binary})
+            response = client.put(url, data, content_type)
+            self.assertEquals(response.status_code, 200)
+
+    def check_expected_info_list(self, info_list, expected_info_list):
+        self.assertEqual(type(info_list), list)
+        info_list = get_sorted_list(info_list, 'name', True)
+        expected_info_list = get_sorted_list(expected_info_list, None, True)
+        self.assertEqual(len(info_list), len(expected_info_list))
+        for i in range(len(info_list)):
+            data = info_list[i]
+            expected_data = expected_info_list[i]
+            expected_name = expected_data[0]
+            expected_url_path = 'rest/v1/data/' + expected_name + '/'
+            self.assertEqual(data['name'], expected_name)
+            self.assertEqual(data['url_path'], expected_url_path)
+
+    def get_info_list(self, client, query_params=None):
+        url = test_construct_rest_url('data', query_params)
+        get_response = client.get(url)
+        self.assertEqual(get_response.status_code, 200)
+        info_list = simplejson.loads(get_response.content)
+        
+        # The view for listing user data items hard-codes inclusion of some
+        # *magic* user data items corresponding to the startup-config
+        # and update-config, depending on the presence of certain files
+        # in the file system. To make the unit tests work correctly if/when
+        # either/both of those items are added we do this filtering pass
+        # over the items returned from the REST API. Ugly hack!
+        info_list = [item for item in info_list if '-config' not in item['name']]
+        
+        return info_list
+
+    def test_user_data_basic(self):
+        c = Client()
+        self.add_user_data(c, self.TEST_DATA)
+        for name, data, binary, content_type in self.TEST_DATA:
+            if not content_type:
+                if binary:
+                    content_type = 'application/octet-stream'
+                else:
+                    content_type = 'text/plain'
+            url = test_construct_rest_url('data/' + name)
+            get_response = c.get(url)
+            self.assertEqual(get_response.content, data)
+            self.assertEqual(get_response['Content-Type'], content_type)
+
+    def test_user_data_delete(self):
+        c = Client()
+        
+        self.add_user_data(c, self.TEST_DATA)
+        self.add_user_data(c, self.MORE_TEST_DATA)
+        
+        # Do a delete with a query filter
+        url = test_construct_rest_url('data', {'name__startswith':'moretests/'})
+        response = c.delete(url)
+        self.assertEqual(response.status_code, 200)
+        
+        info_list = self.get_info_list(c)
+        expected_info_list = self.TEST_DATA
+        self.check_expected_info_list(info_list, expected_info_list)
+        
+        # Do a delete of a specific user data element
+        url = test_construct_rest_url('data/' + self.TEST_DATA[0][0])
+        response = c.delete(url)
+        self.assertEqual(response.status_code, 200)
+
+        info_list = self.get_info_list(c)
+        expected_info_list = self.TEST_DATA[1:]
+        self.check_expected_info_list(info_list, expected_info_list)
+
+    def test_user_data_info(self):
+
+        c = Client()
+        
+        self.add_user_data(c, self.TEST_DATA)
+        self.add_user_data(c, self.MORE_TEST_DATA)
+        
+        info_list = self.get_info_list(c)
+        expected_info_list = self.TEST_DATA + self.MORE_TEST_DATA
+        self.check_expected_info_list(info_list, expected_info_list)
+        
+        info_list = self.get_info_list(c, {'name__startswith':'test/'})
+        expected_info_list = self.TEST_DATA
+        self.check_expected_info_list(info_list, expected_info_list)
diff --git a/cli/sdncon/rest/validators.py b/cli/sdncon/rest/validators.py
new file mode 100755
index 0000000..856038a
--- /dev/null
+++ b/cli/sdncon/rest/validators.py
@@ -0,0 +1,448 @@
+#
+# Copyright (c) 2013 Big Switch Networks, Inc.
+#
+# Licensed under the Eclipse Public License, Version 1.0 (the
+# "License"); you may not use this file except in compliance with the
+# License. You may obtain a copy of the License at
+#
+#      http://www.eclipse.org/legal/epl-v10.html
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# permissions and limitations under the License.
+#
+
+import re
+import os.path
+
+from django.forms import ValidationError
+from django.core.validators import RegexValidator, MinValueValidator, MaxValueValidator
+
+validate_mac_address = RegexValidator('^(([A-Fa-f\d]){2}:?){5}[A-Fa-f\d]{2}$', 'MAC address is a 48-bit quantity, expressed in hex as AA:BB:CC:DD:EE:FF', 'invalid')
+validate_dpid = RegexValidator('^([A-Fa-f\d]{2}:?){7}[A-Fa-f\d]{2}$', 'Switch DPID is a 64-bit quantity, expressed in hex as AA:BB:CC:DD:EE:FF:00:11', 'invalid')
+validate_controller_id = RegexValidator('^[A-Fa-f\d]{8}-([A-Fa-f\d]{4}-){3}[A-Fa-f\d]{12}$', 'Controller ID is a 128-bit quantity, expressed in hex as aabbccdd-eeff-0011-2233-445566778899', 'invalid')
+
+class UfwProtocolValditor(object):
+    def __init__(self):
+        pass
+    
+    def __call__(self, value):
+        if not (value != "tcp" or value != "udp" or value != 'vrrp'):
+            raise ValidationError("Protocol must be either 'tcp' or 'udp' or 'vrrp'")
+
+octet = r'(?:0|[1-9][0-9]*)'
+IP_RE = re.compile("^" + octet + '[.]' + octet
+                   + '[.]' + octet + '[.]' + octet + "$")
+
+class IpValidator(object):
+    def __call__(self, value):
+        if not IP_RE.match(value) or len([x for x in value.split('.') if int(x) < 256]) != 4:
+            raise ValidationError("IP must be in dotted decimal format, 234.0.59.1")
+            
+class IpMaskValidator(object):
+    def __call__(self, value):
+        if not IP_RE.match(value):
+            raise ValidationError("IP must be in dotted decimal format, 255.255.128.0")
+        invalid_netmask = False
+        invalid_inverse_netmask = False
+        lookForZero = False
+        for x in value.split('.'):
+            xInt = int(x)
+            if lookForZero:
+                if xInt:
+                    invalid_netmask = True
+                    break  # go check inverse mask
+            if xInt != 255:
+                if xInt:
+                    if xInt >= 128:
+                        while (xInt % 2) == 0:
+                            xInt = xInt >> 1
+                        if xInt & (xInt + 1) == 0:
+                            continue
+                    invalid_netmask = True
+                    break # go check inverse mask
+                lookForZero = True
+
+        lookForOne = False
+        for x in value.split('.'):
+            xInt = int(x)
+            if lookForOne: # all bits should be 1 bits
+                if (xInt != 255):
+                    invalid_inverse_netmask = True
+                    break
+                else:
+                    continue
+            if xInt != 0:
+                # check for contiguous 1-bits from LSb
+                lookForOne = True
+                if xInt == 255:
+                    continue
+                if xInt & (xInt + 1) != 0:
+                    invalid_inverse_netmask = True
+                    break
+        if invalid_netmask and invalid_inverse_netmask: # no valid mask, then raise exception
+            raise ValidationError("Invalid IP Mask, should be like 255.255.128.0 or 0.0.127.255")
+
+
+class PortRangeSpecValidator(object):
+    def __init__(self):
+        self.RANGE_RE = re.compile(r'^([A-Za-z0-9-/\.:]*?)(\d+)\-(\d+)$')
+        self.SINGLE_RE = re.compile(r'^([A-Za-z0-9-/\.:]+)$')
+    def __call__(self, value):
+        values = value.split(',')
+        for v in values:
+            m = self.RANGE_RE.match(v);
+            if (m):
+                startport = int(m.group(2))
+                endport = int(m.group(3))
+                if (startport >= endport):
+                    raise ValidationError("Invalid range numerals: %d-%d" % (startport, endport))
+            elif not self.SINGLE_RE.match(v):
+                raise ValidationError("Must be a list of ports or ranges, such as 'A10-15,B25,C1-13'")
+
+class VLANRangeSpecValidator(object):
+    def __init__(self):
+        self.RANGE_RE = re.compile(r'^([\d]+)\-([\d]+)$')
+        self.SINGLE_RE = re.compile(r'^[\d]+$')
+
+    def __call__(self, value):
+        values = value.split(',')
+        for v in values:
+            m = self.RANGE_RE.match(v);
+            if v == 'untagged':
+                pass
+            elif (m):
+                if (int(m.group(1)) >= int(m.group(2))):
+                    raise ValidationError("Invalid range numerals in {}: {} must be less than {}".format(v, m.group(1), m.group(2)))
+                elif (int(m.group(1)) > 4095):
+                    raise ValidationError("Invalid VLAN: {} must be in range 0-4095".format(m.group(1)))
+                elif (int(m.group(2)) < 0):
+                    raise ValidationError("Invalid VLAN: {} must be in range 0-4095".format(m.group(2)))
+            elif not self.SINGLE_RE.match(v):
+                raise ValidationError("Must be a list of VLANs or ranges,"
+                                      " such as '5-20,45,4053,untagged'")
+            elif int(v) > 4095 or int(v) < 0:
+                raise ValidationError("Invalid VLAN: {} must be in range 0-4095".format(v))
+
+class TagSpecValidator(object):
+    def __init__(self):
+        self.TAG_NAME_RE = re.compile(r'^([a-zA-Z0-9_-]+(?:\.?[a-zA-Z0-9_-]+)*)=+([a-zA-Z0-9_-]+)$')
+
+    def __call__(self,value):
+        values = value.split(',')
+        for v in values:
+            v = v.strip()
+            if not self.TAG_NAME_RE.match(v):
+                raise ValidationError("Invalid tag: " + 
+                        '='.join(v.split('|')) +
+                        "\nFormat: tag namespace.namel value1, " +
+                        "where namespace may be empty or must be a-z, A-Z, 0-9, -, . or _, " + 
+                        "name and value must be a-z, A-Z, 0-9, - or _")
+
+class CidrValidator(object):
+    def __init__(self, mask_required=True):
+        self.mask_required = mask_required
+        self.CIDR_RE = re.compile(r'^(\d{1,3}\.){3}\d{1,3}/\d{1,2}?$')
+
+    def __call__(self, value):
+        ip_validator = IpValidator()
+        if self.CIDR_RE.match(value):
+            ip, mask = value.split('/')
+            ip_validator(ip)
+            if int(mask) < 1 or int(mask) > 32:
+                raise ValidationError("Mask should be between 1-32")
+        else:
+            if self.mask_required: # if mask was required and we didn't match above, problem!
+                raise ValidationError("Must be in dotted decimal format with a mask between 1-32")
+            else:
+                ip_validator(value)
+        
+class EnumerationValidator(object):
+    def __init__(self, enumerated_values, case_sensitive=False, message=None, code=None):
+        self.case_sensitive = case_sensitive
+        if case_sensitive:
+            self.enumerated_values = enumerated_values
+        else:
+            self.enumerated_values = [s.lower() for s in enumerated_values]
+        if not message:
+            message = 'Invalid enumerated value'
+        self.message = message
+        if not code:
+            code = 'invalid'
+        self.code = code
+
+    def __call__(self, value):
+        if not self.case_sensitive:
+            value = value.lower()
+        if value not in self.enumerated_values:
+            raise ValidationError(self.message, self.code)
+
+class ChoicesValidator(EnumerationValidator):
+    def __init__(self, choices, case_sensitive=False, message=None, code=None):
+        enumerated_values = [choice[0] for choice in choices]
+        EnumerationValidator.__init__(self, enumerated_values, case_sensitive, message, code)
+
+class RangeValidator(object):
+    def __init__(self, min, max):
+        self.min_value_validator = MinValueValidator(min)
+        self.max_value_validator = MaxValueValidator(max)
+
+    def __call__(self, value):
+        try:
+            self.min_value_validator(int(value))
+            self.max_value_validator(int(value))
+        except ValidationError:
+            raise ValidationError('Ensure this value is within the range (%d-%d)' %
+                                  (self.min_value_validator.limit_value,
+                                   self.max_value_validator.limit_value))
+
+class ControllerAliaVsalidator(object):
+    def __init__(self):
+        self.CONTROLLER_ALIAS_RE = re.compile(r'^[a-zA-Z0-9_-]+$')
+
+    def __call__(self, value):
+        if not self.CONTROLLER_ALIAS_RE.match(value):
+            raise ValidationError("controller alias name must ba a-z, 0-9, _ or _")
+
+class SwitchAliasValidator(object):
+    def __init__(self):
+        self.SWITCH_ALIAS_RE = re.compile(r'^[a-zA-Z0-9_-]+$')
+
+    def __call__(self, value):
+        if not self.SWITCH_ALIAS_RE.match(value):
+            raise ValidationError("switch alias name must ba a-z, 0-9, _ or _")
+
+class PortAliasValidator(object):
+    def __init__(self):
+        self.PORT_ALIAS_RE = re.compile(r'^[a-zA-Z0-9_-]+$')
+
+    def __call__(self, value):
+        if not self.PORT_ALIAS_RE.match(value):
+            raise ValidationError("port alias name must ba a-z, 0-9, _ or _")
+
+class HostAliasValidator(object):
+    def __init__(self):
+        self.HOST_ALIAS_RE = re.compile(r'^[a-zA-Z0-9_-]+$')
+
+    def __call__(self, value):
+        if not self.HOST_ALIAS_RE.match(value):
+            raise ValidationError("host alias name must ba a-z, 0-9, _ or _")
+
+class AddressSpaceNameValidator(object):
+    def __init__(self):
+        self.ADDRESS_SPACE_NAME_RE = re.compile(r'^[a-zA-Z0-9_-]+$')
+
+    def __call__(self,value):
+        if not self.ADDRESS_SPACE_NAME_RE.match(value):
+            raise ValidationError("address-space name must be a-z, A-Z, 0-9, - or _")
+
+class TenantNameValidator(object):
+    def __init__(self):
+        self.TENANT_NAME_RE = re.compile(r'^[a-zA-Z0-9_-]+$')
+
+    def __call__(self,value):
+        if not self.TENANT_NAME_RE.match(value):
+            raise ValidationError("tenant name must be a-z, A-Z, 0-9, - or _")
+
+class GeneralNameValidator(object):
+    def __init__(self):
+        self.GENERAL_NAME_RE = re.compile(r'^[a-zA-Z0-9_-]+$')
+
+    def __call__(self,value):
+        if not self.GENERAL_NAME_RE.match(value):
+            raise ValidationError("Name must be a-z, A-Z, 0-9, - or _")
+
+class VnsNameValidator(object):
+    def __init__(self):
+        self.VNS_NAME_RE = re.compile(r'^[a-zA-Z0-9_-]+$')
+
+    def __call__(self,value):
+        if not self.VNS_NAME_RE.match(value):
+            raise ValidationError("vns name must be a-z, A-Z, 0-9, - or _")
+
+class VnsInterfaceNameValidator(object):
+    def __init__(self):
+        self.VNS_INTERFACE_RE = re.compile(r'^[a-zA-Z0-9_-]+$')
+        self.PORT_RE = re.compile(r'^([A-Za-z0-9-]*?)(\d+)$')
+        self.MAC_RE = re.compile(r'^(([A-Fa-f\d]){2}:?){5}[A-Fa-f\d]{2}$')
+
+    def __call__(self,value):
+        if not self.VNS_INTERFACE_RE.match(value):
+            items = value.split('/')
+            if len(items) == 2:
+                if not self.PORT_RE.match(items[1]) and \
+                    not self.MAC_RE.match(items[1]):
+                        raise ValidationError("interface name after '/' must be either a port or a mac address")
+            else:
+                raise ValidationError("invalid syntax for interface name")
+
+class VnsAclNameValidator(object):
+    def __init__(self):
+        self.VNS_ACL_NAME_RE = re.compile(r'[a-zA-Z0-9_-]+$')
+
+    def __call__(self,value):
+        if not self.VNS_ACL_NAME_RE.match(value):
+            raise ValidationError("acl name must be a-z, A-Z, 0-9, - or _")
+
+class VnsAclEntryActionValidator(object):
+    def __call__(self,value):
+        if not "permit".startswith(value.lower()) and \
+           not "deny".startswith(value.lower()):
+            raise ValidationError("acl entry action must be 'permit' or 'deny'")
+
+class VnsInterfaceAclInOutValidator(object):
+    def __call__(self,value):
+        if not "in".startswith(value.lower()) and \
+           not "out".startswith(value.lower()):
+            raise ValidationError("acl entry action must be 'permit' or 'deny'")
+
+class VnsRuleNameValidator(object):
+    def __init__(self):
+        self.IF_NAME_RE = re.compile(r'^\d*$')
+
+    def __call__(self, value):
+        if not self.IF_NAME_RE.match(value):
+            print value
+            raise ValidationError("Invalid rule name, only digits allowed")
+
+class TagNameValidator(object):
+    def __init__(self):
+        self.TAG_NAME_RE = re.compile(r'^[a-zA-Z0-9_-]+(?:\.?[a-zA-Z0-9_-]+)*(?:\|[a-zA-Z0-9_-]+)+$')
+
+    def __call__(self,value):
+        if not self.TAG_NAME_RE.match(value):
+            raise ValidationError("Invalid tag: " + value + "\nFormat: tag namespace|namel|value1, " +
+                        "where namespace may be empty or must be a-z, A-Z, 0-9, -, . or _, " + 
+                        "name and value must be a-z, A-Z, 0-9, - or _")
+
+
+class VnsArpModeValidator(object):
+    def __init__(self):
+        self.ARP_MODES = ['flood-if-unknown', 'always-flood', 'drop-if-unknown']
+
+    def __call__(self,value):
+        if not value in self.ARP_MODES:
+            raise ValidationError("must be one of %s" % ', '.join(self.ARP_MODES))
+
+class VnsDhcpModeValidator(object):
+    def __init__(self):
+        self.DHCP_MODES = ['flood-if-unknown', 'always-flood', 'static']
+
+    def __call__(self,value):
+        if not value in self.DHCP_MODES:
+            raise ValidationError("must be one of %s" % ', '.join(self.DHCP_MODES))
+
+class VnsBroadcastModeValidator(object):
+    def __init__(self):
+        self.BROADCAST_MODES = ['drop', 'always-flood', 'forward-to-known']
+
+    def __call__(self,value):
+        if not value in self.BROADCAST_MODES:
+            raise ValidationError("must be one of %s" % ', '.join(self.BROADCAST_MODES))
+        
+class IntfNameValidator(object):
+    def __init__(self):
+        self.IntfName_RE = re.compile(r'^ethernet[0-9]')
+        
+    def __call__(self, value):
+        if not self.IntfName_RE.match(value):
+            raise ValidationError("Interface name must be Ethernet<num>")
+
+class DomainNameValidator(object):
+    def __init__(self):
+        self.DomainName_RE = re.compile(r'^([a-zA-Z0-9-]+.?)+$')
+    def __call__(self,value):
+        if not self.DomainName_RE.match(value):
+            raise ValidationError("Value must be a valid domain name")
+
+class IpOrDomainNameValidator(object):
+    def __init__(self):
+        self.DomainName_RE = re.compile(r'^([a-zA-Z0-9-]+.?)+$')
+        
+    def __call__(self,value):
+        if not IP_RE.match(value) and not self.DomainName_RE.match(value):
+            raise ValidationError("Value must be a valid IP address or domain name")
+        
+class TimeZoneValidator(object):
+    timezones = None
+    def __call__(self, value):
+        if not TimeZoneValidator.timezones:
+            import pytz
+            TimeZoneValidator.timezones = pytz.all_timezones
+        if value not in TimeZoneValidator.timezones:
+            raise ValidationError("Invalid time zone string")
+        
+class VCenterMgrIdsValidator(object):
+    def __init__(self):
+        self.VCenterMgrId_RE = re.compile(r'^[a-zA-Z]([a-zA-Z0-9_-])*')
+    def __call__(self,value):
+        if not self.VCenterMgrId_RE.match(value):
+            raise ValidationError("Value must be a valid name, starts with a alphabet and can have alphabets, numbers, _ and -")
+        
+class VCenterObjNamesValidator(object):
+    def __init__(self):
+        self.VCenterObjName_RE = re.compile(r'(?!.*\|.*)')
+    def __call__(self, value):
+        if not self.VCenterObjName_RE.match(value):
+            raise ValidationError("VCenter object names can contain any character except '|'")        
+    
+class FeatureValidator(object):
+    """Validates that a specific feature has been installed
+
+    We assume that a feature has been installed if the following file exists:
+        {sdncon.SDN_ROOT}/feature/<feature-name>
+
+    NOTE: The feature is enabled/disabled for use by updating the controller
+    object, this validator just ensures that before we enable a feature, it
+    has been actually installed.
+    """
+    def __init__(self, feature, featuredir=None):
+        self.feature = feature
+        if featuredir is None:
+            featuredir = \
+                    os.path.join(os.path.sep, 'opt', 'sdnplatform', 'feature')
+        self.featurefile = os.path.join(featuredir, feature)
+
+    def __call__(self, value):
+        if value and not os.path.isfile(self.featurefile):
+            raise ValidationError(
+                    'Feature not installed, please install "%s"' %
+                    self.feature)
+
+class VlanStringValidator(object):
+    def __call__(self, value):
+        self.vlan = 0
+        try:
+            self.vlan = int(value)
+            if (self.vlan < 1 or self.vlan > 4095):
+                raise ValidationError("VLAN must be in the range of 1 to 4095 with 4095 as untagged")
+        except ValueError:
+            raise ValidationError(
+             "VLAN must be in the range of 1 to 4095 with 4095 as untagged")
+
+
+class SafeForPrimaryKeyValidator(object):
+    """Validates that a string does not contain any character that would be 
+    illegal for use in a primary key. Currently this is the pipe | symbol
+    """
+
+    def __call__(self, value):
+        if "|" in value:
+            raise ValidationError(
+             "The pipe symbol '|' is not a valid character")
+        
+
+class IsRegexValidator(object):
+    """Validates that a given string is a valid regular expression
+    """
+
+    def __call__(self, value):
+        try:
+            value = str(value)
+            re.compile(value)
+        except re.error as e:
+            raise ValidationError(
+              "Input is not a valid regular expression: %s", e)
+
diff --git a/cli/sdncon/rest/views.py b/cli/sdncon/rest/views.py
new file mode 100755
index 0000000..6355c35
--- /dev/null
+++ b/cli/sdncon/rest/views.py
@@ -0,0 +1,2054 @@
+#
+# Copyright (c) 2013 Big Switch Networks, Inc.
+#
+# Licensed under the Eclipse Public License, Version 1.0 (the
+# "License"); you may not use this file except in compliance with the
+# License. You may obtain a copy of the License at
+#
+#      http://www.eclipse.org/legal/epl-v10.html
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# permissions and limitations under the License.
+#
+
+from django.db.models import AutoField, BooleanField, IntegerField, FloatField, ForeignKey
+from django.forms import ValidationError
+from django.http import HttpResponse
+from django.utils import simplejson
+from functools import wraps
+from sdncon.controller.models import Controller, ControllerAlias, ControllerDomainNameServer, ControllerInterface, FirewallRule, Switch, Port, PortAlias, Link
+from sdncon.rest.models import UserData
+from sdncon.controller.notification import begin_batch_notification, end_batch_notification
+from django.views.decorators.csrf import csrf_exempt
+import base64
+import urllib
+import urllib2
+import sys
+import json
+import os
+import re
+import time
+import traceback
+import collections
+import uuid
+import subprocess
+from datetime import datetime
+from sdncon.controller.oswrapper import exec_os_wrapper
+from sdncon.controller.config import get_local_controller_id
+from sdncon.rest.config import config_check_state
+from sdncon.controller.oswrapper import get_system_version_string
+import sdncon
+
+TEXT_PLAIN_CONTENT_TYPE = 'text/plain'
+TEXT_JAVASCRIPT_CONTENT_TYPE = 'text/javascript'
+JSON_CONTENT_TYPE = 'application/json'
+BINARY_DATA_CONTENT_TYPE = 'application/octet-stream'
+
+CONTROLLER_URL_PREFIX = 'http://localhost:8080/wm/'
+
+def controller_url(*elements):
+    return CONTROLLER_URL_PREFIX + '/'.join(elements)
+
+class RestException(Exception):
+    pass
+        
+class RestInvalidDataTypeException(RestException):
+    def __init__(self, name):
+        super(RestInvalidDataTypeException,self).__init__('Invalid data type: ' + name)
+
+class RestInvalidMethodException(RestException):
+    def __init__(self):
+        super(RestInvalidMethodException,self).__init__('Invalid HTTP method')
+
+class RestResourceNotFoundException(RestException):
+    def __init__(self, url):
+        super(RestResourceNotFoundException,self).__init__('Resource not found: ' + url)
+
+class RestDatabaseConnectionException(RestException):
+    def __init__(self):
+        super(RestDatabaseConnectionException,self).__init__('Error connecting to database')
+        
+class RestAuthenticationRequiredException(RestException):
+    def __init__(self):
+        super(RestAuthenticationRequiredException,self).__init__('Authentication required')
+
+class RestInvalidQueryParameterException(RestException):
+    def __init__(self, param_name):
+        super(RestInvalidQueryParameterException, self).__init__('Invalid query parameter: ' + str(param_name))
+
+class RestInvalidFilterParameterException(RestException):
+    def __init__(self, param_name):
+        super(RestInvalidFilterParameterException, self).__init__('Filter query parameters not allowed when URL contains resource iD: ' + str(param_name))
+
+class RestNoListResultException(RestException):
+    def __init__(self):
+        super(RestNoListResultException, self).__init__('The query result must be a single instance if the "nolist" query param is set')
+
+class RestInvalidPutDataException(RestException):
+    def __init__(self):
+        super(RestInvalidPutDataException, self).__init__('The request data for a PUT request must be a JSON dictionary object')
+
+class RestMissingRequiredQueryParamException(RestException):
+    def __init__(self, param_name):
+        super(RestMissingRequiredQueryParamException, self).__init__('Missing required query parameter: ' + str(param_name))
+
+class RestValidationException(RestException):
+    def __init__(self, model_error=None, field_errors=None):
+        # Build the exception message from model error and field errors
+        message = 'Validation error'
+        if model_error:
+            message = message + '; ' + model_error
+        if field_errors:
+            message += '; invalid fields: {'
+            first_time = True
+            for field_name, field_message in field_errors.items():
+                if not first_time:
+                    message += '; '
+                else:
+                    first_time = False
+                message = message + field_name + ': ' + field_message
+                
+            message += '}'
+            
+        super(RestValidationException, self).__init__(message)
+        self.model_error = model_error
+        self.field_errors = field_errors
+        
+class RestModelException(RestException):
+    def __init__(self, exc):
+        super(RestModelException, self).__init__('Error: ' + str(exc))
+
+class RestSaveException(RestException):
+    def __init__(self, exc):
+        super(RestSaveException, self).__init__('Error saving data: ' + str(exc))
+
+class RestInvalidOrderByException(RestException):
+    def __init__(self,field_name):
+        super(RestInvalidOrderByException, self).__init__('Invalid orderby field: ' + field_name)
+        
+class RestInternalException(RestException):
+    def __init__(self, exc):
+        super(RestInternalException,self).__init__('Unknown REST error: ' + unicode(exc))
+
+class RestUpgradeException(RestException):
+    def __init__(self, exc):
+        super(RestUpgradeException, self).__init__('Error: ' + str(exc))
+
+class RestProvisionException(RestException):
+    def __init__(self, exc):
+        super(RestProvisionException, self).__init__('Error: ' + str(exc))
+
+class RestDecommissionException(RestException):
+    def __init__(self, exc):
+        super(RestDecommissionException, self).__init__('Error: ' + str(exc))
+
+class RestInvalidLog(RestException):
+    def __init__(self, exc):
+        super(RestInvalidLog, self).__init__('Error: ' + str(exc))
+
+def handle_validation_error(model_info, validation_error):
+    model_error = None
+    field_errors = None
+    if hasattr(validation_error, 'message_dict'):
+        # The field errors we get in the ValidationError are a bit different
+        # then what we want for the RestValidationException. First, we
+        # need to convert the Django field name to the (possibly renamed)
+        # REST field name. Second, the per-field error message is possibly a
+        # list of messages, which we concatenate into a single string for the
+        # RestValidationException
+        for field_name, field_message in validation_error.message_dict.items():
+            if type(field_message) in (list, tuple):
+                converted_field_message = ''
+                for msg in field_message:
+                    converted_field_message = converted_field_message + msg + ' '
+            else:
+                converted_field_message += unicode(field_message)
+            if field_name == '__all__':
+                model_error = converted_field_message
+            else:
+                if not field_errors:
+                    field_errors = {}
+                field_info = model_info.field_name_dict.get(field_name)
+                if field_info:
+                    field_errors[field_info.rest_name] = converted_field_message
+                else:
+                    field_errors[field_name] = 'Private field invalid; ' + converted_field_message
+    elif hasattr(validation_error, 'messages'):
+        model_error = ':'.join(validation_error.messages)
+    else:
+        model_error = str(validation_error)
+    raise RestValidationException(model_error, field_errors)
+
+def get_successful_response(description=None, status_code=200):
+    content = get_successful_response_data(description)
+    return HttpResponse(content, JSON_CONTENT_TYPE, status_code)
+
+def get_successful_response_data(description = 'success'):
+    obj = {'description': description}
+    return simplejson.dumps(obj)
+ 
+def get_sdnplatform_response(url, timeout = None):
+    
+    try:
+        response_text = urllib2.urlopen(url, timeout=timeout).read()
+        return HttpResponse(response_text, JSON_CONTENT_TYPE)
+    except urllib2.HTTPError, e:
+        response_text = e.read()
+        response = simplejson.loads(response_text)
+        response['error_type'] = "SDNPlatformError"
+        return HttpResponse(content=simplejson.dumps(response), 
+                            status=e.code, 
+                            content_type=JSON_CONTENT_TYPE)
+
+def get_sdnplatform_query(request, path):
+    """
+    This returns controller-level storage table list
+    """
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+    url = controller_url(path) + '/?%s' % request.META['QUERY_STRING']
+    return get_sdnplatform_response(url)        
+
+def safe_rest_view(func):
+    """
+    This is a decorator that takes care of exception handling for the
+    REST views so that we return an appropriate error HttpResponse if
+    an exception is thrown from the view
+    """
+    @wraps(func)
+    def _func(*args, **kwargs):
+        try:
+            response = func(*args, **kwargs)
+        except Exception, exc:
+            end_batch_notification(True)
+            if not isinstance(exc, RestException):
+                # traceback.print_exc()
+                exc = RestInternalException(exc)
+            response_obj = {'error_type': exc.__class__.__name__, 'description': unicode(exc)}
+            if isinstance(exc, RestValidationException):
+                if exc.model_error:
+                    response_obj['model_error'] = exc.model_error
+                if exc.field_errors:
+                    response_obj['field_errors'] = exc.field_errors
+            content = simplejson.dumps(response_obj)
+            content_type = JSON_CONTENT_TYPE
+            
+            if isinstance(exc, RestInvalidMethodException):
+                status_code = 405
+            elif isinstance(exc, RestResourceNotFoundException):
+                status_code = 404
+            elif isinstance(exc, RestInternalException):
+                status_code = 500
+            else:
+                status_code = 400
+            response = HttpResponse(content, content_type, status_code)
+            if isinstance(exc, RestInvalidMethodException):
+                response['Allow'] = "GET, PUT, DELETE"
+        return response
+    return _func
+
+rest_model_info_dict = {}
+
+class RestFieldInfo(object):
+    def __init__(self, name, django_field_info, hidden=False,
+                 rest_name=None, json_serialize=False):
+        self.name = name
+        self.django_field_info = django_field_info
+        self.rest_name = rest_name
+        self.json_serialize = json_serialize
+        
+class RestModelInfo(object):
+    def __init__(self, rest_name, model_class):
+        self.rest_name = rest_name
+        self.model_class = model_class
+        self.primary_key = None
+        self.field_name_dict = {}
+        self.rest_name_dict = {}
+        
+        for field in model_class._meta.local_fields:
+            field_name = field.name
+            rest_name = field.name
+            if field.primary_key:
+                self.primary_key = field_name
+            # TODO: Are there other field types that should be included here?
+            json_serialize =  type(field) not in (AutoField, BooleanField, IntegerField, FloatField)
+            self.set_field_info(field_name, rest_name, field, json_serialize)
+
+    
+    # this is how a RestFieldInfo is created - pass in django_field_info
+    def get_field_info(self, field_name, django_field_info=None):
+        field_info = self.field_name_dict.get(field_name)
+        if not field_info and django_field_info:
+            field_info = RestFieldInfo(field_name, django_field_info)
+            self.field_name_dict[field_name] = field_info
+        return field_info
+    
+    def hide_field(self, field_name):
+        field_info = self.get_field_info(field_name)
+        del self.field_name_dict[field_name]
+        del self.rest_name_dict[field_info.rest_name]
+        
+    def set_field_info(self, field_name, rest_name, django_field_info, json_serialize=None):
+        field_info = self.get_field_info(field_name, django_field_info)
+        if field_info.rest_name in self.rest_name_dict:
+            del self.rest_name_dict[field_info.rest_name]
+        field_info.rest_name = rest_name
+        if json_serialize != None:
+            field_info.json_serialize = json_serialize
+        self.rest_name_dict[rest_name] = field_info
+
+def get_rest_model_info(name):
+    return rest_model_info_dict[name]
+
+def add_rest_model_info(info):
+    if rest_model_info_dict.get(info.rest_name):
+        raise RestException('REST model info already exists')
+    rest_model_info_dict[info.rest_name] = info
+    
+rest_initialized = False
+
+def get_default_rest_name(model):
+    # TODO: Ideally should do something a bit smarter here.
+    # Something like convert from camel-case class names to hyphenated names:
+    # For example:
+    #  MyTestClass => my-test-class
+    #  MyURLClass => my-url-class
+    #
+    # This isn't super-important for now, since you can set it explicitly
+    # with the nested Rest class.
+    return model.__name__.lower()
+
+def initialize_rest():
+    global rest_initialized
+    if rest_initialized:
+        return
+    
+    from django.db.models import get_models
+    for model in get_models():
+                
+        # If the model class has a nested class named 'Rest' then that means
+        # the model should be exposed in the REST API.
+        if hasattr(model, 'Rest'):
+            # By default the REST API uses the lower-case-converted name
+            # of the model class as the name in the REST URL, but this can
+            # be overridden by defining the 'NAME' attribute in the Rest class.
+            if hasattr(model.Rest, 'NAME'):
+                rest_name = model.Rest.NAME
+            else:
+                rest_name = get_default_rest_name(model)
+
+            if model._meta.proxy:
+                # This is a proxy class, drop through to the real one
+                base_model = model._meta.proxy_for_model
+            else:
+                base_model = model
+            
+            # OK, we have the basic REST info, so we can create the info class
+            rest_model_info = RestModelInfo(rest_name, base_model)
+            
+            # Check if there are any private or renamed fields
+            if hasattr(model.Rest, 'FIELD_INFO'):
+                for field_info in model.Rest.FIELD_INFO:
+                    field_name = field_info['name']
+                    rest_field_info = rest_model_info.get_field_info(field_name)
+                    # Check if field exists in models - don't allow field only here in FIELD_INFO)
+                    if not rest_field_info:
+                        # LOOK! This field only exists in FIELD_INFO - skip
+                        print "ERROR: %s for %s only in FIELD_INFO" % (field_name, rest_name)
+                        continue
+                    
+                    if field_info.get('private', False):
+                        rest_model_info.hide_field(field_name)
+                    else:
+                        rest_name = field_info.get('rest_name')
+                        if rest_name:
+                            rest_model_info.set_field_info(field_name, rest_name, rest_field_info.django_field_info)
+            
+            # Finished setting it up, so now add it to the list
+            add_rest_model_info(rest_model_info)
+    
+    rest_initialized = True
+
+initialize_rest()
+
+@safe_rest_view
+def do_model_list(request):
+    """
+    This returns the list of models available in the REST API.
+    """
+    
+    json_model_list = []
+    for model_name in rest_model_info_dict.keys():
+        json_model_info = {}
+        json_model_info["name"] = model_name
+        json_model_info["url_path"] = "rest/v1/model/" + model_name + "/"
+        json_model_list.append(json_model_info)
+
+    json_data = simplejson.dumps(json_model_list)
+    return HttpResponse(json_data, JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_realtimestats(request, stattype, dpid):
+    """
+    This returns realtime statistics (flows, ports, table, aggregate,
+    desc, ...)  for a dpid by calling the localhost sdnplatform
+    """
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+    url = controller_url('core', 'switch', dpid, stattype, 'json')
+    return get_sdnplatform_response(url)
+
+@safe_rest_view
+def do_sdnplatform_realtimestats(request, stattype, dpid=None, portid=None):
+    """
+    This returns realtime statistics from sdnplatform
+    """
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+    if dpid == None:
+        url = controller_url('core', 'counter', stattype, 'json')
+    elif portid == None:
+        url = controller_url('core', 'counter', dpid, stattype, 'json')
+    else:
+        url = controller_url('core', 'counter', dpid, portid, stattype, 'json')
+    return get_sdnplatform_response(url)
+
+@safe_rest_view
+def do_topology_tunnel_verify(request, srcdpid=None, dstdpid=None):
+    """
+    This initiates a liveness detection of tunnels.
+    """
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+
+    urlstring = srcdpid + '/' + dstdpid
+    url = controller_url('topology/tunnelverify', urlstring, 'json')
+
+    response_text = urllib2.urlopen(url).read()
+    time.sleep(4)
+    return do_topology_tunnel_status(request, srcdpid, dstdpid)
+
+@safe_rest_view
+def do_topology_tunnel_status(request, srcdpid='all', dstdpid='all'):
+    """
+    This returns the list of tunnels that have failed over the last observation interval.
+    """
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+
+    urlstring = srcdpid + '/' + dstdpid
+    url = controller_url('topology/tunnelstatus', urlstring, 'json')
+    response_text = urllib2.urlopen(url).read()
+    return HttpResponse(response_text, JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_sdnplatform_realtimestatus(request, category=None, subcategory=None, srcdpid=None, dstdpid = None):
+    """
+    This returns realtime status of sdnplatform
+    """
+
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+
+    response_text = None
+    url = None
+    if category == 'network':
+        if subcategory == 'cluster':
+            url = controller_url('topology', 'switchclusters', 'json')
+        if subcategory == 'externalports':
+            url = controller_url('topology', 'externalports', 'json')
+        if subcategory == 'tunnelverify':
+            urlstring = subcategory+ '/' + srcdpid + '/' + dstdpid
+            url = controller_url('topology', urlstring, 'json')
+        if subcategory == 'tunnelstatus':
+            url = controller_url('topology', 'tunnelstatus', 'json')
+
+    if url:
+        response_text = urllib2.urlopen(url).read()
+    return HttpResponse(response_text, JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_sdnplatform_realtimetest(http_request, category=None, subcategory=None):
+    """
+    This does a realtime test by sending an "explain packet" as packet in 
+    and collecting the operations performed on the packet
+    """
+
+    if http_request.method != 'PUT':
+        raise RestInvalidMethodException()
+
+    response_text = None
+    url = None
+    if category == 'network':
+        if subcategory == 'explain-packet':
+            # set up the sdnplatform URL for explain packet (at internal port 8080
+            url = controller_url('vns', 'explain-packet', 'json')
+            post_data = http_request.raw_post_data
+            request = urllib2.Request(url, post_data)
+            request.add_header('Content-Type', 'application/json')
+            response = urllib2.urlopen(request)
+            response_text = response.read()
+        elif subcategory == 'path':
+            post_data = json.loads(http_request.raw_post_data)
+            url = controller_url('topology', 'route',
+                                 post_data['src-switch'],
+                                 str(post_data['src-switch-port']),
+                                 post_data['dst-switch'],
+                                 str(post_data['dst-switch-port']),
+                                 'json')
+
+            return get_sdnplatform_response(url)
+
+    return HttpResponse(response_text, JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_sdnplatform_performance_monitor(http_request, category=None,
+                    subcategory=None, type='all'):
+    """
+    This API returns performance related information from the sdnplatform and
+    sdnplatform components
+    """
+
+    if http_request.method != 'GET':
+        raise RestInvalidMethodException()
+
+    response_text = None
+    url = None
+    if category == 'performance-monitor':
+        # set up the sdnplatform URL for explain packet (at internal port 8080
+        url = controller_url('performance', type, 'json')
+        request = urllib2.Request(url)
+        response = urllib2.urlopen(request)
+        response_text = response.read()
+    return HttpResponse(response_text, JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_sdnplatform_internal_debugs(http_request, category=None, subcategory=None,
+                              query='all', component='device-manager'):
+    """
+    This API returns debugging related information from the sdnplatform and
+    sdnplatform components
+    """
+
+    if http_request.method != 'GET':
+        raise RestInvalidMethodException()
+
+    response_text = None
+    url = None
+    if category == 'internal-debugs':
+        # set up the sdnplatform URL for explain packet (at internal port 8080
+        url = controller_url('vns', 'internal-debugs', component, query, 'json')
+        request = urllib2.Request(url)
+        response = urllib2.urlopen(request)
+        response_text = response.read()
+    return HttpResponse(response_text, JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_sdnplatform_event_history(http_request, category=None, subcategory=None,
+                              evHistName='all', count='100'):
+    """
+    This API returns real-time event-history information from the sdnplatform and
+    sdnplatform components
+    """
+
+    if http_request.method != 'GET':
+        raise RestInvalidMethodException()
+
+    response_text = None
+    url = None
+    if category == 'event-history':
+        # set up the sdnplatform URL for explain packet (at internal port 8080
+        url = controller_url('core', 'event-history', evHistName, count, 'json')
+        request  = urllib2.Request(url)
+        response = urllib2.urlopen(request)
+        response_text = response.read()
+    return HttpResponse(response_text, JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_flow_cache(http_request, category=None, subcategory=None,
+                          applName='None', applInstName='all', queryType='all'):
+    """
+    This API returns real-time event-history information from the sdnplatform and
+    sdnplatform components
+    """
+
+    if http_request.method != 'GET':
+        raise RestInvalidMethodException()
+
+    response_text = None
+    url = None
+    if category == 'flow-cache':
+        # set up the sdnplatform URL for explain packet (at internal port 8080
+        url = controller_url('vns', 'flow-cache', applName, applInstName, queryType, 'json')
+        request  = urllib2.Request(url)
+        response = urllib2.urlopen(request)
+        response_text = response.read()
+    return HttpResponse(response_text, JSON_CONTENT_TYPE)
+
+
+@safe_rest_view
+def do_vns_realtimestats_flow(http_request, category=None, vnsName="all"):
+    """
+    This gets realtime flows for one or more vnses
+    """
+
+    if http_request.method != 'GET':
+        raise RestInvalidMethodException()
+
+    # set up the sdnplatform URL for per-vns flow (at internal port 8080
+    url = controller_url('vns', 'flow', vnsName, 'json')
+    return get_sdnplatform_response(url)
+
+@safe_rest_view
+def do_sdnplatform_counter_categories(request, stattype, layer, dpid=None, portid=None):
+    """
+    This returns counter categories from sdnplatform
+    """
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+    if dpid == None:
+        url = controller_url('core', 'counter', 'categories', stattype, layer, 'json')
+    elif portid == None:
+        url = controller_url('core', 'counter', 'categories', dpid, stattype, layer, 'json')
+    else:
+        url = controller_url('core', 'counter', 'categories', dpid, portid, stattype, layer, 'json')
+
+    return get_sdnplatform_response(url)
+
+@safe_rest_view
+@csrf_exempt
+def do_packettrace(request):
+    """
+    This sets a packet trace in sdnplatform.
+    period: 
+        . >0 starts a trace session with the period
+        . =0 starts a trace session with no timeout
+        . <0 ends an ongoing session
+
+    The request method has to be POST since each request gets an unique sessionID
+    """
+    SESSIONID = 'sessionId'
+    sessionId = ""
+    filter = ""
+    if request.method != 'POST':
+        raise RestInvalidMethodException()
+
+    url = 'http://localhost:8080/wm/vns/packettrace/json'
+    request = urllib2.Request(url, request.raw_post_data, {'Content-Type':'application/json'})
+    try:
+        response = urllib2.urlopen(request)
+        response_text = response.read()
+    except Exception, e:
+        # SDNPlatform may not be running, but we don't want that to be a fatal
+        # error, so we just ignore the exception in that case.
+        pass
+
+    #response_data = json.loads(response_text) 
+    #if SESSIONID in response_data:
+    #    sessionId = response_data[SESSIONID]
+    #response_text = {SESSIONID:sessionId}
+
+    return HttpResponse(response_text, mimetype=JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_controller_stats(request, stattype):
+    """
+    This returns controller-level statistics/info from sdnplatform
+    """
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+    url = 'http://127.0.0.1:8080/wm/core/controller/%s/json' % stattype
+    return get_sdnplatform_response(url)
+
+@safe_rest_view
+def do_controller_storage_table_list(request):
+    """
+    This returns controller-level storage table list
+    """
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+    url = 'http://127.0.0.1:8080/wm/core/storage/tables/json'
+    return get_sdnplatform_response(url)
+
+@safe_rest_view
+def do_device(request):
+    return get_sdnplatform_query(request, "device")
+
+@safe_rest_view
+def do_switches(request):
+    url = controller_url("core", "controller", "switches", "json")
+    if request.META['QUERY_STRING']:
+        url += '?' + request.META['QUERY_STRING']
+    return get_sdnplatform_response(url)        
+
+@safe_rest_view
+def do_links(request):
+    url = controller_url("topology", "links", "json")
+    if request.META['QUERY_STRING']:
+        url += '?' + request.META['QUERY_STRING']
+    return get_sdnplatform_response(url)        
+
+@safe_rest_view
+def do_vns_device_interface(request):
+    return get_sdnplatform_query(request, "vns/device-interface")
+
+@safe_rest_view
+def do_vns_interface(request):
+    return get_sdnplatform_query(request, "vns/interface")
+
+@safe_rest_view
+def do_vns(request):
+    return get_sdnplatform_query(request, "vns")
+
+@safe_rest_view
+def do_system_version(request):
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+    version = get_system_version_string()
+    response_text = simplejson.dumps([{ 'controller' : version }])
+    return HttpResponse(response_text, JSON_CONTENT_TYPE)
+
+available_log_files = {
+    'syslog'           : '/var/log/syslog',
+    'sdnplatform'       : '/opt/sdnplatform/sdnplatform/log/sdnplatform.log',
+    'console-access'   : '/opt/sdnplatform/con/log/access.log',
+    'cassandra'        : '/opt/sdnplatform/db/log/system.log',
+    'authlog'          : '/var/log/auth.log',
+    'pre-start'        : '/tmp/pre-start',
+    'post-start'       : '/tmp/post-start',
+    # 'ftp'              : '/var/log/ftp.log',
+}
+
+available_log_commands = {
+    'dmesg'     : 'dmesg',
+    'process'   : 'ps lax'
+}
+
+@safe_rest_view
+def do_system_log_list(request):
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+    existing_logs = []
+    for (log_name, log_path) in available_log_files.items():
+        try:
+            log_file = open(log_path, 'r')
+            existing_logs.append({ 'log' : log_name })
+            log_file.close()
+        except Exception, e:
+            pass
+
+    print '??'
+    for log_name in available_log_commands.keys():
+        print 'ADD', log_name
+        existing_logs.append({ 'log' : log_name })
+    response_text = simplejson.dumps(existing_logs)
+    return HttpResponse(response_text, JSON_CONTENT_TYPE)
+
+
+def generate_subprocess_output(cmd):
+
+    process = subprocess.Popen(cmd, shell=True,
+                                   stdout=subprocess.PIPE,
+                                   stderr=subprocess.STDOUT,
+                                   bufsize=1)
+    while True:
+        line = process.stdout.readline()
+        if line != None and line != "":
+            yield line
+        else:
+            break
+
+
+@safe_rest_view
+def do_system_log(request, log_name):
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+    print 'do system log', log_name
+
+    # manage command ouput differently
+    if log_name in available_log_commands:
+        cmd = available_log_commands[log_name]
+        print 'DOING COMMAND', cmd
+
+        return HttpResponse(generate_subprocess_output(cmd),
+                            TEXT_PLAIN_CONTENT_TYPE)
+        return
+
+    log_path = available_log_files.get(log_name)
+    if log_name == None:
+        raise RestInvalidLog('No such log: %s' % log_name)
+    
+    try:
+        log_file = open(log_path, 'r')
+    except Exception,e:
+        raise RestInvalidLog('Log does not exist: %s' % log_name)
+        
+    # use a generator so that the complete log is not ever held in memory
+    def response(log_name, file):
+        for line in file:
+            yield line
+        file.close()
+
+    return HttpResponse(response(log_name, log_file), TEXT_PLAIN_CONTENT_TYPE)
+
+
+@safe_rest_view
+def do_system_uptime(request):
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+    url = controller_url('core', 'system', 'uptime', 'json')
+    return get_sdnplatform_response(url)        
+
+
+def _collect_system_interfaces(lo = False):
+    from netifaces import interfaces, ifaddresses, AF_INET, AF_LINK
+    result = []
+    for iface in interfaces():
+        if iface.startswith('lo') and not lo:
+            continue # ignore loopback
+        addrs = ifaddresses(iface)
+        if AF_INET in addrs:
+            for addr in ifaddresses(iface)[AF_INET]:
+                result.append({'name'      : iface,
+                               'addr'      : addr.get('addr', ''),
+                               'netmask'   : addr.get('netmask', ''),
+                               'broadcast' : addr.get('broadcast', ''),
+                               'peer'      : addr.get('peer', '')})
+    return result
+
+
+def do_system_inet4_interfaces(request):
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+    response_text = simplejson.dumps(_collect_system_interfaces(lo = True))
+    return HttpResponse(response_text, JSON_CONTENT_TYPE)
+
+
+@safe_rest_view
+def do_system_time_zone_strings(request, list_type):
+    import pytz
+    if list_type == 'common':
+        string_list = pytz.common_timezones
+    elif list_type == "all":
+        string_list = pytz.all_timezones
+    else:
+        raise RestResourceNotFoundException(request.path)
+    
+    response_text = simplejson.dumps(string_list)
+    
+    return HttpResponse(response_text, JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_check_config(request):
+    config_check_state()
+    return get_successful_response('checked config')
+
+@safe_rest_view
+def do_local_controller_id(request):
+    if request.method == 'GET':
+        controller_id = get_local_controller_id()
+        if not controller_id:
+            raise Exception("Unspecified local controller id")
+        response_text = simplejson.dumps({'id': controller_id})
+    elif request.method == 'PUT':
+        put_data = json.loads(request.raw_post_data)
+        controller_id = put_data.get('id')
+        _result = exec_os_wrapper("ControllerId", 'set', [controller_id])
+        response_text = get_successful_response_data('updated')
+    else:
+        raise RestInvalidMethodException()
+    
+    response = HttpResponse(response_text, JSON_CONTENT_TYPE)
+    
+    return response
+
+@safe_rest_view
+def do_ha_failback(request):
+    if request.method != 'PUT':
+        raise RestInvalidMethodException()
+    _result = exec_os_wrapper("HAFailback", 'set', [])
+    response_text = get_successful_response_data('forced failback')
+    response = HttpResponse(response_text, JSON_CONTENT_TYPE)
+    return response
+
+def delete_ha_firewall_rules(ip):
+    rules = FirewallRule.objects.filter(action='allow', src_ip=ip)
+    rules.filter(port=80).delete()
+    rules.filter(proto='tcp', port=7000).delete()
+    rules.filter(proto='vrrp').delete()
+
+def cascade_delete_controller_node(controller_id):
+    ControllerAlias.objects.filter(controller=controller_id).delete()
+    ControllerDomainNameServer.objects.filter(controller=controller_id).delete()
+    for iface in ControllerInterface.objects.filter(controller=controller_id):
+        FirewallRule.objects.filter(interface=iface.id).delete()
+    ControllerInterface.objects.filter(controller=controller_id).delete()
+    Controller.objects.filter(id=controller_id).delete()
+
+# FIXME: this assume a single-interface design and will look for the IP on eth0
+#        need to fix this when we have a proper multi-interface design
+def get_controller_node_ip(controller_id):
+    node_ip = ''
+    iface = ControllerInterface.objects.filter(controller=controller_id, type='Ethernet', number=0)
+    if iface:
+        node_ip = iface[0].discovered_ip
+    return node_ip
+
+# This method is "external" facing
+# It is designed to be called by CLI or other REST clients
+# This should only run on the master node, where decommissioning of a remote node is initiated
+@safe_rest_view
+def do_decommission(request):
+    if request.method != 'PUT':
+        raise RestInvalidMethodException()
+    data = simplejson.loads(request.raw_post_data)
+    node_id = data['id']
+
+    # Disallow self-decommissioning
+    local_id = get_local_controller_id()
+    if local_id == node_id:
+        raise RestDecommissionException("Decommissioning of the master node is not allowed. " + \
+                                        "Please perform a failover first.")
+
+    try :
+        controller = Controller.objects.get(id=node_id)
+    except Controller.DoesNotExist:
+        raise RestDecommissionException("No controller found")
+
+    node_ip = get_controller_node_ip(node_id)
+
+    # Case 1: controller node has IP
+    if node_ip:
+        result = exec_os_wrapper("Decommission", 'set', [node_ip])
+        output = result['out'].strip()
+        if result['out'].strip().endswith('is already decommissioned'):
+            delete_ha_firewall_rules(node_ip)
+            cascade_delete_controller_node(node_id)
+
+    # Case 2: controller node has NO IP
+    else:
+        output = '%s is already decommissioned' % node_id
+        cascade_delete_controller_node(node_id)
+
+    jsondict = {}
+    jsondict['status'] = "OK"
+    jsondict['description'] = output
+    return HttpResponse(simplejson.dumps(jsondict), JSON_CONTENT_TYPE)
+
+# This method is "internal" facing
+# It is designed to be called only by sys/remove-node.sh
+# This should only run on the node being decommissioned (slave)
+@safe_rest_view
+def do_decommission_internal(request):
+    if request.method != 'PUT':
+        raise RestInvalidMethodException()
+    data = simplejson.loads(request.raw_post_data)
+    node_ip = data['ip']
+    exec_os_wrapper("DecommissionLocal", 'set', [node_ip])
+
+    jsondict = {}
+    jsondict['status'] = "OK"
+    return HttpResponse(simplejson.dumps(jsondict), JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_ha_provision(request):
+    if request.method != 'PUT':
+        raise RestInvalidMethodException()
+    data = simplejson.loads(request.raw_post_data)
+    node_ip = data['ip']
+    
+    try :
+        ci = ControllerInterface.objects.get(ip=node_ip)
+        id = ci.controller.id
+        print 'got id', id
+        try:
+            a = ControllerAlias.objects.get(controller=id)
+            alias = a.alias
+        except:
+            alias = '(no controller alias)'
+
+        print 'alias:', alias
+        raise RestProvisionException('ip address already in controller %s %s' %
+                                     (id, alias))
+        
+    except ControllerInterface.DoesNotExist:
+        id = uuid.uuid4().urn[9:]
+        print "generated id = ", id
+        c = Controller(id=id)
+        try:
+            c.save()
+        except:
+            # describe failure
+            raise RestProvisionException('can\t save controller')
+            pass
+        print "save controller"
+        ci = ControllerInterface(controller=c,
+                                 ip=node_ip,
+                                 discovered_ip=node_ip)
+        try:
+            ci.save()
+        except:
+            # describe failure
+            raise RestProvisionException('can\t save controllar interfacer')
+
+    for c in Controller.objects.all():
+        if c.id != id:
+            #if there are multiple interfaces, assume the
+            # ethernet0 interface is for management purpose 
+            # XXX this could be better.
+            iface = ControllerInterface.objects.get(controller=c.id,
+                                                    type='Ethernet',
+                                                    number=0)
+            ip = iface.ip
+            fw = FirewallRule(interface=iface, action='allow',
+                              src_ip=node_ip, port=80, proto='tcp')
+            try:
+                fw.save()
+            except:
+                # describe failure
+                raise RestProvisionException('can\t save firewall rule from master')
+                
+            fw = FirewallRule(interface=ci, action='allow',
+                              src_ip=ip, port=80, proto='tcp')
+            try:
+                fw.save()
+            except:
+                raise RestProvisionException('can\t save firewall from slave')
+        
+
+    response_text = get_successful_response_data(id)
+    response = HttpResponse(response_text, JSON_CONTENT_TYPE)
+
+    return response
+
+
+def get_clustername():
+    name = os.popen("grep cluster_name /opt/sdnplatform/db/conf/cassandra.yaml | awk  '{print $2}'").readline()
+    # name may be '', perhaps this ought to None?
+    return name
+
+
+@safe_rest_view
+def do_clustername(request):
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+    response_text = simplejson.dumps([{ 'clustername' : get_clustername() }])
+    return HttpResponse(response_text, JSON_CONTENT_TYPE)
+    
+    
+@safe_rest_view
+def do_local_ha_role(request):
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+
+    url = controller_url('core', 'role', 'json')
+    try:
+        response_text = urllib2.urlopen(url, timeout=2).read()
+        response = json.loads(response_text)
+    except Exception:
+        response = HttpResponse('{"role":"UNAVAILABLE"}', JSON_CONTENT_TYPE)
+        return response
+    # now determine our controller id
+    controller_id = get_local_controller_id()
+
+    # find all the interfaces
+    ifs = ControllerInterface.objects.filter(controller=controller_id)
+
+    for intf in ifs:
+        firewall_id = '%s|%s|%s' % (controller_id, intf.type, intf.number)
+
+        rules = FirewallRule.objects.filter(interface=firewall_id)
+        for rule in rules:
+            if rule.action == 'reject' and rule.proto == 'tcp' and rule.port == 6633:
+                if response['role'] in {'MASTER', 'SLAVE'}:
+                    response['role']+='-BLOCKED'
+
+    response['clustername'] = get_clustername()
+        
+    return HttpResponse(simplejson.dumps(response), JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_system_clock(request, local=True):
+    local_or_utc_str = 'local' if local else 'utc'
+    if request.method == 'GET':
+        result = exec_os_wrapper("DateTime", 'get', [local_or_utc_str])
+    elif request.method == 'PUT':
+        time_info = simplejson.loads(request.raw_post_data)
+        dt = datetime(**time_info)
+        new_date_time_string = dt.strftime('%Y:%m:%d:%H:%M:%S')
+        result = exec_os_wrapper("DateTime", 'set', [local_or_utc_str, new_date_time_string])
+    else:
+        raise RestInvalidMethodException()
+
+    if len(result) == 0:
+        raise Exception('Error executing date command')
+    
+    # The DateTime OS wrapper only has a single command so the return
+    # date/time is the first line of the first element of the out array
+    values = result['out'].strip().split(':')
+    date_time_info = {
+        'year': int(values[0]),
+        'month': int(values[1]),
+        'day': int(values[2]),
+        'hour': int(values[3]),
+        'minute': int(values[4]),
+        'second': int(values[5]),
+        'tz': values[6]
+    }
+    response_text = simplejson.dumps(date_time_info)
+    response = HttpResponse(response_text, JSON_CONTENT_TYPE)
+
+    return response
+
+@safe_rest_view
+def do_instance(request, model_name,id=None):
+    """
+    This function handles both GET and PUT methods.
+    
+    For a GET request it returns a list of all of the instances of the
+    model corresponding to the specified type that match the specified
+    query parameters. If there are no query parameters then it returns
+    a list of all of the instances of the specified type. The names of
+    the query parameters can use the Django double underscore syntax
+    for doing more complicated tests than just equality
+    (e.g. mac__startswith=192.168).
+    
+    For a PUT request it can either update an existing instance or
+    insert one or more new instances. If there are any query parameters
+    then it assumes that its the update case and that the query
+    parameters identify exactly one instance. If that's not the case
+    then an error response is returned. For the update case any subset
+    of the fields can be updated with the PUT data. The format of the
+    PUT data is a JSON dictionary
+    """
+    
+    # FIXME: Hack to remap 'localhost' id for the controller-node model
+    # to the real ID for this controller
+    if model_name == 'controller-node' and id == 'localhost':
+        id = get_local_controller_id()
+        
+    # Lookup the model class associated with the specified name
+    model_info = rest_model_info_dict.get(model_name)
+    if not model_info:
+        raise RestInvalidDataTypeException(model_name)
+            
+    # Set up the keyword argument dictionary we use to filter the QuerySet.
+    filter_keyword_args = {}
+
+    jsonp_prefix = None
+    nolist = False
+    order_by = None
+    
+    # Now iterate over the query params and add further filter keyword arguments
+    query_param_dict = request.GET
+    for query_param_name, query_param_value in query_param_dict.items():
+        add_query_param = False
+        query_param_name = str(query_param_name)
+        query_param_value = str(query_param_value)
+        if query_param_name == 'callback': #switching to match up with jquery getJSON call naming convention.
+            jsonp_prefix = query_param_value
+        elif query_param_name == 'nolist':
+            if query_param_value not in ('False', 'false', '0', ''):
+                nolist = True
+        elif query_param_name == 'orderby':
+            order_by = query_param_value.split(',')
+            for i in range(len(order_by)):
+                name = order_by[i]
+                if name.startswith('-'):
+                    descending = True
+                    name = name[1:]
+                else:
+                    descending = False
+                field_info = model_info.rest_name_dict.get(name)
+                if not field_info:
+                    raise RestInvalidOrderByException(name)
+                name = field_info.name
+                if descending:
+                    name = '-' + name
+                order_by[i] = name
+        elif query_param_name in model_info.rest_name_dict:
+            field_info = model_info.rest_name_dict.get(query_param_name)
+            # For booleans, translate True/False strings into 0/1.
+            if field_info and type(field_info.django_field_info) == BooleanField:
+                if query_param_value.lower() == 'false':
+                    query_param_value = 0
+                elif query_param_value.lower() == 'true':
+                    query_param_value = 1
+            query_param_name = field_info.name
+            if model_name == 'controller-node' and \
+              query_param_name == 'id' and query_param_value == 'localhost':
+                query_param_value = get_local_controller_id()
+            if model_name in 'controller-interface' and \
+              query_param_name == 'controller' and \
+              query_param_value == 'localhost':
+                query_param_value = get_local_controller_id()
+            add_query_param = True
+        else:
+            double_underscore_start = query_param_name.find("__")
+            if double_underscore_start >= 0:
+                rest_name = query_param_name[:double_underscore_start]
+                field_info = model_info.rest_name_dict.get(rest_name)
+                if field_info:
+                    operation = query_param_name[double_underscore_start:]
+                    query_param_name = field_info.name
+                    if type(field_info.django_field_info) == ForeignKey:
+                        query_param_name = query_param_name + '__' + field_info.django_field_info.rel.field_name
+                    # Substitute in the model field name for the (possible renamed) rest name
+                    query_param_name += operation
+                    add_query_param = True
+        if add_query_param:
+            filter_keyword_args[query_param_name] = query_param_value
+
+    if id != None:
+        if len(filter_keyword_args) > 0:
+            raise RestInvalidFilterParameterException(filter_keyword_args.keys()[0])
+        try:
+            get_args = {model_info.primary_key:id}
+            instance = model_info.model_class.objects.get(**get_args)
+            instance_list = (instance,)
+            nolist = True
+        except model_info.model_class.DoesNotExist,e:
+            raise RestResourceNotFoundException(request.path)
+        except model_info.model_class.MultipleObjectsReturned, exc:
+            # traceback.print_exc()
+            raise RestInternalException(exc)
+    elif (request.method != 'PUT') or (len(filter_keyword_args) > 0):
+        # Get the QuerySet based on the keyword arguments we constructed
+        instance_list = model_info.model_class.objects.filter(**filter_keyword_args)
+        if order_by:
+            instance_list = instance_list.order_by(*order_by)
+    else:
+        # We're inserting new objects, so there's no need to do a query
+        instance_list = None
+        
+    response_content_type = JSON_CONTENT_TYPE
+    
+    if request.method == 'GET':
+        json_response_data = []
+        for instance in instance_list:
+            json_instance = {}
+            for field_info in model_info.field_name_dict.values():
+                # Made some minor edits to deal with a) fields that are empty and b) fields that are not strings -Kyle
+                # Changed this to only do an explicit string conversion if it's a unicode string.
+                # The controller is expecting to get the unstringified value (e.g. for boolean values)
+                # Not sure if this will break things in the UI, but we'll need to resolve how
+                # we want to handle this. Also, how do we want to handle unicode strings? -- robv
+                field_name = field_info.name
+                if type(field_info.django_field_info) == ForeignKey:
+                    field_name += '_id'
+                value = instance.__dict__.get(field_name)
+                if value != None:
+                    if field_info.json_serialize:
+                        value = str(value)
+                    json_instance[field_info.rest_name] = value
+            json_response_data.append(json_instance)
+        
+        # If the nolist query param was enabled then check to make sure
+        # that there was only a single instance in the response list and,
+        # if so, unpack it from the list
+        if nolist:
+            if len(json_response_data) != 1:
+                raise RestNoListResultException()
+            json_response_data = json_response_data[0]
+        
+        # Convert to json
+        response_data = simplejson.dumps(json_response_data)
+        
+        # If the jsonp query parameter was specified, wrap the data with
+        # the jsonp prefix
+        if jsonp_prefix:
+            response_data = jsonp_prefix + '(' + response_data + ')'
+            # We don't really know what the content type is here, but it's typically javascript
+            response_content_type = TEXT_JAVASCRIPT_CONTENT_TYPE
+    elif request.method == 'PUT':
+        response_data = get_successful_response_data('saved')
+        response_content_type = JSON_CONTENT_TYPE
+        
+        begin_batch_notification()
+        json_object = simplejson.loads(request.raw_post_data)
+        if instance_list is not None:
+
+            # don't allow the ip address of the first interface to
+            # be updated once it is set.  This really applies to
+            # the interface cassandra uses to sync the db.
+            if model_name == 'controller-interface':
+                for instance in instance_list:
+                    if instance.number == 0 and instance.ip != '':
+                        if 'ip' in json_object and json_object['ip'] != instance.ip:
+                            raise RestModelException("In this version, ip-address of primary interface can't be updated after initial configuration")
+
+            # In this case the URL includes query parameter(s) which we assume
+            # narrow the request to the instances of the model to be updated
+            # updated with the PUT data. So we're updating existing instances
+            
+            # If it's a list with one element then extract the single element
+            if (type(json_object) == list) and (len(json_object) == 1):
+                json_object = json_object[0]
+                
+            # We're expecting a dictionary where the keys match the model field names
+            # If the data isn't a dictionary then return an error
+            if type(json_object) != dict:
+                raise RestInvalidPutDataException() # TODO: Should return something different here
+            
+            # Set the fields in the model instance with the data from the dictionary
+            for instance in instance_list:
+                for rest_name, value in json_object.items():
+                    if not rest_name in model_info.rest_name_dict:
+                        raise RestModelException("Model '%s' has no field '%s'" % 
+                                                 (model_name, rest_name))
+                    field_info = model_info.rest_name_dict[rest_name]
+                    field_name = str(field_info.name)   # FIXME: Do we need the str cast?
+                    if type(field_info.django_field_info) == ForeignKey:
+                        field_name += '_id'
+                    # TODO: Does Django still not like unicode strings here?
+                    if type(value) == unicode:
+                        value = str(value)
+                    instance.__dict__[field_name] = value
+                # Save the updated model instance
+                try:
+                    instance.full_clean()
+                    instance.save()
+                except ValidationError, err:
+                    handle_validation_error(model_info, err)
+                    #raise RestValidationException(err)
+                except Exception, exc:
+                    raise RestSaveException(exc)
+        else:
+            # In this case no query parameters or id were specified so we're inserting new
+            # instances into the database. The PUT data can be either a list of new
+            # items to add (i.e. top level json object is a list) or else a single
+            # new element (i.e. top-level json object is a dict).
+            #print "creating object(s)"
+
+            # To simplify the logic below we turn the single object case into a list
+            if type(json_object) != list:
+                json_object = [json_object]
+            
+            # Create new model instances for all of the items in the list
+            for instance_data_dict in json_object:
+                # We expect the data to be a dictionary keyed by the field names
+                # in the model. If it's not a dict return an error
+                if type(instance_data_dict) != dict:
+                    raise RestInvalidPutDataException()
+                
+                converted_dict = {}
+                
+                # Now add the fields specified in the PUT data
+                for rest_name, value in instance_data_dict.items():
+
+                    #print "  processing " + str(name) + " " + str(value)
+
+                    if not rest_name in model_info.rest_name_dict:
+                        raise RestModelException("Model '%s' has no field '%s'" % 
+                                                 (model_name, rest_name))
+                    field_info = model_info.rest_name_dict[rest_name]
+                    # simplejson uses unicode strings when it loads the objects which
+                    # Django doesn't like that, so we convert these to ASCII strings
+                    if type(rest_name) == unicode:
+                        rest_name = str(rest_name)
+                    if type(value) == unicode:
+                        value = str(value)
+                    field_name = field_info.name
+                    # FIXME: Hack to remap localhost controller node id alias to the actual
+                    # ID for the controller node. We shouldn't be doing this here (this code
+                    # shouldn't have anything about specific models), but it's the easiest
+                    # way to handle it for now and this code is likely going away sometime
+                    # pretty soon (written in May, 2012, let's see how long "pretty soon"
+                    # is :-) )
+                    if model_name == 'controller-node' and field_name == 'id' and value == 'localhost':
+                        value = get_local_controller_id()
+                    if type(field_info.django_field_info) == ForeignKey:
+                        field_name += '_id'
+                    converted_dict[field_name] = value
+                
+                try:
+                    instance = model_info.model_class(**converted_dict)
+                    instance.full_clean()
+                    instance.save()
+                except ValidationError, err:
+                    handle_validation_error(model_info, err)
+                    #raise RestValidationException(err)
+                except Exception, e:
+                    # traceback.print_exc()
+                    raise RestSaveException(e)
+                
+        end_batch_notification()
+    elif request.method == 'DELETE':
+        begin_batch_notification()
+        for instance in instance_list:
+            try:
+                instance.delete()
+            except ValidationError, err:
+                handle_validation_error(model_info, err)
+            except Exception, e:
+                raise RestException(e)
+        end_batch_notification()
+        response_data = "deleted"
+        response_content_type = 'text/plain'
+    else:
+        raise RestInvalidMethodException()
+    
+    return HttpResponse(response_data, response_content_type)
+
+def synthetic_controller_interface(model_name, query_param_dict, json_response_data):
+    # ---
+    if model_name == 'controller-interface':
+        # For controller-interfaces, when an ip address (netmask too)
+        # is left unconfigured, then it may be possible to associate
+        # ifconfig details with the interface.
+        #
+        # Since controller-interfaces has no mechanism to associate 
+        # specific ifconfig interfaces with rows, it's only possible to
+        # associate ip's when a single unconfigured ip address exists,
+        # using a process of elimination.  For all ip address in the
+        # ifconfig output, all statically configured controller-interface
+        # items are removed.  If only one result is left, and only
+        # one controller-interface has an unconfigured ip address
+        # (either a dhcp acquired address, or a static address where
+        # the ip address is uncofigured), the ifconfig ip address
+        # is very-likely to be the one associated with the 
+        # controller-interface row.
+
+        # Check the list of values to see if any are configured as dhcp
+        dhcp_count = 0
+        unconfigured_static_ip = 0
+        this_host = get_local_controller_id()
+
+        for entry in json_response_data:
+            if 'mode' in entry and entry['mode'] == 'dhcp' and \
+               'controller' in entry and entry['controller'] == this_host:
+                    dhcp_count += 1
+            if 'mode' in entry and entry['mode'] == 'static' and \
+               'ip' in entry and entry['ip'] == '':
+                    unconfigured_static_ip += 1
+        if dhcp_count + unconfigured_static_ip != 1:
+            for entry in json_response_data:
+                entry['found-ip']        = entry['ip']
+            return
+
+        need_controller_query = False
+        # determine whether the complete list of interfaces needs
+        # to be collected to associate the dhcp address.
+        for query_param_name, query_param_value in query_param_dict.items():
+            if query_param_name != 'controller':
+                need_controller_query = True
+            if query_param_name == 'controller' and \
+               query_param_value != this_host:
+                need_controller_query = True
+
+        if need_controller_query == False:
+            model_interfaces = [x for x in json_response_data
+                                if 'controller' in x and x['controller'] == this_host]
+        else:
+            # print 'need to collect all interfaces'
+            filter_keyword_args = {'controller' : this_host}
+            model_info = rest_model_info_dict.get(model_name)
+            instance_list = model_info.model_class.objects.filter(**filter_keyword_args)
+            response_data = []
+            for instance in instance_list:
+                data = {}
+                for field_info in model_info.field_name_dict.values():
+                    field_name = field_info.name
+                    if type(field_info.django_field_info) == ForeignKey:
+                        field_name += '_id'
+                    value = instance.__dict__.get(field_name)
+                    if value != None:
+                        if field_info.json_serialize:
+                            value = str(value)
+                        data[field_info.rest_name] = value
+                response_data.append(data)
+            model_interfaces = response_data
+
+        # Recompute the number of dhcp configured interfaces,
+        # model_interfaces is the collection of interface for 'this_host'
+        dhcp_count = 0
+        unconfigured_static_ip = 0
+        for ifs in model_interfaces:
+            if 'mode' in ifs and ifs['mode'] == 'dhcp':
+                dhcp_count += 1
+            if 'mode' in ifs and ifs['mode'] == 'static' and \
+               'ip' in ifs and ifs['ip'] == '':
+                    unconfigured_static_ip += 1
+
+        if dhcp_count + unconfigured_static_ip != 1:
+            # print "Sorry, %s dhcp + %s unconfigured static interfaces on %s" % \
+                  # (dhcp_count, unconfigured_static_ip, this_host)
+            # copy over static ip's
+            for entry in json_response_data:
+                entry['found-ip']        = entry['ip']
+            return
+
+        # collect current details for all the network interfaces
+        inet4_ifs = _collect_system_interfaces()
+
+        # iterate over the model_interfaces's interfaces, and 
+        # remove ip addresses from inet4_ifs which are static, and
+        # have the correct static value.
+
+        report_static = False
+        match_id = ''
+
+        for ifs in model_interfaces:
+            if 'mode' in ifs and ifs['mode'] == 'static':
+                if 'ip' in ifs and ifs['ip'] == '':
+                    # print "Unconfigured static ip for %s", ifs['id']
+                    match_id = ifs['id']
+                if 'ip' in ifs and ifs['ip'] != '':
+                    # find this address in the known addresses
+                    remove_entry = -1
+                    for index, inet4_if in enumerate(inet4_ifs):
+                        if inet4_if['addr'] == ifs['ip']:
+                            remove_entry = index
+                            break
+                    if remove_entry == -1:
+                        # print "Static ip %s not found" % ifs['ip']
+                        pass 
+                    else:
+                        del inet4_ifs[remove_entry]
+            elif 'mode' in ifs and ifs['mode'] == 'dhcp':
+                match_id = ifs['id']
+            else:
+                # ought to assert here, not_reached()
+                pass
+
+        # When only one entry is left in inet, its possible to do the assocation
+        if len(inet4_ifs) != 1:
+            # print "Incorrect number %s of inet4 interfaces left" % len(inet4_ifs)
+            pass
+
+        for entry in json_response_data:
+            entry['found-ip']        = entry['ip']
+            entry['found-netmask']   = entry['netmask']
+
+            if entry['id'] == match_id:
+                # make sure the address isn't set
+                if entry['ip'] == '':
+                    entry['found-ip']        = inet4_ifs[0]['addr']
+                    entry['found-netmask']   = inet4_ifs[0]['netmask']
+                    entry['found-broadcast'] = inet4_ifs[0]['broadcast']
+                else:
+                    # ought to assert here, not_reached()
+                    pass
+
+@safe_rest_view
+def do_synthetic_instance(request, model_name, id=None):
+    
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+
+    # Lookup the model class associated with the specified name
+    model_info = rest_model_info_dict.get(model_name)
+    if not model_info:
+        raise RestInvalidDataTypeException(model_name)
+            
+    # Set up the keyword argument dictionary we use to filter the QuerySet.
+    filter_keyword_args = {}
+
+    jsonp_prefix = None
+    nolist = False
+    order_by = None
+    
+    # Now iterate over the query params and add further filter keyword arguments
+    query_param_dict = request.GET
+    for query_param_name, query_param_value in query_param_dict.items():
+        add_query_param = False
+        query_param_name = str(query_param_name)
+        query_param_value = str(query_param_value)
+        if query_param_name == 'callback': #switching to match up with jquery getJSON call naming convention.
+            jsonp_prefix = query_param_value
+        elif query_param_name == 'nolist':
+            if query_param_value not in ('False', 'false', '0', ''):
+                nolist = True
+        elif query_param_name == 'orderby':
+            order_by = query_param_value.split(',')
+            for i in range(len(order_by)):
+                name = order_by[i]
+                if name.startswith('-'):
+                    descending = True
+                    name = name[1:]
+                else:
+                    descending = False
+                field_info = model_info.rest_name_dict.get(name)
+                if not field_info:
+                    raise RestInvalidOrderByException(name)
+                name = field_info.name
+                if descending:
+                    name = '-' + name
+                order_by[i] = name
+        elif query_param_name in model_info.rest_name_dict:
+            field_info = model_info.rest_name_dict.get(query_param_name)
+            query_param_name = field_info.name
+            add_query_param = True
+        else:
+            double_underscore_start = query_param_name.find("__")
+            if double_underscore_start >= 0:
+                rest_name = query_param_name[:double_underscore_start]
+                field_info = model_info.rest_name_dict.get(rest_name)
+                if field_info:
+                    operation = query_param_name[double_underscore_start:]
+                    query_param_name = field_info.name
+                    if type(field_info.django_field_info) == ForeignKey:
+                        query_param_name = query_param_name + '__' + field_info.django_field_info.rel.field_name
+                    # Substitute in the model field name for the (possible renamed) rest name
+                    query_param_name += operation
+                    add_query_param = True
+        if add_query_param:
+            filter_keyword_args[query_param_name] = query_param_value
+
+    if id != None:
+        if len(filter_keyword_args) > 0:
+            raise RestInvalidFilterParameterException(filter_keyword_args.keys()[0])
+        try:
+            get_args = {model_info.primary_key:id}
+            instance = model_info.model_class.objects.get(**get_args)
+            instance_list = (instance,)
+            nolist = True
+        except model_info.model_class.DoesNotExist,e:
+            raise RestResourceNotFoundException(request.path)
+        except model_info.model_class.MultipleObjectsReturned, exc:
+            # traceback.print_exc()
+            raise RestInternalException(exc)
+    elif (request.method != 'PUT') or (len(filter_keyword_args) > 0):
+        # Get the QuerySet based on the keyword arguments we constructed
+        instance_list = model_info.model_class.objects.filter(**filter_keyword_args)
+        if order_by:
+            instance_list = instance_list.order_by(*order_by)
+    else:
+        # We're inserting new objects, so there's no need to do a query
+        instance_list = None
+        
+    response_content_type = JSON_CONTENT_TYPE
+    
+    # Syntheric types only do requests --
+    json_response_data = []
+    for instance in instance_list:
+        json_instance = {}
+        for field_info in model_info.field_name_dict.values():
+            # Made some minor edits to deal with a) fields that are empty and b) fields that are not strings -Kyle
+            # Changed this to only do an explicit string conversion if it's a unicode string.
+            # The controller is expecting to get the unstringified value (e.g. for boolean values)
+            # Not sure if this will break things in the UI, but we'll need to resolve how
+            # we want to handle this. Also, how do we want to handle unicode strings? -- robv
+            field_name = field_info.name
+            if type(field_info.django_field_info) == ForeignKey:
+                field_name += '_id'
+            value = instance.__dict__.get(field_name)
+            if value != None:
+                if field_info.json_serialize:
+                    value = str(value)
+                json_instance[field_info.rest_name] = value
+        json_response_data.append(json_instance)
+    
+    # ---
+    if model_name == 'controller-interface':
+        synthetic_controller_interface(model_name, query_param_dict, json_response_data)
+
+    # Convert to json
+    response_data = simplejson.dumps(json_response_data)
+    
+    # If the nolist query param was enabled then check to make sure
+    # that there was only a single instance in the response list and,
+    # if so, unpack it from the list
+    if nolist:
+        if len(json_response_data) != 1:
+            raise RestNoListResultException()
+        json_response_data = json_response_data[0]
+    
+    # If the jsonp query parameter was specified, wrap the data with
+    # the jsonp prefix
+    if jsonp_prefix:
+        response_data = jsonp_prefix + '(' + response_data + ')'
+        # We don't really know what the content type is here, but it's typically javascript
+        response_content_type = TEXT_JAVASCRIPT_CONTENT_TYPE
+
+    return HttpResponse(response_data, response_content_type)
+
+@safe_rest_view
+def do_user_data_list(request):
+    # Now iterate over the query params and add any valid filter keyword arguments
+    filter_keyword_args = {}
+    for query_param_name, query_param_value in request.GET.items():
+        query_param_name = str(query_param_name)
+        double_underscore_start = query_param_name.find("__")
+        if double_underscore_start >= 0:
+            attribute_name = query_param_name[:double_underscore_start]
+        else:
+            attribute_name = query_param_name
+            
+        # In the future, if we add support for things like mod_date, creation_date, etc.
+        # which would be supported in query params, then they'd be added to this list/tuple.
+        if attribute_name not in ('name',):
+            raise RestInvalidFilterParameterException(query_param_name)
+        filter_keyword_args[query_param_name] = query_param_value
+
+    instance_list = UserData.objects.filter(**filter_keyword_args)
+    
+    if request.method == 'GET':
+        user_data_info_list = []
+
+        # FIXME: robv: It's incorrect to *always* add this to the user data,
+        # because it means we're not respecting the filter query parameters.
+        # To work completely correctly we'd need to duplicate a lot of logic
+        # for processing the query parameters, which would be tedious.
+        # Should talk to Mandeep about why it was done this way. Maybe we
+        # should expose these special cases in a different URL/view.
+        for fn in ['startup-config', 'upgrade-config']:
+            try:
+                sc = "%s/run/%s" % (sdncon.SDN_ROOT, fn)
+                f = open(sc, 'r')
+                f.close()
+                t = time.strftime("%Y-%m-%d.%H:%M:%S",
+                                  time.localtime(os.path.getmtime(sc)))
+                instance_name = fn + '/timestamp=' + t + \
+                                '/version=1/length=' + \
+                                str(os.path.getsize(sc))
+                url_path = 'rest/v1/data/' + instance_name + '/'
+                            
+                user_data_info = { 'name' : instance_name,
+                                   'url_path' : url_path, }
+                user_data_info_list.append(user_data_info)
+            except:
+                pass
+
+        for instance in instance_list:
+            user_data_info = {'name': instance.name,
+                              'url_path': 'rest/v1/data/' + instance.name + '/'}
+            user_data_info_list.append(user_data_info)
+        
+        response_data = simplejson.dumps(user_data_info_list)
+    elif request.method == 'DELETE':
+        instance_list.delete()
+        response_data = {}
+        response_data['status'] = 'success'
+        response_data['message'] = 'user data deleted'
+        response_data = simplejson.dumps(response_data)
+        response_content_type = JSON_CONTENT_TYPE
+    else:
+        raise RestInvalidMethodException()
+        
+    return HttpResponse(response_data, JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_user_data(request, name):
+    query_param_dict = request.GET
+    #
+    # Manage startup-config/update-config differently
+    if name.find('/') >= 0 and \
+       name.split('/')[0] in ['startup-config', 'upgrade-config']:
+        path = "%s/run/%s" % (sdncon.SDN_ROOT, name.split('/')[0])
+        response_data = {}
+
+        if request.method == 'GET':
+            with open(path, 'r') as f:
+                response_data = f.read()
+            response_content_type = "text/plain"
+        elif request.method == 'PUT':
+            try:
+                with open(path, 'w') as f:
+                    f.write(request.raw_post_data)
+                response_data['status'] = 'success'
+                response_data['message'] = 'user data updated'
+            except:
+                response_data['status'] = 'failure'
+                response_data['message'] = "can't write file"
+            response_content_type = JSON_CONTENT_TYPE
+            response_data = simplejson.dumps(response_data)
+        elif request.method == 'DELETE':
+            try:
+                f = open(path, "r")
+                f.close()
+            except:
+                raise RestResourceNotFoundException(request.path)
+
+            try:
+                os.remove(path)
+                response_data['status'] = 'success'
+                response_data['message'] = 'user data deleted'
+            except:
+                response_data['status'] = 'failure'
+                response_data['message'] = "can't delete file"
+            response_data = simplejson.dumps(response_data)
+            response_content_type = JSON_CONTENT_TYPE
+        else:
+            raise RestInvalidMethodException()
+            
+        return HttpResponse(response_data, response_content_type)
+            
+    
+    # Default values for optional query parameters
+    #private = False
+    binary = False
+    
+    for param_name, param_value in query_param_dict.items():
+        if param_name == 'binary':
+            if request.method != 'PUT':
+                raise RestInvalidQueryParameterException(name)
+            binary = param_value.lower() == 'true' or param_value == '1'
+        #elif param_name == 'private':
+        #    private = param_value
+        else:
+            raise RestInvalidQueryParameterException(param_name)
+    
+    # FIXME: Need HTTP basic/digest auth support for the following
+    # code to work.
+    #if private:
+    #    user = request.user
+    #else:
+    #    user = None
+    #if user != None and not user.is_authenticated():
+    #    raise RestAuthenticationRequiredException()
+    user = None
+    
+    # There's currently an issue with filtering on the user when using the
+    # Cassandra database backend. Since we don't support private per-user
+    # data right now, I'm just disabling filtering on the user and only
+    # filter on the name
+    #user_data_query_set = UserData.objects.filter(user=user, name=name)
+    user_data_query_set = UserData.objects.filter(name=name)
+
+    count = user_data_query_set.count()
+    if count > 1:
+        raise RestInternalException('Duplicate user data values for the same name')
+    
+    if request.method == 'GET':
+        if count == 0:
+            raise RestResourceNotFoundException(request.path)
+        user_data = user_data_query_set[0]
+        response_data = user_data.data
+        if user_data.binary:
+            response_data = base64.b64decode(response_data)
+        response_content_type = user_data.content_type
+    elif request.method == 'PUT':
+        content_type = request.META['CONTENT_TYPE']
+        if content_type == None:
+            if binary:
+                content_type = BINARY_DATA_CONTENT_TYPE
+            else:
+                content_type = JSON_CONTENT_TYPE
+        response_data = {}
+        if count == 1:
+            response_data['status'] = 'success'
+            response_data['message'] = 'user data updated'
+            user_data = user_data_query_set[0]
+        else:
+            response_data['status'] = 'success'
+            response_data['message'] = 'user data created'
+            user_data = UserData(user=user,name=name)
+        user_data.binary = binary
+        user_data.content_type = content_type
+        data = request.raw_post_data
+        if binary:
+            data = base64.b64encode(data)
+        user_data.data = data
+        user_data.save()
+        response_data = simplejson.dumps(response_data)
+        response_content_type = JSON_CONTENT_TYPE
+    elif request.method == 'DELETE':
+        if count == 0:
+            raise RestResourceNotFoundException(request.path)
+        user_data = user_data_query_set[0]
+        user_data.delete()
+        response_data = {}
+        response_data['status'] = 'success'
+        response_data['message'] = 'user data deleted'
+        response_data = simplejson.dumps(response_data)
+        response_content_type = JSON_CONTENT_TYPE
+    else:
+        raise RestInvalidMethodException()
+   
+    return HttpResponse(response_data, response_content_type)
+
+@safe_rest_view
+def do_sdnplatform_tunnel_manager(request, dpid=None):
+    """
+    This returns realtime statistics from sdnplatform
+    """
+
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+    if dpid == None:
+        raise RestInvalidMethodException()
+
+    print 'DPID', dpid
+    if dpid == 'all':
+        url = controller_url('vns', 'tunnel-manager', 'all', 'json')
+    else:
+        url = controller_url('vns', 'tunnel-manager', 'switch='+dpid, 'json')
+
+    response_text = urllib2.urlopen(url).read()
+    entries = simplejson.loads(response_text)
+
+    if 'error' in entries and entries['error'] != None:
+        RestInternalException(entries['error'])
+
+    return HttpResponse(json.dumps(entries['tunnMap']), JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_sdnplatform_controller_summary(request):
+    """
+    This returns summary statistics from sdnplatform modules
+    """
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+
+    url = controller_url('core', 'controller', 'summary', 'json')
+    return get_sdnplatform_response(url)
+
+def filter_queries(choice_list, param_dict):
+    return dict([[x, param_dict[x]] for x in choice_list
+                if x in param_dict and param_dict[x] != 'all'])
+
+@safe_rest_view
+def do_reload(request):
+    """
+    This calls an oswrapper that reloads the box.
+    """
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+    exec_os_wrapper("ReloadController", 'set', [])
+    response_text = '{"status":"reloading"}'
+    return HttpResponse(response_text, JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_resetbsc(request):
+    """
+    This calls an oswrapper that resets the box.
+    """
+    if request.method != 'PUT':
+        raise RestInvalidMethodException()
+    exec_os_wrapper("ResetBsc", 'set', [])
+    response_text = '{"status":"resetting"}'
+    return HttpResponse(response_text, JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_abort_upgrade(request):
+    """
+    This calls an oswrapper that reloads the box.
+    """
+    if request.method != 'PUT':
+        raise RestInvalidMethodException()
+    controller_id = get_local_controller_id()
+    controller = Controller.objects.get(id=controller_id)
+    if controller.status != 'Upgrading':
+        raise RestUpgradeException("No Upgrade pending")
+    exec_os_wrapper("AbortUpgrade", 'set', [])
+    controller.status = 'Ready'
+    controller.save()
+    response_text = '{"status":"Ready"}'
+    return HttpResponse(response_text, JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_config_rollback(request):
+    data = simplejson.loads(request.raw_post_data)
+    path = data['path']
+    print "Executing config rollback with config @", path
+    
+    if request.method != 'PUT':
+        raise RestInvalidMethodException()
+    exec_os_wrapper("RollbackConfig", 'set', [path])
+    response_text = get_successful_response_data('prepared config rollbacked')
+    return HttpResponse(response_text, JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_upload_data(request):
+    if request.method != 'PUT':
+        raise RestInvalidMethodException()
+    data = simplejson.loads(request.raw_post_data)
+    content = data['data']
+    path = data['dst']
+    print "Executing config rollback with config @", path
+    
+    exec_os_wrapper("WriteDataToFile", 'set', [content, path])
+    response_text = get_successful_response_data('written data')
+    return HttpResponse(response_text, JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_diff_config(request):
+    if request.method != 'PUT':
+        raise RestInvalidMethodException()
+    data = simplejson.loads(request.raw_post_data)
+    config1 = data['config-1']
+    config2 = data['config-2']
+    print "diffing '%s' with '%s'" %(config1, config2)
+    
+    result = exec_os_wrapper("DiffConfig", 'set', [config1, config2])
+    return HttpResponse(simplejson.dumps(result), JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_extract_upgrade_pkg_manifest(request):
+    """
+    This calls an oswrapper that extracts the upgrade package.
+    This returns the install package 'manifest'.
+    """
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+    exec_os_wrapper("GetLatestUpgradePkg", 'get', [])
+    output = exec_os_wrapper("CatUpgradeImagesFile", 'get')
+    upgradePkg = output['out'].strip()
+    exec_os_wrapper("ExtractUpgradePkgManifest", 'set', [upgradePkg])
+    output = exec_os_wrapper("ExtractUpgradePkgManifest", 'get')
+    manifest = output['out']
+    return HttpResponse(manifest, JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_extract_upgrade_pkg(request):
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+    exec_os_wrapper("GetLatestUpgradePkg", 'get', [])
+    output = exec_os_wrapper("CatUpgradeImagesFile", 'get')
+    upgradePkg = output['out'].strip()
+    exec_os_wrapper("ExtractUpgradePkg", 'set', [upgradePkg])
+    return HttpResponse('{"status": "OK"}', JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_get_upgrade_pkg(request):
+    """
+    This calls an oswrapper to get the latest upgrade
+    package uploaded to the controller.
+    """
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+    exec_os_wrapper("GetLatestUpgradePkg", 'get')
+    result = exec_os_wrapper("CatUpgradeImagesFile", 'get')
+    jsondict = {'file': result['out'].strip()}
+    return HttpResponse(simplejson.dumps(jsondict), JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_cleanup_old_pkgs(request):
+    if request.method != 'GET':
+        raise RestInvalidMethodException()
+    exec_os_wrapper("CleanupOldUpgradeImages", 'get')
+    return HttpResponse('{"status": "OK"}', JSON_CONTENT_TYPE)
+
+@safe_rest_view
+def do_execute_upgrade_step(request):
+    """
+    Executes a particular upgrade step according to the
+    upgrade package manifest.
+    """
+    if request.method != 'PUT':
+        raise RestInvalidMethodException()
+
+    put_data = json.loads(request.raw_post_data)
+    imageName = put_data.get('imageName')
+    stepNum = put_data.get('step')
+    force = put_data.get('force')
+    
+    args = [stepNum, imageName]
+    if force:
+        args.append("--force")
+    result = exec_os_wrapper("ExecuteUpgradeStep", 'get', 
+                             args)
+    jsondict = {}
+    if len(str(result['err']).strip()) > 0:
+        jsondict['status'] = "ERROR"
+        jsondict['description'] = str(result['err']).strip()
+    else:
+        jsondict['status'] = "OK"
+        jsondict['description'] = str(result['out']).strip()
+
+    return HttpResponse(simplejson.dumps(jsondict), JSON_CONTENT_TYPE)
+