Consolidate glance.utils into glance.common.utils

Change-Id: I2512b1d3d47f7d3f45707aa5774d9b4b3cb5c26e
This commit is contained in:
Brian Waldon 2011-11-01 11:14:59 -04:00
parent 45c7280783
commit bd00433233
8 changed files with 238 additions and 268 deletions

View File

@ -43,8 +43,8 @@ gettext.install('glance', unicode=1)
from glance import client as glance_client
from glance import version
from glance.common import exception
from glance.common import utils as common_utils
from glance import utils
from glance.common import utils
SUCCESS = 0
FAILURE = 1
@ -214,7 +214,7 @@ EXAMPLES
return FAILURE
image_meta = {'name': fields.pop('name'),
'is_public': common_utils.bool_from_string(
'is_public': utils.bool_from_string(
fields.pop('is_public', False)),
'disk_format': fields.pop('disk_format', 'raw'),
'min_disk': fields.pop('min_disk', 0),
@ -340,7 +340,7 @@ to spell field names correctly. :)"""
# Have to handle "boolean" values specially...
if 'is_public' in fields:
image_meta['is_public'] = common_utils.bool_from_string(
image_meta['is_public'] = utils.bool_from_string(
fields.pop('is_public'))
# Add custom attributes, which are all the arguments remaining

View File

@ -47,7 +47,6 @@ from glance.store import (get_from_backend,
get_store_from_scheme,
UnsupportedBackend)
from glance import registry
from glance import utils
logger = logging.getLogger('glance.api.v1.images')
@ -596,7 +595,7 @@ class ImageDeserializer(wsgi.JSONRequestDeserializer):
def _deserialize(self, request):
result = {}
result['image_meta'] = utils.get_image_meta_from_headers(request)
result['image_meta'] = wsgi.get_image_meta_from_headers(request)
data = request.body_file if self.has_body(request) else None
result['image_data'] = data
return result
@ -630,7 +629,7 @@ class ImageSerializer(wsgi.JSONResponseSerializer):
:param response: The Webob Response object
:param image_meta: Mapping of image metadata
"""
headers = utils.image_meta_to_http_headers(image_meta)
headers = wsgi.image_meta_to_http_headers(image_meta)
for k, v in headers.items():
response.headers[k] = v

View File

@ -26,7 +26,7 @@ import os
from glance.api.v1 import images as v1_images
from glance.common import client as base_client
from glance.common import exception
from glance import utils
from glance.common import wsgi
#TODO(jaypipes) Allow a logger param for client classes
@ -82,7 +82,7 @@ class V1Client(base_client.BaseClient):
"""
res = self.do_request("GET", "/images/%s" % image_id)
image = utils.get_image_meta_from_headers(res)
image = wsgi.get_image_meta_from_headers(res)
return image, base_client.ImageBodyIterator(res)
def get_image_meta(self, image_id):
@ -93,7 +93,7 @@ class V1Client(base_client.BaseClient):
"""
res = self.do_request("HEAD", "/images/%s" % image_id)
image = utils.get_image_meta_from_headers(res)
image = wsgi.get_image_meta_from_headers(res)
return image
def _get_image_size(self, image_data):
@ -136,7 +136,7 @@ class V1Client(base_client.BaseClient):
:retval The newly-stored image's metadata.
"""
headers = utils.image_meta_to_http_headers(image_meta or {})
headers = wsgi.image_meta_to_http_headers(image_meta or {})
if image_data:
body = image_data
@ -159,7 +159,7 @@ class V1Client(base_client.BaseClient):
if image_meta is None:
image_meta = {}
headers = utils.image_meta_to_http_headers(image_meta)
headers = wsgi.image_meta_to_http_headers(image_meta)
if image_data:
body = image_data
@ -275,7 +275,7 @@ class V1Client(base_client.BaseClient):
Pre-fetch a specified image from the cache
"""
res = self.do_request("HEAD", "/images/%s" % image_id)
image = utils.get_image_meta_from_headers(res)
image = wsgi.get_image_meta_from_headers(res)
self.do_request("PUT", "/cached_images/%s" % image_id)
return True

View File

@ -33,6 +33,9 @@ import uuid
from glance.common import exception
logger = logging.getLogger('glance.utils')
TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
@ -128,3 +131,65 @@ def safe_remove(path):
except OSError, e:
if e.errno != errno.ENOENT:
raise
class PrettyTable(object):
"""Creates an ASCII art table for use in bin/glance
Example:
ID Name Size Hits
--- ----------------- ------------ -----
122 image 22 0
"""
def __init__(self):
self.columns = []
def add_column(self, width, label="", just='l'):
"""Add a column to the table
:param width: number of characters wide the column should be
:param label: column heading
:param just: justification for the column, 'l' for left,
'r' for right
"""
self.columns.append((width, label, just))
def make_header(self):
label_parts = []
break_parts = []
for width, label, _ in self.columns:
# NOTE(sirp): headers are always left justified
label_part = self._clip_and_justify(label, width, 'l')
label_parts.append(label_part)
break_part = '-' * width
break_parts.append(break_part)
label_line = ' '.join(label_parts)
break_line = ' '.join(break_parts)
return '\n'.join([label_line, break_line])
def make_row(self, *args):
row = args
row_parts = []
for data, (width, _, just) in zip(row, self.columns):
row_part = self._clip_and_justify(data, width, just)
row_parts.append(row_part)
row_line = ' '.join(row_parts)
return row_line
@staticmethod
def _clip_and_justify(data, width, just):
# clip field to column width
clipped_data = str(data)[:width]
if just == 'r':
# right justify
justified = clipped_data.rjust(width)
else:
# left justify
justified = clipped_data.ljust(width)
return justified

View File

@ -405,3 +405,73 @@ class Resource(object):
pass
return args
def image_meta_to_http_headers(image_meta):
"""
Returns a set of image metadata into a dict
of HTTP headers that can be fed to either a Webob
Request object or an httplib.HTTP(S)Connection object
:param image_meta: Mapping of image metadata
"""
headers = {}
for k, v in image_meta.items():
if v is None:
v = ''
if k == 'properties':
for pk, pv in v.items():
if pv is None:
pv = ''
headers["x-image-meta-property-%s"
% pk.lower()] = unicode(pv)
else:
headers["x-image-meta-%s" % k.lower()] = unicode(v)
return headers
def get_image_meta_from_headers(response):
"""
Processes HTTP headers from a supplied response that
match the x-image-meta and x-image-meta-property and
returns a mapping of image metadata and properties
:param response: Response to process
"""
result = {}
properties = {}
if hasattr(response, 'getheaders'): # httplib.HTTPResponse
headers = response.getheaders()
else: # webob.Response
headers = response.headers.items()
for key, value in headers:
key = str(key.lower())
if key.startswith('x-image-meta-property-'):
field_name = key[len('x-image-meta-property-'):].replace('-', '_')
properties[field_name] = value or None
elif key.startswith('x-image-meta-'):
field_name = key[len('x-image-meta-'):].replace('-', '_')
result[field_name] = value or None
result['properties'] = properties
if 'size' in result:
result['size'] = int(result['size'])
if 'is_public' in result:
result['is_public'] = bool_from_header_value(result['is_public'])
if 'deleted' in result:
result['deleted'] = bool_from_header_value(result['deleted'])
return result
def bool_from_header_value(value):
"""
Returns True if value is a boolean True or the
string 'true', case-insensitive, False otherwise
"""
if isinstance(value, bool):
return value
elif isinstance(value, (basestring, unicode)):
if str(value).lower() == 'true':
return True
return False

View File

@ -17,100 +17,15 @@
import unittest
from glance import utils
from glance.common import utils as common_utils
from glance.common import utils
class TestUtils(unittest.TestCase):
"""Test routines in glance.utils"""
def test_headers_are_unicode(self):
"""
Verifies that the headers returned by conversion code are unicode.
Headers are passed via http in non-testing mode, which automatically
converts them to unicode. Verifying that the method does the
conversion proves that we aren't passing data that works in tests
but will fail in production.
"""
fixture = {'name': 'fake public image',
'is_public': True,
'type': 'kernel',
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {'distro': 'Ubuntu 10.04 LTS'}}
headers = utils.image_meta_to_http_headers(fixture)
for k, v in headers.iteritems():
self.assert_(isinstance(v, unicode), "%s is not unicode" % v)
def test_data_passed_properly_through_headers(self):
"""
Verifies that data is the same after being passed through headers
"""
fixture = {'name': 'fake public image',
'is_public': True,
'deleted': False,
'type': 'kernel',
'name': None,
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {'distro': 'Ubuntu 10.04 LTS'}}
headers = utils.image_meta_to_http_headers(fixture)
class FakeResponse():
pass
response = FakeResponse()
response.headers = headers
result = utils.get_image_meta_from_headers(response)
for k, v in fixture.iteritems():
self.assertEqual(v, result[k])
def test_boolean_header_values(self):
"""
Tests that boolean headers like is_public can be set
to True if any of ('True', 'On', 1) is provided, case-insensitive
"""
fixtures = [{'is_public': True},
{'is_public': 'True'},
{'is_public': 'true'}]
expected = {'is_public': True}
class FakeResponse():
pass
for fixture in fixtures:
headers = utils.image_meta_to_http_headers(fixture)
response = FakeResponse()
response.headers = headers
result = utils.get_image_meta_from_headers(response)
for k, v in expected.items():
self.assertEqual(v, result[k])
# Ensure False for other values...
fixtures = [{'is_public': False},
{'is_public': 'Off'},
{'is_public': 'on'},
{'is_public': '1'},
{'is_public': 'False'}]
expected = {'is_public': False}
for fixture in fixtures:
headers = utils.image_meta_to_http_headers(fixture)
response = FakeResponse()
response.headers = headers
result = utils.get_image_meta_from_headers(response)
for k, v in expected.items():
self.assertEqual(v, result[k])
def test_generate_uuid_format(self):
"""Check the format of a uuid"""
uuid = common_utils.generate_uuid()
uuid = utils.generate_uuid()
self.assertTrue(isinstance(uuid, basestring))
self.assertTrue(len(uuid), 36)
# make sure there are 4 dashes
@ -118,15 +33,15 @@ class TestUtils(unittest.TestCase):
def test_generate_uuid_unique(self):
"""Ensure generate_uuid will return unique values"""
uuids = [common_utils.generate_uuid() for i in range(5)]
uuids = [utils.generate_uuid() for i in range(5)]
# casting to set will drop duplicate values
unique = set(uuids)
self.assertEqual(len(uuids), len(list(unique)))
def test_is_uuid_like_success(self):
fixture = 'b694bf02-6b01-4905-a50e-fcf7bce7e4d2'
self.assertTrue(common_utils.is_uuid_like(fixture))
self.assertTrue(utils.is_uuid_like(fixture))
def test_is_uuid_like_fails(self):
fixture = 'pants'
self.assertFalse(common_utils.is_uuid_like(fixture))
self.assertFalse(utils.is_uuid_like(fixture))

View File

@ -182,3 +182,89 @@ class JSONRequestDeserializerTest(unittest.TestCase):
actual = wsgi.JSONRequestDeserializer().default(request)
expected = {"body": {"key": "value"}}
self.assertEqual(actual, expected)
class TestHelpers(unittest.TestCase):
def test_headers_are_unicode(self):
"""
Verifies that the headers returned by conversion code are unicode.
Headers are passed via http in non-testing mode, which automatically
converts them to unicode. Verifying that the method does the
conversion proves that we aren't passing data that works in tests
but will fail in production.
"""
fixture = {'name': 'fake public image',
'is_public': True,
'type': 'kernel',
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {'distro': 'Ubuntu 10.04 LTS'}}
headers = wsgi.image_meta_to_http_headers(fixture)
for k, v in headers.iteritems():
self.assert_(isinstance(v, unicode), "%s is not unicode" % v)
def test_data_passed_properly_through_headers(self):
"""
Verifies that data is the same after being passed through headers
"""
fixture = {'name': 'fake public image',
'is_public': True,
'deleted': False,
'type': 'kernel',
'name': None,
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {'distro': 'Ubuntu 10.04 LTS'}}
headers = wsgi.image_meta_to_http_headers(fixture)
class FakeResponse():
pass
response = FakeResponse()
response.headers = headers
result = wsgi.get_image_meta_from_headers(response)
for k, v in fixture.iteritems():
self.assertEqual(v, result[k])
def test_boolean_header_values(self):
"""
Tests that boolean headers like is_public can be set
to True if any of ('True', 'On', 1) is provided, case-insensitive
"""
fixtures = [{'is_public': True},
{'is_public': 'True'},
{'is_public': 'true'}]
expected = {'is_public': True}
class FakeResponse():
pass
for fixture in fixtures:
headers = wsgi.image_meta_to_http_headers(fixture)
response = FakeResponse()
response.headers = headers
result = wsgi.get_image_meta_from_headers(response)
for k, v in expected.items():
self.assertEqual(v, result[k])
# Ensure False for other values...
fixtures = [{'is_public': False},
{'is_public': 'Off'},
{'is_public': 'on'},
{'is_public': '1'},
{'is_public': 'False'}]
expected = {'is_public': False}
for fixture in fixtures:
headers = wsgi.image_meta_to_http_headers(fixture)
response = FakeResponse()
response.headers = headers
result = wsgi.get_image_meta_from_headers(response)
for k, v in expected.items():
self.assertEqual(v, result[k])

View File

@ -1,165 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
A few utility routines used throughout Glance
"""
import logging
logger = logging.getLogger('glance.utils')
def image_meta_to_http_headers(image_meta):
"""
Returns a set of image metadata into a dict
of HTTP headers that can be fed to either a Webob
Request object or an httplib.HTTP(S)Connection object
:param image_meta: Mapping of image metadata
"""
headers = {}
for k, v in image_meta.items():
if v is None:
v = ''
if k == 'properties':
for pk, pv in v.items():
if pv is None:
pv = ''
headers["x-image-meta-property-%s"
% pk.lower()] = unicode(pv)
else:
headers["x-image-meta-%s" % k.lower()] = unicode(v)
return headers
def get_image_meta_from_headers(response):
"""
Processes HTTP headers from a supplied response that
match the x-image-meta and x-image-meta-property and
returns a mapping of image metadata and properties
:param response: Response to process
"""
result = {}
properties = {}
if hasattr(response, 'getheaders'): # httplib.HTTPResponse
headers = response.getheaders()
else: # webob.Response
headers = response.headers.items()
for key, value in headers:
key = str(key.lower())
if key.startswith('x-image-meta-property-'):
field_name = key[len('x-image-meta-property-'):].replace('-', '_')
properties[field_name] = value or None
elif key.startswith('x-image-meta-'):
field_name = key[len('x-image-meta-'):].replace('-', '_')
result[field_name] = value or None
result['properties'] = properties
if 'size' in result:
result['size'] = int(result['size'])
if 'is_public' in result:
result['is_public'] = bool_from_header_value(result['is_public'])
if 'deleted' in result:
result['deleted'] = bool_from_header_value(result['deleted'])
return result
def bool_from_header_value(value):
"""
Returns True if value is a boolean True or the
string 'true', case-insensitive, False otherwise
"""
if isinstance(value, bool):
return value
elif isinstance(value, (basestring, unicode)):
if str(value).lower() == 'true':
return True
return False
def has_body(req):
"""
Returns whether a Webob.Request object will possess an entity body.
:param req: Webob.Request object
"""
return req.content_length or 'transfer-encoding' in req.headers
class PrettyTable(object):
"""Creates an ASCII art table for use in bin/glance
Example:
ID Name Size Hits
--- ----------------- ------------ -----
122 image 22 0
"""
def __init__(self):
self.columns = []
def add_column(self, width, label="", just='l'):
"""Add a column to the table
:param width: number of characters wide the column should be
:param label: column heading
:param just: justification for the column, 'l' for left,
'r' for right
"""
self.columns.append((width, label, just))
def make_header(self):
label_parts = []
break_parts = []
for width, label, _ in self.columns:
# NOTE(sirp): headers are always left justified
label_part = self._clip_and_justify(label, width, 'l')
label_parts.append(label_part)
break_part = '-' * width
break_parts.append(break_part)
label_line = ' '.join(label_parts)
break_line = ' '.join(break_parts)
return '\n'.join([label_line, break_line])
def make_row(self, *args):
row = args
row_parts = []
for data, (width, _, just) in zip(row, self.columns):
row_part = self._clip_and_justify(data, width, just)
row_parts.append(row_part)
row_line = ' '.join(row_parts)
return row_line
@staticmethod
def _clip_and_justify(data, width, just):
# clip field to column width
clipped_data = str(data)[:width]
if just == 'r':
# right justify
justified = clipped_data.rjust(width)
else:
# left justify
justified = clipped_data.ljust(width)
return justified