diff --git a/examples/jenkins.py b/examples/jenkins.py index b0c31c1f..b4b624c5 100644 --- a/examples/jenkins.py +++ b/examples/jenkins.py @@ -54,7 +54,7 @@ def create_jenkins(conn, name, opts): server = conn.get(server) print(str(server)) print('Waiting for the server to come up....') - conn.compute.wait_for_status(server) + conn.compute.wait_for_server(server) print('Server is up.') if len(server.get_floating_ips()) <= 0: diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 00eaa972..79dbbee9 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -19,6 +19,7 @@ from openstack.compute.v2 import server from openstack.compute.v2 import server_interface from openstack.compute.v2 import server_ip from openstack import proxy +from openstack import resource class Proxy(proxy.BaseProxy): @@ -174,10 +175,10 @@ class Proxy(proxy.BaseProxy): def update_server(self, **data): return server.Server(data).update(self.session) - def wait_for_status(self, server, status='ACTIVE', failures=['ERROR'], + def wait_for_server(self, value, status='ACTIVE', failures=['ERROR'], interval=2, wait=120): - return server.wait_for_status(self.session, status, failures, interval, - wait) + return resource.wait_for_status(self.session, value, status, + failures, interval, wait) def create_server_interface(self, **data): return server_interface.ServerInterface(data).create(self.session) diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index 8d5f072c..d8f825f9 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -10,13 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -import time - from openstack.compute import compute_service from openstack.compute.v2 import flavor from openstack.compute.v2 import image from openstack.compute.v2 import server_ip -from openstack import exceptions from openstack import resource from openstack import utils @@ -146,45 +143,6 @@ class Server(resource.Resource): body = {'createImage': action} return self.action(session, body) - def wait_for_status(self, session, status='ACTIVE', failures=None, - interval=5, wait=120): - """Wait for the server to be in some status. - - :param session: The session to use for making this request. - :type session: :class:`~openstack.session.Session` - :param status: Desired status of the server. - :param list failures: Statuses that would indicate the transition - failed such as 'ERROR'. - :param interval: Number of seconds to wait between checks. - :param wait: Maximum number of seconds to wait for transition. - - :return: Method returns self on success. - :raises: :class:`~openstack.exceptions.ResourceTimeout` transition - to status failed to occur in wait seconds. - :raises: :class:`~openstack.exceptions.ResourceFailure` resource - transitioned to one of the failure states. - """ - try: - if self.status == status: - return self - except AttributeError: - pass - total_sleep = 0 - if failures is None: - failures = [] - while total_sleep < wait: - self.get(session) - if self.status == status: - return self - if self.status in failures: - msg = ("Resource %s transitioned to failure state %s" % - (self.id, self.status)) - raise exceptions.ResourceFailure(msg) - time.sleep(interval) - total_sleep += interval - msg = "Timeout waiting for %s to transition to %s" % (self.id, status) - raise exceptions.ResourceTimeout(msg) - def get_floating_ips(self): """Get the floating ips associated with this server.""" addresses = self.addresses[self.name] diff --git a/openstack/orchestration/v1/_proxy.py b/openstack/orchestration/v1/_proxy.py index b1ef8bb8..27f223fb 100644 --- a/openstack/orchestration/v1/_proxy.py +++ b/openstack/orchestration/v1/_proxy.py @@ -12,6 +12,7 @@ from openstack.orchestration.v1 import stack from openstack import proxy +from openstack import resource class Proxy(proxy.BaseProxy): @@ -43,3 +44,8 @@ class Proxy(proxy.BaseProxy): :returns: ``None`` """ self._delete(stack.Stack, value, ignore_missing) + + def wait_for_stack(self, value, status='CREATE_COMPLETE', + failures=['CREATE_FAILED'], interval=2, wait=120): + return resource.wait_for_status(self.session, value, status, + failures, interval, wait) diff --git a/openstack/resource.py b/openstack/resource.py index 28b96cb1..1ea6d029 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -32,6 +32,7 @@ There are plenty of examples of use of this class in the SDK code. import abc import collections import itertools +import time import six from six.moves.urllib import parse as url_parse @@ -942,3 +943,47 @@ class Resource(collections.MutableMapping): raise exceptions.DuplicateResource(msg) return None + + +def wait_for_status(session, resource, status=None, failures=None, + interval=5, wait=120): + """Wait for the resource to be in a particular status. + + :param resource: The resource to wait on to reach the status. The resource + must have a status attribute. + :type resource: :class:`~openstack.resource.Resource` + :param session: The session to use for making this request. + :type session: :class:`~openstack.session.Session` + :param status: Desired status of the resource. + :param list failures: Statuses that would indicate the transition + failed such as 'ERROR'. + :param interval: Number of seconds to wait between checks. + :param wait: Maximum number of seconds to wait for transition. + + :return: Method returns self on success. + :raises: :class:`~openstack.exceptions.ResourceTimeout` transition + to status failed to occur in wait seconds. + :raises: :class:`~openstack.exceptions.ResourceFailure` resource + transitioned to one of the failure states. + :raises: :class:`~AttributeError` if the resource does not have a status + attribute + """ + if resource.status == status: + return resource + + total_sleep = 0 + if failures is None: + failures = [] + + while total_sleep < wait: + resource.get(session) + if resource.status == status: + return resource + if resource.status in failures: + msg = ("Resource %s transitioned to failure state %s" % + (resource.id, resource.status)) + raise exceptions.ResourceFailure(msg) + time.sleep(interval) + total_sleep += interval + msg = "Timeout waiting for %s to transition to %s" % (resource.id, status) + raise exceptions.ResourceTimeout(msg) diff --git a/openstack/tests/functional/orchestration/v1/test_stack.py b/openstack/tests/functional/orchestration/v1/test_stack.py index 02270261..bac9f853 100644 --- a/openstack/tests/functional/orchestration/v1/test_stack.py +++ b/openstack/tests/functional/orchestration/v1/test_stack.py @@ -45,5 +45,7 @@ class TestStack(unittest.TestCase): 'heat-templates/plain/hot/F20/WordPress_Native.yaml' ) + self.conn.orchestration.wait_for_stack(stack) + self.assertIsNotNone(stack.id) self.assertEqual('test_stack', stack.name) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 1bddd13a..ecdf9d04 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -188,3 +188,11 @@ class TestComputeProxy(test_proxy_base.TestProxyBase): def test_server_update(self): self.verify_update('openstack.compute.v2.server.Server.update', self.proxy.update_server) + + def test_server_wait_for(self): + value = server.Server(attrs={'id': '1234'}) + self.verify_wait_for_status( + 'openstack.resource.wait_for_status', + self.proxy.wait_for_server, + method_args=[value], + expected_args=[value, 'ACTIVE', ['ERROR'], 2, 120]) diff --git a/openstack/tests/unit/compute/v2/test_server.py b/openstack/tests/unit/compute/v2/test_server.py index 2c7f37e0..ad0b6db5 100644 --- a/openstack/tests/unit/compute/v2/test_server.py +++ b/openstack/tests/unit/compute/v2/test_server.py @@ -16,7 +16,6 @@ import testtools from openstack.compute.v2 import flavor from openstack.compute.v2 import image from openstack.compute.v2 import server -from openstack import exceptions IDENTIFIER = 'IDENTIFIER' EXAMPLE = { @@ -221,67 +220,6 @@ class TestServer(testtools.TestCase): body = {"createImage": {'name': name}} self.sess.put.assert_called_with(url, service=sot.service, json=body) - def test_wait_for_status_nothing(self): - self.sess.get = mock.MagicMock() - sot = server.Server(attrs={'id': IDENTIFIER, 'status': 'ACTIVE'}) - - self.assertEqual(sot, sot.wait_for_status(self.sess, 'ACTIVE', [], - 1, 2)) - - expected = [] - self.assertEqual(expected, self.sess.get.call_args_list) - - def test_wait_for_status(self): - resp1 = mock.Mock() - resp1.body = {'server': {'status': 'BUILDING'}} - resp2 = mock.Mock() - resp2.body = {'server': {'status': 'ACTIVE'}} - self.sess.get = mock.MagicMock() - self.sess.get.side_effect = [resp1, resp2] - sot = server.Server(attrs={'id': IDENTIFIER}) - - self.assertEqual(sot, sot.wait_for_status(self.sess, 'ACTIVE', [], - 1, 2)) - - url = 'servers/IDENTIFIER' - thecall = mock.call(url, service=sot.service) - expected = [thecall, thecall] - self.assertEqual(expected, self.sess.get.call_args_list) - - def test_wait_for_status_timeout(self): - resp1 = mock.Mock() - resp1.body = {'server': {'status': 'BUILDING'}} - resp2 = mock.Mock() - resp2.body = {'server': {'status': 'BUILDING'}} - self.sess.get = mock.MagicMock() - self.sess.get.side_effect = [resp1, resp2] - sot = server.Server(attrs={'id': IDENTIFIER}) - - self.assertRaises(exceptions.ResourceTimeout, sot.wait_for_status, - self.sess, 'ACTIVE', ['ERROR'], 1, 2) - - url = 'servers/IDENTIFIER' - thecall = mock.call(url, service=sot.service) - expected = [thecall, thecall] - self.assertEqual(expected, self.sess.get.call_args_list) - - def test_wait_for_status_failures(self): - resp1 = mock.Mock() - resp1.body = {'server': {'status': 'BUILDING'}} - resp2 = mock.Mock() - resp2.body = {'server': {'status': 'ERROR'}} - self.sess.get = mock.MagicMock() - self.sess.get.side_effect = [resp1, resp2] - sot = server.Server(attrs={'id': IDENTIFIER}) - - self.assertRaises(exceptions.ResourceFailure, sot.wait_for_status, - self.sess, 'ACTIVE', ['ERROR'], 1, 2) - - url = 'servers/IDENTIFIER' - thecall = mock.call(url, service=sot.service) - expected = [thecall, thecall] - self.assertEqual(expected, self.sess.get.call_args_list) - def test_get_ips(self): name = "jenkins" fixed = { diff --git a/openstack/tests/unit/orchestration/v1/test_proxy.py b/openstack/tests/unit/orchestration/v1/test_proxy.py index 337dd770..308497bc 100644 --- a/openstack/tests/unit/orchestration/v1/test_proxy.py +++ b/openstack/tests/unit/orchestration/v1/test_proxy.py @@ -41,3 +41,12 @@ class TestOrchestrationProxy(test_proxy_base.TestProxyBase): def test_stack_delete_ignore(self): self.verify_delete2(stack.Stack, self.proxy.delete_stack, True) + + def test_stack_wait_for(self): + value = stack.Stack(attrs={'id': '1234'}) + self.verify_wait_for_status( + 'openstack.resource.wait_for_status', + self.proxy.wait_for_stack, + method_args=[value], + expected_args=[value, 'CREATE_COMPLETE', ['CREATE_FAILED'], + 2, 120]) diff --git a/openstack/tests/unit/orchestration/v1/test_stack.py b/openstack/tests/unit/orchestration/v1/test_stack.py index 38edabbb..852907bb 100644 --- a/openstack/tests/unit/orchestration/v1/test_stack.py +++ b/openstack/tests/unit/orchestration/v1/test_stack.py @@ -15,6 +15,7 @@ import testtools from openstack.orchestration.v1 import stack + FAKE_ID = 'ce8ae86c-9810-4cb1-8888-7fb53bc523bf' FAKE_NAME = 'test_stack' FAKE = { diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py index e315a7d2..d5df2a33 100644 --- a/openstack/tests/unit/test_proxy_base.py +++ b/openstack/tests/unit/test_proxy_base.py @@ -99,3 +99,6 @@ class TestProxyBase(base.TestCase): def verify_update(self, mock_method, test_method, **kwargs): self._verify(mock_method, test_method, expected_result="result", **kwargs) + + def verify_wait_for_status(self, mock_method, test_method, **kwargs): + self._verify(mock_method, test_method, **kwargs) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index cf43c185..2d856e69 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import json import os @@ -41,7 +42,8 @@ fake_data = {'id': fake_id, 'enabled': True, 'name': fake_name, 'attr1': fake_attr1, - 'attr2': fake_attr2} + 'attr2': fake_attr2, + 'status': None} fake_body = {fake_resource: fake_data} @@ -59,6 +61,7 @@ class FakeResource(resource.Resource): first = resource.prop('attr1') second = resource.prop('attr2') third = resource.prop('attr3', alias='attr_three') + status = resource.prop('status') class FakeResourceNoKeys(FakeResource): @@ -1224,3 +1227,63 @@ class TestFind(base.TestCase): FakeResource.name_attribute = None self.assertEqual(None, FakeResource.find(self.mock_session, self.NAME)) + + +class TestWaitForStatus(base.TestCase): + + def __init__(self, *args, **kwargs): + super(TestWaitForStatus, self).__init__(*args, **kwargs) + self.build = FakeResponse(self.body_with_status(fake_body, 'BUILD')) + self.active = FakeResponse(self.body_with_status(fake_body, 'ACTIVE')) + self.error = FakeResponse(self.body_with_status(fake_body, 'ERROR')) + + def setUp(self): + super(TestWaitForStatus, self).setUp() + self.sess = mock.MagicMock() + + def body_with_status(self, body, status): + body_copy = copy.deepcopy(body) + body_copy[fake_resource]['status'] = status + return body_copy + + def test_wait_for_status_nothing(self): + self.sess.get = mock.MagicMock() + sot = FakeResource.new(**fake_data) + sot.status = 'ACTIVE' + + self.assertEqual(sot, resource.wait_for_status( + self.sess, sot, 'ACTIVE', [], 1, 2)) + self.assertEqual([], self.sess.get.call_args_list) + + def test_wait_for_status(self): + self.sess.get = mock.MagicMock() + self.sess.get.side_effect = [self.build, self.active] + sot = FakeResource.new(**fake_data) + + self.assertEqual(sot, resource.wait_for_status( + self.sess, sot, 'ACTIVE', [], 1, 2)) + + def test_wait_for_status_timeout(self): + self.sess.get = mock.MagicMock() + self.sess.get.side_effect = [self.build, self.build] + sot = FakeResource.new(**fake_data) + + self.assertRaises(exceptions.ResourceTimeout, resource.wait_for_status, + self.sess, sot, 'ACTIVE', ['ERROR'], 1, 2) + + def test_wait_for_status_failures(self): + self.sess.get = mock.MagicMock() + self.sess.get.side_effect = [self.build, self.error] + sot = FakeResource.new(**fake_data) + + self.assertRaises(exceptions.ResourceFailure, resource.wait_for_status, + self.sess, sot, 'ACTIVE', ['ERROR'], 1, 2) + + def test_wait_for_status_no_status(self): + class FakeResourceNoStatus(resource.Resource): + allow_retrieve = True + + sot = FakeResourceNoStatus.new(id=123) + + self.assertRaises(AttributeError, resource.wait_for_status, + self.sess, sot, 'ACTIVE', ['ERROR'], 1, 2)