diff options
Diffstat (limited to 'aria/multivim-plugin/glance_plugin')
-rw-r--r-- | aria/multivim-plugin/glance_plugin/__init__.py | 14 | ||||
-rw-r--r-- | aria/multivim-plugin/glance_plugin/image.py | 177 | ||||
-rw-r--r-- | aria/multivim-plugin/glance_plugin/tests/resources/test-image-start.yaml | 30 | ||||
-rw-r--r-- | aria/multivim-plugin/glance_plugin/tests/test.py | 148 |
4 files changed, 369 insertions, 0 deletions
diff --git a/aria/multivim-plugin/glance_plugin/__init__.py b/aria/multivim-plugin/glance_plugin/__init__.py new file mode 100644 index 0000000000..809f033a55 --- /dev/null +++ b/aria/multivim-plugin/glance_plugin/__init__.py @@ -0,0 +1,14 @@ +######### +# Copyright (c) 2015 GigaSpaces Technologies Ltd. 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. diff --git a/aria/multivim-plugin/glance_plugin/image.py b/aria/multivim-plugin/glance_plugin/image.py new file mode 100644 index 0000000000..a8d5b203f4 --- /dev/null +++ b/aria/multivim-plugin/glance_plugin/image.py @@ -0,0 +1,177 @@ +######### +# Copyright (c) 2015 GigaSpaces Technologies Ltd. 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 httplib +from urlparse import urlparse + +from cloudify import ctx +from cloudify.decorators import operation +from cloudify.exceptions import NonRecoverableError + +from openstack_plugin_common import ( + with_glance_client, + get_resource_id, + use_external_resource, + get_openstack_ids_of_connected_nodes_by_openstack_type, + delete_resource_and_runtime_properties, + validate_resource, + COMMON_RUNTIME_PROPERTIES_KEYS, + OPENSTACK_ID_PROPERTY, + OPENSTACK_TYPE_PROPERTY, + OPENSTACK_NAME_PROPERTY) + + +IMAGE_OPENSTACK_TYPE = 'image' +IMAGE_STATUS_ACTIVE = 'active' + +RUNTIME_PROPERTIES_KEYS = COMMON_RUNTIME_PROPERTIES_KEYS +REQUIRED_PROPERTIES = ['container_format', 'disk_format'] + + +@operation +@with_glance_client +def create(glance_client, **kwargs): + if use_external_resource(ctx, glance_client, IMAGE_OPENSTACK_TYPE): + return + + img_dict = { + 'name': get_resource_id(ctx, IMAGE_OPENSTACK_TYPE) + } + _validate_image_dictionary() + img_properties = ctx.node.properties['image'] + img_dict.update({key: value for key, value in img_properties.iteritems() + if key != 'data'}) + img = glance_client.images.create(**img_dict) + img_path = img_properties.get('data', '') + img_url = ctx.node.properties.get('image_url') + try: + _validate_image() + if img_path: + with open(img_path, 'rb') as image_file: + glance_client.images.upload( + image_id=img.id, + image_data=image_file) + elif img_url: + img = glance_client.images.add_location(img.id, img_url, {}) + + except: + _remove_protected(glance_client) + glance_client.images.delete(image_id=img.id) + raise + + ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY] = img.id + ctx.instance.runtime_properties[OPENSTACK_TYPE_PROPERTY] = \ + IMAGE_OPENSTACK_TYPE + ctx.instance.runtime_properties[OPENSTACK_NAME_PROPERTY] = img.name + + +def _get_image_by_ctx(glance_client, ctx): + return glance_client.images.get( + image_id=ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY]) + + +@operation +@with_glance_client +def start(glance_client, start_retry_interval, **kwargs): + img = _get_image_by_ctx(glance_client, ctx) + if img.status != IMAGE_STATUS_ACTIVE: + return ctx.operation.retry( + message='Waiting for image to get uploaded', + retry_after=start_retry_interval) + + +@operation +@with_glance_client +def delete(glance_client, **kwargs): + _remove_protected(glance_client) + delete_resource_and_runtime_properties(ctx, glance_client, + RUNTIME_PROPERTIES_KEYS) + + +@operation +@with_glance_client +def creation_validation(glance_client, **kwargs): + validate_resource(ctx, glance_client, IMAGE_OPENSTACK_TYPE) + _validate_image_dictionary() + _validate_image() + + +def _validate_image_dictionary(): + img = ctx.node.properties['image'] + missing = '' + try: + for prop in REQUIRED_PROPERTIES: + if prop not in img: + missing += '{0} '.format(prop) + except TypeError: + missing = ' '.join(REQUIRED_PROPERTIES) + if missing: + raise NonRecoverableError('Required properties are missing: {' + '0}. Please update your image ' + 'dictionary.'.format(missing)) + + +def _validate_image(): + img = ctx.node.properties['image'] + img_path = img.get('data') + img_url = ctx.node.properties.get('image_url') + if not img_url and not img_path: + raise NonRecoverableError('Neither image url nor image path was ' + 'provided') + if img_url and img_path: + raise NonRecoverableError('Multiple image sources provided') + if img_url: + _check_url(img_url) + if img_path: + _check_path() + + +def _check_url(url): + p = urlparse(url) + conn = httplib.HTTPConnection(p.netloc) + conn.request('HEAD', p.path) + resp = conn.getresponse() + if resp.status >= 400: + raise NonRecoverableError('Invalid image URL') + + +def _check_path(): + img = ctx.node.properties['image'] + img_path = img.get('data') + try: + with open(img_path, 'rb'): + pass + except TypeError: + if not img.get('url'): + raise NonRecoverableError('No path or url provided') + except IOError: + raise NonRecoverableError( + 'Unable to open image file with path: "{}"'.format(img_path)) + + +def _remove_protected(glance_client): + if use_external_resource(ctx, glance_client, IMAGE_OPENSTACK_TYPE): + return + + is_protected = ctx.node.properties['image'].get('protected', False) + if is_protected: + img_id = ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY] + glance_client.images.update(img_id, protected=False) + + +def handle_image_from_relationship(obj_dict, property_name_to_put, ctx): + images = get_openstack_ids_of_connected_nodes_by_openstack_type( + ctx, IMAGE_OPENSTACK_TYPE) + if images: + obj_dict.update({property_name_to_put: images[0]}) diff --git a/aria/multivim-plugin/glance_plugin/tests/resources/test-image-start.yaml b/aria/multivim-plugin/glance_plugin/tests/resources/test-image-start.yaml new file mode 100644 index 0000000000..12c9aa79b7 --- /dev/null +++ b/aria/multivim-plugin/glance_plugin/tests/resources/test-image-start.yaml @@ -0,0 +1,30 @@ + +tosca_definitions_version: cloudify_dsl_1_3 + +imports: + - https://raw.githubusercontent.com/cloudify-cosmo/cloudify-manager/4.1/resources/rest-service/cloudify/types/types.yaml + - plugin.yaml + +inputs: + use_password: + type: boolean + default: false + +node_templates: + image: + type: cloudify.openstack.nodes.Image + properties: + image: + disk_format: test_format + container_format: test_format + data: test_path + openstack_config: + username: aaa + password: aaa + tenant_name: aaa + auth_url: aaa + interfaces: + cloudify.interfaces.lifecycle: + start: + inputs: + start_retry_interval: 1 diff --git a/aria/multivim-plugin/glance_plugin/tests/test.py b/aria/multivim-plugin/glance_plugin/tests/test.py new file mode 100644 index 0000000000..4a88cba4e7 --- /dev/null +++ b/aria/multivim-plugin/glance_plugin/tests/test.py @@ -0,0 +1,148 @@ +######### +# Copyright (c) 2014 GigaSpaces Technologies Ltd. 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 +import os +import tempfile +import unittest + +import glance_plugin +from glance_plugin import image + +from cloudify.mocks import MockCloudifyContext +from cloudify.test_utils import workflow_test +from cloudify.exceptions import NonRecoverableError + + +def ctx_mock(image_dict): + return MockCloudifyContext( + node_id='d', + properties=image_dict) + + +class TestCheckImage(unittest.TestCase): + + @mock.patch('glance_plugin.image.ctx', + ctx_mock({'image': {}})) + def test_check_image_no_file_no_url(self): + # Test if it throws exception no file & no url + self.assertRaises(NonRecoverableError, + image._validate_image) + + @mock.patch('glance_plugin.image.ctx', + ctx_mock({'image_url': 'test-url', 'image': {'data': '.'}})) + def test_check_image_and_url(self): + # Test if it throws exception file & url + self.assertRaises(NonRecoverableError, + image._validate_image) + + @mock.patch('glance_plugin.image.ctx', + ctx_mock({'image_url': 'test-url', 'image': {}})) + def test_check_image_url(self): + # test if it passes no file & url + http_connection_mock = mock.MagicMock() + http_connection_mock.return_value.getresponse.return_value.status = 200 + with mock.patch('httplib.HTTPConnection', http_connection_mock): + glance_plugin.image._validate_image() + + def test_check_image_file(self): + # test if it passes file & no url + image_file_path = tempfile.mkstemp()[1] + with mock.patch('glance_plugin.image.ctx', + ctx_mock({'image': {'data': image_file_path}})): + glance_plugin.image._validate_image() + + @mock.patch('glance_plugin.image.ctx', + ctx_mock({'image': {'data': '/test/path'}})) + # test when open file throws IO error + def test_check_image_bad_file(self): + open_name = '%s.open' % __name__ + with mock.patch(open_name, create=True) as mock_open: + mock_open.side_effect = [mock_open(read_data='Data').return_value] + self.assertRaises(NonRecoverableError, + glance_plugin.image._validate_image) + + @mock.patch('glance_plugin.image.ctx', + ctx_mock({'image_url': '?', 'image': {}})) + # test when bad url + def test_check_image_bad_url(self): + http_connection_mock = mock.MagicMock() + http_connection_mock.return_value.getresponse.return_value.status = 400 + with mock.patch('httplib.HTTPConnection', http_connection_mock): + self.assertRaises(NonRecoverableError, + glance_plugin.image._validate_image) + + +class TestValidateProperties(unittest.TestCase): + + @mock.patch('glance_plugin.image.ctx', + ctx_mock({'image': {'container_format': 'bare'}})) + def test_check_image_container_format_no_disk_format(self): + # Test if it throws exception no file & no url + self.assertRaises(NonRecoverableError, + image._validate_image_dictionary) + + @mock.patch('glance_plugin.image.ctx', + ctx_mock({'image': {'disk_format': 'qcow2'}})) + def test_check_image_no_container_format_disk_format(self): + # Test if it throws exception no container_format & disk_format + self.assertRaises(NonRecoverableError, + image._validate_image_dictionary) + + @mock.patch('glance_plugin.image.ctx', + ctx_mock({'image': {}})) + def test_check_image_no_container_format_no_disk_format(self): + # Test if it throws exception no container_format & no disk_format + self.assertRaises(NonRecoverableError, + image._validate_image_dictionary) + + @mock.patch('glance_plugin.image.ctx', + ctx_mock( + {'image': + {'container_format': 'bare', + 'disk_format': 'qcow2'}})) + def test_check_image_container_format_disk_format(self): + # Test if it do not throw exception container_format & disk_format + image._validate_image_dictionary() + + +class TestStartImage(unittest.TestCase): + blueprint_path = os.path.join('resources', + 'test-image-start.yaml') + + @mock.patch('glance_plugin.image.create') + @workflow_test(blueprint_path, copy_plugin_yaml=True) + def test_image_lifecycle_start(self, cfy_local, *_): + test_vars = { + 'counter': 0, + 'image': mock.MagicMock() + } + + def _mock_get_image_by_ctx(*_): + i = test_vars['image'] + if test_vars['counter'] == 0: + i.status = 'different image status' + else: + i.status = glance_plugin.image.IMAGE_STATUS_ACTIVE + test_vars['counter'] += 1 + return i + + with mock.patch('openstack_plugin_common.GlanceClient'): + with mock.patch('glance_plugin.image._get_image_by_ctx', + side_effect=_mock_get_image_by_ctx): + cfy_local.execute('install', task_retries=3) + + self.assertEqual(2, test_vars['counter']) + self.assertEqual(0, test_vars['image'].start.call_count) |