Snapshot support
Change-Id: If56531e366b21368984eb5ba14af661dc4892876
This commit is contained in:
parent
56ce25c969
commit
ddbcd6cfac
|
@ -15,6 +15,12 @@
|
||||||
import copy
|
import copy
|
||||||
import eventlet
|
import eventlet
|
||||||
import mock
|
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.compute import power_state
|
||||||
from nova import context
|
from nova import context
|
||||||
|
@ -25,9 +31,13 @@ from nova import test
|
||||||
from nova.tests.unit import fake_instance
|
from nova.tests.unit import fake_instance
|
||||||
from nova.tests import uuidsentinel
|
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 driver as zvmdriver
|
||||||
from nova_zvm.virt.zvm import utils as zvmutils
|
from nova_zvm.virt.zvm import utils as zvmutils
|
||||||
|
|
||||||
|
CONF = conf.CONF
|
||||||
|
|
||||||
|
|
||||||
class TestZVMDriver(test.NoDBTestCase):
|
class TestZVMDriver(test.NoDBTestCase):
|
||||||
|
|
||||||
|
@ -117,6 +127,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(self):
|
def test_driver_init(self):
|
||||||
self.assertEqual(self.driver._hypervisor_hostname, 'TESTHOST')
|
self.assertEqual(self.driver._hypervisor_hostname, 'TESTHOST')
|
||||||
self.assertIsInstance(self.driver._reqh,
|
self.assertIsInstance(self.driver._reqh,
|
||||||
|
@ -384,7 +396,7 @@ class TestZVMDriver(test.NoDBTestCase):
|
||||||
get_host, setup_network, wait_ready):
|
get_host, setup_network, wait_ready):
|
||||||
_inst = copy.copy(self._instance)
|
_inst = copy.copy(self._instance)
|
||||||
_bdi = copy.copy(self._block_device_info)
|
_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'
|
gen_conf_file.return_value = 'transportfiles'
|
||||||
set_disk_list.return_value = 'disk_list', 'eph_list'
|
set_disk_list.return_value = 'disk_list', 'eph_list'
|
||||||
get_host.return_value = 'test@192.168.1.1'
|
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',
|
call.test_assert_called_once_with('guest_get_console_output',
|
||||||
'test0001')
|
'test0001')
|
||||||
self.assertEqual('console output', outputs)
|
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)
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import eventlet
|
import eventlet
|
||||||
import os
|
import os
|
||||||
|
@ -20,8 +19,10 @@ import pwd
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from nova.compute import power_state
|
from nova.compute import power_state
|
||||||
|
from nova.compute import task_states
|
||||||
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.virt import driver
|
from nova.virt import driver
|
||||||
from nova.virt import hardware
|
from nova.virt import hardware
|
||||||
|
@ -56,6 +57,7 @@ class ZVMDriver(driver.ComputeDriver):
|
||||||
super(ZVMDriver, self).__init__(virtapi)
|
super(ZVMDriver, self).__init__(virtapi)
|
||||||
self._reqh = zvmutils.zVMConnectorRequestHandler()
|
self._reqh = zvmutils.zVMConnectorRequestHandler()
|
||||||
self._vmutils = zvmutils.VMUtils()
|
self._vmutils = zvmutils.VMUtils()
|
||||||
|
self._pathutils = zvmutils.PathUtils()
|
||||||
self._imageop_semaphore = eventlet.semaphore.Semaphore(1)
|
self._imageop_semaphore = eventlet.semaphore.Semaphore(1)
|
||||||
|
|
||||||
# get hypervisor host name
|
# get hypervisor host name
|
||||||
|
@ -183,7 +185,7 @@ class ZVMDriver(driver.ComputeDriver):
|
||||||
context, instance, injected_files, admin_password)
|
context, instance, injected_files, admin_password)
|
||||||
|
|
||||||
resp = self._get_image_info(context, image_meta.id, os_distro)
|
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,
|
disk_list, eph_list = self._set_disk_list(instance,
|
||||||
spawn_image_name,
|
spawn_image_name,
|
||||||
block_device_info)
|
block_device_info)
|
||||||
|
@ -405,3 +407,83 @@ class ZVMDriver(driver.ComputeDriver):
|
||||||
|
|
||||||
def get_console_output(self, context, instance):
|
def get_console_output(self, context, instance):
|
||||||
return self._reqh.call('guest_get_console_output', instance.name)
|
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)
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
from nova.api.metadata import base as instance_metadata
|
from nova.api.metadata import base as instance_metadata
|
||||||
from nova.compute import power_state
|
from nova.compute import power_state
|
||||||
|
@ -71,6 +72,14 @@ class PathUtils(object):
|
||||||
os.makedirs(instance_folder)
|
os.makedirs(instance_folder)
|
||||||
return 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):
|
class VMUtils(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
Loading…
Reference in New Issue