summaryrefslogtreecommitdiffstats
path: root/snaps
diff options
context:
space:
mode:
authorspisarski <s.pisarski@cablelabs.com>2017-10-19 14:31:22 -0600
committerspisarski <s.pisarski@cablelabs.com>2017-10-19 14:31:22 -0600
commit38d6a8ce3c9bce63cf1bc8222c5a94070701ef17 (patch)
treebad212d34bcf2b01ff1f851b0b5e6feeb81cb421 /snaps
parentd5d282dd1ffab96e85332bea580689c3297a25f8 (diff)
Third patch for volume support.
* Added support for volumes integrated with QoS and encryption. * Created tests for volumes at an API and state machine level. JIRA: SNAPS-197 Change-Id: I07326875b9f1a30e50389531d0d2571ee648675f Signed-off-by: spisarski <s.pisarski@cablelabs.com>
Diffstat (limited to 'snaps')
-rw-r--r--snaps/domain/test/volume_tests.py32
-rw-r--r--snaps/domain/volume.py34
-rw-r--r--snaps/openstack/create_user.py5
-rw-r--r--snaps/openstack/create_volume.py269
-rw-r--r--snaps/openstack/create_volume_type.py2
-rw-r--r--snaps/openstack/tests/create_volume_tests.py315
-rw-r--r--snaps/openstack/utils/cinder_utils.py84
-rw-r--r--snaps/openstack/utils/tests/cinder_utils_tests.py110
-rw-r--r--snaps/test_suite_builder.py31
9 files changed, 877 insertions, 5 deletions
diff --git a/snaps/domain/test/volume_tests.py b/snaps/domain/test/volume_tests.py
index ec5f7b7..fa0a95a 100644
--- a/snaps/domain/test/volume_tests.py
+++ b/snaps/domain/test/volume_tests.py
@@ -14,7 +14,37 @@
# limitations under the License.
import unittest
-from snaps.domain.volume import QoSSpec, VolumeType, VolumeTypeEncryption
+from snaps.domain.volume import (
+ QoSSpec, VolumeType, VolumeTypeEncryption, Volume)
+
+
+class VolumeDomainObjectTests(unittest.TestCase):
+ """
+ Tests the construction of the snaps.domain.volume.Volume class
+ """
+
+ def test_construction_positional(self):
+ volume = Volume('name1', 'id1', 'desc_val1', 2, 'type_val1',
+ 'avail_zone1', False)
+ self.assertEqual('name1', volume.name)
+ self.assertEqual('id1', volume.id)
+ self.assertEqual('desc_val1', volume.description)
+ self.assertEqual(2, volume.size)
+ self.assertEqual('type_val1', volume.type)
+ self.assertEqual('avail_zone1', volume.availability_zone)
+ self.assertFalse(volume.multi_attach)
+
+ def test_construction_named(self):
+ volume = Volume(multi_attach=True, availability_zone='avail_zone2',
+ vol_type='type_val2', size=3, description='desc_val2',
+ volume_id='id2', name='name2')
+ self.assertEqual('name2', volume.name)
+ self.assertEqual('id2', volume.id)
+ self.assertEqual('desc_val2', volume.description)
+ self.assertEqual(3, volume.size)
+ self.assertEqual('type_val2', volume.type)
+ self.assertEqual('avail_zone2', volume.availability_zone)
+ self.assertTrue(volume.multi_attach)
class VolumeTypeDomainObjectTests(unittest.TestCase):
diff --git a/snaps/domain/volume.py b/snaps/domain/volume.py
index e82a60a..96094a8 100644
--- a/snaps/domain/volume.py
+++ b/snaps/domain/volume.py
@@ -14,6 +14,40 @@
# limitations under the License.
+class Volume:
+ """
+ SNAPS domain object for Volumes. Should contain attributes that
+ are shared amongst cloud providers
+ """
+ def __init__(self, name, volume_id, description, size, vol_type,
+ availability_zone, multi_attach):
+ """
+ Constructor
+ :param name: the volume's name
+ :param volume_id: the volume's id
+ :param description: the volume's description
+ :param size: the volume's size in GB
+ :param vol_type: the volume's type
+ :param availability_zone: the volume's availability zone
+ :param multi_attach: When true, volume can be attached to multiple VMs
+ """
+ self.name = name
+ self.id = volume_id
+ self.description = description
+ self.size = size
+ self.type = vol_type
+ self.availability_zone = availability_zone
+ self.multi_attach = multi_attach
+
+ def __eq__(self, other):
+ return (self.name == other.name and self.id == other.id
+ and self.description == other.description
+ and self.size == other.size
+ and self.type == other.type
+ and self.availability_zone == other.availability_zone
+ and self.multi_attach == other.multi_attach)
+
+
class VolumeType:
"""
SNAPS domain object for Volume Types. Should contain attributes that
diff --git a/snaps/openstack/create_user.py b/snaps/openstack/create_user.py
index bcf4790..64a0b5f 100644
--- a/snaps/openstack/create_user.py
+++ b/snaps/openstack/create_user.py
@@ -96,6 +96,11 @@ class OpenStackUser(OpenStackIdentityObject):
auth_url=self._os_creds.auth_url,
project_name=project_name,
identity_api_version=self._os_creds.identity_api_version,
+ image_api_version=self._os_creds.image_api_version,
+ network_api_version=self._os_creds.network_api_version,
+ compute_api_version=self._os_creds.compute_api_version,
+ heat_api_version=self._os_creds.heat_api_version,
+ volume_api_version=self._os_creds.volume_api_version,
user_domain_name=self._os_creds.user_domain_name,
user_domain_id=self._os_creds.user_domain_id,
project_domain_name=self._os_creds.project_domain_name,
diff --git a/snaps/openstack/create_volume.py b/snaps/openstack/create_volume.py
new file mode 100644
index 0000000..9baad7e
--- /dev/null
+++ b/snaps/openstack/create_volume.py
@@ -0,0 +1,269 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+# and others. 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 logging
+import time
+
+from cinderclient.exceptions import NotFound
+
+from snaps.openstack.openstack_creator import OpenStackVolumeObject
+from snaps.openstack.utils import cinder_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('create_volume')
+
+VOLUME_ACTIVE_TIMEOUT = 300
+VOLUME_DELETE_TIMEOUT = 60
+POLL_INTERVAL = 3
+STATUS_ACTIVE = 'available'
+STATUS_FAILED = 'failed'
+STATUS_DELETED = 'deleted'
+
+
+class OpenStackVolume(OpenStackVolumeObject):
+ """
+ Class responsible for managing an volume in OpenStack
+ """
+
+ def __init__(self, os_creds, volume_settings):
+ """
+ Constructor
+ :param os_creds: The OpenStack connection credentials
+ :param volume_settings: The volume settings
+ :return:
+ """
+ super(self.__class__, self).__init__(os_creds)
+
+ self.volume_settings = volume_settings
+ self.__volume = None
+
+ def initialize(self):
+ """
+ Loads the existing Volume
+ :return: The Volume domain object or None
+ """
+ super(self.__class__, self).initialize()
+
+ self.__volume = cinder_utils.get_volume(
+ self._cinder, volume_settings=self.volume_settings)
+ return self.__volume
+
+ def create(self, block=False):
+ """
+ Creates the volume in OpenStack if it does not already exist and
+ returns the domain Volume object
+ :return: The Volume domain object or None
+ """
+ self.initialize()
+
+ if not self.__volume:
+ self.__volume = cinder_utils.create_volume(
+ self._cinder, self.volume_settings)
+
+ logger.info(
+ 'Created volume with name - %s', self.volume_settings.name)
+ if self.__volume:
+ if block:
+ if self.volume_active(block=True):
+ logger.info('Volume is now active with name - %s',
+ self.volume_settings.name)
+ return self.__volume
+ else:
+ raise VolumeCreationError(
+ 'Volume was not created or activated in the '
+ 'alloted amount of time')
+ else:
+ logger.info('Did not create volume due to cleanup mode')
+
+ return self.__volume
+
+ def clean(self):
+ """
+ Cleanse environment of all artifacts
+ :return: void
+ """
+ if self.__volume:
+ try:
+ if self.volume_active(block=True):
+ cinder_utils.delete_volume(self._cinder, self.__volume)
+ else:
+ logger.warn('Timeout waiting to delete volume %s',
+ self.__volume.name)
+ except NotFound:
+ pass
+
+ try:
+ if self.volume_deleted(block=True):
+ logger.info(
+ 'Volume has been properly deleted with name - %s',
+ self.volume_settings.name)
+ self.__vm = None
+ else:
+ logger.error(
+ 'Volume not deleted within the timeout period of %s '
+ 'seconds', VOLUME_DELETE_TIMEOUT)
+ except Exception as e:
+ logger.error(
+ 'Unexpected error while checking VM instance status - %s',
+ e)
+
+ self.__volume = None
+
+ def get_volume(self):
+ """
+ Returns the domain Volume object as it was populated when create() was
+ called
+ :return: the object
+ """
+ return self.__volume
+
+ def volume_active(self, block=False, timeout=VOLUME_ACTIVE_TIMEOUT,
+ poll_interval=POLL_INTERVAL):
+ """
+ Returns true when the volume status returns the value of
+ expected_status_code
+ :param block: When true, thread will block until active or timeout
+ value in seconds has been exceeded (False)
+ :param timeout: The timeout value
+ :param poll_interval: The polling interval in seconds
+ :return: T/F
+ """
+ return self._volume_status_check(STATUS_ACTIVE, block, timeout,
+ poll_interval)
+
+ def volume_deleted(self, block=False, poll_interval=POLL_INTERVAL):
+ """
+ Returns true when the VM status returns the value of
+ expected_status_code or instance retrieval throws a NotFound exception.
+ :param block: When true, thread will block until active or timeout
+ value in seconds has been exceeded (False)
+ :param poll_interval: The polling interval in seconds
+ :return: T/F
+ """
+ try:
+ return self._volume_status_check(
+ STATUS_DELETED, block, VOLUME_DELETE_TIMEOUT, poll_interval)
+ except NotFound as e:
+ logger.debug(
+ "Volume not found when querying status for %s with message "
+ "%s", STATUS_DELETED, e)
+ return True
+
+ def _volume_status_check(self, expected_status_code, block, timeout,
+ poll_interval):
+ """
+ Returns true when the volume status returns the value of
+ expected_status_code
+ :param expected_status_code: instance status evaluated with this string
+ value
+ :param block: When true, thread will block until active or timeout
+ value in seconds has been exceeded (False)
+ :param timeout: The timeout value
+ :param poll_interval: The polling interval in seconds
+ :return: T/F
+ """
+ # sleep and wait for volume status change
+ if block:
+ start = time.time()
+ else:
+ start = time.time() - timeout + 10
+
+ while timeout > time.time() - start:
+ status = self._status(expected_status_code)
+ if status:
+ logger.debug('Volume is active with name - %s',
+ self.volume_settings.name)
+ return True
+
+ logger.debug('Retry querying volume status in %s seconds',
+ str(poll_interval))
+ time.sleep(poll_interval)
+ logger.debug('Volume status query timeout in %s',
+ str(timeout - (time.time() - start)))
+
+ logger.error(
+ 'Timeout checking for volume status for ' + expected_status_code)
+ return False
+
+ def _status(self, expected_status_code):
+ """
+ Returns True when active else False
+ :param expected_status_code: instance status evaluated with this string
+ value
+ :return: T/F
+ """
+ status = cinder_utils.get_volume_status(self._cinder, self.__volume)
+ if not status:
+ logger.warning(
+ 'Cannot volume status for volume with ID - %s',
+ self.__volume.id)
+ return False
+
+ if status == 'ERROR':
+ raise VolumeCreationError(
+ 'Instance had an error during deployment')
+ logger.debug('Instance status is - ' + status)
+ return status == expected_status_code
+
+
+class VolumeSettings:
+ def __init__(self, **kwargs):
+ """
+ Constructor
+ :param name: the volume's name (required)
+ :param description: the volume's name (required)
+ :param size: the volume's size in GB (default 1)
+ :param image_name: when a glance image is used for the image source
+ (optional)
+ :param type_name: the associated volume's type name (optional)
+ :param availability_zone: the name of the compute server on which to
+ deploy the volume (optional)
+ :param multi_attach: when true, volume can be attached to more than one
+ server (default False)
+ """
+
+ self.name = kwargs.get('name')
+ self.description = kwargs.get('description')
+ self.size = int(kwargs.get('size', 1))
+ self.image_name = kwargs.get('image_name')
+ self.type_name = kwargs.get('type_name')
+ self.availability_zone = kwargs.get('availability_zone')
+
+ if kwargs.get('availability_zone'):
+ self.multi_attach = bool(kwargs.get('availability_zone'))
+ else:
+ self.multi_attach = False
+
+ if not self.name:
+ raise VolumeSettingsError("The attribute name is required")
+
+
+class VolumeSettingsError(Exception):
+ """
+ Exception to be thrown when an volume settings are incorrect
+ """
+
+ def __init__(self, message):
+ Exception.__init__(self, message)
+
+
+class VolumeCreationError(Exception):
+ """
+ Exception to be thrown when an volume cannot be created
+ """
+
+ def __init__(self, message):
+ Exception.__init__(self, message)
diff --git a/snaps/openstack/create_volume_type.py b/snaps/openstack/create_volume_type.py
index a60bb1e..830fb21 100644
--- a/snaps/openstack/create_volume_type.py
+++ b/snaps/openstack/create_volume_type.py
@@ -56,7 +56,7 @@ class OpenStackVolumeType(OpenStackVolumeObject):
return self.__volume_type
- def create(self, block=False):
+ def create(self):
"""
Creates the volume in OpenStack if it does not already exist and
returns the domain Volume object
diff --git a/snaps/openstack/tests/create_volume_tests.py b/snaps/openstack/tests/create_volume_tests.py
new file mode 100644
index 0000000..d0c59f7
--- /dev/null
+++ b/snaps/openstack/tests/create_volume_tests.py
@@ -0,0 +1,315 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+# and others. 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 cinderclient.exceptions import NotFound, BadRequest
+
+from snaps.openstack.create_image import OpenStackImage
+from snaps.openstack.create_volume_type import (
+ VolumeTypeSettings, OpenStackVolumeType)
+from snaps.openstack.tests import openstack_tests
+
+try:
+ from urllib.request import URLError
+except ImportError:
+ from urllib2 import URLError
+
+import logging
+import unittest
+import uuid
+
+from snaps.openstack import create_volume
+from snaps.openstack.create_volume import (
+ VolumeSettings, VolumeSettingsError, OpenStackVolume)
+from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase
+from snaps.openstack.utils import cinder_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('create_volume_tests')
+
+
+class VolumeSettingsUnitTests(unittest.TestCase):
+ """
+ Tests the construction of the VolumeSettings class
+ """
+
+ def test_no_params(self):
+ with self.assertRaises(VolumeSettingsError):
+ VolumeSettings()
+
+ def test_empty_config(self):
+ with self.assertRaises(VolumeSettingsError):
+ VolumeSettings(**dict())
+
+ def test_name_only(self):
+ settings = VolumeSettings(name='foo')
+ self.assertEqual('foo', settings.name)
+ self.assertIsNone(settings.description)
+ self.assertEquals(1, settings.size)
+ self.assertIsNone(settings.image_name)
+ self.assertIsNone(settings.type_name)
+ self.assertIsNone(settings.availability_zone)
+ self.assertFalse(settings.multi_attach)
+
+ def test_config_with_name_only(self):
+ settings = VolumeSettings(**{'name': 'foo'})
+ self.assertEqual('foo', settings.name)
+ self.assertIsNone(settings.description)
+ self.assertEquals(1, settings.size)
+ self.assertIsNone(settings.image_name)
+ self.assertIsNone(settings.type_name)
+ self.assertIsNone(settings.availability_zone)
+ self.assertFalse(settings.multi_attach)
+
+ def test_all_strings(self):
+ settings = VolumeSettings(
+ name='foo', description='desc', size='2', image_name='image',
+ type_name='type', availability_zone='zone1', multi_attach='true')
+
+ self.assertEqual('foo', settings.name)
+ self.assertEqual('desc', settings.description)
+ self.assertEqual(2, settings.size)
+ self.assertEqual('image', settings.image_name)
+ self.assertEqual('type', settings.type_name)
+ self.assertEqual('zone1', settings.availability_zone)
+ self.assertTrue(settings.multi_attach)
+
+ def test_all_correct_type(self):
+ settings = VolumeSettings(
+ name='foo', description='desc', size=2, image_name='image',
+ type_name='bar', availability_zone='zone1', multi_attach=True)
+
+ self.assertEqual('foo', settings.name)
+ self.assertEqual('desc', settings.description)
+ self.assertEqual(2, settings.size)
+ self.assertEqual('image', settings.image_name)
+ self.assertEqual('bar', settings.type_name)
+ self.assertEqual('zone1', settings.availability_zone)
+ self.assertTrue(settings.multi_attach)
+
+ def test_config_all(self):
+ settings = VolumeSettings(
+ **{'name': 'foo', 'description': 'desc', 'size': '2',
+ 'image_name': 'foo', 'type_name': 'bar',
+ 'availability_zone': 'zone1', 'multi_attach': 'true'})
+
+ self.assertEqual('foo', settings.name)
+ self.assertEqual('desc', settings.description)
+ self.assertEqual(2, settings.size)
+ self.assertEqual('foo', settings.image_name)
+ self.assertEqual('bar', settings.type_name)
+ self.assertEqual('zone1', settings.availability_zone)
+ self.assertTrue(settings.multi_attach)
+
+
+class CreateSimpleVolumeSuccessTests(OSIntegrationTestCase):
+ """
+ Test for the CreateVolume class defined in create_volume.py
+ """
+
+ def setUp(self):
+ """
+ Instantiates the CreateVolume object that is responsible for
+ downloading and creating an OS volume file within OpenStack
+ """
+ super(self.__class__, self).__start__()
+
+ guid = uuid.uuid4()
+ self.volume_settings = VolumeSettings(
+ name=self.__class__.__name__ + '-' + str(guid))
+
+ self.cinder = cinder_utils.cinder_client(self.os_creds)
+ self.volume_creator = None
+
+ def tearDown(self):
+ """
+ Cleans the volume and downloaded volume file
+ """
+ if self.volume_creator:
+ self.volume_creator.clean()
+
+ super(self.__class__, self).__clean__()
+
+ def test_create_volume_simple(self):
+ """
+ Tests the creation of an OpenStack volume from a URL.
+ """
+ # Create Volume
+ self.volume_creator = create_volume.OpenStackVolume(
+ self.os_creds, self.volume_settings)
+ created_volume = self.volume_creator.create(block=True)
+ self.assertIsNotNone(created_volume)
+
+ retrieved_volume = cinder_utils.get_volume(
+ self.cinder, volume_settings=self.volume_settings)
+
+ self.assertIsNotNone(retrieved_volume)
+ self.assertEqual(created_volume.id, retrieved_volume.id)
+ self.assertTrue(created_volume == retrieved_volume)
+
+ def test_create_delete_volume(self):
+ """
+ Tests the creation then deletion of an OpenStack volume to ensure
+ clean() does not raise an Exception.
+ """
+ # Create Volume
+ self.volume_creator = create_volume.OpenStackVolume(
+ self.os_creds, self.volume_settings)
+ created_volume = self.volume_creator.create(block=True)
+ self.assertIsNotNone(created_volume)
+
+ retrieved_volume = cinder_utils.get_volume(
+ self.cinder, volume_settings=self.volume_settings)
+ self.assertIsNotNone(retrieved_volume)
+ self.assertEqual(created_volume, retrieved_volume)
+
+ # Delete Volume manually
+ self.volume_creator.clean()
+
+ self.assertIsNone(cinder_utils.get_volume(
+ self.cinder, volume_settings=self.volume_settings))
+
+ # Must not throw an exception when attempting to cleanup non-existent
+ # volume
+ self.volume_creator.clean()
+ self.assertIsNone(self.volume_creator.get_volume())
+
+ def test_create_same_volume(self):
+ """
+ Tests the creation of an OpenStack volume when one already exists.
+ """
+ # Create Volume
+ self.volume_creator = OpenStackVolume(
+ self.os_creds, self.volume_settings)
+ volume1 = self.volume_creator.create(block=True)
+
+ retrieved_volume = cinder_utils.get_volume(
+ self.cinder, volume_settings=self.volume_settings)
+ self.assertEqual(volume1, retrieved_volume)
+
+ # Should be retrieving the instance data
+ os_volume_2 = OpenStackVolume(
+ self.os_creds, self.volume_settings)
+ volume2 = os_volume_2.create(block=True)
+ self.assertEqual(volume1, volume2)
+
+
+class CreateVolumeWithTypeTests(OSIntegrationTestCase):
+ """
+ Test cases for the CreateVolume when attempting to associate it to a
+ Volume Type
+ """
+
+ def setUp(self):
+ super(self.__class__, self).__start__()
+
+ guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+ self.volume_name = guid + '-vol'
+ self.volume_type_name = guid + '-vol-type'
+
+ self.volume_type_creator = OpenStackVolumeType(
+ self.os_creds, VolumeTypeSettings(name=self.volume_type_name))
+ self.volume_type_creator.create()
+ self.volume_creator = None
+
+ def tearDown(self):
+ if self.volume_creator:
+ self.volume_creator.clean()
+ if self.volume_type_creator:
+ self.volume_type_creator.clean()
+
+ super(self.__class__, self).__clean__()
+
+ def test_bad_volume_type(self):
+ """
+ Expect a NotFound to be raised when the volume type does not exist
+ """
+ self.volume_creator = OpenStackVolume(
+ self.os_creds,
+ VolumeSettings(name=self.volume_name, type_name='foo'))
+
+ with self.assertRaises(NotFound):
+ self.volume_creator.create()
+
+ def test_valid_volume_type(self):
+ """
+ Expect a NotFound to be raised when the volume type does not exist
+ """
+ self.volume_creator = OpenStackVolume(
+ self.os_creds,
+ VolumeSettings(name=self.volume_name,
+ type_name=self.volume_type_name))
+
+ created_volume = self.volume_creator.create()
+ self.assertIsNotNone(created_volume)
+ self.assertEqual(self.volume_type_name, created_volume.type)
+
+
+class CreateVolumeWithImageTests(OSIntegrationTestCase):
+ """
+ Test cases for the CreateVolume when attempting to associate it to an Image
+ """
+
+ def setUp(self):
+ super(self.__class__, self).__start__()
+
+ guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+ self.volume_name = guid + '-vol'
+ self.image_name = guid + '-image'
+
+ os_image_settings = openstack_tests.cirros_image_settings(
+ name=self.image_name, image_metadata=self.image_metadata)
+ # Create Image
+ self.image_creator = OpenStackImage(self.os_creds,
+ os_image_settings)
+ self.image_creator.create()
+ self.volume_creator = None
+
+ def tearDown(self):
+ if self.volume_creator:
+ try:
+ self.volume_creator.clean()
+ except:
+ pass
+ if self.image_creator:
+ try:
+ self.image_creator.clean()
+ except:
+ pass
+
+ super(self.__class__, self).__clean__()
+
+ def test_bad_image_name(self):
+ """
+ Expect a NotFound to be raised when the volume type does not exist
+ """
+ self.volume_creator = OpenStackVolume(
+ self.os_creds,
+ VolumeSettings(name=self.volume_name, image_name='foo'))
+
+ with self.assertRaises(BadRequest):
+ self.volume_creator.create(block=True)
+
+ def test_valid_volume_image(self):
+ """
+ Expect a NotFound to be raised when the volume type does not exist
+ """
+ self.volume_creator = OpenStackVolume(
+ self.os_creds,
+ VolumeSettings(name=self.volume_name, image_name=self.image_name))
+
+ created_volume = self.volume_creator.create(block=True)
+ self.assertIsNotNone(created_volume)
+ self.assertIsNone(created_volume.type)
+ self.assertTrue(self.volume_creator.volume_active())
diff --git a/snaps/openstack/utils/cinder_utils.py b/snaps/openstack/utils/cinder_utils.py
index d13277d..e40b471 100644
--- a/snaps/openstack/utils/cinder_utils.py
+++ b/snaps/openstack/utils/cinder_utils.py
@@ -17,7 +17,8 @@ import logging
from cinderclient.client import Client
from cinderclient.exceptions import NotFound
-from snaps.domain.volume import QoSSpec, VolumeType, VolumeTypeEncryption
+from snaps.domain.volume import (
+ QoSSpec, VolumeType, VolumeTypeEncryption, Volume)
from snaps.openstack.utils import keystone_utils
__author__ = 'spisarski'
@@ -42,6 +43,87 @@ def cinder_client(os_creds):
region_name=os_creds.region_name)
+def get_volume(cinder, volume_name=None, volume_settings=None):
+ """
+ Returns an OpenStack volume object for a given name
+ :param cinder: the Cinder client
+ :param volume_name: the volume name to lookup
+ :param volume_settings: the volume settings used for lookups
+ :return: the volume object or None
+ """
+ if volume_settings:
+ volume_name = volume_settings.name
+
+ volumes = cinder.volumes.list()
+ for volume in volumes:
+ if volume.name == volume_name:
+ return Volume(
+ name=volume.name, volume_id=volume.id,
+ description=volume.description, size=volume.size,
+ vol_type=volume.volume_type,
+ availability_zone=volume.availability_zone,
+ multi_attach=volume.multiattach)
+
+
+def get_volume_by_id(cinder, volume_id):
+ """
+ Returns an OpenStack volume object for a given name
+ :param cinder: the Cinder client
+ :param volume_id: the volume ID to lookup
+ :return: the SNAPS-OO Domain Volume object or None
+ """
+ volume = cinder.volumes.get(volume_id)
+ return Volume(
+ name=volume.name, volume_id=volume.id, description=volume.description,
+ size=volume.size, vol_type=volume.volume_type,
+ availability_zone=volume.availability_zone,
+ multi_attach=volume.multiattach)
+
+
+def get_volume_status(cinder, volume):
+ """
+ Returns a new OpenStack Volume object for a given OpenStack volume object
+ :param cinder: the Cinder client
+ :param volume: the domain Volume object
+ :return: the OpenStack Volume object
+ """
+ os_volume = cinder.volumes.get(volume.id)
+ return os_volume.status
+
+
+def create_volume(cinder, volume_settings):
+ """
+ Creates and returns OpenStack volume object with an external URL
+ :param cinder: the cinder client
+ :param volume_settings: the volume settings object
+ :return: the OpenStack volume object
+ :raise Exception if using a file and it cannot be found
+ """
+ created_volume = cinder.volumes.create(
+ name=volume_settings.name, description=volume_settings.description,
+ size=volume_settings.size, imageRef=volume_settings.image_name,
+ volume_type=volume_settings.type_name,
+ availability_zone=volume_settings.availability_zone,
+ multiattach=volume_settings.multi_attach)
+
+ return Volume(
+ name=created_volume.name, volume_id=created_volume.id,
+ description=created_volume.description,
+ size=created_volume.size, vol_type=created_volume.volume_type,
+ availability_zone=created_volume.availability_zone,
+ multi_attach=created_volume.multiattach)
+
+
+def delete_volume(cinder, volume):
+ """
+ Deletes an volume from OpenStack
+ :param cinder: the cinder client
+ :param volume: the volume to delete
+ """
+ logger.info('Deleting volume named - %s', volume.name)
+ cinder.volumes.delete(volume.id)
+
+
def get_volume_type(cinder, volume_type_name=None, volume_type_settings=None):
"""
Returns an OpenStack volume type object for a given name
diff --git a/snaps/openstack/utils/tests/cinder_utils_tests.py b/snaps/openstack/utils/tests/cinder_utils_tests.py
index a45167e..6fd92e3 100644
--- a/snaps/openstack/utils/tests/cinder_utils_tests.py
+++ b/snaps/openstack/utils/tests/cinder_utils_tests.py
@@ -15,9 +15,12 @@
import logging
import uuid
+import time
from cinderclient.exceptions import NotFound, BadRequest
+from snaps.openstack import create_volume
from snaps.openstack.create_qos import QoSSettings, Consumer
+from snaps.openstack.create_volume import VolumeSettings
from snaps.openstack.create_volume_type import (
VolumeTypeSettings, VolumeTypeEncryptionSettings, ControlLocation)
from snaps.openstack.tests import validation_utils
@@ -57,6 +60,113 @@ class CinderSmokeTests(OSComponentTestCase):
cinder.volumes.list()
+class CinderUtilsVolumeTests(OSComponentTestCase):
+ """
+ Test for the CreateVolume class defined in create_volume.py
+ """
+
+ def setUp(self):
+ """
+ Instantiates the CreateVolume object that is responsible for
+ downloading and creating an OS volume file within OpenStack
+ """
+ guid = uuid.uuid4()
+ self.volume_name = self.__class__.__name__ + '-' + str(guid)
+ self.volume = None
+ self.cinder = cinder_utils.cinder_client(self.os_creds)
+
+ def tearDown(self):
+ """
+ Cleans the remote OpenStack objects
+ """
+ if self.volume:
+ try:
+ cinder_utils.delete_volume(self.cinder, self.volume)
+ except NotFound:
+ pass
+
+ self.assertTrue(volume_deleted(self.cinder, self.volume))
+
+ def test_create_simple_volume(self):
+ """
+ Tests the cinder_utils.create_volume()
+ """
+ volume_settings = VolumeSettings(name=self.volume_name)
+ self.volume = cinder_utils.create_volume(
+ self.cinder, volume_settings)
+ self.assertIsNotNone(self.volume)
+ self.assertEqual(self.volume_name, self.volume.name)
+
+ self.assertTrue(volume_active(self.cinder, self.volume))
+
+ volume = cinder_utils.get_volume(
+ self.cinder, volume_settings=volume_settings)
+ self.assertIsNotNone(volume)
+ validation_utils.objects_equivalent(self.volume, volume)
+
+ def test_create_delete_volume(self):
+ """
+ Tests the cinder_utils.create_volume()
+ """
+ volume_settings = VolumeSettings(name=self.volume_name)
+ self.volume = cinder_utils.create_volume(
+ self.cinder, volume_settings)
+ self.assertIsNotNone(self.volume)
+ self.assertEqual(self.volume_name, self.volume.name)
+
+ self.assertTrue(volume_active(self.cinder, self.volume))
+
+ volume = cinder_utils.get_volume(
+ self.cinder, volume_settings=volume_settings)
+ self.assertIsNotNone(volume)
+ validation_utils.objects_equivalent(self.volume, volume)
+
+ cinder_utils.delete_volume(self.cinder, self.volume)
+ self.assertTrue(volume_deleted(self.cinder, self.volume))
+ self.assertIsNone(
+ cinder_utils.get_volume(self.cinder, volume_settings))
+
+
+def volume_active(cinder, volume):
+ """
+ Returns true if volume becomes active
+ :param cinder:
+ :param volume:
+ :return:
+ """
+ end_time = time.time() + create_volume.VOLUME_ACTIVE_TIMEOUT
+ while time.time() < end_time:
+ status = cinder_utils.get_volume_status(cinder, volume)
+ if status == create_volume.STATUS_ACTIVE:
+ return True
+ elif status == create_volume.STATUS_FAILED:
+ return False
+ time.sleep(3)
+
+ return False
+
+
+def volume_deleted(cinder, volume):
+ """
+ Returns true if volume becomes active
+ :param cinder:
+ :param volume:
+ :return:
+ """
+ end_time = time.time() + create_volume.VOLUME_ACTIVE_TIMEOUT
+ while time.time() < end_time:
+ try:
+ status = cinder_utils.get_volume_status(cinder, volume)
+ if status == create_volume.STATUS_DELETED:
+ return True
+ except NotFound:
+ return True
+
+ time.sleep(3)
+
+ return False
+
+
class CinderUtilsQoSTests(OSComponentTestCase):
"""
Test for the CreateQos class defined in create_qos.py
diff --git a/snaps/test_suite_builder.py b/snaps/test_suite_builder.py
index a1b72aa..2fba92d 100644
--- a/snaps/test_suite_builder.py
+++ b/snaps/test_suite_builder.py
@@ -34,7 +34,7 @@ from snaps.domain.test.vm_inst_tests import (
VmInstDomainObjectTests, FloatingIpDomainObjectTests)
from snaps.domain.test.volume_tests import (
QoSSpecDomainObjectTests, VolumeTypeDomainObjectTests,
- VolumeTypeEncryptionObjectTests)
+ VolumeTypeEncryptionObjectTests, VolumeDomainObjectTests)
from snaps.openstack.tests.conf.os_credentials_tests import (
ProxySettingsUnitTests, OSCredsUnitTests)
from snaps.openstack.tests.create_flavor_tests import (
@@ -70,6 +70,9 @@ from snaps.openstack.tests.create_stack_tests import (
CreateComplexStackTests)
from snaps.openstack.tests.create_user_tests import (
UserSettingsUnitTests, CreateUserSuccessTests)
+from snaps.openstack.tests.create_volume_tests import (
+ VolumeSettingsUnitTests, CreateSimpleVolumeSuccessTests,
+ CreateVolumeWithTypeTests, CreateVolumeWithImageTests)
from snaps.openstack.tests.create_volume_type_tests import (
VolumeTypeSettingsUnitTests, CreateSimpleVolumeTypeSuccessTests,
CreateVolumeTypeComplexTests)
@@ -77,7 +80,8 @@ from snaps.openstack.tests.os_source_file_test import (
OSComponentTestCase, OSIntegrationTestCase)
from snaps.openstack.utils.tests.cinder_utils_tests import (
CinderSmokeTests, CinderUtilsQoSTests, CinderUtilsSimpleVolumeTypeTests,
- CinderUtilsAddEncryptionTests, CinderUtilsVolumeTypeCompleteTests)
+ CinderUtilsAddEncryptionTests, CinderUtilsVolumeTypeCompleteTests,
+ CinderUtilsVolumeTests)
from snaps.openstack.utils.tests.glance_utils_tests import (
GlanceSmokeTests, GlanceUtilsTests)
from snaps.openstack.utils.tests.heat_utils_tests import (
@@ -179,6 +183,8 @@ def add_unit_tests(suite):
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
VolumeTypeEncryptionObjectTests))
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+ VolumeDomainObjectTests))
+ suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
QoSSpecDomainObjectTests))
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
VmInstDomainObjectTests))
@@ -188,6 +194,8 @@ def add_unit_tests(suite):
QoSSettingsUnitTests))
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
VolumeTypeSettingsUnitTests))
+ suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+ VolumeSettingsUnitTests))
def add_openstack_client_tests(suite, os_creds, ext_net_name,
@@ -312,6 +320,10 @@ def add_openstack_api_tests(suite, os_creds, ext_net_name, use_keystone=True,
ext_net_name=ext_net_name, log_level=log_level,
image_metadata=image_metadata))
suite.addTest(OSComponentTestCase.parameterize(
+ CinderUtilsVolumeTests, os_creds=os_creds,
+ ext_net_name=ext_net_name, log_level=log_level,
+ image_metadata=image_metadata))
+ suite.addTest(OSComponentTestCase.parameterize(
CinderUtilsSimpleVolumeTypeTests, os_creds=os_creds,
ext_net_name=ext_net_name, log_level=log_level,
image_metadata=image_metadata))
@@ -417,6 +429,21 @@ def add_openstack_integration_tests(suite, os_creds, ext_net_name,
ext_net_name=ext_net_name, use_keystone=use_keystone,
flavor_metadata=flavor_metadata, image_metadata=image_metadata,
log_level=log_level))
+ suite.addTest(OSIntegrationTestCase.parameterize(
+ CreateSimpleVolumeSuccessTests, os_creds=os_creds,
+ ext_net_name=ext_net_name, use_keystone=use_keystone,
+ flavor_metadata=flavor_metadata, image_metadata=image_metadata,
+ log_level=log_level))
+ suite.addTest(OSIntegrationTestCase.parameterize(
+ CreateVolumeWithTypeTests, os_creds=os_creds,
+ ext_net_name=ext_net_name, use_keystone=use_keystone,
+ flavor_metadata=flavor_metadata, image_metadata=image_metadata,
+ log_level=log_level))
+ suite.addTest(OSIntegrationTestCase.parameterize(
+ CreateVolumeWithImageTests, os_creds=os_creds,
+ ext_net_name=ext_net_name, use_keystone=use_keystone,
+ flavor_metadata=flavor_metadata, image_metadata=image_metadata,
+ log_level=log_level))
# VM Instances
suite.addTest(OSIntegrationTestCase.parameterize(