Add image endpoint

This patch adds image endpoint for zun-api. As of now
supports image_show() and image_create().

Change-Id: I2ef1865e21b99f3bed3a5b7c53816cfe808a2fc2
Partial-Implements: blueprint add-image-endpoint
This commit is contained in:
shubham.git 2016-10-15 10:21:51 +05:30
parent 605818302b
commit ea963601a5
21 changed files with 1088 additions and 15 deletions

View File

@ -90,6 +90,26 @@ class NameType(String):
raise exception.InvalidValue(message)
class ImageNameType(String):
type_name = 'ImageNameType'
# ImageNameType allows to be Non-None or a string matches pattern
# `[a-zA-Z0-9][a-zA-Z0-9_.-].` with minimum length is 2 and maximum length
# 255 string type.
@classmethod
def validate(cls, value):
if value is None:
message = _('Repo/Image is mandatory. Cannot be left blank.')
raise exception.InvalidValue(message)
super(ImageNameType, cls).validate(value, min_length=2, max_length=255)
match = name_pattern.match(value)
if match:
return value
else:
message = _('%s does not match [a-zA-Z0-9][a-zA-Z0-9_.-].') % value
raise exception.InvalidValue(message)
class Integer(object):
type_name = 'Integer'
@ -251,8 +271,8 @@ class DateTime(object):
type=cls.type_name)
class ContainerMemory(object):
type_name = 'ContainerMemory'
class MemoryType(object):
type_name = 'MemoryType'
@classmethod
def validate(cls, value, default=None):
@ -271,3 +291,25 @@ class ContainerMemory(object):
in both cases"""
raise exception.InvalidValue(message=message, value=value,
type=cls.type_name)
class ImageSize(object):
type_name = 'ImageSize'
@classmethod
def validate(cls, value, default=None):
if value is None:
return
elif value.isdigit():
return value
elif (value.isalnum() and
value[:-1].isdigit() and value[-1] in VALID_UNITS.keys()):
return int(value[:-1]) * VALID_UNITS[value[-1]]
else:
LOG.exception(_LE('Failed to validate image size'))
message = _("""
size must be either integer or string of below format.
<integer><memory_unit> memory_unit must be 'k','b','m','g'
in both cases""")
raise exception.InvalidValue(message=message, value=value,
type=cls.type_name)

View File

@ -26,6 +26,7 @@ from zun.api.controllers import base as controllers_base
from zun.api.controllers import link
from zun.api.controllers import types
from zun.api.controllers.v1 import containers as container_controller
from zun.api.controllers.v1 import images as image_controller
from zun.api.controllers.v1 import zun_services
LOG = logging.getLogger(__name__)
@ -63,6 +64,9 @@ class V1(controllers_base.APIBase):
'containers': {
'validate': types.List(types.Custom(link.Link)).validate
},
'images': {
'validate': types.List(types.Custom(link.Link)).validate
},
}
@staticmethod
@ -90,6 +94,12 @@ class V1(controllers_base.APIBase):
pecan.request.host_url,
'containers', '',
bookmark=True)]
v1.images = [link.Link.make_link('self', pecan.request.host_url,
'images', ''),
link.Link.make_link('bookmark',
pecan.request.host_url,
'images', '',
bookmark=True)]
return v1
@ -98,6 +108,7 @@ class Controller(rest.RestController):
services = zun_services.ZunServiceController()
containers = container_controller.ContainersController()
images = image_controller.ImagesController()
@pecan.expose('json')
def get(self):

View File

@ -94,7 +94,7 @@ class Container(base.APIBase):
'validate': types.Float.validate,
},
'memory': {
'validate': types.ContainerMemory.validate,
'validate': types.MemoryType.validate,
},
'environment': {
'validate': types.Dict(types.String, types.String).validate,

View File

@ -0,0 +1,188 @@
# 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 oslo_log import log as logging
from oslo_utils import timeutils
import pecan
from pecan import rest
from zun.api.controllers import base
from zun.api.controllers import link
from zun.api.controllers import types
from zun.api.controllers.v1 import collection
from zun.api import utils as api_utils
from zun.common import exception
from zun.common.i18n import _LE
from zun.common import policy
from zun import objects
LOG = logging.getLogger(__name__)
class Image(base.APIBase):
"""API representation of an image.
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of
an image.
"""
fields = {
'uuid': {
'validate': types.Uuid.validate,
},
'image_id': {
'validate': types.NameType.validate,
},
'repo': {
'validate': types.ImageNameType.validate,
},
'tag': {
'validate': types.NameType.validate,
},
'size': {
'validate': types.ImageSize.validate,
},
}
def __init__(self, **kwargs):
super(Image, self).__init__(**kwargs)
@staticmethod
def _convert_with_links(image, url, expand=True):
if not expand:
image.unset_fields_except([
'uuid', 'image_id', 'repo', 'tag', 'size'])
image.links = [link.Link.make_link(
'self', url,
'images', image.uuid),
link.Link.make_link(
'bookmark', url,
'images', image.uuid,
bookmark=True)]
return image
@classmethod
def convert_with_links(cls, rpc_image, expand=True):
image = Image(**rpc_image)
return cls._convert_with_links(image, pecan.request.host_url,
expand)
@classmethod
def sample(cls, expand=True):
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f35c',
repo='ubuntu',
tag='latest',
size='700m',
created_at=timeutils.utcnow(),
updated_at=timeutils.utcnow())
return cls._convert_with_links(sample, 'http://localhost:9517', expand)
class ImageCollection(collection.Collection):
"""API representation of a collection of images."""
fields = {
'images': {
'validate': types.List(types.Custom(Image)).validate,
},
}
"""A list containing images objects"""
def __init__(self, **kwargs):
self._type = 'images'
@staticmethod
def convert_with_links(rpc_images, limit, url=None,
expand=False, **kwargs):
collection = ImageCollection()
collection.images = [Image.convert_with_links(p, expand)
for p in rpc_images]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
@classmethod
def sample(cls):
sample = cls()
sample.images = [Image.sample(expand=False)]
return sample
class ImagesController(rest.RestController):
'''Controller for Images'''
@pecan.expose('json')
@exception.wrap_pecan_controller_exception
def get_all(self, **kwargs):
'''Retrieve a list of images.'''
context = pecan.request.context
policy.enforce(context, "image:get_all",
action="image:get_all")
return self._get_images_collection(**kwargs)
def _get_images_collection(self, **kwargs):
context = pecan.request.context
limit = api_utils.validate_limit(kwargs.get('limit', None))
sort_dir = api_utils.validate_sort_dir(kwargs.get('sort_dir', 'asc'))
sort_key = kwargs.get('sort_key', 'id')
resource_url = kwargs.get('resource_url', None)
expand = kwargs.get('expand', None)
filters = None
marker_obj = None
marker = kwargs.get('marker', None)
if marker:
marker_obj = objects.Image.get_by_uuid(context, marker)
images = objects.Image.list(context,
limit,
marker_obj,
sort_key,
sort_dir,
filters=filters)
for i, c in enumerate(images):
try:
images[i] = pecan.request.rpcapi.image_show(context, c)
except Exception as e:
LOG.exception(_LE("Error while list image %(uuid)s: "
"%(e)s."), {'uuid': c.uuid, 'e': e})
return ImageCollection.convert_with_links(images, limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir)
@pecan.expose('json')
@api_utils.enforce_content_types(['application/json'])
@exception.wrap_pecan_controller_exception
def post(self, **image_dict):
"""Create a new image.
:param image: an image within the request body.
"""
context = pecan.request.context
policy.enforce(context, "image:create",
action="image:create")
image_dict = Image(**image_dict).as_dict()
image_dict['project_id'] = context.project_id
image_dict['user_id'] = context.user_id
repo_tag = image_dict.get('repo')
image_dict['repo'], image_dict['tag'] = api_utils.parse_image_tag(
repo_tag)
new_image = objects.Image(context, **image_dict)
new_image.create()
pecan.request.rpcapi.image_create(context, new_image)
# Set the HTTP Location Header
pecan.response.location = link.build_url('images',
new_image.uuid)
pecan.response.status = 202
return Image.convert_with_links(new_image)

View File

@ -68,6 +68,18 @@ def validate_docker_memory(mem_str):
% DOCKER_MINIMUM_MEMORY)
def parse_image_tag(image):
image_parts = image.split(':', 1)
image_repo = image_parts[0]
image_tag = 'latest'
if len(image_parts) > 1:
image_tag = image_parts[1]
return image_repo, image_tag
def apply_jsonpatch(doc, patch):
for p in patch:
if p['op'] == 'add' and p['path'].count('/') == 1:

View File

@ -337,10 +337,18 @@ class ContainerNotFound(HTTPNotFound):
message = _("Container %(container)s could not be found.")
class ImageNotFound(HTTPNotFound):
message = _("Image %(image)s could not be found.")
class ContainerAlreadyExists(ResourceExists):
message = _("A container with UUID %(uuid)s already exists.")
class ImageAlreadyExists(ResourceExists):
message = _("An image with this tag and repo already exists.")
class InvalidStateException(ZunException):
message = _("Cannot %(action)s container %(id)s in %(actual_state)s state")
code = 409

View File

@ -23,6 +23,7 @@ class API(rpc_service.API):
API version history:
* 1.0 - Initial version.
* 1.1 - Add image endpoints.
'''
def __init__(self, transport=None, context=None, topic=None):
@ -67,3 +68,9 @@ class API(rpc_service.API):
def container_kill(self, context, container, signal):
return self._call('container_kill', container=container,
signal=signal)
def image_show(self, context, image):
return self._call('image_show', image=image)
def image_create(self, context, image):
return self._cast('image_create', image=image)

View File

@ -267,6 +267,35 @@ class Manager(object):
LOG.error(_LE("Error occured while calling docker API: %s"),
six.text_type(e))
raise
def image_create(self, context, image):
utils.spawn_n(self._do_image_create, context, image)
def _do_image_create(self, context, image):
LOG.debug('Creating image...', context=context,
image=image)
try:
repo_tag = image.repo + ":" + image.tag
self.driver.pull_image(repo_tag)
image_dict = self.driver.inspect_image(repo_tag)
image.image_id = image_dict['Id']
image.size = image_dict['Size']
image.save()
except exception.DockerError as e:
LOG.error(_LE("Error occured while calling docker API: %s"),
six.text_type(e))
raise e
except Exception as e:
LOG.exception(_LE("Unexpected exception: %s"),
six.text_type(e))
raise e
@translate_exception
def image_show(self, context, image):
LOG.debug('Listing image...', context=context)
try:
self.image.list()
return image
except Exception as e:
LOG.exception(_LE("Unexpected exception: %s"), str(e))
raise e

View File

@ -39,6 +39,17 @@ class DockerDriver(driver.ContainerDriver):
image_repo, image_tag = docker_utils.parse_docker_image(image)
docker.pull(image_repo, tag=image_tag)
def inspect_image(self, image):
with docker_utils.docker_client() as docker:
LOG.debug('Inspecting image %s' % image)
image_dict = docker.inspect_image(image)
return image_dict
def images(self, repo, quiet=False):
with docker_utils.docker_client() as docker:
response = docker.images(repo, quiet)
return response
def create(self, container):
with docker_utils.docker_client() as docker:
name = container.name

View File

@ -197,5 +197,79 @@ class Connection(object):
:returns: A list of tuples of the specified columns.
"""
dbdriver = get_instance()
return dbdriver.get_zun_service_list(
context, disabled, limit, marker, sort_key, sort_dir)
return dbdriver.get_zun_service_list(context, disabled, limit,
marker, sort_key, sort_dir)
@classmethod
def create_image(cls, values):
"""Create a new image.
:param values: A dict containing several items used to identify
and track the image, and several dicts which are
passed
into the Drivers when managing this image. For
example:
::
{
'uuid': uuidutils.generate_uuid(),
'repo': 'hello-world',
'tag': 'latest'
}
:returns: An image.
"""
dbdriver = get_instance()
return dbdriver.create_image(values)
@classmethod
def update_image(self, image_id, values):
"""Update properties of an image.
:param container_id: The id or uuid of an image.
:returns: An Image.
:raises: ImageNotFound
"""
dbdriver = get_instance()
return dbdriver.update_image(image_id, values)
@classmethod
def list_image(cls, context, filters=None,
limit=None, marker=None,
sort_key=None, sort_dir=None):
"""Get matching images.
Return a list of the specified columns for all images that
match the specified filters.
:param context: The security context
:param filters: Filters to apply. Defaults to None.
:param limit: Maximum number of images to return.
:param marker: the last item of the previous page; we
return the next
:param sort_key: Attribute by which results should be sorted.
(asc, desc)
:returns: A list of tuples of the specified columns.
"""
dbdriver = get_instance()
return dbdriver.list_image(context, filters, limit, marker,
sort_key, sort_dir)
@classmethod
def get_image_by_id(cls, context, image_id):
"""Return an image.
:param context: The security context
:param image_id: The id of an image.
:returns: An image.
"""
dbdriver = get_instance()
return dbdriver.get_image_by_id(context, image_id)
@classmethod
def get_image_by_uuid(cls, context, image_uuid):
"""Return an image.
:param context: The security context
:param image_uuid: The uuid of an image.
:returns: An image.
"""
dbdriver = get_instance()
return dbdriver.get_image_by_uuid(context, image_uuid)

View File

@ -0,0 +1,50 @@
# 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.
"""create_table_image
Revision ID: 72c6947c6636
Revises: 1192ba19a6e9
Create Date: 2016-09-23 20:30:02.425937
"""
# revision identifiers, used by Alembic.
revision = '72c6947c6636'
down_revision = '1192ba19a6e9'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
def upgrade():
# commands auto generated by Alembic - please adjust! #
op.create_table(
'image',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('project_id', sa.String(length=255), nullable=True),
sa.Column('user_id', sa.String(length=255), nullable=True),
sa.Column('uuid', sa.String(length=36), nullable=True),
sa.Column('image_id', sa.String(length=255), nullable=True),
sa.Column('repo', sa.String(length=255), nullable=True),
sa.Column('tag', sa.String(length=255), nullable=True),
sa.Column('size', sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('repo', 'tag', name='uniq_image0repotag'),
mysql_charset='utf8',
mysql_engine='InnoDB'
)
# end Alembic commands #

View File

@ -31,7 +31,6 @@ from zun.db.sqlalchemy import models
CONF = cfg.CONF
_FACADE = None
@ -270,3 +269,72 @@ class Connection(api.Connection):
return _paginate_query(models.ZunService, limit, marker,
sort_key, sort_dir, query)
def create_image(self, values):
# ensure defaults are present for new containers
if not values.get('uuid'):
values['uuid'] = uuidutils.generate_uuid()
image = models.Image()
image.update(values)
try:
image.save()
except db_exc.DBDuplicateEntry:
raise exception.ImageAlreadyExists()
return image
def update_image(self, image_id, values):
# NOTE(dtantsur): this can lead to very strange errors
if 'uuid' in values:
msg = _("Cannot overwrite UUID for an existing Image.")
raise exception.InvalidParameterValue(err=msg)
return self._do_update_image(image_id, values)
def _do_update_image(self, image_id, values):
session = get_session()
with session.begin():
query = model_query(models.Image, session=session)
query = add_identity_filter(query, image_id)
try:
ref = query.with_lockmode('update').one()
except NoResultFound:
raise exception.ImageNotFound(image=image_id)
ref.update(values)
return ref
def _add_image_filters(self, query, filters):
if filters is None:
filters = {}
filter_names = ['repo', 'project_id', 'user_id', 'size']
for name in filter_names:
if name in filters:
query = query.filter_by(**{name: filters[name]})
return query
def list_image(self, context, filters=None, limit=None, marker=None,
sort_key=None, sort_dir=None):
query = model_query(models.Image)
query = self._add_tenant_filters(context, query)
query = self._add_image_filters(query, filters)
return _paginate_query(models.Image, limit, marker, sort_key,
sort_dir, query)
def get_image_by_id(self, context, image_id):
query = model_query(models.Image)
query = self._add_tenant_filters(context, query)
query = query.filter_by(id=image_id)
try:
return query.one()
except NoResultFound:
raise exception.ImageNotFound(image=image_id)
def get_image_by_uuid(self, context, image_uuid):
query = model_query(models.Image)
query = self._add_tenant_filters(context, query)
query = query.filter_by(uuid=image_uuid)
try:
return query.one()
except NoResultFound:
raise exception.ImageNotFound(image=image_uuid)

View File

@ -139,3 +139,21 @@ class Container(Base):
ports = Column(JSONEncodedList)
hostname = Column(String(255))
labels = Column(JSONEncodedDict)
class Image(Base):
"""Represents an image. """
__tablename__ = 'image'
__table_args__ = (
schema.UniqueConstraint('repo', 'tag', name='uniq_image0repotag'),
table_args()
)
id = Column(Integer, primary_key=True)
project_id = Column(String(255))
user_id = Column(String(255))
uuid = Column(String(36))
image_id = Column(String(255))
repo = Column(String(255))
tag = Column(String(255))
size = Column(String(255))

View File

@ -12,11 +12,13 @@
from zun.objects import container
from zun.objects import image
from zun.objects import zun_service
Container = container.Container
ZunService = zun_service.ZunService
Image = image.Image
__all__ = (Container,
ZunService)
ZunService,
Image)

129
zun/objects/image.py Normal file
View File

@ -0,0 +1,129 @@
# 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 oslo_versionedobjects import fields
from zun.db import api as dbapi
from zun.objects import base
@base.ZunObjectRegistry.register
class Image(base.ZunPersistentObject, base.ZunObject,
base.ZunObjectDictCompat):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'id': fields.IntegerField(),
'uuid': fields.StringField(nullable=True),
'image_id': fields.StringField(nullable=True),
'project_id': fields.StringField(nullable=True),
'user_id': fields.StringField(nullable=True),
'repo': fields.StringField(nullable=True),
'tag': fields.StringField(nullable=True),
'size': fields.StringField(nullable=True),
}
@staticmethod
def _from_db_object(image, db_image):
"""Converts a database entity to a formal object."""
for field in image.fields:
image[field] = db_image[field]
image.obj_reset_changes()
return image
@staticmethod
def _from_db_object_list(db_objects, cls, context):
"""Converts a list of database entities to a list of formal objects."""
return [Image._from_db_object(cls(context), obj)
for obj in db_objects]
@base.remotable_classmethod
def get_by_id(cls, context, image_id):
"""Find an image based on its integer id and return a Image object.
:param image_id: the id of an image.
:param context: Security context
:returns: a :class:`Image` object.
"""
db_image = dbapi.Connection.get_image_by_id(context, image_id)
image = Image._from_db_object(cls(context), db_image)
return image
@base.remotable_classmethod
def get_by_uuid(cls, context, uuid):
"""Find an image based on uuid and return a :class:`Image` object.
:param uuid: the uuid of an image.
:param context: Security context
:returns: a :class:`Image` object.
"""
db_image = dbapi.Connection.get_image_by_uuid(context, uuid)
image = Image._from_db_object(cls(context), db_image)
return image
@base.remotable_classmethod
def list(cls, context=None, limit=None, marker=None,
sort_key=None, sort_dir=None, filters=None):
"""Return a list of Image objects.
:param context: Security context.
:param limit: maximum number of resources to return in a single result.
:param marker: pagination marker for large data sets.
:param sort_key: column to sort results by.
:param sort_dir: direction to sort. "asc" or "desc".
:param filters: filters when list images, the filter name could be
'repo', 'image_id', 'project_id', 'user_id', 'size'
:returns: a list of :class:`Image` object.
"""
db_images = dbapi.Connection.list_image(context, limit=limit,
marker=marker,
sort_key=sort_key,
sort_dir=sort_dir,
filters=filters)
return Image._from_db_object_list(db_images, cls, context)
@base.remotable
def create(self, context=None):
"""Create an image record in the DB.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Image(context)
"""
values = self.obj_get_changes()
db_image = dbapi.Connection.create_image(values)
self._from_db_object(self, db_image)
@base.remotable
def save(self, context=None):
"""Save updates to this Image.
Updates will be made column by column based on the result
of self.what_changed().
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Image(context)
"""
updates = self.obj_get_changes()
dbapi.Connection.update_image(self.uuid, updates)
self.obj_reset_changes()

View File

@ -50,6 +50,11 @@ class TestRootController(api_base.FunctionalTest):
u'containers': [{u'href': u'http://localhost/v1/containers/',
u'rel': u'self'},
{u'href': u'http://localhost/containers/',
u'rel': u'bookmark'}],
u'id': u'v1',
u'images': [{u'href': u'http://localhost/v1/images/',
u'rel': u'self'},
{u'href': u'http://localhost/images/',
u'rel': u'bookmark'}]}
def make_app(self, paste_file):

View File

@ -181,27 +181,54 @@ class TestTypes(test_base.BaseTestCase):
self.assertRaises(exception.InvalidValue,
types.NameType.validate, test_value)
def test_image_name_type(self):
test_value = ""
self.assertRaises(exception.InvalidValue,
types.ImageNameType.validate, test_value)
test_value = '*' * 256
self.assertRaises(exception.InvalidValue,
types.ImageNameType.validate, test_value)
def test_container_memory_type(self):
test_value = '4m'
value = types.ContainerMemory.validate(test_value)
value = types.MemoryType.validate(test_value)
self.assertEqual(value, test_value)
test_value = '4'
self.assertRaises(exception.InvalidValue,
types.ContainerMemory.validate, test_value)
types.MemoryType.validate, test_value)
test_value = '10000A'
self.assertRaises(exception.InvalidValue,
types.ContainerMemory.validate, test_value)
types.MemoryType.validate, test_value)
test_value = '4K'
self.assertRaises(exception.InvalidValue,
types.ContainerMemory.validate, test_value)
types.MemoryType.validate, test_value)
test_value = '4194304'
value = types.ContainerMemory.validate(test_value)
value = types.MemoryType.validate(test_value)
self.assertEqual(value, test_value)
test_value = '4194304.0'
self.assertRaises(exception.InvalidValue,
types.ContainerMemory.validate, test_value)
types.MemoryType.validate, test_value)
def test_image_size(self):
test_value = '400'
value = types.ImageSize.validate(test_value)
self.assertEqual(value, test_value)
test_value = '4194304.0'
self.assertRaises(exception.InvalidValue,
types.ImageSize.validate, test_value)
test_value = '10000A'
self.assertRaises(exception.InvalidValue,
types.ImageSize.validate, test_value)
test_value = '4K'
expected_value = 4096
value = types.ImageSize.validate(test_value)
self.assertEqual(value, expected_value)

View File

@ -0,0 +1,119 @@
# 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.
import mock
from mock import patch
from zun.common import utils as comm_utils
from zun import objects
from zun.tests.unit.api import base as api_base
from zun.tests.unit.db import utils
class TestImageController(api_base.FunctionalTest):
@patch('zun.compute.api.API.image_create')
def test_image_create(self, mock_image_create):
mock_image_create.side_effect = lambda x, y: y
params = ('{"repo": "hello-world"}')
response = self.app.post('/v1/images/',
params=params,
content_type='application/json')
self.assertEqual(202, response.status_int)
self.assertTrue(mock_image_create.called)
@patch('zun.compute.api.API.image_create')
def test_create_image_set_project_id_and_user_id(
self, mock_image_create):
def _create_side_effect(cnxt, image):
self.assertEqual(self.context.project_id, image.project_id)
self.assertEqual(self.context.user_id, image.user_id)
return image
mock_image_create.side_effect = _create_side_effect
params = ('{"repo": "hello-world"}')
self.app.post('/v1/images/',
params=params,
content_type='application/json')
@patch('zun.compute.api.API.image_create')
def test_image_create_with_tag(self, mock_image_create):
mock_image_create.side_effect = lambda x, y: y
params = ('{"repo": "hello-world:latest"}')
response = self.app.post('/v1/images/',
params=params,
content_type='application/json')
self.assertEqual(202, response.status_int)
self.assertTrue(mock_image_create.called)
@patch('zun.compute.api.API.image_show')
@patch('zun.objects.Image.list')
def test_get_all_images(self, mock_image_list, mock_image_show):
test_image = utils.get_test_image()
images = [objects.Image(self.context, **test_image)]
mock_image_list.return_value = images
mock_image_show.return_value = images[0]
response = self.app.get('/v1/images/')
mock_image_list.assert_called_once_with(mock.ANY,
1000, None, 'id', 'asc',
filters=None)
self.assertEqual(200, response.status_int)
actual_images = response.json['images']
self.assertEqual(1, len(actual_images))
self.assertEqual(test_image['uuid'],
actual_images[0].get('uuid'))
@patch('zun.compute.api.API.image_show')
@patch('zun.objects.Image.list')
def test_get_all_images_with_pagination_marker(self, mock_image_list,
mock_image_show):
image_list = []
for id_ in range(4):
test_image = utils.create_test_image(
id=id_,
uuid=comm_utils.generate_uuid())
image_list.append(objects.Image(self.context, **test_image))
mock_image_list.return_value = image_list[-1:]
mock_image_show.return_value = image_list[-1]
response = self.app.get('/v1/images/?limit=3&marker=%s'
% image_list[2].uuid)
self.assertEqual(200, response.status_int)
actual_images = response.json['images']
self.assertEqual(1, len(actual_images))
self.assertEqual(image_list[-1].uuid,
actual_images[0].get('uuid'))
@patch('zun.compute.api.API.image_show')
@patch('zun.objects.Image.list')
def test_get_all_images_with_exception(self, mock_image_list,
mock_image_show):
test_image = utils.get_test_image()
images = [objects.Image(self.context, **test_image)]
mock_image_list.return_value = images
mock_image_show.side_effect = Exception
response = self.app.get('/v1/images/')
mock_image_list.assert_called_once_with(mock.ANY, 1000,
None, 'id', 'asc',
filters=None)
self.assertEqual(200, response.status_int)
actual_images = response.json['images']
self.assertEqual(1, len(actual_images))
self.assertEqual(test_image['uuid'],
actual_images[0].get('uuid'))

View File

@ -0,0 +1,132 @@
# 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.
"""Tests for manipulating Images via the DB API"""
from oslo_utils import uuidutils
import six
from zun.common import exception
from zun.tests.unit.db import base
from zun.tests.unit.db import utils
class DbImageTestCase(base.DbTestCase):
def test_create_image(self):
utils.create_test_image(repo="ubuntu:latest")
def test_create_image_duplicate_repo(self):
utils.create_test_image(repo="ubuntu:latest")
utils.create_test_image(repo="ubuntu:14.04")
def test_create_image_duplicate_tag(self):
utils.create_test_image(repo="ubuntu:latest")
utils.create_test_image(repo="centos:latest")
def test_create_image_already_exists(self):
utils.create_test_image(repo="ubuntu:latest")
self.assertRaises(exception.ResourceExists,
utils.create_test_image,
repo="ubuntu:latest")
def test_get_image_by_id(self):
image = utils.create_test_image()
res = self.dbapi.get_image_by_id(self.context, image.id)
self.assertEqual(image.id, res.id)
self.assertEqual(image.uuid, res.uuid)
def test_get_image_by_uuid(self):
image = utils.create_test_image()
res = self.dbapi.get_image_by_uuid(self.context, image.uuid)
self.assertEqual(image.id, res.id)
self.assertEqual(image.uuid, res.uuid)
def test_get_image_that_does_not_exist(self):
self.assertRaises(exception.ImageNotFound,
self.dbapi.get_image_by_id, self.context, 99)
self.assertRaises(exception.ImageNotFound,
self.dbapi.get_image_by_uuid,
self.context,
uuidutils.generate_uuid())
def test_list_images(self):
uuids = []
for i in range(1, 6):
image = utils.create_test_image(
repo="testrepo" + str(i))
uuids.append(six.text_type(image['uuid']))
res = self.dbapi.list_image(self.context)
res_uuids = [r.uuid for r in res]
self.assertEqual(sorted(uuids), sorted(res_uuids))
def test_list_image_sorted(self):
uuids = []
for _ in range(5):
image = utils.create_test_image(
uuid=uuidutils.generate_uuid())
uuids.append(six.text_type(image.uuid))
res = self.dbapi.list_image(self.context, sort_key='uuid')
res_uuids = [r.uuid for r in res]
self.assertEqual(sorted(uuids), res_uuids)
self.assertRaises(exception.InvalidParameterValue,
self.dbapi.list_image,
self.context,
sort_key='foo')
def test_list_image_with_filters(self):
image1 = utils.create_test_image(
repo='image-one',
uuid=uuidutils.generate_uuid())
image2 = utils.create_test_image(
repo='image-two',
uuid=uuidutils.generate_uuid())
res = self.dbapi.list_image(self.context,
filters={'repo': 'image-one'})
self.assertEqual([image1.id], [r.id for r in res])
res = self.dbapi.list_image(self.context,
filters={'repo': 'image-two'})
self.assertEqual([image2.id], [r.id for r in res])
res = self.dbapi.list_image(self.context,
filters={'repo': 'bad-image'})
self.assertEqual([], [r.id for r in res])
res = self.dbapi.list_image(
self.context,
filters={'repo': image1.repo})
self.assertEqual([image1.id], [r.id for r in res])
def test_update_image(self):
image = utils.create_test_image()
old_size = image.size
new_size = '2000'
self.assertNotEqual(old_size, new_size)
res = self.dbapi.update_image(image.id,
{'size': new_size})
self.assertEqual(new_size, res.size)
def test_update_image_not_found(self):
image_uuid = uuidutils.generate_uuid()
new_size = '2000'
self.assertRaises(exception.ImageNotFound,
self.dbapi.update_image,
image_uuid, {'size': new_size})
def test_update_image_uuid(self):
image = utils.create_test_image()
self.assertRaises(exception.InvalidParameterValue,
self.dbapi.update_image, image.id,
{'uuid': ''})

View File

@ -12,6 +12,7 @@
"""Zun test utilities."""
from zun.common import name_generator
from zun.db import api as db_api
@ -52,3 +53,42 @@ def create_test_container(**kw):
del container['id']
dbapi = db_api.get_instance()
return dbapi.create_container(container)
def get_test_image(**kw):
return {
'id': kw.get('id', 42),
'uuid': kw.get('uuid', 'ea8e2a25-2901-438d-8157-de7ffd68d051'),
'repo': kw.get('repo', 'image1'),
'tag': kw.get('tag', 'latest'),
'image_id': kw.get('image_id', 'sha256:c54a2cc56cbb2f0400'),
'size': kw.get('size', '1848'),
'project_id': kw.get('project_id', 'fake_project'),
'user_id': kw.get('user_id', 'fake_user'),
'created_at': kw.get('created_at'),
'updated_at': kw.get('updated_at'),
}
def create_test_image(**kw):
"""Create test image entry in DB and return Image DB object.
Function to be used to create test Image objects in the database.
:param kw: kwargs with overriding values for image's attributes.
:returns: Test Image DB object.
"""
image = get_test_image(**kw)
# Let DB generate ID if it isn't specified explicitly
if 'id' not in kw:
del image['id']
if 'repo' not in kw:
image['repo'] = _generate_repo_for_image()
dbapi = db_api.get_instance()
return dbapi.create_image(image)
def _generate_repo_for_image():
'''Generate a random name like: zeta-22-image.'''
name_gen = name_generator.NameGenerator()
name = name_gen.generate()
return name + '-image'

View File

@ -0,0 +1,101 @@
# Copyright 2015 OpenStack Foundation
# 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.
import mock
from testtools.matchers import HasLength
from zun import objects
from zun.tests.unit.db import base
from zun.tests.unit.db import utils
class TestImageObject(base.DbTestCase):
def setUp(self):
super(TestImageObject, self).setUp()
self.fake_image = utils.get_test_image()
def test_get_by_id(self):
image_id = self.fake_image['id']
with mock.patch.object(self.dbapi, 'get_image_by_id',
autospec=True) as mock_get_image:
mock_get_image.return_value = self.fake_image
image = objects.Image.get_by_id(self.context,
image_id)
mock_get_image.assert_called_once_with(self.context,
image_id)
self.assertEqual(self.context, image._context)
def test_get_by_uuid(self):
uuid = self.fake_image['uuid']
with mock.patch.object(self.dbapi, 'get_image_by_uuid',
autospec=True) as mock_get_image:
mock_get_image.return_value = self.fake_image
image = objects.Image.get_by_uuid(self.context, uuid)
mock_get_image.assert_called_once_with(self.context, uuid)
self.assertEqual(self.context, image._context)
def test_list(self):
with mock.patch.object(self.dbapi, 'list_image',
autospec=True) as mock_get_list:
mock_get_list.return_value = [self.fake_image]
images = objects.Image.list(self.context)
self.assertEqual(1, mock_get_list.call_count)
self.assertThat(images, HasLength(1))
self.assertIsInstance(images[0], objects.Image)
self.assertEqual(self.context, images[0]._context)
def test_list_with_filters(self):
with mock.patch.object(self.dbapi, 'list_image',
autospec=True) as mock_get_list:
mock_get_list.return_value = [self.fake_image]
filt = {'id': '1'}
images = objects.Image.list(self.context, filters=filt)
self.assertEqual(1, mock_get_list.call_count)
self.assertThat(images, HasLength(1))
self.assertIsInstance(images[0], objects.Image)
self.assertEqual(self.context, images[0]._context)
mock_get_list.assert_called_once_with(self.context,
filters=filt,
limit=None, marker=None,
sort_key=None, sort_dir=None)
def test_create(self):
with mock.patch.object(self.dbapi, 'create_image',
autospec=True) as mock_create_image:
mock_create_image.return_value = self.fake_image
image = objects.Image(self.context, **self.fake_image)
image.create()
mock_create_image.assert_called_once_with(self.fake_image)
self.assertEqual(self.context, image._context)
def test_save(self):
uuid = self.fake_image['uuid']
with mock.patch.object(self.dbapi, 'get_image_by_uuid',
autospec=True) as mock_get_image:
mock_get_image.return_value = self.fake_image
with mock.patch.object(self.dbapi, 'update_image',
autospec=True) as mock_update_image:
image = objects.Image.get_by_uuid(self.context, uuid)
image.repo = 'image-test'
image.tag = '512'
image.save()
mock_get_image.assert_called_once_with(self.context, uuid)
mock_update_image.assert_called_once_with(uuid,
{'repo':
'image-test',
'tag': '512'})
self.assertEqual(self.context, image._context)