From 38d6a8ce3c9bce63cf1bc8222c5a94070701ef17 Mon Sep 17 00:00:00 2001 From: spisarski Date: Thu, 19 Oct 2017 14:31:22 -0600 Subject: 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 --- snaps/openstack/create_volume.py | 269 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 snaps/openstack/create_volume.py (limited to 'snaps/openstack/create_volume.py') 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) -- cgit 1.2.3-korg