Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CFY-5395 Add image type to openstack plugin
Add image from url Add container_format & disk_format validation and tests Add create volume/server from image
- Loading branch information
1 parent
a5f2225
commit b2c49cb
Showing
11 changed files
with
511 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
|
||
tosca_definitions_version: cloudify_dsl_1_3 | ||
|
||
imports: | ||
- http://www.getcloudify.org/spec/cloudify/3.4/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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
Oops, something went wrong.