diff --git a/doc/source/formats.rst b/doc/source/formats.rst index fc2a98f17f..952f68eac5 100644 --- a/doc/source/formats.rst +++ b/doc/source/formats.rst @@ -17,10 +17,11 @@ Disk and Container Formats ========================== -When adding an image to Glance, you are may specify what the virtual -machine image's *disk format* and *container format* are. - -This document explains exactly what these formats are. +When adding an image to Glance, you must specify what the virtual +machine image's *disk format* and *container format* are. Disk and container +formats are configurable on a per-deployment basis. This document intends to +establish a global convention for what specific values of *disk_format* and +*container_format* mean. Disk Format ----------- diff --git a/etc/glance-api.conf b/etc/glance-api.conf index e1a907dda0..bdc878e829 100644 --- a/etc/glance-api.conf +++ b/etc/glance-api.conf @@ -97,6 +97,13 @@ workers = 1 # The default value is 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 ============================ # Send logs to syslog (/dev/log) instead of to file specified diff --git a/glance/api/v1/images.py b/glance/api/v1/images.py index fdf5a6b065..1b0ce4ef57 100644 --- a/glance/api/v1/images.py +++ b/glance/api/v1/images.py @@ -53,13 +53,13 @@ from glance.store import (get_from_backend, get_store_from_location, get_store_from_scheme) -CONF = cfg.CONF LOG = logging.getLogger(__name__) SUPPORTED_PARAMS = glance.api.v1.SUPPORTED_PARAMS SUPPORTED_FILTERS = glance.api.v1.SUPPORTED_FILTERS -CONTAINER_FORMATS = ['ami', 'ari', 'aki', 'bare', 'ovf'] -DISK_FORMATS = ['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi', - 'iso'] + +CONF = cfg.CONF +CONF.import_opt('disk_formats', 'glance.domain') +CONF.import_opt('container_formats', 'glance.domain') def validate_image_meta(req, values): @@ -69,12 +69,12 @@ def validate_image_meta(req, values): container_format = values.get('container_format') 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 raise HTTPBadRequest(explanation=msg, request=req) 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 raise HTTPBadRequest(explanation=msg, request=req) diff --git a/glance/api/v2/images.py b/glance/api/v2/images.py index 92e3ca2a7c..f1adf7b684 100644 --- a/glance/api/v2/images.py +++ b/glance/api/v2/images.py @@ -37,6 +37,8 @@ import glance.store LOG = logging.getLogger(__name__) CONF = cfg.CONF +CONF.import_opt('disk_formats', 'glance.domain') +CONF.import_opt('container_formats', 'glance.domain') class ImagesController(object): @@ -384,8 +386,8 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer): partial_image = None if len(change['path']) == 1: partial_image = {path_root: change['value']} - elif ((path_root in _BASE_PROPERTIES.keys()) and - (_BASE_PROPERTIES[path_root].get('type', '') == 'array')): + elif ((path_root in _get_base_properties().keys()) and + (_get_base_properties()[path_root].get('type', '') == 'array')): # NOTE(zhiyan): cient can use PATCH API to adding element to # the image's existing set property directly. # Such as: 1. using '/locations/N' path to adding a location @@ -591,123 +593,125 @@ class ResponseSerializer(wsgi.JSONResponseSerializer): response.status_int = 204 -_BASE_PROPERTIES = { - 'id': { - '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, - }, - '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': { +def _get_base_properties(): + return { + 'id': { '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, }, - }, - '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'], + 'status': { + 'type': 'string', + 'description': _('Status of the image'), + 'enum': ['queued', 'saving', 'active', 'killed', + 'deleted', 'pending_delete'], }, - 'description': _('A set of URLs to access the image file kept in ' - 'external store'), - }, -} + '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': 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}'}, - {'rel': 'enclosure', 'href': '{file}'}, - {'rel': 'describedby', 'href': '{schema}'}, -] + +def _get_base_links(): + return [ + {'rel': 'self', 'href': '{self}'}, + {'rel': 'enclosure', 'href': '{file}'}, + {'rel': 'describedby', 'href': '{schema}'}, + ] def get_schema(custom_properties=None): - properties = copy.deepcopy(_BASE_PROPERTIES) - links = copy.deepcopy(_BASE_LINKS) + properties = _get_base_properties() + links = _get_base_links() if CONF.allow_additional_image_properties: schema = glance.schema.PermissiveSchema('image', properties, links) else: diff --git a/glance/domain/__init__.py b/glance/domain/__init__.py index 677bf3b68a..89874b6bb5 100644 --- a/glance/domain/__init__.py +++ b/glance/domain/__init__.py @@ -13,11 +13,30 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo.config import cfg + from glance.common import exception from glance.openstack.common import timeutils 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): _readonly_properties = ['created_at', 'updated_at', 'status', 'checksum', 'size'] diff --git a/glance/tests/unit/v1/test_api.py b/glance/tests/unit/v1/test_api.py index 304a5a1ffd..0598d95a1c 100644 --- a/glance/tests/unit/v1/test_api.py +++ b/glance/tests/unit/v1/test_api.py @@ -150,6 +150,80 @@ class TestGlanceAPI(base.IsolatedUnitTest): self.assertEquals(res.status_int, 400) 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): fixture_headers = { 'x-image-meta-store': 'bad', diff --git a/glance/tests/unit/v2/test_images_resource.py b/glance/tests/unit/v2/test_images_resource.py index 128a667fed..cc3ffa3fc0 100644 --- a/glance/tests/unit/v2/test_images_resource.py +++ b/glance/tests/unit/v2/test_images_resource.py @@ -2245,3 +2245,32 @@ class TestImagesSerializerDirectUrl(test_utils.BaseTestCase): self.config(show_image_direct_url=False) image = self._do_show(self.active_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)