diff --git a/nova_zvm/tests/unit/virt/zvm/test_driver.py b/nova_zvm/tests/unit/virt/zvm/test_driver.py index bbaf081..52fba9c 100644 --- a/nova_zvm/tests/unit/virt/zvm/test_driver.py +++ b/nova_zvm/tests/unit/virt/zvm/test_driver.py @@ -15,6 +15,12 @@ import copy import eventlet import mock +import os +import six +if six.PY2: + import __builtin__ as builtins +elif six.PY3: + import builtins from nova.compute import power_state from nova import context @@ -25,9 +31,13 @@ from nova import test from nova.tests.unit import fake_instance from nova.tests import uuidsentinel +from nova_zvm.virt.zvm import conf +from nova_zvm.virt.zvm import const from nova_zvm.virt.zvm import driver as zvmdriver from nova_zvm.virt.zvm import utils as zvmutils +CONF = conf.CONF + class TestZVMDriver(test.NoDBTestCase): @@ -117,6 +127,8 @@ class TestZVMDriver(test.NoDBTestCase): network_model.VIF(**self._network_values) ]) + self.mock_update_task_state = mock.Mock() + def test_driver_init(self): self.assertEqual(self.driver._hypervisor_hostname, 'TESTHOST') self.assertIsInstance(self.driver._reqh, @@ -384,7 +396,7 @@ class TestZVMDriver(test.NoDBTestCase): get_host, setup_network, wait_ready): _inst = copy.copy(self._instance) _bdi = copy.copy(self._block_device_info) - get_image_info.return_value = [['image_name']] + get_image_info.return_value = [{'imagename': 'image_name'}] gen_conf_file.return_value = 'transportfiles' set_disk_list.return_value = 'disk_list', 'eph_list' get_host.return_value = 'test@192.168.1.1' @@ -479,3 +491,50 @@ class TestZVMDriver(test.NoDBTestCase): call.test_assert_called_once_with('guest_get_console_output', 'test0001') self.assertEqual('console output', outputs) + + @mock.patch.object(builtins, 'open') + @mock.patch('nova_zvm.virt.zvm.driver.ZVMDriver._get_host') + @mock.patch('nova.image.glance.get_remote_image_service', ) + @mock.patch('nova_zvm.virt.zvm.utils.zVMConnectorRequestHandler.call') + def test_snapshot(self, call, get_image_service, get_host, mock_open): + image_service = mock.Mock() + image_id = 'e9ee1562-3ea1-4cb1-9f4c-f2033000eab1' + get_image_service.return_value = (image_service, image_id) + host_info = 'nova@192.168.99.1' + get_host.return_value = host_info + 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': const.ARCHITECTURE, + 'hypervisor_type': const.HYPERVISOR_TYPE + }, + '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) + + call.assert_any_call('guest_capture', + self._instance['name'], image_id) + mock_open.assert_called_once_with(image_path, 'r') + image_service.update.assert_called_once_with(self._context, + image_id, + new_image_meta, + mock_open.return_value.__enter__.return_value, + purge_props=False) + call.assert_any_call('image_export', image_id, dest_path, + remote_host=host_info) + call.assert_any_call('image_delete', image_id) diff --git a/nova_zvm/virt/zvm/driver.py b/nova_zvm/virt/zvm/driver.py index 0bd64ee..19af63a 100644 --- a/nova_zvm/virt/zvm/driver.py +++ b/nova_zvm/virt/zvm/driver.py @@ -12,7 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. - import datetime import eventlet import os @@ -20,8 +19,10 @@ import pwd import time from nova.compute import power_state +from nova.compute import task_states from nova import exception from nova.i18n import _ +from nova.image import glance from nova.objects import fields as obj_fields from nova.virt import driver from nova.virt import hardware @@ -56,6 +57,7 @@ class ZVMDriver(driver.ComputeDriver): super(ZVMDriver, self).__init__(virtapi) self._reqh = zvmutils.zVMConnectorRequestHandler() self._vmutils = zvmutils.VMUtils() + self._pathutils = zvmutils.PathUtils() self._imageop_semaphore = eventlet.semaphore.Semaphore(1) # get hypervisor host name @@ -183,7 +185,7 @@ class ZVMDriver(driver.ComputeDriver): context, instance, injected_files, admin_password) resp = self._get_image_info(context, image_meta.id, os_distro) - spawn_image_name = resp[0][0] + spawn_image_name = resp[0]['imagename'] disk_list, eph_list = self._set_disk_list(instance, spawn_image_name, block_device_info) @@ -405,3 +407,83 @@ class ZVMDriver(driver.ComputeDriver): def get_console_output(self, context, instance): return self._reqh.call('guest_get_console_output', instance.name) + + def snapshot(self, context, instance, image_id, update_task_state): + """Create snapshot from a running VM instance. + + :param context: security context + :param instance: nova.objects.instance.Instance + :param image_id: Reference to a pre-created image that will + hold the snapshot. + :param update_task_state: Callback function to update the task_state + on the instance while the snapshot operation progresses. The + function takes a task_state argument and an optional + expected_task_state kwarg which defaults to + nova.compute.task_states.IMAGE_SNAPSHOT. See + nova.objects.instance.Instance.save for expected_task_state usage. + """ + + # Check the image status + (image_service, image_id) = glance.get_remote_image_service( + context, image_id) + + # Update the instance task_state to image_pending_upload + update_task_state(task_state=task_states.IMAGE_PENDING_UPLOAD) + + # Call zvmsdk guest_capture to generate the image + try: + self._reqh.call('guest_capture', instance['name'], image_id) + except Exception as err: + with excutils.save_and_reraise_exception(): + LOG.error(_("Failed to capture the instance %(instance)s " + "to generate an image with reason: %(err)s"), + {'instance': instance['name'], '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._reqh.call('image_export', image_id, + dest_path, + remote_host=self._get_host()) + except Exception as err: + LOG.error(_("Failed to export image %s from SDK server to nova " + "compute server") % image_id) + self._reqh.call('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': const.ARCHITECTURE, + 'hypervisor_type': const.HYPERVISOR_TYPE + }, + '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: + self._pathutils.clean_up_file(image_path) + self._reqh.call('image_delete', image_id) + LOG.debug("Snapshot image upload complete", instance=instance) diff --git a/nova_zvm/virt/zvm/utils.py b/nova_zvm/virt/zvm/utils.py index 079751b..2b7470c 100644 --- a/nova_zvm/virt/zvm/utils.py +++ b/nova_zvm/virt/zvm/utils.py @@ -14,6 +14,7 @@ import os +import shutil from nova.api.metadata import base as instance_metadata from nova.compute import power_state @@ -71,6 +72,14 @@ class PathUtils(object): os.makedirs(instance_folder) return instance_folder + def clean_up_folder(self, fpath): + if os.path.isdir(fpath): + shutil.rmtree(fpath) + + def clean_up_file(self, filepath): + if os.path.exists(filepath): + os.remove(filepath) + class VMUtils(object): def __init__(self):