blob: fb563725c1b39ca8d8140591b0d2508590f47dea [file] [log] [blame]
Srikanth Vavilapalli1725e492014-12-01 17:50:52 -08001#
2# Copyright (c) 2010,2011,2012,2013 Big Switch Networks, Inc.
3#
4# Licensed under the Eclipse Public License, Version 1.0 (the
5# "License"); you may not use this file except in compliance with the
6# License. You may obtain a copy of the License at
7#
8# http://www.eclipse.org/legal/epl-v10.html
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13# implied. See the License for the specific language governing
14# permissions and limitations under the License.
15#
16
17#
18# module: storeclient.py
19#
20# This module manages communication with the console, i.e. the REST interface
21# of a Big Switch Controller node.
22
23import urllib
24import urllib2
25import ftplib
26import json
27import datetime
28import time
29import traceback
30import url_cache
31
32
33class StringReader():
34 # used for ftp, as a replacement for read from an existing file
35 def __init__(self, value):
36 """
37 Value can be a string, or a generator.
38 """
39 self.value = value
40 self.offset = 0
41 if type(value) == str or type(value) == unicode:
42 self.len = len(value)
43 else:
44 self.last = None
45
46 def read(self, size = None):
47 if size:
48 if size > self.len - self.offset:
49 size = self.len - self.offset
50 result = self.value[self.offset:size]
51 self.offset += size
52 return result
53 # supporing generators.
54 if self.last: # use remainder
55 if size > self.len - self.offset:
56 size = self.len - self.offset
57 result = self.last[self.offset:size]
58 self.offset += size
59 if self.offset == self.len:
60 self.last = None
61 return result
62 item = value.next()
63 len_item = len(item)
64 if len_item <= size:
65 return item
66 # set up remainder
67 result = item[:size]
68 self.last = item[size:]
69 self.offset = 0
70 self.len = len(self.last)
71 return result
72
73
74class StoreClient():
75
76 controller = None
77 display_rest = False
78 display_rest_reply = False
79
80 table_read_url = "http://%s/rest/v1/model/%s/"
81 entry_post_url = "http://%s/rest/v1/model/%s/"
82 user_data_url = "http://%s/rest/v1/data/"
Srikanth Vavilapalli193322d2014-12-02 11:28:24 -080083 sdn_platform_data_url = "http://%s/rest/v1/system/"
Srikanth Vavilapalli1725e492014-12-01 17:50:52 -080084
85 def set_controller(self,controller):
86 self.controller = controller
87
88 def display_mode(self, mode):
89 self.display_rest = mode
90
91 def display_reply_mode(self, mode):
92 self.display_rest_reply = mode
Srikanth Vavilapalli193322d2014-12-02 11:28:24 -080093
94 def set_sdn_controller_platform_rest_if(self, sdn_controller_rest_if):
95 url = self.sdn_platform_data_url % (self.controller)
96 url = url + "restifaddr/"
97 data = self.rest_post_request(url, sdn_controller_rest_if)
Srikanth Vavilapalli1725e492014-12-01 17:50:52 -080098
99 def rest_simple_request(self,url, use_cache = None, timeout = None):
100 # include a trivial retry mechanism ... other specific
101 # urllib2 exception types may need to be included
102 retry_count = 0
103 if use_cache == None or use_cache:
104 result = url_cache.get_cached_url(url)
105 if result != None:
106 return result
107 while retry_count > 0:
108 try:
109 return urllib2.urlopen(url, timeout = timeout).read()
110 except urllib2.URLError:
111 retry_count -= 1
112 time.sleep(1)
113 # try again without the try...
114 if self.display_rest:
115 print "REST-SIMPLE:", 'GET', url
116 result = urllib2.urlopen(url, timeout = timeout).read()
117 if self.display_rest_reply:
118 print 'REST-SIMPLE: %s reply "%s"' % (url, result)
119 url_cache.save_url(url, result)
120 return result
121
122
123 def rest_json_request(self, url):
124 entries = url_cache.get_cached_url(url)
125 if entries != None:
126 return entries
127
128 result = self.rest_simple_request(url)
129 # XXX check result
130 entries = json.loads(result)
131 url_cache.save_url(url, entries)
132
133 return entries
134
135 def rest_post_request(self, url, obj, verb='PUT'):
136 post_data = json.dumps(obj)
137 if self.display_rest:
138 print "REST-POST:", verb, url, post_data
139 request = urllib2.Request(url, post_data, {'Content-Type':'application/json'})
140 request.get_method = lambda: verb
141 response = urllib2.urlopen(request)
142 result = response.read()
143 if self.display_rest_reply:
144 print 'REST-POST: %s reply: "%s"' % (url, result)
145 return result
146
147
148 def get_table_from_store(self, table_name, key=None, val=None, match=None):
149 if not self.controller:
150 print "No controller specified. Set using 'controller <server:port>'."
151 return
152 url = self.table_read_url % (self.controller, table_name)
153 if not match:
154 match = "startswith"
155 if key and val:
156 url = "%s?%s__%s=%s" % (url, key, match, urllib.quote_plus(val))
157 result = url_cache.get_cached_url(url)
158 if result != None:
159 return result
160 data = self.rest_simple_request(url)
161 entries = json.loads(data)
162 url_cache.save_url(url, entries)
163 return entries
164
165
166 def get_object_from_store(self, table_name, pk_value):
167 if not self.controller:
168 print "No controller specified. Set using 'controller <server:port>'."
169 return
170 url = self.table_read_url % (self.controller, table_name)
171 url += (pk_value + '/')
172 result = url_cache.get_cached_url(url)
173 if result != None:
174 return result
175 if self.display_rest:
176 print "REST-MODEL:", url
177 response = urllib2.urlopen(url)
178 if response.code != 200:
179 # LOOK! Should probably raise exception here instead.
180 # In general we need to rethink the store interface and how
181 # we should use exceptions.
182 return None
183 data = response.read()
184 result = json.loads(data)
185 if self.display_rest_reply:
186 print 'REST-MODEL: %s reply: "%s"' % (url, result)
187 url_cache.save_url(url, result)
188 return result
189
190
191 # obj_data must contain a key/val and any other required data
192 def rest_create_object(self, obj_type, obj_data):
193 if not self.controller:
194 print "No controller specified. Set using 'controller <server:port>'."
195 return
196 url_cache.clear_cached_urls()
197 url = self.entry_post_url % (self.controller, obj_type)
198 data = self.rest_post_request(url, obj_data)
199 # LOOK! successful stuff should be returned in json too.
200 if data != "saved":
201 result = json.loads(data)
202 return result
203 url_cache.clear_cached_urls()
204
205 def find_object_from_store(self, obj_type, key, val):
206 if not self.controller:
207 print "No controller specified. Set using 'controller <server:port>'."
208 return
209 url = self.table_read_url % (self.controller, obj_type)
210 result = url_cache.get_cached_url(url)
211 if result != None:
212 return result
213 data = self.rest_simple_request("%s?%s__exact=%s" % (url, key, urllib.quote_plus(val)))
214 entries = json.loads(data)
215 url_cache.save_url(url, entries)
216 return entries
217
218 def rest_query_objects(self, obj_type, query_params=None):
219 if not self.controller:
220 print "No controller specified. Set using 'controller <server:port>'."
221 return
222 url = self.table_read_url % (self.controller, obj_type)
223 if query_params:
224 url += '?'
225 # Convert any data:None fields to <id>__isnull=True
226 non_null_query_params = dict([[n,v] if v != None else [n + '__isnull', True]
227 for (n,v) in query_params.items()])
228 url += urllib.urlencode(non_null_query_params)
229 result = url_cache.get_cached_url(url)
230 if result != None:
231 return result
232 data = self.rest_simple_request(url)
233 entries = json.loads(data)
234 url_cache.save_url(url, entries)
235 return entries
236
237 #
238 # either must contain a key/val and any other required data
239 # of the key must be a dictionary identifying the item to delete.
240 def rest_delete_object(self, obj_type, key, val = None):
241 dict_ = {}
242 url = self.entry_post_url % (self.controller, obj_type)
243 if val == None:
244 if not type(key) == type(dict_):
245 return None
246 dict_ = key
247 else:
248 url += "?%s__exact=%s" % (key, urllib.quote_plus(val))
249
250 # LOOK! I'm not sure this works the way it seems to me it's
251 # designed to work. I think the intent is that you can specify
252 # query parameters in the key argument which controls which
253 # instance(s) should be deleted. But when I try it it seems to
254 # always delete all instances, so it seems like the parameters
255 # don't filter properly when passed via the POST data as opposed
256 # to being specified as query parameters in the URL. The latter
257 # way does work -- see rest_delete_objects that follows this.
258 data = self.rest_post_request(url, dict_, 'DELETE')
259 # LOOK! successful stuff should be returned in json too.
260 if data != "deleted":
261 dict_ = json.loads(data)
262 return dict_
263 url_cache.clear_cached_urls()
264
265 def rest_delete_objects(self, obj_type, query_params):
266 url = self.entry_post_url % (self.controller, obj_type)
267 if query_params:
268 url += '?'
269 # Convert any data:None fields to <id>__isnull=True
270 non_null_query_params = dict([[n,v] if v != None else [n + '__isnull', True]
271 for (n,v) in query_params.items()])
272 url += urllib.urlencode(non_null_query_params)
273
274 data = self.rest_post_request(url, {}, 'DELETE')
275 # LOOK! successful stuff should be returned in json too.
276 if data != "deleted":
277 result = json.loads(data)
278 return result
279 url_cache.clear_cached_urls()
280
281 def rest_update_object(self, obj_type, obj_key_name, obj_key_val, obj_data):
282 if not self.controller:
283 print "No controller specified. Set using 'controller <server:port>'."
284 return
285 url = self.entry_post_url % (self.controller, obj_type)
286 url += "?%s=%s" % (obj_key_name, urllib.quote_plus(obj_key_val)) # add a query string
287 data = self.rest_post_request(url, obj_data)
288 # LOOK! successful stuff should be returned in json too.
289 result = json.loads(data)
290 if result.get('description', '') != "saved":
291 return result
292 url_cache.clear_cached_urls()
293
294 def set_user_data_file(self, name, text):
295 url = self.user_data_url % (self.controller)
296 version = 1 # default
297 # find the latest version for a name
298 existing_data = self.get_user_data_table(name, "latest")
299 if len(existing_data) > 0: # should be at most 1, but just in case...
300 version = max([int(f['version']) for f in existing_data]) + 1 # LOOK! race?
301 length = len(text)
302 # LOOK! what to do about time in a distributed system!
303 timestamp = datetime.datetime.utcnow().strftime("%Y-%m-%d.%H:%M:%S")
304 url += "%s/timestamp=%s/version=%s/length=%s/" % (name, timestamp, version, length)
305 return self.copy_text_to_url(url, text)
306
307 def get_user_data_file(self, name):
308 url = self.user_data_url % (self.controller)
309 url += name + "/"
310 return self.rest_simple_request(url)
311
312 def delete_user_data_file(self, name):
313 url = self.user_data_url % (self.controller)
314 url += name + "/"
315 data = self.rest_post_request(url, {}, 'DELETE')
316 if data != "deleted":
317 result = json.loads(data)
318 return result
319
320 def get_user_data_table(self, name=None, show_version="latest"):
321 if not self.controller:
322 print "No controller specified. Set using 'controller <server:port>'."
323 return None
324 url = self.user_data_url % self.controller
325 if name:
326 url += "?name__startswith=%s" % name
327 data = self.rest_simple_request(url)
328 new_data = []
329 data = json.loads(data)
330 latest_versions = {} # dict of latest version per name
331 for d in data: # list of dicts
332 l = d['name'].split('/') # ex: startup/timestamp=2010-11-03.05:51:27/version=1/length=2038
333 nd = dict([item.split('=') for item in l[1:]])
334 nd['name'] = l[0]
335 nd['full_name'] = d['name']
336 new_data.append(nd)
337 if not nd['name'] in latest_versions or int(nd['version']) > int(latest_versions[nd['name']]):
338 latest_versions[nd['name']] = nd['version'] # initialize first time
339
340 # prune if needed to a name or a particular version
341 if name:
342 new_data = [ nd for nd in new_data if nd['name'].startswith(name) ]
343 if show_version == "latest":
344 new_data = [ nd for nd in new_data if not int(nd['version']) < int(latest_versions[nd['name']]) ]
345 elif show_version != "all":
346 new_data = [ nd for nd in new_data if nd['version'] == show_version ]
347 return new_data
348
349
350 # LOOK! looks a lot like a rest_post_request except we don't jsonify and we handle
351 # errors differently... refactor? Same with get_text and rest_simple_request
352 def copy_text_to_url(self, url, src_text, message = None):
353 post_data = src_text
354 if url.startswith('ftp://'):
355 url_suffix = url[6:]
356 user = 'anonymous'
357 password = ''
358 if url_suffix.find('@') != -1:
359 url_parts = url_suffix.split('@')
360 url_user_and_password = url_parts[0]
361 url_suffix = url_parts[1]
362 if url_user_and_password.find(':') != -1:
363 user_and_password = url_user_and_password.split(':')
364 user = user_and_password[0]
365 password = user_and_password[1]
366 else:
367 user = url_user_and_password
368
369 host = url_suffix
370 path = None
371 if url_suffix.find('/'):
372 url_suffix_parts = url_suffix.split('/')
373 host = url_suffix_parts[0]
374 path = url_suffix_parts[1]
375 ftp_target = ftplib.FTP(host, user, password)
376
377 ftp_target.storbinary('STOR %s' % path, StringReader(post_data))
378 # apparently, storbinary doesn't provide a return value
379 result = { "result" : "success" } # don't display any other error messages
380 else:
381 request = urllib2.Request(url, post_data, {'Content-Type':'text/plain'})
382 request.get_method = lambda: 'PUT'
383 if self.display_rest:
384 print "REST-TEXT-TO:", request
385 response = urllib2.urlopen(request)
386 result = response.read()
387 if self.display_rest_reply:
388 print 'REST-TEXT-TO: %s reply "%s"' % (request, result)
389 return result
390
391 def get_text_from_url(self, url):
392 if self.display_rest:
393 print "REST-TEXT-FROM:", url
394 result = urllib2.urlopen(url).read()
395 if self.display_rest_reply:
396 print 'REST-TEXT-FROM: %s result:"%s"' % (url, result)
397 return result