From 523f22d015f080da5521ed2c87487536aa1a6009 Mon Sep 17 00:00:00 2001 From: Jose Idar Date: Thu, 12 Mar 2015 16:18:53 -0500 Subject: [PATCH] Added behaviors and behavior tests * Added verify_volume_status_progression_during_detachment behavior. * Modified verify_volume_status_progression_during_attachment behavior for better code reuse. * Added metatests for both behaviors. Change-Id: I0f3290e01b1380ecfcbc34e02e2aae2abf384a8c --- .../volume_attachments_api/behaviors.py | 90 ++++++++++++++----- .../volume_attachments_api/__init__.py | 15 ++++ .../volume_attachments_api/behaviors_test.py | 89 ++++++++++++++++++ 3 files changed, 171 insertions(+), 23 deletions(-) create mode 100644 metatests/cloudcafe/compute/volume_attachments_api/__init__.py create mode 100644 metatests/cloudcafe/compute/volume_attachments_api/behaviors_test.py diff --git a/cloudcafe/compute/volume_attachments_api/behaviors.py b/cloudcafe/compute/volume_attachments_api/behaviors.py index 9975cb7c..660a3c54 100644 --- a/cloudcafe/compute/volume_attachments_api/behaviors.py +++ b/cloudcafe/compute/volume_attachments_api/behaviors.py @@ -1,5 +1,6 @@ from time import sleep, time -from cloudcafe.common.behaviors import StatusProgressionVerifier +from cloudcafe.common.behaviors import ( + StatusProgressionVerifier, StatusProgressionVerifierError) from cloudcafe.compute.common.behaviors import BaseComputeBehavior from cloudcafe.compute.volume_attachments_api.config import \ VolumeAttachmentsAPIConfig @@ -14,6 +15,7 @@ class VolumeAttachmentsAPI_Behaviors(BaseComputeBehavior): def __init__( self, volume_attachments_client=None, volume_attachments_config=None, volumes_client=None): + super(VolumeAttachmentsAPI_Behaviors, self).__init__() self.client = volume_attachments_client self.config = volume_attachments_config or VolumeAttachmentsAPIConfig() @@ -34,53 +36,95 @@ class VolumeAttachmentsAPI_Behaviors(BaseComputeBehavior): else: return False + def _get_volume_status(self, volume_id): + resp = self.volumes_client.get_volume_info(volume_id=volume_id) + if not resp.ok: + msg = ( + "get_volume_status() failure: get_volume_info() call" + " failed with a {0} status code".format(resp.status_code)) + self._log.error(msg) + raise Exception(msg) + + if resp.entity is None: + msg = ( + "get_volume_status() failure: unable to deserialize" + " response from get_volume_info() call") + self._log.error(msg) + raise Exception(msg) + + return resp.entity.status + def verify_volume_status_progression_during_attachment( self, volume_id, state_list=None): - def _get_volume_status(self, volume_id): - resp = self.volumes_client.get_volume_info(volume_id=volume_id) - if not resp.ok: - msg = ( - "get_volume_status() failure: get_volume_info() call" - " failed with a {0} status code".format(resp.status_code)) - self._log.error(msg) - raise Exception(msg) - - if resp.entity is None: - msg = ( - "get_volume_status() failure: unable to deserialize" - " response from get_volume_info() call") - self._log.error(msg) - raise Exception(msg) - - return resp.entity.status - verifier = StatusProgressionVerifier( - 'volume', volume_id, _get_volume_status, *[self, volume_id]) + 'volume', volume_id, self._get_volume_status, volume_id) + verifier.set_global_state_properties( + timeout=self.config.attachment_timeout) verifier.add_state( expected_statuses=['available'], acceptable_statuses=['attaching', 'in-use'], error_statuses=['error', 'creating'], - timeout=10, poll_rate=1, poll_failure_retry_limit=3) + poll_rate=self.config.api_poll_rate, + poll_failure_retry_limit=3) verifier.add_state( expected_statuses=['attaching'], acceptable_statuses=['in-use'], error_statuses=['error', 'creating'], - timeout=self.config.attachment_timeout, poll_rate=self.config.api_poll_rate, poll_failure_retry_limit=3) verifier.add_state( expected_statuses=['in-use'], error_statuses=['available', 'error', 'creating'], - timeout=self.config.attachment_timeout, poll_rate=self.config.api_poll_rate, poll_failure_retry_limit=3) verifier.start() + def verify_volume_status_progression_during_detachment( + self, volume_id, raise_on_error=True): + """ + Track the status progression of volume volume_id being detached. + + Optionally fails silently if rais_on_error is set to False. + :param volume_id: the uuid of the volume being tracked + :returns: None + """ + + verifier = StatusProgressionVerifier( + 'volume', volume_id, self._get_volume_status, volume_id) + verifier.set_global_state_properties( + timeout=self.config.attachment_timeout) + + verifier.add_state( + expected_statuses=['in-use'], + acceptable_statuses=['detaching', 'available'], + error_statuses=['error', 'attaching', 'creating', 'deleting'], + poll_rate=self.config.api_poll_rate, + poll_failure_retry_limit=3) + + verifier.add_state( + expected_statuses=['detaching'], + acceptable_statuses=['available'], + error_statuses=['error', 'attaching', 'creating', 'deleting'], + poll_rate=self.config.api_poll_rate, + poll_failure_retry_limit=3) + + verifier.add_state( + expected_statuses=['available'], + error_statuses=['error', 'attaching', 'creating', 'deleting'], + poll_rate=self.config.api_poll_rate, + poll_failure_retry_limit=3) + + try: + verifier.start() + except Exception as exception: + if raise_on_error: + raise exception + def delete_volume_attachment( self, attachment_id, server_id, timeout=None, poll_rate=None): """Waits timeout seconds for volume attachment to 404 after issuing diff --git a/metatests/cloudcafe/compute/volume_attachments_api/__init__.py b/metatests/cloudcafe/compute/volume_attachments_api/__init__.py new file mode 100644 index 00000000..14b45c9a --- /dev/null +++ b/metatests/cloudcafe/compute/volume_attachments_api/__init__.py @@ -0,0 +1,15 @@ +""" +Copyright 2015 Rackspace + +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/metatests/cloudcafe/compute/volume_attachments_api/behaviors_test.py b/metatests/cloudcafe/compute/volume_attachments_api/behaviors_test.py new file mode 100644 index 00000000..d2fc770e --- /dev/null +++ b/metatests/cloudcafe/compute/volume_attachments_api/behaviors_test.py @@ -0,0 +1,89 @@ +""" +Copyright 2015 Rackspace + +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 collections import OrderedDict +from mock import MagicMock, Mock +from requests import Response +import sys +import unittest + +from cafe.common.reporting import cclogging +from cafe.configurator.managers import _lazy_property +cclogging.init_root_log_handler() +from cloudcafe.common.behaviors import StatusProgressionVerifierError +from cloudcafe.compute.volume_attachments_api.behaviors import \ + VolumeAttachmentsAPI_Behaviors +from cloudcafe.compute.volume_attachments_api.client import \ + VolumeAttachmentsAPIClient +from cloudcafe.compute.volume_attachments_api.config import \ + VolumeAttachmentsAPIConfig +from cloudcafe.blockstorage.volumes_api.v2.client import \ + VolumesClient +from cloudcafe.blockstorage.volumes_api.v2.models.responses import \ + VolumeResponse + +class MockBuilder(object): + + def _mock(self, name, **defaults): + defaults.update(**self._overrides.get(name, {})) + setattr(self, name, Mock(**defaults)) + + +class TestMocks(MockBuilder): + + def __init__(self, **kwargs): + self._overrides = kwargs + self._mock( + 'volume_model', spec=VolumeResponse, status='in-use', id_='111111') + self._mock( + 'response', spec=Response, ok=True, entity=self.volume_model) + self._mock( + 'volumes_client', spec=VolumesClient, + get_volume_info=MagicMock(return_value=self.response)) + self._mock( + 'volume_attachments_client', spec=VolumeAttachmentsAPIClient) + self._mock( + 'volume_attachments_config', spec=VolumeAttachmentsAPIConfig, + attachment_timeout=1, api_poll_rate=1) + +class BaseTestCase(object): + mocks = TestMocks() + + def behavior_class_under_test(self): + return VolumeAttachmentsAPI_Behaviors( + self.mocks.volume_attachments_client, + self.mocks.volume_attachments_config, + self.mocks.volumes_client) + + def setUp(self): + self.behaviors = self.behavior_class_under_test() + + +class MethodTests_verify_volume_status_progression_during_attachment( + BaseTestCase, unittest.TestCase): + + def test_volume_is_attached(self): + r = self.behaviors.verify_volume_status_progression_during_attachment( + self.mocks.volume_model.id_) + + +class MethodTests_verify_volume_status_progression_during_detachment( + BaseTestCase, unittest.TestCase): + mocks = TestMocks(volume_model=dict(status='available')) + + def test_volume_is_detached(self): + r = self.behaviors.verify_volume_status_progression_during_detachment( + self.mocks.volume_model.id_)