1 # Copyright 2011 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
11 # 2. Redistributions in binary form must reproduce the above
12 # copyright notice, this list of conditions and the following
13 # disclaimer in the documentation and/or other materials
14 # provided with the distribution.
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
34 from httplib import HTTPConnection, HTTP
36 from xml.dom import minidom
41 import pithos.api.faults
43 ERROR_CODES = {304:'Not Modified',
48 411:'Length Required',
49 412:'Precondition Failed',
50 416:'Range Not Satisfiable',
51 422:'Unprocessable Entity',
52 503:'Service Unavailable'}
54 class Fault(Exception):
55 def __init__(self, data='', status=None):
56 if data == '' and status in ERROR_CODES.keys():
57 data = ERROR_CODES[status]
58 Exception.__init__(self, data)
63 def __init__(self, host, token, account, api='v1', verbose=False, debug=False):
64 """`host` can also include a port, e.g '127.0.0.1:8000'."""
67 self.account = account
69 self.verbose = verbose or debug
73 def _req(self, method, path, body=None, headers={}, format='text',
75 full_path = '/%s/%s%s?format=%s' % (self.api, self.account, path,
77 for k,v in params.items():
79 full_path = '%s&%s=%s' %(full_path, k, v)
81 full_path = '%s&%s' %(full_path, k)
82 conn = HTTPConnection(self.host)
85 full_path = full_path.replace(' ', '%20')
88 for k,v in headers.items():
90 k = k.replace('_', '-')
93 kwargs['headers'] = headers
94 kwargs['headers']['X-Auth-Token'] = self.token
97 kwargs['headers'].setdefault('content-type',
98 'application/octet-stream')
99 kwargs['headers'].setdefault('content-length', len(body) if body else 0)
101 #print '*', method, full_path, kwargs
102 conn.request(method, full_path, **kwargs)
103 except socket.error, e:
104 raise Fault(status=503)
106 resp = conn.getresponse()
107 headers = dict(resp.getheaders())
110 print '%d %s' % (resp.status, resp.reason)
111 for key, val in headers.items():
112 print '%s: %s' % (key.capitalize(), val)
115 length = resp.getheader('content-length', None)
116 data = resp.read(length)
121 if int(resp.status) in ERROR_CODES.keys():
122 #print '**', resp.status
123 raise Fault(data, int(resp.status))
125 #print '**', resp.status, headers, data
126 return resp.status, headers, data
128 def delete(self, path, format='text', params={}):
129 return self._req('DELETE', path, format=format, params=params)
131 def get(self, path, format='text', headers=None, params={}):
132 return self._req('GET', path, headers=headers, format=format,
135 def head(self, path, format='text', params={}):
136 return self._req('HEAD', path, format=format, params=params)
138 def post(self, path, body=None, format='text', headers=None, params={}):
139 return self._req('POST', path, body, headers=headers, format=format,
142 def put(self, path, body=None, format='text', headers=None):
143 return self._req('PUT', path, body, headers=headers, format=format)
145 def _list(self, path, format='text', params={}, **headers):
146 status, headers, data = self.get(path, format=format, headers=headers,
149 data = json.loads(data) if data else ''
150 elif format == 'xml':
151 data = minidom.parseString(data)
153 data = data.strip().split('\n') if data else ''
156 def _get_metadata(self, path, prefix=None, params={}):
157 status, headers, data = self.head(path, params=params)
158 prefixlen = len(prefix) if prefix else 0
160 for key, val in headers.items():
161 if prefix and not key.startswith(prefix):
163 elif prefix and key.startswith(prefix):
164 key = key[prefixlen:]
168 def _filter(self, l, d):
170 filter out from l elements having the metadata values provided
174 if type(elem) == types.DictionaryType:
176 k = 'x_object_meta_%s' % key
177 if k in elem.keys() and elem[k] == d[key]:
182 class OOS_Client(Client):
183 """Openstack Object Storage Client"""
185 def _update_metadata(self, path, entity, **meta):
186 """adds new and updates the values of previously set metadata"""
187 ex_meta = self.retrieve_account_metadata(restricted=True)
190 prefix = 'x-%s-meta-' % entity
191 for k,v in ex_meta.items():
192 k = '%s%s' % (prefix, k)
194 return self.post(path, headers=headers)
196 def _reset_metadata(self, path, entity, **meta):
198 overwrites all user defined metadata
201 prefix = 'x-%s-meta-' % entity
202 for k,v in meta.items():
203 k = '%s%s' % (prefix, k)
205 return self.post(path, headers=headers)
207 def _delete_metadata(self, path, entity, meta=[]):
208 """delete previously set metadata"""
209 ex_meta = self.retrieve_account_metadata(restricted=True)
211 prefix = 'x-%s-meta-' % entity
212 for k in ex_meta.keys():
214 headers['%s%s' % (prefix, k)] = ex_meta[k]
215 return self.post(path, headers=headers)
217 # Storage Account Services
219 def list_containers(self, format='text', limit=10000, marker=None, params={},
221 """lists containers"""
224 params.update({'limit':limit, 'marker':marker})
225 return self._list('', format, params, **headers)
227 def retrieve_account_metadata(self, restricted=False, **params):
228 """returns the account metadata"""
229 prefix = 'x-account-meta-' if restricted else None
230 return self._get_metadata('', prefix, params)
232 def update_account_metadata(self, **meta):
233 """updates the account metadata"""
234 return self._update_metadata('', 'account', **meta)
236 def delete_account_metadata(self, meta=[]):
237 """deletes the account metadata"""
238 return self._delete_metadata('', 'account', meta)
240 def reset_account_metadata(self, **meta):
241 """resets account metadata"""
242 return self._reset_metadata('', 'account', **meta)
244 # Storage Container Services
246 def _filter_trashed(self, l):
247 return self._filter(l, {'trash':'true'})
249 def list_objects(self, container, format='text', limit=10000, marker=None,
250 prefix=None, delimiter=None, path=None,
251 include_trashed=False, params={}, **headers):
252 """returns a list with the container objects"""
253 params.update({'limit':limit, 'marker':marker, 'prefix':prefix,
254 'delimiter':delimiter, 'path':path})
255 l = self._list('/' + container, format, params, **headers)
256 #TODO support filter trashed with xml also
257 if format != 'xml' and not include_trashed:
258 l = self._filter_trashed(l)
261 def create_container(self, container, **meta):
262 """creates a container"""
264 for k,v in meta.items():
265 headers['x-container-meta-%s' %k.strip().upper()] = v.strip()
266 status, header, data = self.put('/' + container, headers=headers)
270 raise Fault(data, int(status))
273 def delete_container(self, container, params={}):
274 """deletes a container"""
275 return self.delete('/' + container, params=params)
277 def retrieve_container_metadata(self, container, restricted=False, **params):
278 """returns the container metadata"""
279 prefix = 'x-container-meta-' if restricted else None
280 return self._get_metadata('/%s' % container, prefix, params)
282 def update_container_metadata(self, container, **meta):
283 """unpdates the container metadata"""
284 return self._update_metadata('/' + container, 'container', **meta)
286 def delete_container_metadata(self, container, meta=[]):
287 """deletes the container metadata"""
288 path = '/%s' % (container)
289 return self._delete_metadata(path, 'container', meta)
291 # Storage Object Services
293 def request_object(self, container, object, format='text', params={},
295 """returns tuple containing the status, headers and data response for an object request"""
296 path = '/%s/%s' % (container, object)
297 status, headers, data = self.get(path, format, headers, params)
298 return status, headers, data
300 def retrieve_object(self, container, object, format='text', params={},
302 """returns an object's data"""
303 t = self.request_object(container, object, format, params, **headers)
306 def create_directory_marker(self, container, object):
307 """creates a dierectory marker"""
309 raise Fault('Directory markers have to be nested in a container')
310 h = {'Content-Type':'application/directory'}
311 return self.create_zero_length_object(container, object, **h)
313 def create_directory_marker(self, container, object):
314 """creates a dierectory marker"""
315 return self.create_object(container, object, f=None)
317 def create_object(self, container, object, f=stdin, format='text', meta={},
318 etag=None, content_type=None, content_encoding=None,
319 content_disposition=None, **headers):
320 """creates a zero-length object"""
321 path = '/%s/%s' % (container, object)
322 for k, v in headers.items():
326 l = ['etag', 'content_encoding', 'content_disposition', 'content_type']
327 l = [elem for elem in l if eval(elem)]
329 headers.update({elem:eval(elem)})
331 for k,v in meta.items():
332 headers['x-object-meta-%s' %k.strip()] = v.strip()
333 data = f.read() if f else None
334 return self.put(path, data, format, headers=headers)
336 def create_zero_length_object(self, container, object, meta={}, etag=None,
337 content_type=None, content_encoding=None,
338 content_disposition=None, **headers):
340 for elem in ['self', 'container', 'headers']:
343 return self.create_object(container, f=None, **args)
345 def update_object(self, container, object, f=stdin, offset=None, meta={},
346 content_length=None, content_type=None,
347 content_encoding=None, content_disposition=None,
349 path = '/%s/%s' % (container, object)
350 for k, v in headers.items():
354 l = ['content_encoding', 'content_disposition', 'content_type',
356 l = [elem for elem in l if eval(elem)]
358 headers.update({elem:eval(elem)})
360 if 'content_range' not in headers.keys():
362 headers['content_range'] = 'bytes %s-/*' % offset
364 headers['content_range'] = 'bytes */*'
366 for k,v in meta.items():
367 headers['x-object-meta-%s' %k.strip()] = v.strip()
368 data = f.read() if f else None
369 return self.post(path, data, headers=headers)
371 def _change_obj_location(self, src_container, src_object, dst_container,
372 dst_object, remove=False, public=False, **meta):
373 path = '/%s/%s' % (dst_container, dst_object)
375 for k, v in meta.items():
376 headers['x-object-meta-%s' % k] = v
378 headers['x-move-from'] = '/%s/%s' % (src_container, src_object)
380 headers['x-copy-from'] = '/%s/%s' % (src_container, src_object)
381 self._set_public_header(headers, public)
382 self.headers = headers if headers else None
383 headers['content-length'] = 0
384 return self.put(path, headers=headers)
386 def copy_object(self, src_container, src_object, dst_container,
387 dst_object, public=False, **meta):
388 return self._change_obj_location(src_container, src_object,
389 dst_container, dst_object, False,
392 def move_object(self, src_container, src_object, dst_container,
393 dst_object, public=False, **meta):
394 return self._change_obj_location(src_container, src_object,
395 dst_container, dst_object, True,
398 def delete_object(self, container, object, params={}):
399 return self.delete('/%s/%s' % (container, object), params=params)
401 def retrieve_object_metadata(self, container, object, restricted=False,
404 set restricted to True to get only user defined metadata
406 path = '/%s/%s' % (container, object)
407 prefix = 'x-object-meta-' if restricted else None
408 params = {'version':version} if version else {}
409 return self._get_metadata(path, prefix, params=params)
411 def update_object_metadata(self, container, object, **meta):
412 path = '/%s/%s' % (container, object)
413 return self._update_metadata(path, 'object', **meta)
415 def delete_object_metadata(self, container, object, meta=[]):
416 path = '/%s/%s' % (container, object)
417 return self._delete_metadata(path, 'object', meta)
419 class Pithos_Client(OOS_Client):
420 """Pithos Storage Client. Extends OOS_Client"""
422 def _chunked_transfer(self, path, method='PUT', f=stdin, headers=None,
424 """perfomrs a chunked request"""
425 http = HTTPConnection(self.host)
428 path = '/%s/%s%s' % (self.api, self.account, path)
429 http.putrequest(method, path)
430 http.putheader('x-auth-token', self.token)
431 http.putheader('content-type', 'application/octet-stream')
432 http.putheader('transfer-encoding', 'chunked')
434 for header,value in headers.items():
435 http.putheader(header, value)
443 block = f.read(blocksize)
446 data = '%s\r\n%s\r\n' % (hex(len(block)), block)
460 resp = http.getresponse()
462 headers = dict(resp.getheaders())
465 print '%d %s' % (resp.status, resp.reason)
466 for key, val in headers.items():
467 print '%s: %s' % (key.capitalize(), val)
470 length = resp.getheader('Content-length', None)
471 data = resp.read(length)
476 if int(resp.status) in ERROR_CODES.keys():
477 raise Fault(data, int(resp.status))
479 #print '*', resp.status, headers, data
480 return resp.status, headers, data
482 def _update_metadata(self, path, entity, **meta):
484 adds new and updates the values of previously set metadata
486 params = {'update':None}
488 prefix = 'x-%s-meta-' % entity
489 for k,v in meta.items():
490 k = '%s%s' % (prefix, k)
492 return self.post(path, headers=headers, params=params)
494 def _delete_metadata(self, path, entity, meta=[]):
496 delete previously set metadata
498 params = {'update':None}
500 prefix = 'x-%s-meta-' % entity
502 headers['%s%s' % (prefix, m)] = ''
503 return self.post(path, headers=headers, params=params)
505 # Storage Account Services
507 def list_containers(self, format='text', if_modified_since=None,
508 if_unmodified_since=None, limit=1000, marker=None,
510 """returns a list with the account containers"""
511 params = {'until':until} if until else None
512 headers = {'if-modified-since':if_modified_since,
513 'if-unmodified-since':if_unmodified_since}
514 return OOS_Client.list_containers(self, format=format, limit=limit,
515 marker=marker, params=params,
518 def retrieve_account_metadata(self, restricted=False, until=None):
519 """returns the account metadata"""
520 params = {'until':until} if until else {}
521 return OOS_Client.retrieve_account_metadata(self, restricted=restricted,
524 def set_account_groups(self, **groups):
525 """create account groups"""
527 for key, val in groups.items():
528 headers['x-account-group-%s' % key] = val
529 params = {'update':None}
530 return self.post('', headers=headers, params=params)
532 def retrieve_account_groups(self):
533 """returns the account groups"""
534 meta = self.retrieve_account_metadata()
535 prefix = 'x-account-group-'
536 prefixlen = len(prefix)
538 for key, val in meta.items():
539 if prefix and not key.startswith(prefix):
541 elif prefix and key.startswith(prefix):
542 key = key[prefixlen:]
546 def unset_account_groups(self, groups=[]):
547 """delete account groups"""
550 headers['x-account-group-%s' % elem] = ''
551 params = {'update':None}
552 return self.post('', headers=headers, params=params)
554 def reset_account_groups(self, **groups):
555 """overrides account groups"""
557 for key, val in groups.items():
558 headers['x-account-group-%s' % key] = val
559 meta = self.retrieve_account_metadata()
561 return self.post('', headers=headers)
563 # Storage Container Services
565 def list_objects(self, container, format='text', limit=10000, marker=None,
566 prefix=None, delimiter=None, path=None,
567 include_trashed=False, params={}, if_modified_since=None,
568 if_unmodified_since=None, meta={}, until=None):
569 """returns a list with the container objects"""
570 params = {'until':until, 'meta':meta}
572 for elem in ['self', 'container', 'params', 'until', 'meta']:
574 return OOS_Client.list_objects(self, container, params=params,
577 def retrieve_container_metadata(self, container, restricted=False,
579 """returns container's metadata"""
580 params = {'until':until} if until else {}
581 return OOS_Client.retrieve_container_metadata(self, container,
582 restricted=restricted,
585 def set_container_policies(self, container, **policies):
586 """sets containers policies"""
587 path = '/%s' % (container)
590 for key, val in policies.items():
591 headers['x-container-policy-%s' % key] = val
592 return self.post(path, headers=headers)
594 def delete_container(self, container, until=None):
595 """deletes a container or the container history until the date provided"""
596 params = {'until':until} if until else {}
597 return OOS_Client.delete_container(self, container, params)
599 # Storage Object Services
601 def retrieve_object(self, container, object, params={}, format='text', range=None,
602 if_range=None, if_match=None, if_none_match=None,
603 if_modified_since=None, if_unmodified_since=None,
605 """returns an object"""
607 l = ['range', 'if_range', 'if_match', 'if_none_match',
608 'if_modified_since', 'if_unmodified_since']
609 l = [elem for elem in l if eval(elem)]
611 headers.update({elem:eval(elem)})
612 return OOS_Client.retrieve_object(self, container, object, format=format,
613 params=params, **headers)
615 def retrieve_object_version(self, container, object, version, detail=False,
616 range=None, if_range=None, if_match=None,
617 if_none_match=None, if_modified_since=None,
618 if_unmodified_since=None):
619 """returns a specific object version"""
621 l = ['self', 'container', 'object']
624 params = {'version':version}
625 return self.retrieve_object(container, object, params, **args)
627 def retrieve_object_versionlist(self, container, object, range=None,
628 if_range=None, if_match=None,
629 if_none_match=None, if_modified_since=None,
630 if_unmodified_since=None):
631 """returns the object version list"""
633 l = ['self', 'container', 'object']
637 return self.retrieve_object_version(container, object, version='list',
640 def create_zero_length_object(self, container, object, meta={},
641 etag=None, content_type=None, content_encoding=None,
642 content_disposition=None, x_object_manifest=None,
643 x_object_sharing=None, x_object_public=None):
644 """createas a zero length object"""
646 for elem in ['self', 'container', 'object']:
648 return OOS_Client.create_zero_length_object(self, container, object,
651 def create_object(self, container, object, f=stdin, meta={},
652 etag=None, content_type=None, content_encoding=None,
653 content_disposition=None, x_object_manifest=None,
654 x_object_sharing=None, x_object_public=None):
655 """creates an object"""
657 for elem in ['self', 'container', 'object']:
659 return OOS_Client.create_object(self, container, object, **args)
661 def create_object_using_chunks(self, container, object, f=stdin,
662 blocksize=1024, meta={}, etag=None,
663 content_type=None, content_encoding=None,
664 content_disposition=None,
665 x_object_sharing=None,
666 x_object_manifest=None,
667 x_object_public=None):
668 """creates an object (incremental upload)"""
669 path = '/%s/%s' % (container, object)
671 l = ['etag', 'content_type', 'content_encoding', 'content_disposition',
672 'x_object_sharing', 'x_object_manifest', 'x_object_public']
673 l = [elem for elem in l if eval(elem)]
675 headers.update({elem:eval(elem)})
677 for k,v in meta.items():
678 headers['x-object-meta-%s' %k.strip()] = v.strip()
680 return self._chunked_transfer(path, 'PUT', f, headers=headers,
683 def create_object_by_hashmap(container, object, f=stdin, format='json',
684 meta={}, etag=None, content_encoding=None,
685 content_disposition=None, content_type=None,
686 x_object_sharing=None, x_object_manifest=None,
687 x_object_public = None):
688 """creates an object by uploading hashes representing data instead of data"""
690 for elem in ['self', 'container', 'object']:
693 data = f.read() if f else None
694 if data and format == 'json':
697 data = json.dumps(data)
699 raise Fault('Invalid formatting')
702 return self.create_object(container, object, **args)
704 def create_manifestation(self, container, object, manifest):
705 """creates a manifestation"""
706 headers={'x_object_manifest':manifest}
707 return self.create_object(container, object, f=None, **headers)
709 def update_object(self, container, object, f=stdin, offset=None, meta={},
710 content_length=None, content_type=None, content_range=None,
711 content_encoding=None, content_disposition=None,
712 x_object_bytes=None, x_object_manifest=None,
713 x_object_sharing=None, x_object_public=None):
714 """updates an object"""
715 spath = '/%s/%s' % (container, object)
717 for elem in ['self', 'container', 'object']:
720 return OOS_Client.update_object(self, container, object, **args)
722 def update_object_using_chunks(self, container, object, f=stdin,
723 blocksize=1024, offset=None, meta={},
724 content_type=None, content_encoding=None,
725 content_disposition=None, x_object_bytes=None,
726 x_object_manifest=None, x_object_sharing=None,
727 x_object_public=None):
728 """updates an object (incremental upload)"""
729 path = '/%s/%s' % (container, object)
731 l = ['content_type', 'content_encoding', 'content_disposition',
732 'x_object_bytes', 'x_object_manifest', 'x_object_sharing',
734 l = [elem for elem in l if eval(elem)]
736 headers.update({elem:eval(elem)})
739 headers['content_range'] = 'bytes %s-/*' % offset
741 headers['content_range'] = 'bytes */*'
743 for k,v in meta.items():
744 headers['x-object-meta-%s' %k.strip()] = v.strip()
746 return self._chunked_transfer(path, 'POST', f, headers=headers,
749 def delete_object(self, container, object, until=None):
750 """deletes an object or the object history until the date provided"""
751 params = {'until':until} if until else {}
752 return OOS_Client.delete_object(self, container, object, params)
754 def trash_object(self, container, object):
755 """trashes an object"""
756 path = '/%s/%s' % (container, object)
757 meta = {'trash':'true'}
758 return self._update_metadata(path, 'object', **meta)
760 def restore_object(self, container, object):
761 """restores a trashed object"""
762 return self.delete_object_metadata(container, object, ['trash'])
764 def _set_public_header(self, headers, public=False):
765 """sets the public header"""
771 headers['x-object-public'] = public if public else ''
773 def publish_object(self, container, object):
774 """sets a previously created object publicly accessible"""
775 path = '/%s/%s' % (container, object)
776 headers = {'content_range':'bytes */*'}
777 self._set_public_header(headers, public=True)
778 return self.post(path, headers=headers)
780 def unpublish_object(self, container, object):
781 """unpublish an object"""
782 path = '/%s/%s' % (container, object)
783 headers = {'content_range':'bytes */*'}
784 self._set_public_header(headers, public=False)
785 return self.post(path, headers=headers)