diff --git a/nova/tests/unit/virt/zvm/test_driver.py b/nova/tests/unit/virt/zvm/test_driver.py index de96fc4e7206..cb11618399f7 100644 --- a/nova/tests/unit/virt/zvm/test_driver.py +++ b/nova/tests/unit/virt/zvm/test_driver.py @@ -14,6 +14,8 @@ import copy import mock +import os +import six from nova import conf from nova import context @@ -118,6 +120,8 @@ class TestZVMDriver(test.NoDBTestCase): network_model.VIF(**self._network_values) ]) + self.mock_update_task_state = mock.Mock() + def test_driver_init_no_url(self): self.flags(cloud_connector_url=None, group='zvm') self.assertRaises(exception.ZVMDriverException, @@ -324,3 +328,128 @@ class TestZVMDriver(test.NoDBTestCase): injected_files=None, admin_password=None, allocations=None, network_info=self._network_info, block_device_info=None) + + @mock.patch.object(six.moves.builtins, 'open') + @mock.patch('nova.image.glance.get_remote_image_service') + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + def test_snapshot(self, call, get_image_service, mock_open): + image_service = mock.Mock() + image_id = 'e9ee1562-3ea1-4cb1-9f4c-f2033000eab1' + get_image_service.return_value = (image_service, image_id) + call_resp = ['', {"os_version": "rhel7.2", + "dest_url": "file:///path/to/target"}, ''] + call.side_effect = call_resp + new_image_meta = { + 'is_public': False, + 'status': 'active', + 'properties': { + 'image_location': 'snapshot', + 'image_state': 'available', + 'owner_id': self._instance['project_id'], + 'os_distro': call_resp[1]['os_version'], + 'architecture': 's390x', + 'hypervisor_type': 'zvm' + }, + 'disk_format': 'raw', + 'container_format': 'bare', + } + image_path = os.path.join(os.path.normpath( + CONF.zvm.image_tmp_path), image_id) + dest_path = "file://" + image_path + + self._driver.snapshot(self._context, self._instance, image_id, + self.mock_update_task_state) + get_image_service.assert_called_with(self._context, image_id) + + mock_open.assert_called_once_with(image_path, 'r') + ret_file = mock_open.return_value.__enter__.return_value + image_service.update.assert_called_once_with(self._context, + image_id, + new_image_meta, + ret_file, + purge_props=False) + self.mock_update_task_state.assert_has_calls([ + mock.call(task_state='image_pending_upload'), + mock.call(expected_state='image_pending_upload', + task_state='image_uploading') + ]) + call.assert_has_calls([ + mock.call('guest_capture', self._instance.name, image_id), + mock.call('image_export', image_id, dest_path, + remote_host=mock.ANY), + mock.call('image_delete', image_id) + ]) + + @mock.patch('nova.image.glance.get_remote_image_service') + @mock.patch('nova.virt.zvm.hypervisor.Hypervisor.guest_capture') + def test_snapshot_capture_fail(self, mock_capture, get_image_service): + image_service = mock.Mock() + image_id = 'e9ee1562-3ea1-4cb1-9f4c-f2033000eab1' + get_image_service.return_value = (image_service, image_id) + mock_capture.side_effect = exception.ZVMDriverException(error='error') + + self.assertRaises(exception.ZVMDriverException, self._driver.snapshot, + self._context, self._instance, image_id, + self.mock_update_task_state) + + self.mock_update_task_state.assert_called_once_with( + task_state='image_pending_upload') + image_service.delete.assert_called_once_with(self._context, image_id) + + @mock.patch('nova.image.glance.get_remote_image_service') + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + @mock.patch('nova.virt.zvm.hypervisor.Hypervisor.image_delete') + @mock.patch('nova.virt.zvm.hypervisor.Hypervisor.image_export') + def test_snapshot_import_fail(self, mock_import, mock_delete, + call, get_image_service): + image_service = mock.Mock() + image_id = 'e9ee1562-3ea1-4cb1-9f4c-f2033000eab1' + get_image_service.return_value = (image_service, image_id) + + mock_import.side_effect = exception.ZVMDriverException(error='error') + + self.assertRaises(exception.ZVMDriverException, self._driver.snapshot, + self._context, self._instance, image_id, + self.mock_update_task_state) + + self.mock_update_task_state.assert_called_once_with( + task_state='image_pending_upload') + get_image_service.assert_called_with(self._context, image_id) + call.assert_called_once_with('guest_capture', + self._instance.name, image_id) + mock_delete.assert_called_once_with(image_id) + image_service.delete.assert_called_once_with(self._context, image_id) + + @mock.patch.object(six.moves.builtins, 'open') + @mock.patch('nova.image.glance.get_remote_image_service') + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + @mock.patch('nova.virt.zvm.hypervisor.Hypervisor.image_delete') + @mock.patch('nova.virt.zvm.hypervisor.Hypervisor.image_export') + def test_snapshot_update_fail(self, mock_import, mock_delete, call, + get_image_service, mock_open): + image_service = mock.Mock() + image_id = 'e9ee1562-3ea1-4cb1-9f4c-f2033000eab1' + get_image_service.return_value = (image_service, image_id) + image_service.update.side_effect = exception.ImageNotAuthorized( + image_id='dummy') + image_path = os.path.join(os.path.normpath( + CONF.zvm.image_tmp_path), image_id) + + self.assertRaises(exception.ImageNotAuthorized, self._driver.snapshot, + self._context, self._instance, image_id, + self.mock_update_task_state) + + mock_open.assert_called_once_with(image_path, 'r') + + get_image_service.assert_called_with(self._context, image_id) + mock_delete.assert_called_once_with(image_id) + image_service.delete.assert_called_once_with(self._context, image_id) + + self.mock_update_task_state.assert_has_calls([ + mock.call(task_state='image_pending_upload'), + mock.call(expected_state='image_pending_upload', + task_state='image_uploading') + ]) + + call.assert_called_once_with('guest_capture', self._instance.name, + image_id) diff --git a/nova/tests/unit/virt/zvm/test_hypervisor.py b/nova/tests/unit/virt/zvm/test_hypervisor.py index d9adc111c502..49fa0c41b277 100644 --- a/nova/tests/unit/virt/zvm/test_hypervisor.py +++ b/nova/tests/unit/virt/zvm/test_hypervisor.py @@ -97,3 +97,19 @@ class TestZVMHypervisor(test.NoDBTestCase): instance = fake_instance.fake_instance_obj(self._context) res = self._hypervisor.guest_exists(instance) self.assertFalse(res) + + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + def test_guest_capture(self, mcall): + self._hypervisor.guest_capture('n1', 'image-id') + mcall.assert_called_once_with('guest_capture', 'n1', 'image-id') + + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + def test_image_export(self, mcall): + self._hypervisor.image_export('image-id', 'path') + mcall.assert_called_once_with('image_export', 'image-id', 'path', + remote_host=self._hypervisor._rhost) + + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + def test_image_delete(self, mcall): + self._hypervisor.image_delete('image-id') + mcall.assert_called_once_with('image_delete', 'image-id') diff --git a/nova/virt/zvm/driver.py b/nova/virt/zvm/driver.py index 3c21cf074f48..c8d89438e958 100644 --- a/nova/virt/zvm/driver.py +++ b/nova/virt/zvm/driver.py @@ -22,9 +22,11 @@ from oslo_log import log as logging from oslo_serialization import jsonutils from oslo_utils import excutils +from nova.compute import task_states from nova import conf from nova import exception from nova.i18n import _ +from nova.image import glance from nova.objects import fields as obj_fields from nova import utils from nova.virt import driver @@ -299,3 +301,68 @@ class ZVMDriver(driver.ComputeDriver): def get_host_uptime(self): return self._hypervisor.get_host_uptime() + + def snapshot(self, context, instance, image_id, update_task_state): + + (image_service, image_id) = glance.get_remote_image_service( + context, image_id) + + update_task_state(task_state=task_states.IMAGE_PENDING_UPLOAD) + + try: + self._hypervisor.guest_capture(instance.name, image_id) + except Exception as err: + with excutils.save_and_reraise_exception(): + LOG.error("Failed to capture the instance " + "to generate an image with reason: %(err)s", + {'err': err}, instance=instance) + # Clean up the image from glance + image_service.delete(context, image_id) + + # Export the image to nova-compute server temporary + image_path = os.path.join(os.path.normpath( + CONF.zvm.image_tmp_path), image_id) + dest_path = "file://" + image_path + try: + resp = self._hypervisor.image_export(image_id, dest_path) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error("Failed to export image %s from SDK server to " + "nova compute server", image_id) + image_service.delete(context, image_id) + self._hypervisor.image_delete(image_id) + + # Save image to glance + new_image_meta = { + 'is_public': False, + 'status': 'active', + 'properties': { + 'image_location': 'snapshot', + 'image_state': 'available', + 'owner_id': instance['project_id'], + 'os_distro': resp['os_version'], + 'architecture': obj_fields.Architecture.S390X, + 'hypervisor_type': obj_fields.HVType.ZVM, + }, + 'disk_format': 'raw', + 'container_format': 'bare', + } + update_task_state(task_state=task_states.IMAGE_UPLOADING, + expected_state=task_states.IMAGE_PENDING_UPLOAD) + + # Save the image to glance + try: + with open(image_path, 'r') as image_file: + image_service.update(context, + image_id, + new_image_meta, + image_file, + purge_props=False) + except Exception: + with excutils.save_and_reraise_exception(): + image_service.delete(context, image_id) + finally: + zvmutils.clean_up_file(image_path) + self._hypervisor.image_delete(image_id) + + LOG.debug("Snapshot image upload complete", instance=instance) diff --git a/nova/virt/zvm/hypervisor.py b/nova/virt/zvm/hypervisor.py index 2f5f04f6fa28..bc6b2bb3d22a 100644 --- a/nova/virt/zvm/hypervisor.py +++ b/nova/virt/zvm/hypervisor.py @@ -124,6 +124,9 @@ class Hypervisor(object): def guest_config_minidisks(self, name, disk_list): self._reqh.call('guest_config_minidisks', name, disk_list) + def guest_capture(self, name, image_id): + self._reqh.call('guest_capture', name, image_id) + def image_query(self, imagename): """Check whether image is there or not @@ -142,3 +145,15 @@ class Hypervisor(object): def image_import(self, image_href, image_url, image_meta): self._reqh.call('image_import', image_href, image_url, image_meta, remote_host=self._rhost) + + def image_export(self, image_id, dest_path): + """export image to a given place + + :returns: a dict which represent the exported image information. + """ + resp = self._reqh.call('image_export', image_id, + dest_path, remote_host=self._rhost) + return resp + + def image_delete(self, image_id): + self._reqh.call('image_delete', image_id) diff --git a/nova/virt/zvm/utils.py b/nova/virt/zvm/utils.py index db5c6b8d3153..9382cbec8957 100644 --- a/nova/virt/zvm/utils.py +++ b/nova/virt/zvm/utils.py @@ -119,3 +119,8 @@ def generate_configdrive(context, instance, injected_files, network_info, admin_password) return transportfiles + + +def clean_up_file(filepath): + if os.path.exists(filepath): + os.remove(filepath)