Adding ONOS Segment Routing CLI files to new repo
diff --git a/cli/storeclient.py b/cli/storeclient.py
new file mode 100755
index 0000000..e7fb50c
--- /dev/null
+++ b/cli/storeclient.py
@@ -0,0 +1,391 @@
+#
+# Copyright (c) 2010,2011,2012,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.
+#
+
+#
+# module: storeclient.py
+#
+# This module manages communication with the console, i.e. the REST interface
+# of a Big Switch Controller node.
+
+import urllib
+import urllib2
+import ftplib
+import json
+import datetime
+import time
+import traceback
+import url_cache
+
+
+class StringReader():
+ # used for ftp, as a replacement for read from an existing file
+ def __init__(self, value):
+ """
+ Value can be a string, or a generator.
+ """
+ self.value = value
+ self.offset = 0
+ if type(value) == str or type(value) == unicode:
+ self.len = len(value)
+ else:
+ self.last = None
+
+ def read(self, size = None):
+ if size:
+ if size > self.len - self.offset:
+ size = self.len - self.offset
+ result = self.value[self.offset:size]
+ self.offset += size
+ return result
+ # supporing generators.
+ if self.last: # use remainder
+ if size > self.len - self.offset:
+ size = self.len - self.offset
+ result = self.last[self.offset:size]
+ self.offset += size
+ if self.offset == self.len:
+ self.last = None
+ return result
+ item = value.next()
+ len_item = len(item)
+ if len_item <= size:
+ return item
+ # set up remainder
+ result = item[:size]
+ self.last = item[size:]
+ self.offset = 0
+ self.len = len(self.last)
+ return result
+
+
+class StoreClient():
+
+ controller = None
+ display_rest = False
+ display_rest_reply = False
+
+ table_read_url = "http://%s/rest/v1/model/%s/"
+ entry_post_url = "http://%s/rest/v1/model/%s/"
+ user_data_url = "http://%s/rest/v1/data/"
+
+ def set_controller(self,controller):
+ self.controller = controller
+
+ def display_mode(self, mode):
+ self.display_rest = mode
+
+ def display_reply_mode(self, mode):
+ self.display_rest_reply = mode
+
+ def rest_simple_request(self,url, use_cache = None, timeout = None):
+ # include a trivial retry mechanism ... other specific
+ # urllib2 exception types may need to be included
+ retry_count = 0
+ if use_cache == None or use_cache:
+ result = url_cache.get_cached_url(url)
+ if result != None:
+ return result
+ while retry_count > 0:
+ try:
+ return urllib2.urlopen(url, timeout = timeout).read()
+ except urllib2.URLError:
+ retry_count -= 1
+ time.sleep(1)
+ # try again without the try...
+ if self.display_rest:
+ print "REST-SIMPLE:", 'GET', url
+ result = urllib2.urlopen(url, timeout = timeout).read()
+ if self.display_rest_reply:
+ print 'REST-SIMPLE: %s reply "%s"' % (url, result)
+ url_cache.save_url(url, result)
+ return result
+
+
+ def rest_json_request(self, url):
+ entries = url_cache.get_cached_url(url)
+ if entries != None:
+ return entries
+
+ result = self.rest_simple_request(url)
+ # XXX check result
+ entries = json.loads(result)
+ url_cache.save_url(url, entries)
+
+ return entries
+
+ def rest_post_request(self, url, obj, verb='PUT'):
+ post_data = json.dumps(obj)
+ if self.display_rest:
+ print "REST-POST:", verb, url, post_data
+ request = urllib2.Request(url, post_data, {'Content-Type':'application/json'})
+ request.get_method = lambda: verb
+ response = urllib2.urlopen(request)
+ result = response.read()
+ if self.display_rest_reply:
+ print 'REST-POST: %s reply: "%s"' % (url, result)
+ return result
+
+
+ def get_table_from_store(self, table_name, key=None, val=None, match=None):
+ if not self.controller:
+ print "No controller specified. Set using 'controller <server:port>'."
+ return
+ url = self.table_read_url % (self.controller, table_name)
+ if not match:
+ match = "startswith"
+ if key and val:
+ url = "%s?%s__%s=%s" % (url, key, match, urllib.quote_plus(val))
+ result = url_cache.get_cached_url(url)
+ if result != None:
+ return result
+ data = self.rest_simple_request(url)
+ entries = json.loads(data)
+ url_cache.save_url(url, entries)
+ return entries
+
+
+ def get_object_from_store(self, table_name, pk_value):
+ if not self.controller:
+ print "No controller specified. Set using 'controller <server:port>'."
+ return
+ url = self.table_read_url % (self.controller, table_name)
+ url += (pk_value + '/')
+ result = url_cache.get_cached_url(url)
+ if result != None:
+ return result
+ if self.display_rest:
+ print "REST-MODEL:", url
+ response = urllib2.urlopen(url)
+ if response.code != 200:
+ # LOOK! Should probably raise exception here instead.
+ # In general we need to rethink the store interface and how
+ # we should use exceptions.
+ return None
+ data = response.read()
+ result = json.loads(data)
+ if self.display_rest_reply:
+ print 'REST-MODEL: %s reply: "%s"' % (url, result)
+ url_cache.save_url(url, result)
+ return result
+
+
+ # obj_data must contain a key/val and any other required data
+ def rest_create_object(self, obj_type, obj_data):
+ if not self.controller:
+ print "No controller specified. Set using 'controller <server:port>'."
+ return
+ url_cache.clear_cached_urls()
+ url = self.entry_post_url % (self.controller, obj_type)
+ data = self.rest_post_request(url, obj_data)
+ # LOOK! successful stuff should be returned in json too.
+ if data != "saved":
+ result = json.loads(data)
+ return result
+ url_cache.clear_cached_urls()
+
+ def find_object_from_store(self, obj_type, key, val):
+ if not self.controller:
+ print "No controller specified. Set using 'controller <server:port>'."
+ return
+ url = self.table_read_url % (self.controller, obj_type)
+ result = url_cache.get_cached_url(url)
+ if result != None:
+ return result
+ data = self.rest_simple_request("%s?%s__exact=%s" % (url, key, urllib.quote_plus(val)))
+ entries = json.loads(data)
+ url_cache.save_url(url, entries)
+ return entries
+
+ def rest_query_objects(self, obj_type, query_params=None):
+ if not self.controller:
+ print "No controller specified. Set using 'controller <server:port>'."
+ return
+ url = self.table_read_url % (self.controller, obj_type)
+ if query_params:
+ url += '?'
+ # Convert any data:None fields to <id>__isnull=True
+ non_null_query_params = dict([[n,v] if v != None else [n + '__isnull', True]
+ for (n,v) in query_params.items()])
+ url += urllib.urlencode(non_null_query_params)
+ result = url_cache.get_cached_url(url)
+ if result != None:
+ return result
+ data = self.rest_simple_request(url)
+ entries = json.loads(data)
+ url_cache.save_url(url, entries)
+ return entries
+
+ #
+ # either must contain a key/val and any other required data
+ # of the key must be a dictionary identifying the item to delete.
+ def rest_delete_object(self, obj_type, key, val = None):
+ dict_ = {}
+ url = self.entry_post_url % (self.controller, obj_type)
+ if val == None:
+ if not type(key) == type(dict_):
+ return None
+ dict_ = key
+ else:
+ url += "?%s__exact=%s" % (key, urllib.quote_plus(val))
+
+ # LOOK! I'm not sure this works the way it seems to me it's
+ # designed to work. I think the intent is that you can specify
+ # query parameters in the key argument which controls which
+ # instance(s) should be deleted. But when I try it it seems to
+ # always delete all instances, so it seems like the parameters
+ # don't filter properly when passed via the POST data as opposed
+ # to being specified as query parameters in the URL. The latter
+ # way does work -- see rest_delete_objects that follows this.
+ data = self.rest_post_request(url, dict_, 'DELETE')
+ # LOOK! successful stuff should be returned in json too.
+ if data != "deleted":
+ dict_ = json.loads(data)
+ return dict_
+ url_cache.clear_cached_urls()
+
+ def rest_delete_objects(self, obj_type, query_params):
+ url = self.entry_post_url % (self.controller, obj_type)
+ if query_params:
+ url += '?'
+ # Convert any data:None fields to <id>__isnull=True
+ non_null_query_params = dict([[n,v] if v != None else [n + '__isnull', True]
+ for (n,v) in query_params.items()])
+ url += urllib.urlencode(non_null_query_params)
+
+ data = self.rest_post_request(url, {}, 'DELETE')
+ # LOOK! successful stuff should be returned in json too.
+ if data != "deleted":
+ result = json.loads(data)
+ return result
+ url_cache.clear_cached_urls()
+
+ def rest_update_object(self, obj_type, obj_key_name, obj_key_val, obj_data):
+ if not self.controller:
+ print "No controller specified. Set using 'controller <server:port>'."
+ return
+ url = self.entry_post_url % (self.controller, obj_type)
+ url += "?%s=%s" % (obj_key_name, urllib.quote_plus(obj_key_val)) # add a query string
+ data = self.rest_post_request(url, obj_data)
+ # LOOK! successful stuff should be returned in json too.
+ result = json.loads(data)
+ if result.get('description', '') != "saved":
+ return result
+ url_cache.clear_cached_urls()
+
+ def set_user_data_file(self, name, text):
+ url = self.user_data_url % (self.controller)
+ version = 1 # default
+ # find the latest version for a name
+ existing_data = self.get_user_data_table(name, "latest")
+ if len(existing_data) > 0: # should be at most 1, but just in case...
+ version = max([int(f['version']) for f in existing_data]) + 1 # LOOK! race?
+ length = len(text)
+ # LOOK! what to do about time in a distributed system!
+ timestamp = datetime.datetime.utcnow().strftime("%Y-%m-%d.%H:%M:%S")
+ url += "%s/timestamp=%s/version=%s/length=%s/" % (name, timestamp, version, length)
+ return self.copy_text_to_url(url, text)
+
+ def get_user_data_file(self, name):
+ url = self.user_data_url % (self.controller)
+ url += name + "/"
+ return self.rest_simple_request(url)
+
+ def delete_user_data_file(self, name):
+ url = self.user_data_url % (self.controller)
+ url += name + "/"
+ data = self.rest_post_request(url, {}, 'DELETE')
+ if data != "deleted":
+ result = json.loads(data)
+ return result
+
+ def get_user_data_table(self, name=None, show_version="latest"):
+ if not self.controller:
+ print "No controller specified. Set using 'controller <server:port>'."
+ return None
+ url = self.user_data_url % self.controller
+ if name:
+ url += "?name__startswith=%s" % name
+ data = self.rest_simple_request(url)
+ new_data = []
+ data = json.loads(data)
+ latest_versions = {} # dict of latest version per name
+ for d in data: # list of dicts
+ l = d['name'].split('/') # ex: startup/timestamp=2010-11-03.05:51:27/version=1/length=2038
+ nd = dict([item.split('=') for item in l[1:]])
+ nd['name'] = l[0]
+ nd['full_name'] = d['name']
+ new_data.append(nd)
+ if not nd['name'] in latest_versions or int(nd['version']) > int(latest_versions[nd['name']]):
+ latest_versions[nd['name']] = nd['version'] # initialize first time
+
+ # prune if needed to a name or a particular version
+ if name:
+ new_data = [ nd for nd in new_data if nd['name'].startswith(name) ]
+ if show_version == "latest":
+ new_data = [ nd for nd in new_data if not int(nd['version']) < int(latest_versions[nd['name']]) ]
+ elif show_version != "all":
+ new_data = [ nd for nd in new_data if nd['version'] == show_version ]
+ return new_data
+
+
+ # LOOK! looks a lot like a rest_post_request except we don't jsonify and we handle
+ # errors differently... refactor? Same with get_text and rest_simple_request
+ def copy_text_to_url(self, url, src_text, message = None):
+ post_data = src_text
+ if url.startswith('ftp://'):
+ url_suffix = url[6:]
+ user = 'anonymous'
+ password = ''
+ if url_suffix.find('@') != -1:
+ url_parts = url_suffix.split('@')
+ url_user_and_password = url_parts[0]
+ url_suffix = url_parts[1]
+ if url_user_and_password.find(':') != -1:
+ user_and_password = url_user_and_password.split(':')
+ user = user_and_password[0]
+ password = user_and_password[1]
+ else:
+ user = url_user_and_password
+
+ host = url_suffix
+ path = None
+ if url_suffix.find('/'):
+ url_suffix_parts = url_suffix.split('/')
+ host = url_suffix_parts[0]
+ path = url_suffix_parts[1]
+ ftp_target = ftplib.FTP(host, user, password)
+
+ ftp_target.storbinary('STOR %s' % path, StringReader(post_data))
+ # apparently, storbinary doesn't provide a return value
+ result = { "result" : "success" } # don't display any other error messages
+ else:
+ request = urllib2.Request(url, post_data, {'Content-Type':'text/plain'})
+ request.get_method = lambda: 'PUT'
+ if self.display_rest:
+ print "REST-TEXT-TO:", request
+ response = urllib2.urlopen(request)
+ result = response.read()
+ if self.display_rest_reply:
+ print 'REST-TEXT-TO: %s reply "%s"' % (request, result)
+ return result
+
+ def get_text_from_url(self, url):
+ if self.display_rest:
+ print "REST-TEXT-FROM:", url
+ result = urllib2.urlopen(url).read()
+ if self.display_rest_reply:
+ print 'REST-TEXT-FROM: %s result:"%s"' % (url, result)
+ return result