root / snf-app / synnefo / plankton / backend.py @ 921355f8
History | View | Annotate | Download (16.6 kB)
1 |
# Copyright 2011 GRNET S.A. All rights reserved.
|
---|---|
2 |
#
|
3 |
# Redistribution and use in source and binary forms, with or
|
4 |
# without modification, are permitted provided that the following
|
5 |
# conditions are met:
|
6 |
#
|
7 |
# 1. Redistributions of source code must retain the above
|
8 |
# copyright notice, this list of conditions and the following
|
9 |
# disclaimer.
|
10 |
#
|
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.
|
15 |
#
|
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.
|
28 |
#
|
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.
|
33 |
|
34 |
"""
|
35 |
Plankton attributes are divided in 3 categories:
|
36 |
- generated: They are dynamically generated and not stored anywhere.
|
37 |
- user: Stored as user accessible metadata and can be modified from within
|
38 |
Pithos apps. They are visible as prefixed with PLANKTON_PREFIX.
|
39 |
- system: Stored as metadata but can not be modified though Pithos.
|
40 |
|
41 |
In more detail, Plankton attributes are the following:
|
42 |
- checksum: generated based on the merkle hash of the file
|
43 |
- container_format: stored as a user meta
|
44 |
- created_at: generated based on the modified attribute of the first version
|
45 |
- deleted_at: generated based on the timestamp of the last version
|
46 |
- disk_format: stored as a user meta
|
47 |
- id: generated based on location and stored as system meta
|
48 |
- is_public: True if there is a * entry for the read permission
|
49 |
- location: generated based on the object's path
|
50 |
- name: stored as a user meta
|
51 |
- owner: identical to the object's account
|
52 |
- properties: stored as user meta prefixed with PROPERTY_PREFIX
|
53 |
- size: generated based from 'bytes' value
|
54 |
- status: stored as a system meta
|
55 |
- store: is always 'pithos'
|
56 |
- updated_at: generated based on the modified attribute
|
57 |
"""
|
58 |
|
59 |
import json |
60 |
import warnings |
61 |
|
62 |
from binascii import hexlify |
63 |
from functools import partial |
64 |
from hashlib import md5 |
65 |
from operator import itemgetter |
66 |
from time import gmtime, strftime, time |
67 |
from uuid import UUID |
68 |
|
69 |
from django.conf import settings |
70 |
|
71 |
from pithos.backends import connect_backend |
72 |
from pithos.backends.base import NotAllowedError |
73 |
|
74 |
|
75 |
PLANKTON_PREFIX = 'plankton:'
|
76 |
PROPERTY_PREFIX = 'property:'
|
77 |
|
78 |
SYSTEM_META = set(['id', 'status']) |
79 |
USER_META = set(['name', 'container_format', 'disk_format']) |
80 |
|
81 |
|
82 |
def prefix_keys(keys): |
83 |
prefixed = [] |
84 |
for key in keys: |
85 |
if key in SYSTEM_META: |
86 |
key = PLANKTON_PREFIX + key |
87 |
elif key in USER_META: |
88 |
key = 'X-Object-Meta-' + PLANKTON_PREFIX + key
|
89 |
else:
|
90 |
assert False, "Invalid filter key" |
91 |
prefixed.append(key) |
92 |
return prefixed
|
93 |
|
94 |
def prefix_meta(meta): |
95 |
prefixed = {} |
96 |
for key, val in meta.items(): |
97 |
key = key.lower() |
98 |
if key in SYSTEM_META: |
99 |
key = PLANKTON_PREFIX + key |
100 |
elif key in USER_META: |
101 |
key = 'X-Object-Meta-' + PLANKTON_PREFIX + key
|
102 |
elif key == 'properties': |
103 |
for k, v in val.items(): |
104 |
k = k.lower() |
105 |
k = 'X-Object-Meta-' + PLANKTON_PREFIX + PROPERTY_PREFIX + k
|
106 |
prefixed[k] = v |
107 |
continue
|
108 |
else:
|
109 |
assert False, "Invalid metadata key" |
110 |
prefixed[key] = val |
111 |
return prefixed
|
112 |
|
113 |
|
114 |
def get_image_id(location): |
115 |
return str(UUID(bytes=md5(location).digest())) |
116 |
|
117 |
|
118 |
def get_location(account, container, object): |
119 |
assert '/' not in account, "Invalid account" |
120 |
assert '/' not in container, "Invalid container" |
121 |
return 'pithos://%s/%s/%s' % (account, container, object) |
122 |
|
123 |
def split_location(location): |
124 |
"""Returns (accout, container, object) from a location string"""
|
125 |
t = location.split('/', 4) |
126 |
assert len(t) == 5, "Invalid location" |
127 |
return t[2:5] |
128 |
|
129 |
|
130 |
class BackendException(Exception): pass |
131 |
|
132 |
|
133 |
class ImageBackend(object): |
134 |
"""A wrapper arround the pithos backend to simplify image handling."""
|
135 |
|
136 |
def __init__(self, user): |
137 |
self.user = user
|
138 |
self.container = settings.PITHOS_IMAGE_CONTAINER
|
139 |
|
140 |
original_filters = warnings.filters |
141 |
warnings.simplefilter('ignore') # Suppress SQLAlchemy warnings |
142 |
self.backend = connect_backend()
|
143 |
warnings.filters = original_filters # Restore warnings
|
144 |
|
145 |
try:
|
146 |
self.backend.put_container(self.user, self.user, self.container) |
147 |
except NameError: |
148 |
pass # Container already exists |
149 |
|
150 |
def _get_image(self, location): |
151 |
def format_timestamp(t): |
152 |
return strftime('%Y-%m-%d %H:%M:%S', gmtime(t)) |
153 |
|
154 |
account, container, object = split_location(location) |
155 |
|
156 |
try:
|
157 |
versions = self.backend.list_versions(self.user, account, |
158 |
container, object)
|
159 |
except NameError: |
160 |
return None |
161 |
|
162 |
image = {} |
163 |
|
164 |
meta = self._get_meta(location)
|
165 |
if meta:
|
166 |
image['deleted_at'] = '' |
167 |
else:
|
168 |
# Object was deleted, use the latest version
|
169 |
version, timestamp = versions[-1]
|
170 |
meta = self._get_meta(location, version)
|
171 |
image['deleted_at'] = format_timestamp(timestamp)
|
172 |
|
173 |
permissions = self._get_permissions(location)
|
174 |
|
175 |
image['checksum'] = meta['_hash'] |
176 |
image['created_at'] = format_timestamp(versions[0][1]) |
177 |
image['is_public'] = '*' in permissions.get('read', []) |
178 |
image['location'] = location
|
179 |
image['owner'] = account
|
180 |
image['size'] = meta['_bytes'] |
181 |
image['store'] = 'pithos' |
182 |
image['updated_at'] = format_timestamp(meta['_modified']) |
183 |
image['properties'] = {}
|
184 |
|
185 |
for key, val in meta.items(): |
186 |
if key in SYSTEM_META | USER_META: |
187 |
image[key] = val |
188 |
elif key.startswith(PROPERTY_PREFIX):
|
189 |
key = key[len(PROPERTY_PREFIX):]
|
190 |
image['properties'][key] = val
|
191 |
|
192 |
if 'id' in image: |
193 |
return image
|
194 |
else:
|
195 |
return None |
196 |
|
197 |
def _get_meta(self, location, version=None): |
198 |
account, container, object = split_location(location) |
199 |
try:
|
200 |
_meta = self.backend.get_object_meta(self.user, account, container, |
201 |
object, version)
|
202 |
except NameError: |
203 |
return None |
204 |
|
205 |
user_prefix = 'x-object-meta-' + PLANKTON_PREFIX
|
206 |
system_prefix = PLANKTON_PREFIX |
207 |
meta = {} |
208 |
|
209 |
for key, val in _meta.items(): |
210 |
key = key.lower() |
211 |
if key.startswith(user_prefix):
|
212 |
key = key[len(user_prefix):]
|
213 |
elif key.startswith(system_prefix):
|
214 |
key = key[len(system_prefix):]
|
215 |
else:
|
216 |
key = '_' + key
|
217 |
meta[key] = val |
218 |
|
219 |
return meta
|
220 |
|
221 |
def _get_permissions(self, location): |
222 |
account, container, object = split_location(location) |
223 |
action, path, permissions = self.backend.get_object_permissions(
|
224 |
self.user, account, container, object) |
225 |
return permissions
|
226 |
|
227 |
def _iter(self, keys=[], public=False): |
228 |
backend = self.backend
|
229 |
container = self.container
|
230 |
user = None if public else self.user |
231 |
|
232 |
accounts = set(backend.list_accounts(user))
|
233 |
if user:
|
234 |
accounts.add(user) |
235 |
|
236 |
for account in accounts: |
237 |
for path, version_id in backend.list_objects(user, account, |
238 |
container, prefix='', delimiter='/', |
239 |
keys=prefix_keys(keys)): |
240 |
try:
|
241 |
location = get_location(account, container, path) |
242 |
image = self._get_image(location)
|
243 |
if image:
|
244 |
yield image
|
245 |
except NotAllowedError:
|
246 |
continue
|
247 |
|
248 |
def _store(self, f, size=None): |
249 |
"""Breaks data into blocks and stores them in the backend"""
|
250 |
|
251 |
bytes = 0
|
252 |
hashmap = [] |
253 |
backend = self.backend
|
254 |
blocksize = backend.block_size |
255 |
|
256 |
data = f.read(blocksize) |
257 |
while data:
|
258 |
hash = backend.put_block(data) |
259 |
hashmap.append(hash)
|
260 |
bytes += len(data) |
261 |
data = f.read(blocksize) |
262 |
|
263 |
if size and size != bytes: |
264 |
raise BackendException("Invalid size") |
265 |
|
266 |
return hashmap, bytes |
267 |
|
268 |
def _update(self, location, size, hashmap, meta, permissions): |
269 |
account, container, object = split_location(location) |
270 |
self.backend.update_object_hashmap(self.user, account, container, |
271 |
object, size, hashmap, meta=prefix_meta(meta),
|
272 |
replace_meta=True, permissions=permissions)
|
273 |
|
274 |
def _update_meta(self, location, meta): |
275 |
account, container, object = split_location(location) |
276 |
self.backend.update_object_meta(self.user, account, container, object, |
277 |
prefix_meta(meta)) |
278 |
|
279 |
def _update_permissions(self, location, permissions): |
280 |
account, container, object = split_location(location) |
281 |
self.backend.update_object_permissions(self.user, account, container, |
282 |
object, permissions)
|
283 |
|
284 |
def add_user(self, image_id, user): |
285 |
image = self.get_meta(image_id)
|
286 |
assert image, "Image not found" |
287 |
|
288 |
location = image['location']
|
289 |
permissions = self._get_permissions(location)
|
290 |
read = set(permissions.get('read', [])) |
291 |
read.add(user) |
292 |
permissions['read'] = list(read) |
293 |
self._update_permissions(location, permissions)
|
294 |
|
295 |
def close(self): |
296 |
self.backend.close()
|
297 |
|
298 |
def get_data(self, location): |
299 |
account, container, object = split_location(location) |
300 |
size, hashmap = self.backend.get_object_hashmap(self.user, account, |
301 |
container, object)
|
302 |
data = ''.join(self.backend.get_block(hash) for hash in hashmap) |
303 |
assert len(data) == size |
304 |
return data
|
305 |
|
306 |
def get_meta(self, image_id): |
307 |
# This is an inefficient implementation.
|
308 |
# Due to limitations of the backend we have to iterate all files
|
309 |
# in order to find the one with specific id.
|
310 |
for image in self._iter(keys=['id']): |
311 |
if image and image['id'] == image_id: |
312 |
return image
|
313 |
return None |
314 |
|
315 |
def iter_public(self, filters): |
316 |
keys = set()
|
317 |
for key, val in filters.items(): |
318 |
if key in ('size_min', 'size_max'): |
319 |
key = 'size'
|
320 |
keys.add(key) |
321 |
|
322 |
for image in self._iter(keys=keys, public=True): |
323 |
for key, val in filters.items(): |
324 |
if key == 'size_min': |
325 |
if image['size'] < int(val): |
326 |
break
|
327 |
elif key == 'size_max': |
328 |
if image['size'] > int(val): |
329 |
break
|
330 |
else:
|
331 |
if image[key] != val:
|
332 |
break
|
333 |
else:
|
334 |
yield image
|
335 |
|
336 |
def iter_shared(self): |
337 |
for image in self._iter(): |
338 |
yield image
|
339 |
|
340 |
def list_public(self, filters, params): |
341 |
images = list(self.iter_public(filters)) |
342 |
key = itemgetter(params.get('sort_key', 'created_at')) |
343 |
reverse = params.get('sort_dir', 'desc') == 'desc' |
344 |
images.sort(key=key, reverse=reverse) |
345 |
return images
|
346 |
|
347 |
def list_users(self, image_id): |
348 |
image = self.get_meta(image_id)
|
349 |
assert image, "Image not found" |
350 |
|
351 |
permissions = self._get_permissions(image['location']) |
352 |
return [user for user in permissions.get('read', []) if user != '*'] |
353 |
|
354 |
def put(self, name, f, params): |
355 |
assert 'checksum' not in params, "Passing a checksum is not supported" |
356 |
assert 'id' not in params, "Passing an ID is not supported" |
357 |
assert params.pop('store', 'pithos') == 'pithos', "Invalid store" |
358 |
assert params.setdefault('disk_format', |
359 |
settings.DEFAULT_DISK_FORMAT) in \
|
360 |
settings.ALLOWED_DISK_FORMATS, "Invalid disk_format"
|
361 |
assert params.setdefault('container_format', |
362 |
settings.DEFAULT_CONTAINER_FORMAT) in \
|
363 |
settings.ALLOWED_CONTAINER_FORMATS, "Invalid container_format"
|
364 |
|
365 |
filename = params.pop('filename', name)
|
366 |
location = 'pithos://%s/%s/%s' % (self.user, self.container, filename) |
367 |
image_id = get_image_id(location) |
368 |
is_public = params.pop('is_public', False) |
369 |
permissions = {'read': ['*']} if is_public else {} |
370 |
size = params.pop('size', None) |
371 |
|
372 |
hashmap, size = self._store(f, size)
|
373 |
|
374 |
meta = {} |
375 |
meta['properties'] = params.pop('properties', {}) |
376 |
meta.update(id=image_id, name=name, status='available', **params)
|
377 |
|
378 |
self._update(location, size, hashmap, meta, permissions)
|
379 |
return self.get_meta(image_id) |
380 |
|
381 |
def register(self, name, location, params): |
382 |
assert 'id' not in params, "Passing an ID is not supported" |
383 |
assert location.startswith('pithos://'), "Invalid location" |
384 |
assert params.pop('store', 'pithos') == 'pithos', "Invalid store" |
385 |
assert params.setdefault('disk_format', |
386 |
settings.DEFAULT_DISK_FORMAT) in \
|
387 |
settings.ALLOWED_DISK_FORMATS, "Invalid disk_format"
|
388 |
assert params.setdefault('container_format', |
389 |
settings.DEFAULT_CONTAINER_FORMAT) in \
|
390 |
settings.ALLOWED_CONTAINER_FORMATS, "Invalid container_format"
|
391 |
|
392 |
user = self.user
|
393 |
account, container, object = split_location(location) |
394 |
image_id = get_image_id(location) |
395 |
|
396 |
meta = self._get_meta(location)
|
397 |
|
398 |
size = params.pop('size', meta['_bytes']) |
399 |
if size != meta['_bytes']: |
400 |
raise BackendException("Invalid size") |
401 |
|
402 |
checksum = params.pop('checksum', meta['_hash']) |
403 |
if checksum != meta['_hash']: |
404 |
raise BackendException("Invalid checksum") |
405 |
|
406 |
is_public = params.pop('is_public', False) |
407 |
permissions = {'read': ['*']} if is_public else {} |
408 |
|
409 |
meta = {} |
410 |
meta['properties'] = params.pop('properties', {}) |
411 |
meta.update(id=image_id, name=name, status='available', **params)
|
412 |
|
413 |
self._update_meta(location, meta)
|
414 |
self._update_permissions(location, permissions)
|
415 |
return self.get_meta(image_id) |
416 |
|
417 |
def remove_user(self, image_id, user): |
418 |
image = self.get_meta(image_id)
|
419 |
assert image, "Image not found" |
420 |
|
421 |
location = image['location']
|
422 |
permissions = self._get_permissions(location)
|
423 |
try:
|
424 |
permissions.get('read', []).remove(user)
|
425 |
except ValueError: |
426 |
return # User did not have access anyway |
427 |
self._update_permissions(location, permissions)
|
428 |
|
429 |
def replace_users(self, image_id, users): |
430 |
image = self.get_meta(image_id)
|
431 |
assert image, "Image not found" |
432 |
|
433 |
location = image['location']
|
434 |
permissions = self._get_permissions(location)
|
435 |
permissions['read'] = users
|
436 |
if image.get('is_public', False): |
437 |
permissions['read'].append('*') |
438 |
self._update_permissions(location, permissions)
|
439 |
|
440 |
def update(self, image_id, params): |
441 |
image = self.get_meta(image_id)
|
442 |
assert image, "Image not found" |
443 |
|
444 |
location = image['location']
|
445 |
is_public = params.pop('is_public', None) |
446 |
if is_public is not None: |
447 |
permissions = self._get_permissions(location)
|
448 |
read = set(permissions.get('read', [])) |
449 |
if is_public:
|
450 |
read.add('*')
|
451 |
else:
|
452 |
read.discard('*')
|
453 |
permissions['read'] = list(read) |
454 |
self.backend._update_permissions(location, permissions)
|
455 |
|
456 |
meta = {} |
457 |
meta['properties'] = params.pop('properties', {}) |
458 |
meta.update(**params) |
459 |
|
460 |
self._update_meta(location, meta)
|
461 |
return self.get_meta(image_id) |