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)
+