diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ec2dc085f..ca95ff241 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -3314,7 +3314,7 @@ class OpenStackCloud(_normalize.Normalizer): " could not be snapshotted.".format(server=server)) server = server_obj image_id = str(self.manager.submit_task(_tasks.ImageSnapshotCreate( - image_name=name, server=server, metadata=metadata))) + image_name=name, server=server['id'], metadata=metadata))) self.list_images.invalidate(self) image = self.get_image(image_id) diff --git a/shade/tests/fakes.py b/shade/tests/fakes.py index 40a1736bc..f153b4f85 100644 --- a/shade/tests/fakes.py +++ b/shade/tests/fakes.py @@ -28,6 +28,8 @@ STRAWBERRY_FLAVOR_ID = u'0c1d9008-f546-4608-9e8f-f8bdaec8dddf' COMPUTE_ENDPOINT = 'https://compute.example.com/v2.1' ORCHESTRATION_ENDPOINT = 'https://orchestration.example.com/v1/{p}'.format( p=PROJECT_ID) +NO_MD5 = '93b885adfe0da089cdf634904fd59f71' +NO_SHA256 = '6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d' def make_fake_flavor(flavor_id, name, ram=100, disk=1600, vcpus=24): @@ -177,6 +179,37 @@ def make_fake_stack_event( } +def make_fake_image( + image_id=None, md5=NO_MD5, sha256=NO_SHA256, status='active'): + return { + u'image_state': u'available', + u'container_format': u'bare', + u'min_ram': 0, + u'ramdisk_id': None, + u'updated_at': u'2016-02-10T05:05:02Z', + u'file': '/v2/images/' + image_id + '/file', + u'size': 3402170368, + u'image_type': u'snapshot', + u'disk_format': u'qcow2', + u'id': image_id, + u'schema': u'/v2/schemas/image', + u'status': status, + u'tags': [], + u'visibility': u'private', + u'locations': [{ + u'url': u'http://127.0.0.1/images/' + image_id, + u'metadata': {}}], + u'min_disk': 40, + u'virtual_size': None, + u'name': u'fake_image', + u'checksum': u'ee36e35a297980dee1b514de9803ec6d', + u'created_at': u'2016-02-10T05:03:11Z', + u'owner_specified.shade.md5': NO_MD5, + u'owner_specified.shade.sha256': NO_SHA256, + u'owner_specified.shade.object': 'images/fake_image', + u'protected': False} + + class FakeEndpoint(object): def __init__(self, id, service_id, region, publicurl, internalurl=None, adminurl=None): diff --git a/shade/tests/unit/base.py b/shade/tests/unit/base.py index bb9f225d9..ed182ce80 100644 --- a/shade/tests/unit/base.py +++ b/shade/tests/unit/base.py @@ -550,7 +550,7 @@ class RequestsMockTestCase(BaseTestCase): mock_method, mock_uri, params['response_list'], **params['kw_params']) - def assert_calls(self, stop_after=None): + def assert_calls(self, stop_after=None, do_count=True): for (x, (call, history)) in enumerate( zip(self.calls, self.adapter.request_history)): if stop_after and x > stop_after: @@ -571,4 +571,6 @@ class RequestsMockTestCase(BaseTestCase): self.assertEqual( value, history.headers[key], 'header mismatch in call {index}'.format(index=x)) - self.assertEqual(len(self.calls), len(self.adapter.request_history)) + if do_count: + self.assertEqual( + len(self.calls), len(self.adapter.request_history)) diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py index d16835497..ce206cf51 100644 --- a/shade/tests/unit/test_image.py +++ b/shade/tests/unit/test_image.py @@ -24,11 +24,10 @@ import six import shade from shade import exc from shade import meta +from shade.tests import fakes from shade.tests.unit import base -NO_MD5 = '93b885adfe0da089cdf634904fd59f71' -NO_SHA256 = '6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d' CINDER_URL = 'https://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0' @@ -40,33 +39,7 @@ class BaseTestImage(base.RequestsMockTestCase): self.imagefile = tempfile.NamedTemporaryFile(delete=False) self.imagefile.write(b'\0') self.imagefile.close() - self.fake_image_dict = { - u'image_state': u'available', - u'container_format': u'bare', - u'min_ram': 0, - u'ramdisk_id': None, - u'updated_at': u'2016-02-10T05:05:02Z', - u'file': '/v2/images/' + self.image_id + '/file', - u'size': 3402170368, - u'image_type': u'snapshot', - u'disk_format': u'qcow2', - u'id': self.image_id, - u'schema': u'/v2/schemas/image', - u'status': u'active', - u'tags': [], - u'visibility': u'private', - u'locations': [{ - u'url': u'http://127.0.0.1/images/' + self.image_id, - u'metadata': {}}], - u'min_disk': 40, - u'virtual_size': None, - u'name': u'fake_image', - u'checksum': u'ee36e35a297980dee1b514de9803ec6d', - u'created_at': u'2016-02-10T05:03:11Z', - u'owner_specified.shade.md5': NO_MD5, - u'owner_specified.shade.sha256': NO_SHA256, - u'owner_specified.shade.object': 'images/fake_image', - u'protected': False} + self.fake_image_dict = fakes.make_fake_image(image_id=self.image_id) self.fake_search_return = {'images': [self.fake_image_dict]} self.output = uuid.uuid4().bytes @@ -193,9 +166,9 @@ class TestImage(BaseTestImage): json={u'container_format': u'bare', u'disk_format': u'qcow2', u'name': u'fake_image', - u'owner_specified.shade.md5': NO_MD5, + u'owner_specified.shade.md5': fakes.NO_MD5, u'owner_specified.shade.object': u'images/fake_image', # noqa - u'owner_specified.shade.sha256': NO_SHA256, + u'owner_specified.shade.sha256': fakes.NO_SHA256, u'visibility': u'private'}) ), dict(method='PUT', @@ -275,8 +248,8 @@ class TestImage(BaseTestImage): object=image_name), status_code=201, validate=dict( - headers={'x-object-meta-x-shade-md5': NO_MD5, - 'x-object-meta-x-shade-sha256': NO_SHA256}) + headers={'x-object-meta-x-shade-md5': fakes.NO_MD5, + 'x-object-meta-x-shade-sha256': fakes.NO_SHA256}) ), dict(method='GET', uri='https://image.example.com/v2/images', json={'images': []}), @@ -308,9 +281,9 @@ class TestImage(BaseTestImage): container=container_name, object=image_name), u'path': u'/owner_specified.shade.object'}, - {u'op': u'add', u'value': NO_MD5, + {u'op': u'add', u'value': fakes.NO_MD5, u'path': u'/owner_specified.shade.md5'}, - {u'op': u'add', u'value': NO_SHA256, + {u'op': u'add', u'value': fakes.NO_SHA256, u'path': u'/owner_specified.shade.sha256'}], key=operator.itemgetter('value')), headers={ diff --git a/shade/tests/unit/test_image_snapshot.py b/shade/tests/unit/test_image_snapshot.py index 07c4ea5f8..860cb19e1 100644 --- a/shade/tests/unit/test_image_snapshot.py +++ b/shade/tests/unit/test_image_snapshot.py @@ -14,51 +14,100 @@ import uuid -import mock - -import shade from shade import exc +from shade.tests import fakes from shade.tests.unit import base -class TestImageSnapshot(base.TestCase): +class TestImageSnapshot(base.RequestsMockTestCase): def setUp(self): super(TestImageSnapshot, self).setUp() + self.server_id = str(uuid.uuid4()) self.image_id = str(uuid.uuid4()) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - @mock.patch.object(shade.OpenStackCloud, 'get_image') - def test_create_image_snapshot_wait_until_active_never_active(self, - mock_get, - mock_nova): - mock_nova.servers.create_image.return_value = { - 'status': 'queued', - 'id': self.image_id, - } - mock_get.return_value = {'status': 'saving', 'id': self.image_id} - self.assertRaises(exc.OpenStackCloudTimeout, - self.cloud.create_image_snapshot, - 'test-snapshot', dict(id='fake-server'), - wait=True, timeout=0.01) + def test_create_image_snapshot_wait_until_active_never_active(self): + snapshot_name = 'test-snapshot' + fake_image = fakes.make_fake_image(self.image_id, status='pending') + self.register_uris([ + dict( + method='POST', + uri='{endpoint}/servers/{server_id}/action'.format( + endpoint=fakes.COMPUTE_ENDPOINT, + server_id=self.server_id), + headers=dict( + Location='{endpoint}/images/{image_id}'.format( + endpoint='https://images.example.com', + image_id=self.image_id)), + validate=dict( + json={ + "createImage": { + "name": snapshot_name, + "metadata": {}, + }})), + self.get_glance_discovery_mock_dict(), + dict( + method='GET', + uri='https://image.example.com/v2/images', + json=dict(images=[fake_image])), + ]) - @mock.patch.object(shade.OpenStackCloud, 'nova_client') - @mock.patch.object(shade.OpenStackCloud, 'get_image') - def test_create_image_snapshot_wait_active(self, mock_get, mock_nova): - mock_nova.servers.create_image.return_value = { - 'status': 'queued', - 'id': self.image_id, - } - mock_get.return_value = {'status': 'active', 'id': self.image_id} + self.assertRaises( + exc.OpenStackCloudTimeout, + self.cloud.create_image_snapshot, + snapshot_name, dict(id=self.server_id), + wait=True, timeout=0.01) + + # After the fifth call, we just keep polling get images for status. + # Due to mocking sleep, we have no clue how many times we'll call it. + self.assert_calls(stop_after=5, do_count=False) + + def test_create_image_snapshot_wait_active(self): + snapshot_name = 'test-snapshot' + pending_image = fakes.make_fake_image(self.image_id, status='pending') + fake_image = fakes.make_fake_image(self.image_id) + self.register_uris([ + dict( + method='POST', + uri='{endpoint}/servers/{server_id}/action'.format( + endpoint=fakes.COMPUTE_ENDPOINT, + server_id=self.server_id), + headers=dict( + Location='{endpoint}/images/{image_id}'.format( + endpoint='https://images.example.com', + image_id=self.image_id)), + validate=dict( + json={ + "createImage": { + "name": snapshot_name, + "metadata": {}, + }})), + self.get_glance_discovery_mock_dict(), + dict( + method='GET', + uri='https://image.example.com/v2/images', + json=dict(images=[pending_image])), + dict( + method='GET', + uri='https://image.example.com/v2/images', + json=dict(images=[fake_image])), + ]) image = self.cloud.create_image_snapshot( - 'test-snapshot', dict(id='fake-server'), wait=True, timeout=2) + 'test-snapshot', dict(id=self.server_id), wait=True, timeout=2) self.assertEqual(image['id'], self.image_id) - @mock.patch.object(shade.OpenStackCloud, 'get_server') - def test_create_image_snapshot_bad_name_exception( - self, mock_get_server): - mock_get_server.return_value = None + self.assert_calls() + + def test_create_image_snapshot_bad_name_exception(self): + self.register_uris([ + dict( + method='POST', + uri='{endpoint}/servers/{server_id}/action'.format( + endpoint=fakes.COMPUTE_ENDPOINT, + server_id=self.server_id), + json=dict(servers=[])), + ]) self.assertRaises( exc.OpenStackCloudException, self.cloud.create_image_snapshot, - 'test-snapshot', 'missing-server') + 'test-snapshot', self.server_id)