ARIA multivim plugin initial checkin
Change-Id: I3a24ab6fc5ba54466bfecaf596a13b8907248ae8
Issue-id: SO-77
Signed-off-by: DeWayne Filppi <dewayne@gigaspaces.com>
diff --git a/aria/multivim-plugin/cinder_plugin/tests/__init__.py b/aria/multivim-plugin/cinder_plugin/tests/__init__.py
new file mode 100644
index 0000000..a9dfcc4
--- /dev/null
+++ b/aria/multivim-plugin/cinder_plugin/tests/__init__.py
@@ -0,0 +1,14 @@
+#########
+# 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.
diff --git a/aria/multivim-plugin/cinder_plugin/tests/test_volume.py b/aria/multivim-plugin/cinder_plugin/tests/test_volume.py
new file mode 100644
index 0000000..0ee85bc
--- /dev/null
+++ b/aria/multivim-plugin/cinder_plugin/tests/test_volume.py
@@ -0,0 +1,342 @@
+#########
+# 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 unittest
+
+from cloudify import mocks as cfy_mocks
+from cloudify import exceptions as cfy_exc
+from cloudify.state import current_ctx
+from cinder_plugin import volume
+from nova_plugin import server
+from openstack_plugin_common import (OPENSTACK_ID_PROPERTY,
+ OPENSTACK_TYPE_PROPERTY,
+ OPENSTACK_NAME_PROPERTY)
+
+
+class TestCinderVolume(unittest.TestCase):
+
+ def _mock(self, **kwargs):
+ ctx = cfy_mocks.MockCloudifyContext(**kwargs)
+ current_ctx.set(ctx)
+ return ctx
+
+ def tearDown(self):
+ current_ctx.clear()
+
+ def test_create_new(self):
+ volume_name = 'fake volume name'
+ volume_description = 'fake volume'
+ volume_id = '00000000-0000-0000-0000-000000000000'
+ volume_size = 10
+
+ volume_properties = {
+ 'volume': {
+ 'size': volume_size,
+ 'description': volume_description
+ },
+ 'use_external_resource': False,
+ 'device_name': '/dev/fake',
+ 'resource_id': volume_name,
+ }
+
+ creating_volume_m = mock.Mock()
+ creating_volume_m.id = volume_id
+ creating_volume_m.status = volume.VOLUME_STATUS_CREATING
+ available_volume_m = mock.Mock()
+ available_volume_m.id = volume_id
+ available_volume_m.status = volume.VOLUME_STATUS_AVAILABLE
+ cinder_client_m = mock.Mock()
+ cinder_client_m.volumes = mock.Mock()
+ cinder_client_m.volumes.create = mock.Mock(
+ return_value=creating_volume_m)
+ cinder_client_m.volumes.get = mock.Mock(
+ return_value=available_volume_m)
+ ctx_m = self._mock(node_id='a', properties=volume_properties)
+
+ volume.create(cinder_client=cinder_client_m, args={}, ctx=ctx_m,
+ status_attempts=10, status_timeout=2)
+
+ cinder_client_m.volumes.create.assert_called_once_with(
+ size=volume_size,
+ name=volume_name,
+ description=volume_description)
+ cinder_client_m.volumes.get.assert_called_once_with(volume_id)
+ self.assertEqual(
+ volume_id,
+ ctx_m.instance.runtime_properties[OPENSTACK_ID_PROPERTY])
+ self.assertEqual(
+ volume.VOLUME_OPENSTACK_TYPE,
+ ctx_m.instance.runtime_properties[OPENSTACK_TYPE_PROPERTY])
+
+ def test_create_use_existing(self):
+ volume_id = '00000000-0000-0000-0000-000000000000'
+
+ volume_properties = {
+ 'use_external_resource': True,
+ 'device_name': '/dev/fake',
+ 'resource_id': volume_id,
+ }
+ existing_volume_m = mock.Mock()
+ existing_volume_m.id = volume_id
+ existing_volume_m.status = volume.VOLUME_STATUS_AVAILABLE
+ cinder_client_m = mock.Mock()
+ cinder_client_m.volumes = mock.Mock()
+ cinder_client_m.volumes.create = mock.Mock()
+ cinder_client_m.cosmo_get_if_exists = mock.Mock(
+ return_value=existing_volume_m)
+ cinder_client_m.get_id_from_resource = mock.Mock(
+ return_value=volume_id)
+ ctx_m = self._mock(node_id='a', properties=volume_properties)
+
+ volume.create(cinder_client=cinder_client_m, args={}, ctx=ctx_m,
+ status_attempts=10, status_timeout=2)
+
+ self.assertFalse(cinder_client_m.volumes.create.called)
+ self.assertEqual(
+ volume_id,
+ ctx_m.instance.runtime_properties[OPENSTACK_ID_PROPERTY])
+ self.assertEqual(
+ volume.VOLUME_OPENSTACK_TYPE,
+ ctx_m.instance.runtime_properties[OPENSTACK_TYPE_PROPERTY])
+
+ def test_delete(self):
+ volume_id = '00000000-0000-0000-0000-000000000000'
+ volume_name = 'test-volume'
+
+ volume_properties = {
+ 'use_external_resource': False,
+ }
+
+ cinder_client_m = mock.Mock()
+ cinder_client_m.cosmo_delete_resource = mock.Mock()
+
+ ctx_m = self._mock(node_id='a', properties=volume_properties)
+ ctx_m.instance.runtime_properties[OPENSTACK_ID_PROPERTY] = volume_id
+ ctx_m.instance.runtime_properties[OPENSTACK_TYPE_PROPERTY] = \
+ volume.VOLUME_OPENSTACK_TYPE
+ ctx_m.instance.runtime_properties[OPENSTACK_NAME_PROPERTY] = \
+ volume_name
+
+ volume.delete(cinder_client=cinder_client_m, ctx=ctx_m)
+
+ cinder_client_m.cosmo_delete_resource.assert_called_once_with(
+ volume.VOLUME_OPENSTACK_TYPE, volume_id)
+ self.assertTrue(
+ OPENSTACK_ID_PROPERTY not in ctx_m.instance.runtime_properties)
+ self.assertTrue(OPENSTACK_TYPE_PROPERTY
+ not in ctx_m.instance.runtime_properties)
+ self.assertTrue(OPENSTACK_NAME_PROPERTY
+ not in ctx_m.instance.runtime_properties)
+
+ @mock.patch('openstack_plugin_common.NovaClientWithSugar')
+ @mock.patch('openstack_plugin_common.CinderClientWithSugar')
+ @mock.patch.object(volume, 'wait_until_status', return_value=(None, True))
+ def test_attach(self, wait_until_status_m, cinder_m, nova_m):
+ volume_id = '00000000-0000-0000-0000-000000000000'
+ server_id = '11111111-1111-1111-1111-111111111111'
+ device_name = '/dev/fake'
+
+ volume_ctx = cfy_mocks.MockContext({
+ 'node': cfy_mocks.MockContext({
+ 'properties': {volume.DEVICE_NAME_PROPERTY: device_name}
+ }),
+ 'instance': cfy_mocks.MockContext({
+ 'runtime_properties': {
+ OPENSTACK_ID_PROPERTY: volume_id,
+ }
+ })
+ })
+ server_ctx = cfy_mocks.MockContext({
+ 'node': cfy_mocks.MockContext({
+ 'properties': {}
+ }),
+ 'instance': cfy_mocks.MockContext({
+ 'runtime_properties': {
+ server.OPENSTACK_ID_PROPERTY: server_id
+ }
+ })
+ })
+
+ ctx_m = self._mock(node_id='a',
+ target=server_ctx,
+ source=volume_ctx)
+
+ nova_instance = nova_m.return_value
+ cinder_instance = cinder_m.return_value
+
+ server.attach_volume(ctx=ctx_m, status_attempts=10,
+ status_timeout=2)
+
+ nova_instance.volumes.create_server_volume.assert_called_once_with(
+ server_id, volume_id, device_name)
+ wait_until_status_m.assert_called_once_with(
+ cinder_client=cinder_instance,
+ volume_id=volume_id,
+ status=volume.VOLUME_STATUS_IN_USE,
+ num_tries=10,
+ timeout=2,
+ )
+
+ @mock.patch('openstack_plugin_common.NovaClientWithSugar')
+ @mock.patch('openstack_plugin_common.CinderClientWithSugar')
+ def _test_cleanup__after_attach_fails(
+ self, expected_err_cls, expect_cleanup,
+ wait_until_status_m, cinder_m, nova_m):
+ volume_id = '00000000-0000-0000-0000-000000000000'
+ server_id = '11111111-1111-1111-1111-111111111111'
+ attachment_id = '22222222-2222-2222-2222-222222222222'
+ device_name = '/dev/fake'
+
+ attachment = {'id': attachment_id,
+ 'server_id': server_id,
+ 'volume_id': volume_id}
+
+ volume_ctx = cfy_mocks.MockContext({
+ 'node': cfy_mocks.MockContext({
+ 'properties': {volume.DEVICE_NAME_PROPERTY: device_name}
+ }),
+ 'instance': cfy_mocks.MockContext({
+ 'runtime_properties': {
+ OPENSTACK_ID_PROPERTY: volume_id,
+ }
+ })
+ })
+ server_ctx = cfy_mocks.MockContext({
+ 'node': cfy_mocks.MockContext({
+ 'properties': {}
+ }),
+ 'instance': cfy_mocks.MockContext({
+ 'runtime_properties': {
+ server.OPENSTACK_ID_PROPERTY: server_id
+ }
+ })
+ })
+
+ ctx_m = self._mock(node_id='a',
+ target=server_ctx,
+ source=volume_ctx)
+
+ attached_volume = mock.Mock(id=volume_id,
+ status=volume.VOLUME_STATUS_IN_USE,
+ attachments=[attachment])
+ nova_instance = nova_m.return_value
+ cinder_instance = cinder_m.return_value
+ cinder_instance.volumes.get.return_value = attached_volume
+
+ with self.assertRaises(expected_err_cls):
+ server.attach_volume(ctx=ctx_m, status_attempts=10,
+ status_timeout=2)
+
+ nova_instance.volumes.create_server_volume.assert_called_once_with(
+ server_id, volume_id, device_name)
+ volume.wait_until_status.assert_any_call(
+ cinder_client=cinder_instance,
+ volume_id=volume_id,
+ status=volume.VOLUME_STATUS_IN_USE,
+ num_tries=10,
+ timeout=2,
+ )
+ if expect_cleanup:
+ nova_instance.volumes.delete_server_volume.assert_called_once_with(
+ server_id, attachment_id)
+ self.assertEqual(2, volume.wait_until_status.call_count)
+ volume.wait_until_status.assert_called_with(
+ cinder_client=cinder_instance,
+ volume_id=volume_id,
+ status=volume.VOLUME_STATUS_AVAILABLE,
+ num_tries=10,
+ timeout=2)
+
+ def test_cleanup_after_waituntilstatus_throws_recoverable_error(self):
+ err = cfy_exc.RecoverableError('Some recoverable error')
+ with mock.patch.object(volume, 'wait_until_status',
+ side_effect=[err, (None, True)]) as wait_mock:
+ self._test_cleanup__after_attach_fails(type(err), True, wait_mock)
+
+ def test_cleanup_after_waituntilstatus_throws_any_not_nonrecov_error(self):
+ class ArbitraryNonRecoverableException(Exception):
+ pass
+ err = ArbitraryNonRecoverableException('An exception')
+ with mock.patch.object(volume, 'wait_until_status',
+ side_effect=[err, (None, True)]) as wait_mock:
+ self._test_cleanup__after_attach_fails(type(err), True, wait_mock)
+
+ def test_cleanup_after_waituntilstatus_lets_nonrecov_errors_pass(self):
+ err = cfy_exc.NonRecoverableError('Some non recoverable error')
+ with mock.patch.object(volume, 'wait_until_status',
+ side_effect=[err, (None, True)]) as wait_mock:
+ self._test_cleanup__after_attach_fails(type(err), False, wait_mock)
+
+ @mock.patch.object(volume, 'wait_until_status', return_value=(None, False))
+ def test_cleanup_after_waituntilstatus_times_out(self, wait_mock):
+ self._test_cleanup__after_attach_fails(cfy_exc.RecoverableError, True,
+ wait_mock)
+
+ @mock.patch('openstack_plugin_common.NovaClientWithSugar')
+ @mock.patch('openstack_plugin_common.CinderClientWithSugar')
+ @mock.patch.object(volume, 'wait_until_status', return_value=(None, True))
+ def test_detach(self, wait_until_status_m, cinder_m, nova_m):
+ volume_id = '00000000-0000-0000-0000-000000000000'
+ server_id = '11111111-1111-1111-1111-111111111111'
+ attachment_id = '22222222-2222-2222-2222-222222222222'
+
+ attachment = {'id': attachment_id,
+ 'server_id': server_id,
+ 'volume_id': volume_id}
+
+ volume_ctx = cfy_mocks.MockContext({
+ 'node': cfy_mocks.MockContext({
+ 'properties': {}
+ }),
+ 'instance': cfy_mocks.MockContext({
+ 'runtime_properties': {
+ OPENSTACK_ID_PROPERTY: volume_id,
+ }
+ })
+ })
+ server_ctx = cfy_mocks.MockContext({
+ 'node': cfy_mocks.MockContext({
+ 'properties': {}
+ }),
+ 'instance': cfy_mocks.MockContext({
+ 'runtime_properties': {
+ server.OPENSTACK_ID_PROPERTY: server_id
+ }
+ })
+ })
+
+ ctx_m = self._mock(node_id='a',
+ target=server_ctx,
+ source=volume_ctx)
+
+ attached_volume = mock.Mock(id=volume_id,
+ status=volume.VOLUME_STATUS_IN_USE,
+ attachments=[attachment])
+ nova_instance = nova_m.return_value
+ cinder_instance = cinder_m.return_value
+ cinder_instance.volumes.get.return_value = attached_volume
+
+ server.detach_volume(ctx=ctx_m, status_attempts=10, status_timeout=2)
+
+ nova_instance.volumes.delete_server_volume.assert_called_once_with(
+ server_id, attachment_id)
+ volume.wait_until_status.assert_called_once_with(
+ cinder_client=cinder_instance,
+ volume_id=volume_id,
+ status=volume.VOLUME_STATUS_AVAILABLE,
+ num_tries=10,
+ timeout=2,
+ )