blob: e7fb50cb02c78e7b40debcb5a851678d0d0dee6b [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/"
83
84 def set_controller(self,controller):
85 self.controller = controller
86
87 def display_mode(self, mode):
88 self.display_rest = mode
89
90 def display_reply_mode(self, mode):
91 self.display_rest_reply = mode
92
93 def rest_simple_request(self,url, use_cache = None, timeout = None):
94 # include a trivial retry mechanism ... other specific
95 # urllib2 exception types may need to be included
96 retry_count = 0
97 if use_cache == None or use_cache:
98 result = url_cache.get_cached_url(url)
99 if result != None:
100 return result
101 while retry_count > 0:
102 try:
103 return urllib2.urlopen(url, timeout = timeout).read()
104 except urllib2.URLError:
105 retry_count -= 1
106 time.sleep(1)
107 # try again without the try...
108 if self.display_rest:
109 print "REST-SIMPLE:", 'GET', url
110 result = urllib2.urlopen(url, timeout = timeout).read()
111 if self.display_rest_reply:
112 print 'REST-SIMPLE: %s reply "%s"' % (url, result)
113 url_cache.save_url(url, result)
114 return result
115
116
117 def rest_json_request(self, url):
118 entries = url_cache.get_cached_url(url)
119 if entries != None:
120 return entries
121
122 result = self.rest_simple_request(url)
123 # XXX check result
124 entries = json.loads(result)
125 url_cache.save_url(url, entries)
126
127 return entries
128
129 def rest_post_request(self, url, obj, verb='PUT'):
130 post_data = json.dumps(obj)
131 if self.display_rest:
132 print "REST-POST:", verb, url, post_data
133 request = urllib2.Request(url, post_data, {'Content-Type':'application/json'})
134 request.get_method = lambda: verb
135 response = urllib2.urlopen(request)
136 result = response.read()
137 if self.display_rest_reply:
138 print 'REST-POST: %s reply: "%s"' % (url, result)
139 return result
140
141
142 def get_table_from_store(self, table_name, key=None, val=None, match=None):
143 if not self.controller:
144 print "No controller specified. Set using 'controller <server:port>'."
145 return
146 url = self.table_read_url % (self.controller, table_name)
147 if not match:
148 match = "startswith"
149 if key and val:
150 url = "%s?%s__%s=%s" % (url, key, match, urllib.quote_plus(val))
151 result = url_cache.get_cached_url(url)
152 if result != None:
153 return result
154 data = self.rest_simple_request(url)
155 entries = json.loads(data)
156 url_cache.save_url(url, entries)
157 return entries
158
159
160 def get_object_from_store(self, table_name, pk_value):
161 if not self.controller:
162 print "No controller specified. Set using 'controller <server:port>'."
163 return
164 url = self.table_read_url % (self.controller, table_name)
165 url += (pk_value + '/')
166 result = url_cache.get_cached_url(url)
167 if result != None:
168 return result
169 if self.display_rest:
170 print "REST-MODEL:", url
171 response = urllib2.urlopen(url)
172 if response.code != 200:
173 # LOOK! Should probably raise exception here instead.
174 # In general we need to rethink the store interface and how
175 # we should use exceptions.
176 return None
177 data = response.read()
178 result = json.loads(data)
179 if self.display_rest_reply:
180 print 'REST-MODEL: %s reply: "%s"' % (url, result)
181 url_cache.save_url(url, result)
182 return result
183
184
185 # obj_data must contain a key/val and any other required data
186 def rest_create_object(self, obj_type, obj_data):
187 if not self.controller:
188 print "No controller specified. Set using 'controller <server:port>'."
189 return
190 url_cache.clear_cached_urls()
191 url = self.entry_post_url % (self.controller, obj_type)
192 data = self.rest_post_request(url, obj_data)
193 # LOOK! successful stuff should be returned in json too.
194 if data != "saved":
195 result = json.loads(data)
196 return result
197 url_cache.clear_cached_urls()
198
199 def find_object_from_store(self, obj_type, key, val):
200 if not self.controller:
201 print "No controller specified. Set using 'controller <server:port>'."
202 return
203 url = self.table_read_url % (self.controller, obj_type)
204 result = url_cache.get_cached_url(url)
205 if result != None:
206 return result
207 data = self.rest_simple_request("%s?%s__exact=%s" % (url, key, urllib.quote_plus(val)))
208 entries = json.loads(data)
209 url_cache.save_url(url, entries)
210 return entries
211
212 def rest_query_objects(self, obj_type, query_params=None):
213 if not self.controller:
214 print "No controller specified. Set using 'controller <server:port>'."
215 return
216 url = self.table_read_url % (self.controller, obj_type)
217 if query_params:
218 url += '?'
219 # Convert any data:None fields to <id>__isnull=True
220 non_null_query_params = dict([[n,v] if v != None else [n + '__isnull', True]
221 for (n,v) in query_params.items()])
222 url += urllib.urlencode(non_null_query_params)
223 result = url_cache.get_cached_url(url)
224 if result != None:
225 return result
226 data = self.rest_simple_request(url)
227 entries = json.loads(data)
228 url_cache.save_url(url, entries)
229 return entries
230
231 #
232 # either must contain a key/val and any other required data
233 # of the key must be a dictionary identifying the item to delete.
234 def rest_delete_object(self, obj_type, key, val = None):
235 dict_ = {}
236 url = self.entry_post_url % (self.controller, obj_type)
237 if val == None:
238 if not type(key) == type(dict_):
239 return None
240 dict_ = key
241 else:
242 url += "?%s__exact=%s" % (key, urllib.quote_plus(val))
243
244 # LOOK! I'm not sure this works the way it seems to me it's
245 # designed to work. I think the intent is that you can specify
246 # query parameters in the key argument which controls which
247 # instance(s) should be deleted. But when I try it it seems to
248 # always delete all instances, so it seems like the parameters
249 # don't filter properly when passed via the POST data as opposed
250 # to being specified as query parameters in the URL. The latter
251 # way does work -- see rest_delete_objects that follows this.
252 data = self.rest_post_request(url, dict_, 'DELETE')
253 # LOOK! successful stuff should be returned in json too.
254 if data != "deleted":
255 dict_ = json.loads(data)
256 return dict_
257 url_cache.clear_cached_urls()
258
259 def rest_delete_objects(self, obj_type, query_params):
260 url = self.entry_post_url % (self.controller, obj_type)
261 if query_params:
262 url += '?'
263 # Convert any data:None fields to <id>__isnull=True
264 non_null_query_params = dict([[n,v] if v != None else [n + '__isnull', True]
265 for (n,v) in query_params.items()])
266 url += urllib.urlencode(non_null_query_params)
267
268 data = self.rest_post_request(url, {}, 'DELETE')
269 # LOOK! successful stuff should be returned in json too.
270 if data != "deleted":
271 result = json.loads(data)
272 return result
273 url_cache.clear_cached_urls()
274
275 def rest_update_object(self, obj_type, obj_key_name, obj_key_val, obj_data):
276 if not self.controller:
277 print "No controller specified. Set using 'controller <server:port>'."
278 return
279 url = self.entry_post_url % (self.controller, obj_type)
280 url += "?%s=%s" % (obj_key_name, urllib.quote_plus(obj_key_val)) # add a query string
281 data = self.rest_post_request(url, obj_data)
282 # LOOK! successful stuff should be returned in json too.
283 result = json.loads(data)
284 if result.get('description', '') != "saved":
285 return result
286 url_cache.clear_cached_urls()
287
288 def set_user_data_file(self, name, text):
289 url = self.user_data_url % (self.controller)
290 version = 1 # default
291 # find the latest version for a name
292 existing_data = self.get_user_data_table(name, "latest")
293 if len(existing_data) > 0: # should be at most 1, but just in case...
294 version = max([int(f['version']) for f in existing_data]) + 1 # LOOK! race?
295 length = len(text)
296 # LOOK! what to do about time in a distributed system!
297 timestamp = datetime.datetime.utcnow().strftime("%Y-%m-%d.%H:%M:%S")
298 url += "%s/timestamp=%s/version=%s/length=%s/" % (name, timestamp, version, length)
299 return self.copy_text_to_url(url, text)
300
301 def get_user_data_file(self, name):
302 url = self.user_data_url % (self.controller)
303 url += name + "/"
304 return self.rest_simple_request(url)
305
306 def delete_user_data_file(self, name):
307 url = self.user_data_url % (self.controller)
308 url += name + "/"
309 data = self.rest_post_request(url, {}, 'DELETE')
310 if data != "deleted":
311 result = json.loads(data)
312 return result
313
314 def get_user_data_table(self, name=None, show_version="latest"):
315 if not self.controller:
316 print "No controller specified. Set using 'controller <server:port>'."
317 return None
318 url = self.user_data_url % self.controller
319 if name:
320 url += "?name__startswith=%s" % name
321 data = self.rest_simple_request(url)
322 new_data = []
323 data = json.loads(data)
324 latest_versions = {} # dict of latest version per name
325 for d in data: # list of dicts
326 l = d['name'].split('/') # ex: startup/timestamp=2010-11-03.05:51:27/version=1/length=2038
327 nd = dict([item.split('=') for item in l[1:]])
328 nd['name'] = l[0]
329 nd['full_name'] = d['name']
330 new_data.append(nd)
331 if not nd['name'] in latest_versions or int(nd['version']) > int(latest_versions[nd['name']]):
332 latest_versions[nd['name']] = nd['version'] # initialize first time
333
334 # prune if needed to a name or a particular version
335 if name:
336 new_data = [ nd for nd in new_data if nd['name'].startswith(name) ]
337 if show_version == "latest":
338 new_data = [ nd for nd in new_data if not int(nd['version']) < int(latest_versions[nd['name']]) ]
339 elif show_version != "all":
340 new_data = [ nd for nd in new_data if nd['version'] == show_version ]
341 return new_data
342
343
344 # LOOK! looks a lot like a rest_post_request except we don't jsonify and we handle
345 # errors differently... refactor? Same with get_text and rest_simple_request
346 def copy_text_to_url(self, url, src_text, message = None):
347 post_data = src_text
348 if url.startswith('ftp://'):
349 url_suffix = url[6:]
350 user = 'anonymous'
351 password = ''
352 if url_suffix.find('@') != -1:
353 url_parts = url_suffix.split('@')
354 url_user_and_password = url_parts[0]
355 url_suffix = url_parts[1]
356 if url_user_and_password.find(':') != -1:
357 user_and_password = url_user_and_password.split(':')
358 user = user_and_password[0]
359 password = user_and_password[1]
360 else:
361 user = url_user_and_password
362
363 host = url_suffix
364 path = None
365 if url_suffix.find('/'):
366 url_suffix_parts = url_suffix.split('/')
367 host = url_suffix_parts[0]
368 path = url_suffix_parts[1]
369 ftp_target = ftplib.FTP(host, user, password)
370
371 ftp_target.storbinary('STOR %s' % path, StringReader(post_data))
372 # apparently, storbinary doesn't provide a return value
373 result = { "result" : "success" } # don't display any other error messages
374 else:
375 request = urllib2.Request(url, post_data, {'Content-Type':'text/plain'})
376 request.get_method = lambda: 'PUT'
377 if self.display_rest:
378 print "REST-TEXT-TO:", request
379 response = urllib2.urlopen(request)
380 result = response.read()
381 if self.display_rest_reply:
382 print 'REST-TEXT-TO: %s reply "%s"' % (request, result)
383 return result
384
385 def get_text_from_url(self, url):
386 if self.display_rest:
387 print "REST-TEXT-FROM:", url
388 result = urllib2.urlopen(url).read()
389 if self.display_rest_reply:
390 print 'REST-TEXT-FROM: %s result:"%s"' % (url, result)
391 return result