Make disk and container formats configurable

* Add disk_formats config attribute
* Add container_formats config attribute
* Implement bp configurable-formats

Change-Id: Ic52ffb46df9438c247ba063748cadd69b9c90bcd
This commit is contained in:
Brian Waldon 2013-08-19 04:23:07 +00:00
parent 9cd75d0b4a
commit 830f27ba34
7 changed files with 255 additions and 121 deletions

View File

@ -17,10 +17,11 @@
Disk and Container Formats Disk and Container Formats
========================== ==========================
When adding an image to Glance, you are may specify what the virtual When adding an image to Glance, you must specify what the virtual
machine image's *disk format* and *container format* are. machine image's *disk format* and *container format* are. Disk and container
formats are configurable on a per-deployment basis. This document intends to
This document explains exactly what these formats are. establish a global convention for what specific values of *disk_format* and
*container_format* mean.
Disk Format Disk Format
----------- -----------

View File

@ -97,6 +97,13 @@ workers = 1
# The default value is false. # The default value is false.
#send_identity_headers = False #send_identity_headers = False
# Supported values for the 'container_format' image attribute
#container_formats=ami,ari,aki,bare,ovf
# Supported values for the 'disk_format' image attribute
#disk_formats=ami,ari,aki,vhd,vmdk,raw,qcow2,vdi,iso
# ================= Syslog Options ============================ # ================= Syslog Options ============================
# Send logs to syslog (/dev/log) instead of to file specified # Send logs to syslog (/dev/log) instead of to file specified

View File

@ -53,13 +53,13 @@ from glance.store import (get_from_backend,
get_store_from_location, get_store_from_location,
get_store_from_scheme) get_store_from_scheme)
CONF = cfg.CONF
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
SUPPORTED_PARAMS = glance.api.v1.SUPPORTED_PARAMS SUPPORTED_PARAMS = glance.api.v1.SUPPORTED_PARAMS
SUPPORTED_FILTERS = glance.api.v1.SUPPORTED_FILTERS SUPPORTED_FILTERS = glance.api.v1.SUPPORTED_FILTERS
CONTAINER_FORMATS = ['ami', 'ari', 'aki', 'bare', 'ovf']
DISK_FORMATS = ['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi', CONF = cfg.CONF
'iso'] CONF.import_opt('disk_formats', 'glance.domain')
CONF.import_opt('container_formats', 'glance.domain')
def validate_image_meta(req, values): def validate_image_meta(req, values):
@ -69,12 +69,12 @@ def validate_image_meta(req, values):
container_format = values.get('container_format') container_format = values.get('container_format')
if 'disk_format' in values: if 'disk_format' in values:
if disk_format not in DISK_FORMATS: if disk_format not in CONF.disk_formats:
msg = "Invalid disk format '%s' for image." % disk_format msg = "Invalid disk format '%s' for image." % disk_format
raise HTTPBadRequest(explanation=msg, request=req) raise HTTPBadRequest(explanation=msg, request=req)
if 'container_format' in values: if 'container_format' in values:
if container_format not in CONTAINER_FORMATS: if container_format not in CONF.container_formats:
msg = "Invalid container format '%s' for image." % container_format msg = "Invalid container format '%s' for image." % container_format
raise HTTPBadRequest(explanation=msg, request=req) raise HTTPBadRequest(explanation=msg, request=req)

View File

@ -37,6 +37,8 @@ import glance.store
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
CONF.import_opt('disk_formats', 'glance.domain')
CONF.import_opt('container_formats', 'glance.domain')
class ImagesController(object): class ImagesController(object):
@ -384,8 +386,8 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer):
partial_image = None partial_image = None
if len(change['path']) == 1: if len(change['path']) == 1:
partial_image = {path_root: change['value']} partial_image = {path_root: change['value']}
elif ((path_root in _BASE_PROPERTIES.keys()) and elif ((path_root in _get_base_properties().keys()) and
(_BASE_PROPERTIES[path_root].get('type', '') == 'array')): (_get_base_properties()[path_root].get('type', '') == 'array')):
# NOTE(zhiyan): cient can use PATCH API to adding element to # NOTE(zhiyan): cient can use PATCH API to adding element to
# the image's existing set property directly. # the image's existing set property directly.
# Such as: 1. using '/locations/N' path to adding a location # Such as: 1. using '/locations/N' path to adding a location
@ -591,123 +593,125 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
response.status_int = 204 response.status_int = 204
_BASE_PROPERTIES = { def _get_base_properties():
'id': { return {
'type': 'string', 'id': {
'description': _('An identifier for the image'),
'pattern': ('^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}'
'-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$'),
},
'name': {
'type': 'string',
'description': _('Descriptive name for the image'),
'maxLength': 255,
},
'status': {
'type': 'string',
'description': _('Status of the image'),
'enum': ['queued', 'saving', 'active', 'killed',
'deleted', 'pending_delete'],
},
'visibility': {
'type': 'string',
'description': _('Scope of image accessibility'),
'enum': ['public', 'private'],
},
'protected': {
'type': 'boolean',
'description': _('If true, image will not be deletable.'),
},
'checksum': {
'type': 'string',
'description': _('md5 hash of image contents.'),
'type': 'string',
'maxLength': 32,
},
'size': {
'type': 'integer',
'description': _('Size of image file in bytes'),
},
'container_format': {
'type': 'string',
'description': _('Format of the container'),
'type': 'string',
'enum': ['bare', 'ovf', 'ami', 'aki', 'ari'],
},
'disk_format': {
'type': 'string',
'description': _('Format of the disk'),
'type': 'string',
'enum': ['raw', 'vhd', 'vmdk', 'vdi', 'iso', 'qcow2',
'aki', 'ari', 'ami'],
},
'created_at': {
'type': 'string',
'description': _('Date and time of image registration'),
#TODO(bcwaldon): our jsonschema library doesn't seem to like the
# format attribute, figure out why!
#'format': 'date-time',
},
'updated_at': {
'type': 'string',
'description': _('Date and time of the last image modification'),
#'format': 'date-time',
},
'tags': {
'type': 'array',
'description': _('List of strings related to the image'),
'items': {
'type': 'string', 'type': 'string',
'description': _('An identifier for the image'),
'pattern': ('^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}'
'-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$'),
},
'name': {
'type': 'string',
'description': _('Descriptive name for the image'),
'maxLength': 255, 'maxLength': 255,
}, },
}, 'status': {
'direct_url': { 'type': 'string',
'type': 'string', 'description': _('Status of the image'),
'description': _('URL to access the image file kept in external ' 'enum': ['queued', 'saving', 'active', 'killed',
'store'), 'deleted', 'pending_delete'],
},
'min_ram': {
'type': 'integer',
'description': _('Amount of ram (in MB) required to boot image.'),
},
'min_disk': {
'type': 'integer',
'description': _('Amount of disk space (in GB) required to boot '
'image.'),
},
'self': {'type': 'string'},
'file': {'type': 'string'},
'schema': {'type': 'string'},
'locations': {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'url': {
'type': 'string',
'maxLength': 255,
},
'metadata': {
'type': 'object',
},
},
'required': ['url', 'metadata'],
}, },
'description': _('A set of URLs to access the image file kept in ' 'visibility': {
'external store'), 'type': 'string',
}, 'description': _('Scope of image accessibility'),
} 'enum': ['public', 'private'],
},
'protected': {
'type': 'boolean',
'description': _('If true, image will not be deletable.'),
},
'checksum': {
'type': 'string',
'description': _('md5 hash of image contents.'),
'type': 'string',
'maxLength': 32,
},
'size': {
'type': 'integer',
'description': _('Size of image file in bytes'),
},
'container_format': {
'type': 'string',
'description': _('Format of the container'),
'type': 'string',
'enum': CONF.container_formats,
},
'disk_format': {
'type': 'string',
'description': _('Format of the disk'),
'type': 'string',
'enum': CONF.disk_formats,
},
'created_at': {
'type': 'string',
'description': _('Date and time of image registration'),
#TODO(bcwaldon): our jsonschema library doesn't seem to like the
# format attribute, figure out why!
#'format': 'date-time',
},
'updated_at': {
'type': 'string',
'description': _('Date and time of the last image modification'),
#'format': 'date-time',
},
'tags': {
'type': 'array',
'description': _('List of strings related to the image'),
'items': {
'type': 'string',
'maxLength': 255,
},
},
'direct_url': {
'type': 'string',
'description': _('URL to access the image file kept in external '
'store'),
},
'min_ram': {
'type': 'integer',
'description': _('Amount of ram (in MB) required to boot image.'),
},
'min_disk': {
'type': 'integer',
'description': _('Amount of disk space (in GB) required to boot '
'image.'),
},
'self': {'type': 'string'},
'file': {'type': 'string'},
'schema': {'type': 'string'},
'locations': {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'url': {
'type': 'string',
'maxLength': 255,
},
'metadata': {
'type': 'object',
},
},
'required': ['url', 'metadata'],
},
'description': _('A set of URLs to access the image file kept in '
'external store'),
},
}
_BASE_LINKS = [
{'rel': 'self', 'href': '{self}'}, def _get_base_links():
{'rel': 'enclosure', 'href': '{file}'}, return [
{'rel': 'describedby', 'href': '{schema}'}, {'rel': 'self', 'href': '{self}'},
] {'rel': 'enclosure', 'href': '{file}'},
{'rel': 'describedby', 'href': '{schema}'},
]
def get_schema(custom_properties=None): def get_schema(custom_properties=None):
properties = copy.deepcopy(_BASE_PROPERTIES) properties = _get_base_properties()
links = copy.deepcopy(_BASE_LINKS) links = _get_base_links()
if CONF.allow_additional_image_properties: if CONF.allow_additional_image_properties:
schema = glance.schema.PermissiveSchema('image', properties, links) schema = glance.schema.PermissiveSchema('image', properties, links)
else: else:

View File

@ -13,11 +13,30 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from oslo.config import cfg
from glance.common import exception from glance.common import exception
from glance.openstack.common import timeutils from glance.openstack.common import timeutils
from glance.openstack.common import uuidutils from glance.openstack.common import uuidutils
image_format_opts = [
cfg.ListOpt('container_formats',
default=['ami', 'ari', 'aki', 'bare', 'ovf'],
help=_("Supported values for the 'container_format' "
"image attribute")),
cfg.ListOpt('disk_formats',
default=['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2',
'vdi', 'iso'],
help=_("Supported values for the 'disk_format' "
"image attribute")),
]
CONF = cfg.CONF
CONF.register_opts(image_format_opts)
class ImageFactory(object): class ImageFactory(object):
_readonly_properties = ['created_at', 'updated_at', 'status', 'checksum', _readonly_properties = ['created_at', 'updated_at', 'status', 'checksum',
'size'] 'size']

View File

@ -150,6 +150,80 @@ class TestGlanceAPI(base.IsolatedUnitTest):
self.assertEquals(res.status_int, 400) self.assertEquals(res.status_int, 400)
self.assertTrue('Invalid disk format' in res.body, res.body) self.assertTrue('Invalid disk format' in res.body, res.body)
def test_configured_disk_format_good(self):
self.config(disk_formats=['foo'])
fixture_headers = {
'x-image-meta-store': 'bad',
'x-image-meta-name': 'bogus',
'x-image-meta-location': 'http://localhost:0/image.tar.gz',
'x-image-meta-disk-format': 'foo',
'x-image-meta-container-format': 'bare',
}
req = webob.Request.blank("/images")
req.method = 'POST'
for k, v in fixture_headers.iteritems():
req.headers[k] = v
res = req.get_response(self.api)
self.assertEquals(res.status_int, 201)
def test_configured_disk_format_bad(self):
self.config(disk_formats=['foo'])
fixture_headers = {
'x-image-meta-store': 'bad',
'x-image-meta-name': 'bogus',
'x-image-meta-location': 'http://localhost:0/image.tar.gz',
'x-image-meta-disk-format': 'bar',
'x-image-meta-container-format': 'bare',
}
req = webob.Request.blank("/images")
req.method = 'POST'
for k, v in fixture_headers.iteritems():
req.headers[k] = v
res = req.get_response(self.api)
self.assertEquals(res.status_int, 400)
self.assertTrue('Invalid disk format' in res.body, res.body)
def test_configured_container_format_good(self):
self.config(container_formats=['foo'])
fixture_headers = {
'x-image-meta-store': 'bad',
'x-image-meta-name': 'bogus',
'x-image-meta-location': 'http://localhost:0/image.tar.gz',
'x-image-meta-disk-format': 'raw',
'x-image-meta-container-format': 'foo',
}
req = webob.Request.blank("/images")
req.method = 'POST'
for k, v in fixture_headers.iteritems():
req.headers[k] = v
res = req.get_response(self.api)
self.assertEquals(res.status_int, 201)
def test_configured_container_format_bad(self):
self.config(container_formats=['foo'])
fixture_headers = {
'x-image-meta-store': 'bad',
'x-image-meta-name': 'bogus',
'x-image-meta-location': 'http://localhost:0/image.tar.gz',
'x-image-meta-disk-format': 'raw',
'x-image-meta-container-format': 'bar',
}
req = webob.Request.blank("/images")
req.method = 'POST'
for k, v in fixture_headers.iteritems():
req.headers[k] = v
res = req.get_response(self.api)
self.assertEquals(res.status_int, 400)
self.assertTrue('Invalid container format' in res.body, res.body)
def test_container_and_disk_amazon_format_differs(self): def test_container_and_disk_amazon_format_differs(self):
fixture_headers = { fixture_headers = {
'x-image-meta-store': 'bad', 'x-image-meta-store': 'bad',

View File

@ -2245,3 +2245,32 @@ class TestImagesSerializerDirectUrl(test_utils.BaseTestCase):
self.config(show_image_direct_url=False) self.config(show_image_direct_url=False)
image = self._do_show(self.active_image) image = self._do_show(self.active_image)
self.assertFalse('direct_url' in image) self.assertFalse('direct_url' in image)
class TestImageSchemaFormatConfiguration(test_utils.BaseTestCase):
def test_default_disk_formats(self):
schema = glance.api.v2.images.get_schema()
expected = ['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2',
'vdi', 'iso']
actual = schema.properties['disk_format']['enum']
self.assertEqual(expected, actual)
def test_custom_disk_formats(self):
self.config(disk_formats=['gabe'])
schema = glance.api.v2.images.get_schema()
expected = ['gabe']
actual = schema.properties['disk_format']['enum']
self.assertEqual(expected, actual)
def test_default_container_formats(self):
schema = glance.api.v2.images.get_schema()
expected = ['ami', 'ari', 'aki', 'bare', 'ovf']
actual = schema.properties['container_format']['enum']
self.assertEqual(expected, actual)
def test_custom_container_formats(self):
self.config(container_formats=['mark'])
schema = glance.api.v2.images.get_schema()
expected = ['mark']
actual = schema.properties['container_format']['enum']
self.assertEqual(expected, actual)