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:
parent
9cd75d0b4a
commit
830f27ba34
@ -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
|
||||
-----------
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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']
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user