Added min_disk and min_ram properties to images

Fixes LP Bug#849368

Change-Id: I3e17370537144d117d99af5fa5a21df830b7c7ed
This commit is contained in:
Alex Meade 2011-09-20 14:24:50 -04:00 committed by Jay Pipes
parent a0eafeec7a
commit b755623745
10 changed files with 335 additions and 14 deletions

View File

@ -1,4 +1,5 @@
Adam Gandelman <adam.gandelman@canonical.com>
Alex Meade <alex.meade@rackspace.com>
Andrey Brindeyev <abrindeyev@griddynamics.com>
Brian Lamar <brian.lamar@rackspace.com>
Brian Waldon <brian.waldon@rackspace.com>

View File

@ -114,7 +114,7 @@ def get_image_filters_from_args(args):
return FAILURE
SUPPORTED_FILTERS = ['name', 'disk_format', 'container_format', 'status',
'size_min', 'size_max']
'min_ram', 'min_disk', 'size_min', 'size_max']
filters = {}
for (key, value) in fields.items():
if key not in SUPPORTED_FILTERS:
@ -141,6 +141,8 @@ def print_image_formatted(client, image):
print "Size: %d" % int(image['size'])
print "Disk format: %s" % image['disk_format']
print "Container format: %s" % image['container_format']
print "Minimum Ram Required (MB): %s" % image['min_ram']
print "Minimum Disk Required (GB): %s" % image['min_disk']
if image['owner']:
print "Owner: %s" % image['owner']
if len(image['properties']) > 0:
@ -216,6 +218,8 @@ EXAMPLES
'is_public': common_utils.bool_from_string(
fields.pop('is_public', False)),
'disk_format': fields.pop('disk_format', 'raw'),
'min_disk': fields.pop('min_disk', 0),
'min_ram': fields.pop('min_ram', 0),
'container_format': fields.pop('container_format', 'ovf')}
# Strip any args that are not supported
@ -329,7 +333,7 @@ to spell field names correctly. :)"""
fields.pop(field)
base_image_fields = ['disk_format', 'container_format', 'name',
'location', 'owner']
'min_disk', 'min_ram', 'location', 'owner']
for field in base_image_fields:
fvalue = fields.pop(field, None)
if fvalue is not None:

View File

@ -73,6 +73,8 @@ JSON-encoded mapping in the following format::
'deleted_at': '',
'status': 'active',
'is_public': true,
'min_ram': 256,
'min_disk': 5,
'owner': null,
'properties': {'distro': 'Ubuntu 10.04 LTS'}},
...]}
@ -95,6 +97,12 @@ JSON-encoded mapping in the following format::
The `is_public` field is a boolean indicating whether the image is
publically available
The 'min_ram' field is an integer specifying the minimum amount of
ram needed to run this image on an instance, in megabytes
The 'min_disk' field is an integer specifying the minimum amount of
disk space needed to run this image on an instance, in gigabytes
The `owner` field is a string which may either be null or which will
indicate the owner of the image
@ -181,6 +189,8 @@ following shows an example of the HTTP headers returned from the above
x-image-meta-deleted_at
x-image-meta-status available
x-image-meta-is-public true
x-image-meta-min-ram 256
x-image-meta-min-disk 0
x-image-meta-owner null
x-image-meta-property-distro Ubuntu 10.04 LTS
@ -241,6 +251,8 @@ returned from the above ``GET`` request::
x-image-meta-deleted_at
x-image-meta-status available
x-image-meta-is-public true
x-image-meta-min-ram 256
x-image-meta-min-disk 5
x-image-meta-owner null
x-image-meta-property-distro Ubuntu 10.04 LTS
@ -383,6 +395,20 @@ The list of metadata headers that Glance accepts are listed below.
When not present, the image is assumed to be *not public* and specific to
a user.
* ``x-image-meta-min-ram``
This header is optional. When present it shall be the expected minimum ram
required in megabytes to run this image on a server.
When not present, the image is assumed to have a minimum ram requirement of 0.
* ``x-image-meta-min-disk``
This header is optional. When present it shall be the expected minimum disk
space required in gigabytes to run this image on a server.
When not present, the image is assumed to have a minimum disk space requirement of 0.
* ``x-image-meta-owner``
This header is optional and only meaningful for admins.

View File

@ -55,7 +55,8 @@ from glance import utils
logger = logging.getLogger('glance.api.v1.images')
SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
'size_min', 'size_max', 'is_public']
'min_ram', 'min_disk', 'size_min', 'size_max',
'is_public']
SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir')
@ -130,6 +131,8 @@ class Controller(api.BaseController):
'disk_format': <DISK_FORMAT>,
'container_format': <CONTAINER_FORMAT>,
'checksum': <CHECKSUM>,
'min_disk': <MIN_DISK>,
'min_ram': <MIN_RAM>,
'store': <STORE>,
'status': <STATUS>,
'created_at': <TIMESTAMP>,

View File

@ -47,8 +47,8 @@ BASE_MODEL_ATTRS = set(['id', 'created_at', 'updated_at', 'deleted_at',
IMAGE_ATTRS = BASE_MODEL_ATTRS | set(['name', 'status', 'size',
'disk_format', 'container_format',
'is_public', 'location', 'checksum',
'owner'])
'min_disk', 'min_ram', 'is_public',
'location', 'checksum', 'owner'])
CONTAINER_FORMATS = ['ami', 'ari', 'aki', 'bare', 'ovf']
DISK_FORMATS = ['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi',
@ -341,6 +341,12 @@ def _image_update(context, values, image_id, purge_props=False):
if 'size' in values:
values['size'] = int(values['size'])
if 'min_ram' in values:
values['min_ram'] = int(values['min_ram'])
if 'min_disk' in values:
values['min_disk'] = int(values['min_disk'])
values['is_public'] = bool(values.get('is_public', False))
image_ref = models.Image()

View File

@ -0,0 +1,86 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 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.
from migrate.changeset import *
from sqlalchemy import *
from sqlalchemy.sql import and_, not_
from glance.registry.db.migrate_repo.schema import (
Boolean, DateTime, Integer, String, Text, from_migration_import)
def get_images_table(meta):
"""
Returns the Table object for the images table that
corresponds to the images table definition of this version.
"""
images = Table('images', meta,
Column('id', Integer(), primary_key=True, nullable=False),
Column('name', String(255)),
Column('disk_format', String(20)),
Column('container_format', String(20)),
Column('size', Integer()),
Column('status', String(30), nullable=False),
Column('is_public', Boolean(), nullable=False, default=False,
index=True),
Column('location', Text()),
Column('created_at', DateTime(), nullable=False),
Column('updated_at', DateTime()),
Column('deleted_at', DateTime()),
Column('deleted', Boolean(), nullable=False, default=False,
index=True),
Column('checksum', String(32)),
Column('min_disk', Integer(), default=0),
Column('min_ram', Integer(), default=0),
mysql_engine='InnoDB',
useexisting=True)
return images
def get_image_properties_table(meta):
"""
No changes to the image properties table from 008...
"""
(define_image_properties_table,) = from_migration_import(
'008_add_image_members_table', ['define_image_properties_table'])
image_properties = define_image_properties_table(meta)
return image_properties
def upgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine
images = get_images_table(meta)
min_disk = Column('min_disk', Integer(), default=0)
min_disk.create(images)
min_ram = Column('min_ram', Integer(), default=0)
min_ram.create(images)
def downgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine
images = get_images_table(meta)
images.columns['min_disk'].drop()
images.columns['min_ram'].drop()

View File

@ -105,6 +105,8 @@ class Image(BASE, ModelBase):
is_public = Column(Boolean, nullable=False, default=False)
location = Column(Text)
checksum = Column(String(32))
min_disk = Column(Integer(), default=0)
min_ram = Column(Integer(), default=0)
owner = Column(String(255))

View File

@ -37,7 +37,7 @@ DISPLAY_FIELDS_IN_INDEX = ['id', 'name', 'size',
'checksum']
SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
'size_min', 'size_max']
'min_ram', 'min_disk', 'size_min', 'size_max']
SUPPORTED_SORT_KEYS = ('name', 'status', 'container_format', 'disk_format',
'size', 'id', 'created_at', 'updated_at')

View File

@ -318,7 +318,8 @@ class TestBinGlance(functional.FunctionalTest):
_add_args = [
"name=Name1 disk_format=vhd container_format=ovf foo=bar",
"name=Name2 disk_format=ami container_format=ami foo=bar",
"name=Name3 disk_format=vhd container_format=ovf foo=baz",
"name=Name3 disk_format=vhd container_format=ovf foo=baz "
"min_disk=7 min_ram=256",
]
for i, args in enumerate(_add_args):
@ -402,9 +403,27 @@ class TestBinGlance(functional.FunctionalTest):
self.assertEqual(0, exitcode)
image_lines = out.split("\n")[1:-1]
self.assertEqual(20, len(image_lines))
self.assertEqual(24, len(image_lines))
self.assertTrue(image_lines[1].startswith('Id: 2'))
self.assertTrue(image_lines[11].startswith('Id: 1'))
self.assertTrue(image_lines[13].startswith('Id: 1'))
# 10. Check min_ram filter
cmd = "min_ram=256"
exitcode, out, err = execute("%s %s" % (_details_cmd, cmd))
self.assertEqual(0, exitcode)
image_lines = out.split("\n")[2:-1]
self.assertEqual(11, len(image_lines))
self.assertTrue(image_lines[0].startswith('Id: 3'))
# 11. Check min_disk filter
cmd = "min_disk=7"
exitcode, out, err = execute("%s %s" % (_details_cmd, cmd))
self.assertEqual(0, exitcode)
image_lines = out.split("\n")[2:-1]
self.assertEqual(11, len(image_lines))
self.assertTrue(image_lines[0].startswith('Id: 3'))
self.stop_servers()
@ -483,9 +502,9 @@ class TestBinGlance(functional.FunctionalTest):
self.assertEqual(0, exitcode)
image_lines = out.split("\n")[1:-1]
self.assertEqual(18, len(image_lines))
self.assertEqual(22, len(image_lines))
self.assertTrue(image_lines[1].startswith('Id: 3'))
self.assertTrue(image_lines[10].startswith('Id: 1'))
self.assertTrue(image_lines[12].startswith('Id: 1'))
def test_results_sorting(self):
self.cleanup()
@ -555,7 +574,7 @@ class TestBinGlance(functional.FunctionalTest):
self.assertEqual(0, exitcode)
image_lines = out.split("\n")[1:-1]
self.assertEqual(27, len(image_lines))
self.assertEqual(33, len(image_lines))
self.assertTrue(image_lines[1].startswith('Id: 3'))
self.assertTrue(image_lines[10].startswith('Id: 2'))
self.assertTrue(image_lines[19].startswith('Id: 5'))
self.assertTrue(image_lines[12].startswith('Id: 2'))
self.assertTrue(image_lines[23].startswith('Id: 5'))

View File

@ -62,6 +62,8 @@ class TestRegistryAPI(unittest.TestCase):
'deleted_at': None,
'deleted': False,
'checksum': None,
'min_disk': 0,
'min_ram': 0,
'size': 13,
'location': "swift://user:passwd@acct/container/obj.tar.0",
'properties': {'type': 'kernel'}},
@ -76,6 +78,8 @@ class TestRegistryAPI(unittest.TestCase):
'deleted_at': None,
'deleted': False,
'checksum': None,
'min_disk': 5,
'min_ram': 256,
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {}}]
@ -107,6 +111,8 @@ class TestRegistryAPI(unittest.TestCase):
fixture = {'id': 2,
'name': 'fake image #2',
'size': 19,
'min_ram': 256,
'min_disk': 5,
'checksum': None}
req = webob.Request.blank('/images/2')
res = req.get_response(self.api)
@ -779,6 +785,8 @@ class TestRegistryAPI(unittest.TestCase):
'name': 'fake image #2',
'is_public': True,
'size': 19,
'min_disk': 5,
'min_ram': 256,
'checksum': None,
'disk_format': 'vhd',
'container_format': 'ovf',
@ -958,6 +966,84 @@ class TestRegistryAPI(unittest.TestCase):
for image in images:
self.assertEqual('ovf', image['container_format'])
def test_get_details_filter_min_disk(self):
"""
Tests that the /images/detail registry API returns list of
public images that have a specific min_disk
"""
extra_fixture = {'id': 3,
'status': 'active',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'fake image #3',
'size': 19,
'min_disk': 7,
'checksum': None}
db_api.image_create(self.context, extra_fixture)
extra_fixture = {'id': 4,
'status': 'active',
'is_public': True,
'disk_format': 'ami',
'container_format': 'ami',
'name': 'fake image #4',
'size': 19,
'checksum': None}
db_api.image_create(self.context, extra_fixture)
req = webob.Request.blank('/images/detail?min_disk=7')
res = req.get_response(self.api)
res_dict = json.loads(res.body)
self.assertEquals(res.status_int, 200)
images = res_dict['images']
self.assertEquals(len(images), 1)
for image in images:
self.assertEqual(7, image['min_disk'])
def test_get_details_filter_min_ram(self):
"""
Tests that the /images/detail registry API returns list of
public images that have a specific min_ram
"""
extra_fixture = {'id': 3,
'status': 'active',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'fake image #3',
'size': 19,
'min_ram': 514,
'checksum': None}
db_api.image_create(self.context, extra_fixture)
extra_fixture = {'id': 4,
'status': 'active',
'is_public': True,
'disk_format': 'ami',
'container_format': 'ami',
'name': 'fake image #4',
'size': 19,
'checksum': None}
db_api.image_create(self.context, extra_fixture)
req = webob.Request.blank('/images/detail?min_ram=514')
res = req.get_response(self.api)
res_dict = json.loads(res.body)
self.assertEquals(res.status_int, 200)
images = res_dict['images']
self.assertEquals(len(images), 1)
for image in images:
self.assertEqual(514, image['min_ram'])
def test_get_details_filter_disk_format(self):
"""
Tests that the /images/detail registry API returns list of
@ -1307,6 +1393,92 @@ class TestRegistryAPI(unittest.TestCase):
# Test status was updated properly
self.assertEquals('active', res_dict['image']['status'])
def test_create_image_with_min_disk(self):
"""Tests that the /images POST registry API creates the image"""
fixture = {'name': 'fake public image',
'is_public': True,
'min_disk': 5,
'disk_format': 'vhd',
'container_format': 'ovf'}
req = webob.Request.blank('/images')
req.method = 'POST'
req.content_type = 'application/json'
req.body = json.dumps(dict(image=fixture))
res = req.get_response(self.api)
self.assertEquals(res.status_int, 200)
res_dict = json.loads(res.body)
self.assertEquals(5, res_dict['image']['min_disk'])
def test_create_image_with_min_ram(self):
"""Tests that the /images POST registry API creates the image"""
fixture = {'name': 'fake public image',
'is_public': True,
'min_ram': 256,
'disk_format': 'vhd',
'container_format': 'ovf'}
req = webob.Request.blank('/images')
req.method = 'POST'
req.content_type = 'application/json'
req.body = json.dumps(dict(image=fixture))
res = req.get_response(self.api)
self.assertEquals(res.status_int, 200)
res_dict = json.loads(res.body)
self.assertEquals(256, res_dict['image']['min_ram'])
def test_create_image_with_min_ram_default(self):
"""Tests that the /images POST registry API creates the image"""
fixture = {'name': 'fake public image',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf'}
req = webob.Request.blank('/images')
req.method = 'POST'
req.content_type = 'application/json'
req.body = json.dumps(dict(image=fixture))
res = req.get_response(self.api)
self.assertEquals(res.status_int, 200)
res_dict = json.loads(res.body)
self.assertEquals(0, res_dict['image']['min_ram'])
def test_create_image_with_min_disk_default(self):
"""Tests that the /images POST registry API creates the image"""
fixture = {'name': 'fake public image',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf'}
req = webob.Request.blank('/images')
req.method = 'POST'
req.content_type = 'application/json'
req.body = json.dumps(dict(image=fixture))
res = req.get_response(self.api)
self.assertEquals(res.status_int, 200)
res_dict = json.loads(res.body)
self.assertEquals(0, res_dict['image']['min_disk'])
def test_create_image_with_bad_container_format(self):
"""Tests proper exception is raised if a bad disk_format is set"""
fixture = {'id': 3,
@ -1385,6 +1557,8 @@ class TestRegistryAPI(unittest.TestCase):
def test_update_image(self):
"""Tests that the /images PUT registry API updates the image"""
fixture = {'name': 'fake public image #2',
'min_disk': 5,
'min_ram': 256,
'disk_format': 'raw'}
req = webob.Request.blank('/images/2')