Add an image domain model and related helpers.

This patch is the first in a series refactoring the logic around image
manipulation. In this patch, we add a domain module that has Images and
ways of constructing them. In anticipation of proxy classes added in
subsequent patches, we also define some base classes to make proxying
simpler.

Partially implements bp:glance-domain-logic-layer

Change-Id: Ie72f2f86cfcbd15e41c32deed3626290f2bf0e6b
This commit is contained in:
Mark J. Washenberger 2012-11-14 11:35:23 -08:00
parent 517739f9f1
commit 43ba08d8aa
3 changed files with 336 additions and 0 deletions

View File

@ -108,6 +108,10 @@ class ForbiddenPublicImage(Forbidden):
message = _("You are not authorized to complete this action.")
class ProtectedImageDelete(Forbidden):
message = _("Image %(image_id)s is protected and cannot be deleted.")
#NOTE(bcwaldon): here for backwards-compatability, need to deprecate.
class NotAuthorized(Forbidden):
message = _("You are not authorized to complete this action.")
@ -125,6 +129,14 @@ class InvalidFilterRangeValue(Invalid):
message = _("Unable to filter using the specified range.")
class ReadonlyProperty(Forbidden):
message = _("Attribute '%(property)s' is read-only.")
class ReservedProperty(Forbidden):
message = _("Attribute '%(property)s' is reserved.")
class AuthorizationRedirect(GlanceException):
message = _("Redirecting to %(uri)s for authorization.")

173
glance/domain.py Normal file
View File

@ -0,0 +1,173 @@
# Copyright 2012 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 glance.common import exception
from glance.openstack.common import timeutils
from glance.openstack.common import uuidutils
class ImageFactory(object):
_readonly_properties = ['created_at', 'updated_at', 'status', 'checksum',
'size']
_reserved_properties = ['owner', 'is_public', 'location',
'deleted', 'deleted_at', 'direct_url', 'self',
'file', 'schema']
def _check_readonly(self, kwargs):
for key in self._readonly_properties:
if key in kwargs:
raise exception.ReadonlyProperty(property=key)
def _check_unexpected(self, kwargs):
if len(kwargs) > 0:
msg = 'new_image() got unexpected keywords %s'
raise TypeError(msg % kwargs.keys())
def _check_reserved(self, properties):
if properties is not None:
for key in self._reserved_properties:
if key in properties:
raise exception.ReservedProperty(property=key)
def new_image(self, image_id=None, name=None, visibility='private',
min_disk=0, min_ram=0, protected=False, owner=None,
disk_format=None, container_format=None,
extra_properties=None, tags=None, **other_args):
self._check_readonly(other_args)
self._check_unexpected(other_args)
self._check_reserved(extra_properties)
if image_id is None:
image_id = uuidutils.generate_uuid()
created_at = timeutils.utcnow()
updated_at = created_at
status = 'queued'
return Image(image_id=image_id, name=name, status=status,
created_at=created_at, updated_at=updated_at,
visibility=visibility, min_disk=min_disk,
min_ram=min_ram, protected=protected,
owner=owner, disk_format=disk_format,
container_format=container_format,
extra_properties=extra_properties, tags=tags)
class Image(object):
def __init__(self, image_id, status, created_at, updated_at, **kwargs):
self.image_id = image_id
self.status = status
self.created_at = created_at
self.updated_at = updated_at
self.name = kwargs.pop('name', None)
self.visibility = kwargs.pop('visibility', 'private')
self.min_disk = kwargs.pop('min_disk', 0)
self.min_ram = kwargs.pop('min_ram', 0)
self.protected = kwargs.pop('protected', False)
self.location = kwargs.pop('location', None)
self.checksum = kwargs.pop('checksum', None)
self.owner = kwargs.pop('owner', None)
self.disk_format = kwargs.pop('disk_format', None)
self.container_format = kwargs.pop('container_format', None)
self.size = kwargs.pop('size', None)
self.extra_properties = kwargs.pop('extra_properties', None) or {}
self.tags = kwargs.pop('tags', None) or []
if len(kwargs) > 0:
message = "__init__() got unexpected keyword argument '%s'"
raise TypeError(message % kwargs.keys()[0])
@property
def visibility(self):
return self._visibility
@visibility.setter
def visibility(self, visibility):
if visibility not in ('public', 'private'):
raise ValueError('Visibility must be either "public" or "private"')
self._visibility = visibility
@property
def tags(self):
return self._tags
@tags.setter
def tags(self, value):
self._tags = set(value)
def delete(self):
if self.protected:
raise exception.ProtectedImageDelete(image_id=self.image_id)
self.status = 'deleted'
def _proxy(target, attr):
def get_attr(self):
return getattr(getattr(self, target), attr)
def set_attr(self, value):
return setattr(getattr(self, target), attr, value)
def del_attr(self):
return delattr(getattr(self, target), attr)
return property(get_attr, set_attr, del_attr)
class ImageRepoProxy(object):
def __init__(self, base):
self.base = base
def get(self, image_id):
return self.base.get(image_id)
def list(self, *args, **kwargs):
return self.base.list(*args, **kwargs)
def add(self, image):
return self.base.add(image)
def save(self, image):
return self.base.save(image)
def remove(self, image):
return self.base.remove(image)
class ImageProxy(object):
def __init__(self, base):
self.base = base
name = _proxy('base', 'name')
image_id = _proxy('base', 'image_id')
name = _proxy('base', 'name')
status = _proxy('base', 'status')
created_at = _proxy('base', 'created_at')
updated_at = _proxy('base', 'updated_at')
visibility = _proxy('base', 'visibility')
min_disk = _proxy('base', 'min_disk')
min_ram = _proxy('base', 'min_ram')
protected = _proxy('base', 'protected')
location = _proxy('base', 'location')
checksum = _proxy('base', 'checksum')
owner = _proxy('base', 'owner')
disk_format = _proxy('base', 'disk_format')
container_format = _proxy('base', 'container_format')
size = _proxy('base', 'size')
extra_properties = _proxy('base', 'extra_properties')
tags = _proxy('base', 'tags')
def delete(self):
self.base.delete()

View File

@ -0,0 +1,151 @@
# Copyright 2012 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 glance import domain
from glance.common import exception
import glance.tests.utils as test_utils
UUID1 = 'c80a1a6c-bd1f-41c5-90ee-81afedb1d58d'
TENANT1 = '6838eb7b-6ded-434a-882c-b344c77fe8df'
class TestImageFactory(test_utils.BaseTestCase):
def setUp(self):
super(TestImageFactory, self).setUp()
self.image_factory = domain.ImageFactory()
def test_minimal_new_image(self):
image = self.image_factory.new_image()
self.assertTrue(image.image_id is not None)
self.assertTrue(image.created_at is not None)
self.assertEqual(image.created_at, image.updated_at)
self.assertEqual(image.status, 'queued')
self.assertEqual(image.visibility, 'private')
self.assertEqual(image.owner, None)
self.assertEqual(image.name, None)
self.assertEqual(image.size, None)
self.assertEqual(image.min_disk, 0)
self.assertEqual(image.min_ram, 0)
self.assertEqual(image.protected, False)
self.assertEqual(image.disk_format, None)
self.assertEqual(image.container_format, None)
self.assertEqual(image.extra_properties, {})
self.assertEqual(image.tags, set([]))
def test_new_image(self):
image = self.image_factory.new_image(
image_id=UUID1, name='image-1', min_disk=256,
owner=TENANT1)
self.assertEqual(image.image_id, UUID1)
self.assertTrue(image.created_at is not None)
self.assertEqual(image.created_at, image.updated_at)
self.assertEqual(image.status, 'queued')
self.assertEqual(image.visibility, 'private')
self.assertEqual(image.owner, TENANT1)
self.assertEqual(image.name, 'image-1')
self.assertEqual(image.size, None)
self.assertEqual(image.min_disk, 256)
self.assertEqual(image.min_ram, 0)
self.assertEqual(image.protected, False)
self.assertEqual(image.disk_format, None)
self.assertEqual(image.container_format, None)
self.assertEqual(image.extra_properties, {})
self.assertEqual(image.tags, set([]))
def test_new_image_with_extra_properties_and_tags(self):
extra_properties = {'foo': 'bar'}
tags = ['one', 'two']
image = self.image_factory.new_image(
image_id=UUID1, name='image-1',
extra_properties=extra_properties, tags=tags)
self.assertEqual(image.image_id, UUID1)
self.assertTrue(image.created_at is not None)
self.assertEqual(image.created_at, image.updated_at)
self.assertEqual(image.status, 'queued')
self.assertEqual(image.visibility, 'private')
self.assertEqual(image.owner, None)
self.assertEqual(image.name, 'image-1')
self.assertEqual(image.size, None)
self.assertEqual(image.min_disk, 0)
self.assertEqual(image.min_ram, 0)
self.assertEqual(image.protected, False)
self.assertEqual(image.disk_format, None)
self.assertEqual(image.container_format, None)
self.assertEqual(image.extra_properties, {'foo': 'bar'})
self.assertEqual(image.tags, set(['one', 'two']))
def test_new_image_with_extra_properties_and_tags(self):
extra_properties = {'foo': 'bar'}
tags = ['one', 'two']
image = self.image_factory.new_image(
image_id=UUID1, name='image-1',
extra_properties=extra_properties, tags=tags)
self.assertEqual(image.image_id, UUID1)
self.assertTrue(image.created_at is not None)
self.assertEqual(image.created_at, image.updated_at)
self.assertEqual(image.status, 'queued')
self.assertEqual(image.visibility, 'private')
self.assertEqual(image.owner, None)
self.assertEqual(image.name, 'image-1')
self.assertEqual(image.size, None)
self.assertEqual(image.min_disk, 0)
self.assertEqual(image.min_ram, 0)
self.assertEqual(image.protected, False)
self.assertEqual(image.disk_format, None)
self.assertEqual(image.container_format, None)
self.assertEqual(image.extra_properties, {'foo': 'bar'})
self.assertEqual(image.tags, set(['one', 'two']))
def test_new_image_read_only_property(self):
self.assertRaises(exception.ReadonlyProperty,
self.image_factory.new_image, image_id=UUID1,
name='image-1', size=256)
def test_new_image_unexpected_property(self):
self.assertRaises(TypeError,
self.image_factory.new_image, image_id=UUID1,
image_name='name-1')
def test_new_image_reserved_property(self):
extra_properties = {'deleted': True}
self.assertRaises(exception.ReservedProperty,
self.image_factory.new_image, image_id=UUID1,
extra_properties=extra_properties)
class TestImage(test_utils.BaseTestCase):
def setUp(self):
super(TestImage, self).setUp()
self.image_factory = domain.ImageFactory()
self.image = self.image_factory.new_image()
def test_visibility_enumerated(self):
self.image.visibility = 'public'
self.image.visibility = 'private'
self.assertRaises(ValueError, setattr,
self.image, 'visibility', 'ellison')
def test_tags_always_a_set(self):
self.image.tags = ['a', 'b', 'c']
self.assertEqual(self.image.tags, set(['a', 'b', 'c']))
def test_delete_protected_image(self):
self.image.protected = True
self.assertRaises(exception.ProtectedImageDelete, self.image.delete)