Skip to content

Commit

Permalink
CFY-5395 Add image type to openstack plugin
Browse files Browse the repository at this point in the history
Add image from url
Add container_format & disk_format validation and tests
Add create volume/server from image
  • Loading branch information
BartoszStalewski authored and Anna committed Sep 8, 2016
1 parent a5f2225 commit b2c49cb
Show file tree
Hide file tree
Showing 11 changed files with 511 additions and 3 deletions.
2 changes: 2 additions & 0 deletions cinder_plugin/volume.py
Expand Up @@ -29,6 +29,7 @@
OPENSTACK_ID_PROPERTY,
OPENSTACK_TYPE_PROPERTY,
OPENSTACK_NAME_PROPERTY)
from glance_plugin.image import handle_image_from_relationship

VOLUME_STATUS_CREATING = 'creating'
VOLUME_STATUS_DELETING = 'deleting'
Expand Down Expand Up @@ -59,6 +60,7 @@ def create(cinder_client, args, **kwargs):
name = get_resource_id(ctx, VOLUME_OPENSTACK_TYPE)
volume_dict = {'display_name': name}
volume_dict.update(ctx.node.properties['volume'], **args)
handle_image_from_relationship(volume_dict, 'imageRef', ctx)
volume_dict['display_name'] = transform_resource_name(
ctx, volume_dict['display_name'])

Expand Down
14 changes: 14 additions & 0 deletions 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.
177 changes: 177 additions & 0 deletions 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]})
30 changes: 30 additions & 0 deletions glance_plugin/tests/resources/test-image-start.yaml
@@ -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
148 changes: 148 additions & 0 deletions 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)

0 comments on commit b2c49cb

Please sign in to comment.