Commit 6e55d336 authored by Christos Stavrakakis's avatar Christos Stavrakakis
Browse files

plankton: Fix handling of HTTP headers

Improve the handling of all the image parameters that are
requested/returned as HTTP headers. All parameters must be 'utf-8'
encoded. User provided values like the image name and image properties
must also be properly quoted. Lower all image properties(HTTP header
keys) and replace all punctuation characters with underscore.

Finally, add section in the image API guide, explaining the required
format for the image metadata.
parent b6d43c2c
......@@ -48,6 +48,17 @@ identity credentials.
<http://docs.openstack.org/developer/glance/glanceapi.html#authentication>`_,
with the only difference being the suggested identity manager.
Image Metadata Format
---------------------
In Cyclades Image API all image metadata are viewed as HTTP headers that are
starting with the `x-image-meta-` prefix. All metadata must be encoded with the
`UTF-8` encoding. Since the image metadata must be valid HTTP headers, user
defined metadata like the image's name or properties must also be properly
quoted. Finally, image properties that are viewed as HTTP headers and are
starting with the `x-image-meta-property-` prefix, are not case-sensitive and
all punctuation characters will be replaced with underscore.
List Available Images
---------------------
......
......@@ -61,9 +61,10 @@ from operator import itemgetter
from collections import namedtuple
from copy import deepcopy
from urllib import quote, unquote
from django.conf import settings
from django.utils import importlib
from django.utils.encoding import smart_unicode
from django.utils.encoding import smart_unicode, smart_str
from pithos.backends.base import NotAllowedError, VersionNotExists, QuotaError
from snf_django.lib.api import faults
......@@ -489,6 +490,9 @@ def create_url(account, container, name):
"""Create a Pithos URL from the object info"""
assert "/" not in account, "Invalid account"
assert "/" not in container, "Invalid container"
account = quote(smart_str(account, encoding="utf-8"))
container = quote(smart_str(container, encoding="utf-8"))
name = quote(smart_str(name, encoding="utf-8"))
return "pithos://%s/%s/%s" % (account, container, name)
......@@ -498,7 +502,9 @@ def split_url(url):
t = url.split('/', 4)
assert t[0] == "pithos:", "Invalid url"
assert len(t) == 5, "Invalid url"
return t[2:5]
account, container, name = t[2:5]
parse = lambda x: smart_unicode(unquote(x), encoding="utf-8")
return parse(account), parse(container), parse(name)
def image_to_dict(location, metadata, permissions):
......
......@@ -32,6 +32,7 @@
# or implied, of GRNET S.A.
import json
import urllib
from mock import patch
from functools import wraps
......@@ -62,7 +63,7 @@ class PlanktonTest(BaseAPITest):
def test_register_image(self, backend):
required = {
"HTTP_X_IMAGE_META_NAME": u"TestImage\u2602",
"HTTP_X_IMAGE_META_LOCATION": "pithos://4321-4321/images/foo"}
"HTTP_X_IMAGE_META_LOCATION": "pithos://4321-4321/%E2%98%82/foo"}
# Check valid name
headers = deepcopy(required)
headers.pop("HTTP_X_IMAGE_META_NAME")
......@@ -137,7 +138,7 @@ class PlanktonTest(BaseAPITest):
self.assertBadRequest(response)
backend().get_uuid.return_value =\
("4321-4321", "images", "foo")
("4321-4321", u"\u2602", "foo")
backend().get_object_permissions.return_value = \
("foo", "foo", {"read": []})
backend().get_object_meta.side_effect = \
......@@ -156,7 +157,7 @@ class PlanktonTest(BaseAPITest):
response = self.post(IMAGES_URL, **headers)
self.assertSuccess(response)
self.assertEqual(response["x-image-meta-location"],
"pithos://4321-4321/images/foo")
"pithos://4321-4321/%E2%98%82/foo")
self.assertEqual(response["x-image-meta-id"], "1234-1234-1234")
self.assertEqual(response["x-image-meta-status"], "AVAILABLE")
self.assertEqual(response["x-image-meta-deleted-at"], "")
......@@ -164,7 +165,7 @@ class PlanktonTest(BaseAPITest):
self.assertEqual(response["x-image-meta-owner"], "4321-4321")
self.assertEqual(response["x-image-meta-size"], "42")
self.assertEqual(response["x-image-meta-checksum"], "unique_hash")
self.assertEqual(response["x-image-meta-name"],
self.assertEqual(urllib.unquote(response["x-image-meta-name"]),
u"TestImage\u2602".encode("utf-8"))
self.assertEqual(response["x-image-meta-container-format"], "bare")
self.assertEqual(response["x-image-meta-disk-format"], "diskdump")
......@@ -194,8 +195,7 @@ class PlanktonTest(BaseAPITest):
name, args, kwargs = backend().update_object_meta.mock_calls[-1]
metadata = args[5]
self.assertEqual(metadata["plankton:property:key1"], "val1")
self.assertEqual(metadata["plankton:property:key2"],
u"\u2601".encode("utf-8"))
self.assertEqual(metadata["plankton:property:key2"], u"\u2601")
self.assertSuccess(response)
def test_unregister_image(self, backend):
......@@ -281,7 +281,7 @@ class PlanktonTest(BaseAPITest):
self.assertEqual(response["x-image-meta-owner"], "img_owner")
self.assertEqual(response["x-image-meta-size"], "42")
self.assertEqual(response["x-image-meta-checksum"], "unique_hash")
self.assertEqual(response["x-image-meta-name"],
self.assertEqual(urllib.unquote(response["x-image-meta-name"]),
u"TestImage\u2602".encode("utf-8"))
self.assertEqual(response["x-image-meta-container-format"], "bare")
self.assertEqual(response["x-image-meta-disk-format"], "diskdump")
......
......@@ -35,11 +35,11 @@ import json
from logging import getLogger
from string import punctuation
from urllib import unquote
from urllib import unquote, quote
from django.conf import settings
from django.http import HttpResponse
from django.utils.encoding import smart_unicode
from django.utils.encoding import smart_unicode, smart_str
from snf_django.lib import api
from snf_django.lib.api import faults
......@@ -63,7 +63,9 @@ LIST_FIELDS = ('status', 'name', 'disk_format', 'container_format', 'size',
DETAIL_FIELDS = ('name', 'disk_format', 'container_format', 'size', 'checksum',
'location', 'created_at', 'updated_at', 'deleted_at',
'status', 'is_public', 'owner', 'properties', 'id',
"is_snapshot")
'is_snapshot', 'description')
PLANKTON_FIELDS = DETAIL_FIELDS + ('store',)
ADD_FIELDS = ('name', 'id', 'store', 'disk_format', 'container_format', 'size',
'checksum', 'is_public', 'owner', 'properties', 'location')
......@@ -78,6 +80,12 @@ CONTAINER_FORMATS = ('aki', 'ari', 'ami', 'bare', 'ovf')
STORE_TYPES = ('pithos')
META_PREFIX = 'HTTP_X_IMAGE_META_'
META_PREFIX_LEN = len(META_PREFIX)
META_PROPERTY_PREFIX = 'HTTP_X_IMAGE_META_PROPERTY_'
META_PROPERTY_PREFIX_LEN = len(META_PROPERTY_PREFIX)
log = getLogger('synnefo.plankton')
......@@ -88,56 +96,71 @@ API_STATUS_FROM_IMAGE_STATUS = {
def _create_image_response(image):
"""Encode the image parameters to HTTP Response Headers.
This function converts all image parameters to HTTP response headers.
All parameters are 'utf-8' encoded. User provided values like the
image name and image properties are also properly quoted.
"""
response = HttpResponse()
for key in DETAIL_FIELDS:
if key == 'properties':
for k, v in image.get('properties', {}).items():
name = 'x-image-meta-property-' + k.replace('_', '-')
response[name] = smart_unicode(v, encoding="utf-8")
for pkey, pval in image.get('properties', {}).items():
pkey = 'x-image-meta-property-' + pkey.replace('_', '-')
pkey = quote(smart_str(pkey, encoding='utf-8'))
pval = quote(smart_str(pval, encoding='utf-8'))
response[pkey] = pval
else:
if key == "status":
img_status = image.get(key, "").upper()
status = API_STATUS_FROM_IMAGE_STATUS.get(img_status,
"UNKNOWN")
response["x-image-meta-status"] = status
else:
name = 'x-image-meta-' + key.replace('_', '-')
response[name] = smart_unicode(image.get(key, ''),
encoding="utf-8")
val = image.get(key, '')
if key == 'status':
val = API_STATUS_FROM_IMAGE_STATUS.get(val.upper(), "UNKNOWN")
if key == 'name' or key == 'description':
val = quote(smart_str(val, encoding='utf-8'))
key = 'x-image-meta-' + key.replace('_', '-')
response[key] = val
return response
def _get_image_headers(request):
def normalize(s):
return ''.join('_' if c in punctuation else c.lower() for c in s)
def headers_to_image_params(request):
"""Decode the HTTP request headers to the acceptable image parameters.
META_PREFIX = 'HTTP_X_IMAGE_META_'
META_PREFIX_LEN = len(META_PREFIX)
META_PROPERTY_PREFIX = 'HTTP_X_IMAGE_META_PROPERTY_'
META_PROPERTY_PREFIX_LEN = len(META_PROPERTY_PREFIX)
Get the image parameters from the headers of the HTTP request. All
parameters must be encoded using 'utf-8' encoding. User provided parameters
like the image name or the image properties must be quoted, so we need to
unquote them.
Finally, all image parameters name (HTTP header keys) are lowered
and all punctuation characters are replaced with underscore.
headers = {'properties': {}}
"""
for key, val in request.META.items():
if key.startswith(META_PROPERTY_PREFIX):
name = normalize(key[META_PROPERTY_PREFIX_LEN:])
headers['properties'][unquote(name)] = \
unquote(smart_unicode(val, encoding='utf-8'))
elif key.startswith(META_PREFIX):
name = normalize(key[META_PREFIX_LEN:])
headers[unquote(name)] = \
unquote(smart_unicode(val, encoding='utf-8'))
def normalize(s):
return ''.join('_' if c in punctuation else c.lower() for c in s)
is_public = headers.get('is_public', None)
if is_public is not None:
headers['is_public'] = True if is_public.lower() == 'true' else False
params = {}
properties = {}
for key, val in request.META.items():
if key.startswith(META_PREFIX):
if key.startswith(META_PROPERTY_PREFIX):
key = key[META_PROPERTY_PREFIX_LEN:]
key = smart_unicode(unquote(key), encoding='utf-8')
val = smart_unicode(unquote(val), encoding='utf-8')
properties[normalize(key)] = val
else:
key = smart_unicode(key[META_PREFIX_LEN:], encoding='utf-8')
key = normalize(key)
if key in PLANKTON_FIELDS:
if key == "name":
val = smart_unicode(unquote(val), encoding='utf-8')
elif key == "is_public" and not isinstance(val, bool):
val = True if val.lower() == 'true' else False
params[key] = val
if not headers['properties']:
del headers['properties']
params['properties'] = properties
return headers
return params
@api.api_method(http_method="POST", user_required=True, logger=log)
......@@ -159,7 +182,7 @@ def add_image(request):
instead of uploading the data.
"""
params = _get_image_headers(request)
params = headers_to_image_params(request)
log.debug('add_image %s', params)
if not set(params.keys()).issubset(set(ADD_FIELDS)):
......@@ -393,7 +416,7 @@ def update_image(request, image_id):
and status.
"""
meta = _get_image_headers(request)
meta = headers_to_image_params(request)
log.debug('update_image %s', meta)
if not set(meta.keys()).issubset(set(UPDATE_FIELDS)):
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment