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:
parent
517739f9f1
commit
43ba08d8aa
@ -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
173
glance/domain.py
Normal 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()
|
151
glance/tests/unit/test_domain.py
Normal file
151
glance/tests/unit/test_domain.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user