z/VM Driver: add snapshot function
This patch add snapshot function to z/VM driver, it will call underlying zvmsdk to generate the image Change-Id: I0409aba55487e92efa6370eed41936bf8a4ef25d blueprint: add-zvm-driver-rocky
This commit is contained in:
parent
3e1692b966
commit
c50a39f8c8
|
@ -14,6 +14,8 @@
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import mock
|
import mock
|
||||||
|
import os
|
||||||
|
import six
|
||||||
|
|
||||||
from nova import conf
|
from nova import conf
|
||||||
from nova import context
|
from nova import context
|
||||||
|
@ -118,6 +120,8 @@ class TestZVMDriver(test.NoDBTestCase):
|
||||||
network_model.VIF(**self._network_values)
|
network_model.VIF(**self._network_values)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
self.mock_update_task_state = mock.Mock()
|
||||||
|
|
||||||
def test_driver_init_no_url(self):
|
def test_driver_init_no_url(self):
|
||||||
self.flags(cloud_connector_url=None, group='zvm')
|
self.flags(cloud_connector_url=None, group='zvm')
|
||||||
self.assertRaises(exception.ZVMDriverException,
|
self.assertRaises(exception.ZVMDriverException,
|
||||||
|
@ -324,3 +328,128 @@ class TestZVMDriver(test.NoDBTestCase):
|
||||||
injected_files=None, admin_password=None,
|
injected_files=None, admin_password=None,
|
||||||
allocations=None, network_info=self._network_info,
|
allocations=None, network_info=self._network_info,
|
||||||
block_device_info=None)
|
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)
|
||||||
|
|
|
@ -97,3 +97,19 @@ class TestZVMHypervisor(test.NoDBTestCase):
|
||||||
instance = fake_instance.fake_instance_obj(self._context)
|
instance = fake_instance.fake_instance_obj(self._context)
|
||||||
res = self._hypervisor.guest_exists(instance)
|
res = self._hypervisor.guest_exists(instance)
|
||||||
self.assertFalse(res)
|
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')
|
||||||
|
|
|
@ -22,9 +22,11 @@ from oslo_log import log as logging
|
||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
from oslo_utils import excutils
|
from oslo_utils import excutils
|
||||||
|
|
||||||
|
from nova.compute import task_states
|
||||||
from nova import conf
|
from nova import conf
|
||||||
from nova import exception
|
from nova import exception
|
||||||
from nova.i18n import _
|
from nova.i18n import _
|
||||||
|
from nova.image import glance
|
||||||
from nova.objects import fields as obj_fields
|
from nova.objects import fields as obj_fields
|
||||||
from nova import utils
|
from nova import utils
|
||||||
from nova.virt import driver
|
from nova.virt import driver
|
||||||
|
@ -299,3 +301,68 @@ class ZVMDriver(driver.ComputeDriver):
|
||||||
|
|
||||||
def get_host_uptime(self):
|
def get_host_uptime(self):
|
||||||
return self._hypervisor.get_host_uptime()
|
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)
|
||||||
|
|
|
@ -124,6 +124,9 @@ class Hypervisor(object):
|
||||||
def guest_config_minidisks(self, name, disk_list):
|
def guest_config_minidisks(self, name, disk_list):
|
||||||
self._reqh.call('guest_config_minidisks', 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):
|
def image_query(self, imagename):
|
||||||
"""Check whether image is there or not
|
"""Check whether image is there or not
|
||||||
|
|
||||||
|
@ -142,3 +145,15 @@ class Hypervisor(object):
|
||||||
def image_import(self, image_href, image_url, image_meta):
|
def image_import(self, image_href, image_url, image_meta):
|
||||||
self._reqh.call('image_import', image_href, image_url,
|
self._reqh.call('image_import', image_href, image_url,
|
||||||
image_meta, remote_host=self._rhost)
|
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)
|
||||||
|
|
|
@ -119,3 +119,8 @@ def generate_configdrive(context, instance, injected_files,
|
||||||
network_info,
|
network_info,
|
||||||
admin_password)
|
admin_password)
|
||||||
return transportfiles
|
return transportfiles
|
||||||
|
|
||||||
|
|
||||||
|
def clean_up_file(filepath):
|
||||||
|
if os.path.exists(filepath):
|
||||||
|
os.remove(filepath)
|
||||||
|
|
Loading…
Reference in New Issue