diff --git a/nova/conf/zvm.py b/nova/conf/zvm.py index 30b36d1f55ef..21a7bfe1cc6b 100644 --- a/nova/conf/zvm.py +++ b/nova/conf/zvm.py @@ -15,6 +15,9 @@ from oslo_config import cfg +from nova.conf import paths + + zvm_opt_group = cfg.OptGroup('zvm', title='zVM Options', help=""" @@ -38,6 +41,38 @@ URL to be used to communicate with z/VM Cloud Connector. CA certificate file to be verified in httpd server with TLS enabled A string, it must be a path to a CA bundle to use. +"""), + cfg.StrOpt('image_tmp_path', + default=paths.state_path_def('images'), + sample_default="$state_path/images", + help=""" +The path at which images will be stored (snapshot, deploy, etc). + +Images used for deploy and images captured via snapshot +need to be stored on the local disk of the compute host. +This configuration identifies the directory location. + +Possible values: + A file system path on the host running the compute service. +"""), + cfg.IntOpt('reachable_timeout', + default=300, + help=""" +Timeout (seconds) to wait for an instance to start. + +The z/VM driver relies on communication between the instance and cloud +connector. After an instance is created, it must have enough time to wait +for all the network info to be written into the user directory. +The driver will keep rechecking network status to the instance with the +timeout value, If setting network failed, it will notify the user that +starting the instance failed and put the instance in ERROR state. +The underlying z/VM guest will then be deleted. + +Possible Values: + Any positive integer. Recommended to be at least 300 seconds (5 minutes), + but it will vary depending on instance and system load. + A value of 0 is used for debug. In this case the underlying z/VM guest + will not be deleted when the instance is marked in ERROR state. """), ] diff --git a/nova/tests/unit/virt/zvm/test_driver.py b/nova/tests/unit/virt/zvm/test_driver.py index 2a2eadeafd46..de96fc4e7206 100644 --- a/nova/tests/unit/virt/zvm/test_driver.py +++ b/nova/tests/unit/virt/zvm/test_driver.py @@ -12,22 +12,111 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import mock +from nova import conf +from nova import context from nova import exception +from nova.network import model as network_model +from nova import objects from nova import test +from nova.tests.unit import fake_instance +from nova.tests import uuidsentinel +from nova.virt import fake from nova.virt.zvm import driver as zvmdriver +CONF = conf.CONF + + class TestZVMDriver(test.NoDBTestCase): def setUp(self): super(TestZVMDriver, self).setUp() + self.flags(my_ip='192.168.1.1', + instance_name_template='abc%05d') self.flags(cloud_connector_url='https://1.1.1.1:1111', group='zvm') - with mock.patch('nova.virt.zvm.utils.' - 'ConnectorClient.call') as mcall: - mcall.return_value = {'hypervisor_hostname': 'TESTHOST'} - self._driver = zvmdriver.ZVMDriver('virtapi') + + with mock.patch('nova.virt.zvm.utils.ConnectorClient.call') as mcall, \ + mock.patch('pwd.getpwuid', return_value=mock.Mock(pw_name='test')): + mcall.return_value = {'hypervisor_hostname': 'TESTHOST', + 'ipl_time': 'IPL at 11/14/17 10:47:44 EST'} + self._driver = zvmdriver.ZVMDriver(fake.FakeVirtAPI()) + self._hypervisor = self._driver._hypervisor + + self._context = context.RequestContext('fake_user', 'fake_project') + self._image_id = uuidsentinel.imag_id + + self._instance_values = { + 'display_name': 'test', + 'uuid': uuidsentinel.inst_id, + 'vcpus': 1, + 'memory_mb': 1024, + 'image_ref': self._image_id, + 'root_gb': 0, + } + self._instance = fake_instance.fake_instance_obj( + self._context, **self._instance_values) + self._instance.flavor = objects.Flavor(name='testflavor', + vcpus=1, root_gb=3, ephemeral_gb=10, + swap=0, memory_mb=512, extra_specs={}) + + self._eph_disks = [{'guest_format': u'ext3', + 'device_name': u'/dev/sdb', + 'disk_bus': None, + 'device_type': None, + 'size': 1}, + {'guest_format': u'ext4', + 'device_name': u'/dev/sdc', + 'disk_bus': None, + 'device_type': None, + 'size': 2}] + self._block_device_info = {'swap': None, + 'root_device_name': u'/dev/sda', + 'ephemerals': self._eph_disks, + 'block_device_mapping': []} + fake_image_meta = {'status': 'active', + 'properties': {'os_distro': 'rhel7.2'}, + 'name': 'rhel72eckdimage', + 'deleted': False, + 'container_format': 'bare', + 'disk_format': 'raw', + 'id': self._image_id, + 'owner': 'cfc26f9d6af948018621ab00a1675310', + 'checksum': 'b026cd083ef8e9610a29eaf71459cc', + 'min_disk': 0, + 'is_public': False, + 'deleted_at': None, + 'min_ram': 0, + 'size': 465448142} + self._image_meta = objects.ImageMeta.from_dict(fake_image_meta) + subnet_4 = network_model.Subnet(cidr='192.168.0.1/24', + dns=[network_model.IP('192.168.0.1')], + gateway= + network_model.IP('192.168.0.1'), + ips=[ + network_model.IP('192.168.0.100')], + routes=None) + network = network_model.Network(id=0, + bridge='fa0', + label='fake', + subnets=[subnet_4], + vlan=None, + bridge_interface=None, + injected=True) + self._network_values = { + 'id': None, + 'address': 'DE:AD:BE:EF:00:00', + 'network': network, + 'type': network_model.VIF_TYPE_OVS, + 'devname': None, + 'ovs_interfaceid': None, + 'rxtx_cap': 3 + } + self._network_info = network_model.NetworkInfo([ + network_model.VIF(**self._network_values) + ]) def test_driver_init_no_url(self): self.flags(cloud_connector_url=None, group='zvm') @@ -43,3 +132,195 @@ class TestZVMDriver(test.NoDBTestCase): self.assertEqual(0, results['memory_mb_used']) self.assertEqual(0, results['disk_available_least']) self.assertEqual('TESTHOST', results['hypervisor_hostname']) + + def test_driver_template_validation(self): + self.flags(instance_name_template='abc%6d') + self.assertRaises(exception.ZVMDriverException, + self._driver._validate_options) + + @mock.patch('nova.virt.zvm.guest.Guest.get_info') + def test_get_info(self, mock_get): + self._driver.get_info(self._instance) + mock_get.assert_called_once_with() + + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + def test_private_get_image_info_err(self, call): + res = {'overallRC': 500, 'errmsg': 'err', 'rc': 0, 'rs': 0} + call.side_effect = exception.ZVMConnectorError(res) + self.assertRaises(exception.ZVMConnectorError, + self._driver._get_image_info, + 'context', 'image_meta_id', 'os_distro') + + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + @mock.patch('nova.virt.zvm.driver.ZVMDriver._import_spawn_image') + def test_private_get_image_info(self, image_import, call): + res = {'overallRC': 404, 'errmsg': 'err', 'rc': 0, 'rs': 0} + + call_response = [] + call_response.append(exception.ZVMConnectorError(results=res)) + call_response.append([{'imagename': 'image-info'}]) + call.side_effect = call_response + self._driver._get_image_info('context', 'image_meta_id', 'os_distro') + image_import.assert_called_once_with('context', 'image_meta_id', + 'os_distro') + call.assert_has_calls( + [mock.call('image_query', imagename='image_meta_id')] * 2 + ) + + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + def test_private_get_image_info_exist(self, call): + call.return_value = [{'imagename': 'image-info'}] + res = self._driver._get_image_info('context', 'image_meta_id', + 'os_distro') + call.assert_called_once_with('image_query', imagename='image_meta_id') + self.assertEqual('image-info', res) + + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + def _test_set_disk_list(self, call, has_get_root_units=False, + has_eph_disks=False): + disk_list = [{'is_boot_disk': True, 'size': '3g'}] + eph_disk_list = [{'format': u'ext3', 'size': '1g'}, + {'format': u'ext3', 'size': '2g'}] + _inst = copy.deepcopy(self._instance) + _bdi = copy.deepcopy(self._block_device_info) + + if has_get_root_units: + # overwrite + disk_list = [{'is_boot_disk': True, 'size': '3338'}] + call.return_value = '3338' + _inst['root_gb'] = 0 + else: + _inst['root_gb'] = 3 + + if has_eph_disks: + disk_list += eph_disk_list + else: + _bdi['ephemerals'] = [] + eph_disk_list = [] + + res1, res2 = self._driver._set_disk_list(_inst, self._image_meta.id, + _bdi) + + if has_get_root_units: + call.assert_called_once_with('image_get_root_disk_size', + self._image_meta.id) + self.assertEqual(disk_list, res1) + self.assertEqual(eph_disk_list, res2) + + def test_private_set_disk_list_simple(self): + self._test_set_disk_list() + + def test_private_set_disk_list_with_eph_disks(self): + self._test_set_disk_list(has_eph_disks=True) + + def test_private_set_disk_list_with_get_root_units(self): + self._test_set_disk_list(has_get_root_units=True) + + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + def test_private_setup_network(self, call): + inst_nets = [] + _net = {'ip_addr': '192.168.0.100', + 'gateway_addr': '192.168.0.1', + 'cidr': '192.168.0.1/24', + 'mac_addr': 'DE:AD:BE:EF:00:00', + 'nic_id': None} + inst_nets.append(_net) + self._driver._setup_network('vm_name', 'os_distro', + self._network_info, + self._instance) + call.assert_called_once_with('guest_create_network_interface', + 'vm_name', 'os_distro', inst_nets) + + @mock.patch('nova.virt.images.fetch') + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + def test_private_import_spawn_image(self, call, fetch): + + image_name = CONF.zvm.image_tmp_path + '/image_name' + image_url = "file://" + image_name + image_meta = {'os_version': 'os_version'} + with mock.patch('os.path.exists', side_effect=[False]): + self._driver._import_spawn_image(self._context, 'image_name', + 'os_version') + fetch.assert_called_once_with(self._context, 'image_name', + image_name) + call.assert_called_once_with('image_import', 'image_name', image_url, + image_meta, remote_host='test@192.168.1.1') + + @mock.patch('nova.virt.zvm.hypervisor.Hypervisor.guest_exists') + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + def test_destroy(self, call, guest_exists): + guest_exists.return_value = True + self._driver.destroy(self._context, self._instance, + network_info=self._network_info) + call.assert_called_once_with('guest_delete', self._instance['name']) + + @mock.patch('nova.virt.zvm.hypervisor.Hypervisor.guest_exists') + @mock.patch('nova.compute.manager.ComputeVirtAPI.wait_for_instance_event') + @mock.patch('nova.virt.zvm.driver.ZVMDriver._setup_network') + @mock.patch('nova.virt.zvm.driver.ZVMDriver._set_disk_list') + @mock.patch('nova.virt.zvm.utils.generate_configdrive') + @mock.patch('nova.virt.zvm.driver.ZVMDriver._get_image_info') + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + def test_spawn(self, call, get_image_info, gen_conf_file, set_disk_list, + setup_network, mock_wait, mock_exists): + _bdi = copy.copy(self._block_device_info) + get_image_info.return_value = 'image_name' + gen_conf_file.return_value = 'transportfiles' + set_disk_list.return_value = 'disk_list', 'eph_list' + mock_exists.return_value = False + self._driver.spawn(self._context, self._instance, self._image_meta, + injected_files=None, admin_password=None, + allocations=None, network_info=self._network_info, + block_device_info=_bdi) + gen_conf_file.assert_called_once_with(self._context, self._instance, + None, self._network_info, None) + get_image_info.assert_called_once_with(self._context, + self._image_meta.id, + self._image_meta.properties.os_distro) + set_disk_list.assert_called_once_with(self._instance, 'image_name', + _bdi) + setup_network.assert_called_once_with(self._instance.name, + self._image_meta.properties.os_distro, + self._network_info, self._instance) + + call.assert_has_calls([ + mock.call('guest_create', self._instance.name, + 1, 1024, disk_list='disk_list'), + mock.call('guest_deploy', self._instance.name, 'image_name', + transportfiles='transportfiles', + remotehost='test@192.168.1.1'), + mock.call('guest_config_minidisks', self._instance.name, + 'eph_list'), + mock.call('guest_start', self._instance.name) + ]) + + @mock.patch('nova.virt.zvm.hypervisor.Hypervisor.guest_exists') + @mock.patch('nova.virt.zvm.driver.ZVMDriver._get_image_info') + def test_spawn_image_no_distro_empty(self, get_image_info, mock_exists): + meta = {'status': 'active', + 'deleted': False, + 'properties': {'os_distro': ''}, + 'id': self._image_id, + 'size': 465448142} + self._image_meta = objects.ImageMeta.from_dict(meta) + mock_exists.return_value = False + self.assertRaises(exception.InvalidInput, self._driver.spawn, + self._context, self._instance, self._image_meta, + injected_files=None, admin_password=None, + allocations=None, network_info=self._network_info, + block_device_info=None) + + @mock.patch('nova.virt.zvm.hypervisor.Hypervisor.guest_exists') + @mock.patch('nova.virt.zvm.driver.ZVMDriver._get_image_info') + def test_spawn_image_no_distro_none(self, get_image_info, mock_exists): + meta = {'status': 'active', + 'deleted': False, + 'id': self._image_id, + 'size': 465448142} + self._image_meta = objects.ImageMeta.from_dict(meta) + mock_exists.return_value = False + self.assertRaises(exception.InvalidInput, self._driver.spawn, + self._context, self._instance, self._image_meta, + injected_files=None, admin_password=None, + allocations=None, network_info=self._network_info, + block_device_info=None) diff --git a/nova/tests/unit/virt/zvm/test_guest.py b/nova/tests/unit/virt/zvm/test_guest.py new file mode 100644 index 000000000000..5f4175158a8e --- /dev/null +++ b/nova/tests/unit/virt/zvm/test_guest.py @@ -0,0 +1,77 @@ +# Copyright 2017,2018 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from nova.compute import power_state as compute_power_state +from nova import context +from nova import exception +from nova import test +from nova.tests.unit import fake_instance +from nova.virt import fake +from nova.virt.zvm import driver +from nova.virt.zvm import guest + + +class TestZVMGuestOp(test.NoDBTestCase): + def setUp(self): + super(TestZVMGuestOp, self).setUp() + self.flags(cloud_connector_url='https://1.1.1.1:1111', + image_tmp_path='/test/image', + reachable_timeout=300, group='zvm') + self.flags(my_ip='192.168.1.1', + instance_name_template='test%04x') + with test.nested( + mock.patch('nova.virt.zvm.utils.ConnectorClient.call'), + mock.patch('pwd.getpwuid'), + ) as (mcall, getpwuid): + getpwuid.return_value = mock.Mock(pw_name='test') + mcall.return_value = {'hypervisor_hostname': 'TESTHOST', + 'ipl_time': 'TESTTIME'} + self._driver = driver.ZVMDriver(fake.FakeVirtAPI()) + self._hypervisor = self._driver._hypervisor + + self._context = context.RequestContext('fake_user', 'fake_project') + self._instance = fake_instance.fake_instance_obj( + self._context) + self._guest = guest.Guest(self._hypervisor, self._instance, + self._driver.virtapi) + + def test_private_mapping_power_state(self): + status = self._guest._mapping_power_state('on') + self.assertEqual(compute_power_state.RUNNING, status) + status = self._guest._mapping_power_state('off') + self.assertEqual(compute_power_state.SHUTDOWN, status) + status = self._guest._mapping_power_state('bad') + self.assertEqual(compute_power_state.NOSTATE, status) + + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + def test_get_info_err_instance_not_found(self, call): + res = {'overallRC': 404, 'errmsg': 'err', 'rc': 0, 'rs': 0} + call.side_effect = exception.ZVMConnectorError(results=res) + self.assertRaises(exception.InstanceNotFound, self._guest.get_info) + + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + def test_get_info_err_general(self, call): + res = {'overallRC': 500, 'errmsg': 'err', 'rc': 0, 'rs': 0} + call.side_effect = exception.ZVMConnectorError(res) + self.assertRaises(exception.ZVMConnectorError, self._guest.get_info) + + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + def test_get_info(self, call): + call.return_value = 'on' + info = self._guest.get_info() + call.assert_called_once_with('guest_get_power_state', + self._instance['name']) + self.assertEqual(info.state, compute_power_state.RUNNING) diff --git a/nova/tests/unit/virt/zvm/test_hypervisor.py b/nova/tests/unit/virt/zvm/test_hypervisor.py index debe67a8d908..d9adc111c502 100644 --- a/nova/tests/unit/virt/zvm/test_hypervisor.py +++ b/nova/tests/unit/virt/zvm/test_hypervisor.py @@ -14,8 +14,10 @@ import mock +from nova import context from nova import exception from nova import test +from nova.tests.unit import fake_instance from nova.virt.zvm import driver as zvmdriver @@ -23,13 +25,17 @@ class TestZVMHypervisor(test.NoDBTestCase): def setUp(self): super(TestZVMHypervisor, self).setUp() + self.flags(instance_name_template='abc%5d') self.flags(cloud_connector_url='https://1.1.1.1:1111', group='zvm') with mock.patch('nova.virt.zvm.utils.' 'ConnectorClient.call') as mcall: - mcall.return_value = {'hypervisor_hostname': 'TESTHOST'} + mcall.return_value = {'hypervisor_hostname': 'TESTHOST', + 'ipl_time': 'IPL at 11/14/17 10:47:44 EST'} driver = zvmdriver.ZVMDriver('virtapi') self._hypervisor = driver._hypervisor + self._context = context.RequestContext('fake_user', 'fake_project') + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') def test_get_available_resource(self, call): host_info = {'disk_available': 1144, @@ -67,3 +73,27 @@ class TestZVMHypervisor(test.NoDBTestCase): call.return_value = ['vm1', 'vm2'] inst_list = self._hypervisor.list_names() self.assertEqual(['vm1', 'vm2'], inst_list) + + @mock.patch('nova.virt.zvm.utils.ConnectorClient.call') + def test_get_host_uptime(self, call): + host_info = {'disk_available': 1144, + 'ipl_time': 'IPL at 11/14/17 10:47:44 EST', + 'memory_mb_used': 8192.0} + call.return_value = host_info + + time = self._hypervisor.get_host_uptime() + self.assertEqual('IPL at 11/14/17 10:47:44 EST', time) + + @mock.patch('nova.virt.zvm.hypervisor.Hypervisor.list_names') + def test_private_guest_exists_true(self, list_names): + instance = fake_instance.fake_instance_obj(self._context) + list_names.return_value = [instance.name.upper(), 'TEST0002'] + res = self._hypervisor.guest_exists(instance) + self.assertTrue(res) + + @mock.patch('nova.virt.zvm.hypervisor.Hypervisor.list_names') + def test_private_guest_exists_false(self, list_names): + list_names.return_value = ['dummy1', 'dummy2'] + instance = fake_instance.fake_instance_obj(self._context) + res = self._hypervisor.guest_exists(instance) + self.assertFalse(res) diff --git a/nova/tests/unit/virt/zvm/test_utils.py b/nova/tests/unit/virt/zvm/test_utils.py index 5b83044e1c59..640269fd383c 100644 --- a/nova/tests/unit/virt/zvm/test_utils.py +++ b/nova/tests/unit/virt/zvm/test_utils.py @@ -16,8 +16,10 @@ import mock from zvmconnector import connector +from nova import context from nova import exception from nova import test +from nova.tests.unit import fake_instance from nova.virt.zvm import utils as zvmutils @@ -79,3 +81,57 @@ class TestZVMUtils(test.NoDBTestCase): self.assertEqual(expected['rc'], exc.rc) self.assertEqual(expected['rs'], exc.rs) self.assertEqual(expected['errmsg'], exc.errmsg) + + @mock.patch('nova.virt.configdrive.required_by') + @mock.patch('nova.virt.zvm.utils._create_config_drive') + @mock.patch('nova.virt.zvm.utils._get_instance_path') + def test_generate_configdrive(self, get, create, required): + get.return_value = '/test/tmp/fake_uuid' + create.return_value = '/test/cfgdrive.tgz' + required.return_value = True + + ctxt = context.RequestContext('fake_user', 'fake_project') + instance = fake_instance.fake_instance_obj(ctxt) + + file = zvmutils.generate_configdrive('context', instance, + 'injected_files', + 'network_info', + 'admin_password') + required.assert_called_once_with(instance) + create.assert_called_once_with('context', '/test/tmp/fake_uuid', + instance, 'injected_files', + 'network_info', 'admin_password') + self.assertEqual('/test/cfgdrive.tgz', file) + + @mock.patch('nova.api.metadata.base.InstanceMetadata') + @mock.patch('nova.virt.configdrive.ConfigDriveBuilder.make_drive') + def test_create_config_drive(self, make_drive, mock_instance_metadata): + + class FakeInstanceMetadata(object): + def __init__(self): + self.network_metadata = None + + def metadata_for_config_drive(self): + return [] + + mock_instance_metadata.return_value = FakeInstanceMetadata() + + self.flags(config_drive_format='iso9660') + extra_md = {'admin_pass': 'admin_password'} + zvmutils._create_config_drive('context', '/instance_path', + 'instance', 'injected_files', + 'network_info', 'admin_password') + mock_instance_metadata.assert_called_once_with('instance', + content='injected_files', + extra_md=extra_md, + network_info='network_info', + request_context='context') + make_drive.assert_called_once_with('/instance_path/cfgdrive.iso') + + def test_create_config_drive_invalid_format(self): + + self.flags(config_drive_format='vfat') + self.assertRaises(exception.ConfigDriveUnsupportedFormat, + zvmutils._create_config_drive, 'context', + '/instance_path', 'instance', 'injected_files', + 'network_info', 'admin_password') diff --git a/nova/virt/zvm/driver.py b/nova/virt/zvm/driver.py index 537e7b10f9dc..3c21cf074f48 100644 --- a/nova/virt/zvm/driver.py +++ b/nova/virt/zvm/driver.py @@ -12,37 +12,71 @@ # License for the specific language governing permissions and limitations # under the License. +import eventlet +import os +import six +import time + +from oslo_concurrency import lockutils from oslo_log import log as logging from oslo_serialization import jsonutils +from oslo_utils import excutils from nova import conf from nova import exception from nova.i18n import _ from nova.objects import fields as obj_fields +from nova import utils from nova.virt import driver +from nova.virt import images +from nova.virt.zvm import guest from nova.virt.zvm import hypervisor +from nova.virt.zvm import utils as zvmutils LOG = logging.getLogger(__name__) CONF = conf.CONF +DEFAULT_EPH_DISK_FMT = 'ext3' + + class ZVMDriver(driver.ComputeDriver): """z/VM implementation of ComputeDriver.""" def __init__(self, virtapi): super(ZVMDriver, self).__init__(virtapi) - if not CONF.zvm.cloud_connector_url: - error = _('Must specify cloud_connector_url in zvm config ' - 'group to use compute_driver=zvm.driver.ZVMDriver') - raise exception.ZVMDriverException(error=error) + self._validate_options() self._hypervisor = hypervisor.Hypervisor( CONF.zvm.cloud_connector_url, ca_file=CONF.zvm.ca_file) LOG.info("The zVM compute driver has been initialized.") + @staticmethod + def _validate_options(): + if not CONF.zvm.cloud_connector_url: + error = _('Must specify cloud_connector_url in zvm config ' + 'group to use compute_driver=zvm.driver.ZVMDriver') + raise exception.ZVMDriverException(error=error) + + # Try a test to ensure length of give guest is smaller than 8 + try: + _test_instance = CONF.instance_name_template % 0 + except Exception: + msg = _("Template is not usable, the template defined is " + "instance_name_template=%s") % CONF.instance_name_template + raise exception.ZVMDriverException(error=msg) + + # For zVM instance, limit the maximum length of instance name to 8 + if len(_test_instance) > 8: + msg = _("Can't spawn instance with template '%s', " + "The zVM hypervisor does not support instance names " + "longer than 8 characters. Please change your config of " + "instance_name_template.") % CONF.instance_name_template + raise exception.ZVMDriverException(error=msg) + def init_host(self, host): pass @@ -80,3 +114,188 @@ class ZVMDriver(driver.ComputeDriver): def get_available_nodes(self, refresh=False): return self._hypervisor.get_available_nodes(refresh=refresh) + + def get_info(self, instance): + _guest = guest.Guest(self._hypervisor, instance) + return _guest.get_info() + + def spawn(self, context, instance, image_meta, injected_files, + admin_password, allocations, network_info=None, + block_device_info=None): + + LOG.info("Spawning new instance %s on zVM hypervisor", + instance.name, instance=instance) + + if self._hypervisor.guest_exists(instance): + raise exception.InstanceExists(name=instance.name) + + os_distro = image_meta.properties.get('os_distro') + if os_distro is None or len(os_distro) == 0: + reason = _("The `os_distro` image metadata property is required") + raise exception.InvalidInput(reason=reason) + + try: + spawn_start = time.time() + + transportfiles = zvmutils.generate_configdrive(context, + instance, injected_files, network_info, + admin_password) + + spawn_image_name = self._get_image_info(context, image_meta.id, + os_distro) + disk_list, eph_list = self._set_disk_list(instance, + spawn_image_name, + block_device_info) + + # Create the guest vm + self._hypervisor.guest_create(instance.name, + instance.vcpus, instance.memory_mb, + disk_list) + + # Deploy image to the guest vm + self._hypervisor.guest_deploy(instance.name, + spawn_image_name, transportfiles=transportfiles) + + # Handle ephemeral disks + if eph_list: + self._hypervisor.guest_config_minidisks(instance.name, + eph_list) + # Setup network for z/VM instance + self._wait_vif_plug_events(instance.name, os_distro, + network_info, instance) + + self._hypervisor.guest_start(instance.name) + spawn_time = time.time() - spawn_start + LOG.info("Instance spawned successfully in %s seconds", + spawn_time, instance=instance) + except Exception as err: + with excutils.save_and_reraise_exception(): + LOG.error("Deploy instance %(instance)s " + "failed with reason: %(err)s", + {'instance': instance.name, 'err': err}, + instance=instance) + try: + self.destroy(context, instance, network_info, + block_device_info) + except Exception as err: + LOG.exception("Failed to destroy instance", + instance=instance) + + @lockutils.synchronized('IMAGE_INFO_SEMAPHORE') + def _get_image_info(self, context, image_meta_id, os_distro): + try: + res = self._hypervisor.image_query(imagename=image_meta_id) + except exception.ZVMConnectorError as err: + with excutils.save_and_reraise_exception() as sare: + if err.overallRC == 404: + sare.reraise = False + self._import_spawn_image(context, image_meta_id, os_distro) + + res = self._hypervisor.image_query(imagename=image_meta_id) + + return res[0]['imagename'] + + def _set_disk_list(self, instance, image_name, block_device_info): + if instance.root_gb == 0: + root_disk_size = self._hypervisor.image_get_root_disk_size( + image_name) + else: + root_disk_size = '%ig' % instance.root_gb + + disk_list = [] + root_disk = {'size': root_disk_size, + 'is_boot_disk': True + } + disk_list.append(root_disk) + ephemeral_disks_info = driver.block_device_info_get_ephemerals( + block_device_info) + + eph_list = [] + for eph in ephemeral_disks_info: + eph_dict = {'size': '%ig' % eph['size'], + 'format': (CONF.default_ephemeral_format or + DEFAULT_EPH_DISK_FMT)} + eph_list.append(eph_dict) + + if eph_list: + disk_list.extend(eph_list) + return disk_list, eph_list + + def _setup_network(self, vm_name, os_distro, network_info, instance): + LOG.debug("Creating NICs for vm %s", vm_name) + inst_nets = [] + for vif in network_info: + subnet = vif['network']['subnets'][0] + _net = {'ip_addr': subnet['ips'][0]['address'], + 'gateway_addr': subnet['gateway']['address'], + 'cidr': subnet['cidr'], + 'mac_addr': vif['address'], + 'nic_id': vif['id']} + inst_nets.append(_net) + + if inst_nets: + self._hypervisor.guest_create_network_interface(vm_name, + os_distro, inst_nets) + + @staticmethod + def _get_neutron_event(network_info): + if utils.is_neutron() and CONF.vif_plugging_timeout: + return [('network-vif-plugged', vif['id']) + for vif in network_info if vif.get('active') is False] + else: + return [] + + @staticmethod + def _neutron_failed_callback(self, event_name, instance): + LOG.error("Neutron Reported failure on event %s for instance", + event_name, instance=instance) + if CONF.vif_plugging_is_fatal: + raise exception.VirtualInterfaceCreateException() + + def _wait_vif_plug_events(self, vm_name, os_distro, network_info, + instance): + timeout = CONF.vif_plugging_timeout + try: + event = self._get_neutron_event(network_info) + with self.virtapi.wait_for_instance_event( + instance, event, deadline=timeout, + error_callback=self._neutron_failed_callback): + self._setup_network(vm_name, os_distro, network_info, instance) + except eventlet.timeout.Timeout: + LOG.warning("Timeout waiting for vif plugging callback.", + instance=instance) + if CONF.vif_plugging_is_fatal: + raise exception.VirtualInterfaceCreateException() + except Exception as err: + with excutils.save_and_reraise_exception(): + LOG.error("Failed for vif plugging: %s", six.text_type(err), + instance=instance) + + def _import_spawn_image(self, context, image_meta_id, image_os_version): + LOG.debug("Downloading the image %s from glance to nova compute " + "server", image_meta_id) + image_path = os.path.join(os.path.normpath(CONF.zvm.image_tmp_path), + image_meta_id) + if not os.path.exists(image_path): + images.fetch(context, image_meta_id, image_path) + image_url = "file://" + image_path + image_meta = {'os_version': image_os_version} + self._hypervisor.image_import(image_meta_id, image_url, image_meta) + + def destroy(self, context, instance, network_info=None, + block_device_info=None, destroy_disks=False): + if self._hypervisor.guest_exists(instance): + LOG.info("Destroying instance", instance=instance) + try: + self._hypervisor.guest_delete(instance.name) + except exception.ZVMConnectorError as err: + if err.overallRC == 404: + LOG.info("instance disappear during destroying", + instance=instance) + else: + raise + else: + LOG.warning("Instance does not exist", instance=instance) + + def get_host_uptime(self): + return self._hypervisor.get_host_uptime() diff --git a/nova/virt/zvm/guest.py b/nova/virt/zvm/guest.py new file mode 100644 index 000000000000..cfe13f0edf44 --- /dev/null +++ b/nova/virt/zvm/guest.py @@ -0,0 +1,51 @@ +# Copyright 2017,2018 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import log as logging + +from nova.compute import power_state as compute_power_state +from nova import conf +from nova.virt import hardware + + +LOG = logging.getLogger(__name__) +CONF = conf.CONF + + +ZVM_POWER_STATE = { + 'on': compute_power_state.RUNNING, + 'off': compute_power_state.SHUTDOWN, + } + + +class Guest(object): + """z/VM implementation of ComputeDriver.""" + + def __init__(self, hypervisor, instance, virtapi=None): + super(Guest, self).__init__() + + self.virtapi = virtapi + self._hypervisor = hypervisor + self._instance = instance + + def _mapping_power_state(self, power_state): + """Translate power state to OpenStack defined constants.""" + return ZVM_POWER_STATE.get(power_state, compute_power_state.NOSTATE) + + def get_info(self): + """Get the current status of an instance.""" + power_state = self._mapping_power_state( + self._hypervisor.guest_get_power_state( + self._instance.name)) + return hardware.InstanceInfo(power_state) diff --git a/nova/virt/zvm/hypervisor.py b/nova/virt/zvm/hypervisor.py index 0be0964adf36..2f5f04f6fa28 100644 --- a/nova/virt/zvm/hypervisor.py +++ b/nova/virt/zvm/hypervisor.py @@ -12,13 +12,19 @@ # License for the specific language governing permissions and limitations # under the License. +import os +import pwd + from oslo_log import log as logging +from nova.compute import power_state as compute_power_state +from nova import conf from nova import exception from nova.virt.zvm import utils as zvmutils LOG = logging.getLogger(__name__) +CONF = conf.CONF class Hypervisor(object): @@ -29,12 +35,15 @@ class Hypervisor(object): self._reqh = zvmutils.ConnectorClient(zcc_url, ca_file=ca_file) + host_info = self._get_host_info() # Very very unlikely the hostname will be changed, so when create # hypervisor object, store the information in the cache and after # that we can use it directly without query again from connectorclient - self._hypervisor_hostname = self._get_host_info().get( - 'hypervisor_hostname') + self._hypervisor_hostname = host_info['hypervisor_hostname'] + + self._rhost = ''.join([pwd.getpwuid(os.geteuid()).pw_name, '@', + CONF.my_ip]) def _get_host_info(self): host_stats = {} @@ -55,3 +64,81 @@ class Hypervisor(object): def list_names(self): """list names of the servers in the hypervisor""" return self._reqh.call('guest_list') + + def get_host_uptime(self): + host_info = self._get_host_info() + + return host_info['ipl_time'] + + def guest_exists(self, instance): + return instance.name.upper() in self.list_names() + + def guest_get_power_state(self, name): + power_state = compute_power_state.NOSTATE + try: + power_state = self._reqh.call('guest_get_power_state', name) + except exception.ZVMConnectorError as err: + if err.overallRC == 404: + # instance does not exist + LOG.warning("Failed to get power state due to nonexistent " + "instance: %s", name) + raise exception.InstanceNotFound(instance_id=name) + else: + raise + + return power_state + + def guest_create(self, name, vcpus, memory_mb, disk_list): + self._reqh.call('guest_create', name, vcpus, memory_mb, + disk_list=disk_list) + + def guest_deploy(self, name, image_name, transportfiles): + self._reqh.call('guest_deploy', name, image_name, + transportfiles=transportfiles, remotehost=self._rhost) + + def guest_delete(self, name): + self._reqh.call('guest_delete', name) + + def guest_start(self, name): + self._reqh.call('guest_start', name) + + def guest_create_network_interface(self, name, distro, nets): + self._reqh.call('guest_create_network_interface', + name, distro, nets) + + def guest_get_definition_info(self, name): + """Get user direct info + + :returns: User direct is server definition, it will be + returned in a string format + """ + return self._reqh.call('guest_get_definition_info', name) + + def guest_get_nic_vswitch_info(self, name): + """Get the nic and vswitch info + + :returns: Return the nic and vswitch info in dict + """ + return self._reqh.call('guest_get_nic_vswitch_info', name) + + def guest_config_minidisks(self, name, disk_list): + self._reqh.call('guest_config_minidisks', name, disk_list) + + def image_query(self, imagename): + """Check whether image is there or not + + :returns: Query the image and returns a dict of the image info + if the image exists or return {} + """ + return self._reqh.call('image_query', imagename=imagename) + + def image_get_root_disk_size(self, imagename): + """Get the root disk size of image + + :returns: return the size (in string) about the root disk of image + """ + return self._reqh.call('image_get_root_disk_size', imagename) + + 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) diff --git a/nova/virt/zvm/utils.py b/nova/virt/zvm/utils.py index 0ebed6eee64b..db5c6b8d3153 100644 --- a/nova/virt/zvm/utils.py +++ b/nova/virt/zvm/utils.py @@ -12,14 +12,21 @@ # License for the specific language governing permissions and limitations # under the License. +import os from oslo_log import log as logging import six import six.moves.urllib.parse as urlparse from zvmconnector import connector +from oslo_utils import fileutils + +from nova.api.metadata import base as instance_metadata +from nova import conf from nova import exception +from nova.virt import configdrive +CONF = conf.CONF LOG = logging.getLogger(__name__) @@ -59,3 +66,56 @@ class ConnectorClient(object): raise exception.ZVMConnectorError(results=results) return results['output'] + + +def _get_instance_path(instance_uuid): + instance_folder = os.path.join(os.path.normpath(CONF.instances_path), + instance_uuid) + fileutils.ensure_tree(instance_folder) + return instance_folder + + +def _create_config_drive(context, instance_path, instance, + injected_files, network_info, admin_password): + if CONF.config_drive_format != 'iso9660': + raise exception.ConfigDriveUnsupportedFormat( + format=CONF.config_drive_format) + + LOG.debug('Using config drive', instance=instance) + + extra_md = {} + if admin_password: + extra_md['admin_pass'] = admin_password + + inst_md = instance_metadata.InstanceMetadata(instance, + content=injected_files, + extra_md=extra_md, + network_info=network_info, + request_context=context) + + configdrive_iso = os.path.join(instance_path, 'cfgdrive.iso') + LOG.debug('Creating config drive at %s', configdrive_iso, + instance=instance) + with configdrive.ConfigDriveBuilder(instance_md=inst_md) as cdb: + cdb.make_drive(configdrive_iso) + + return configdrive_iso + + +# Prepare and create configdrive for instance +def generate_configdrive(context, instance, injected_files, + network_info, admin_password): + # Create network configuration files + LOG.debug('Creating config drive configuration files ' + 'for instance: %s', instance.name, instance=instance) + + instance_path = _get_instance_path(instance.uuid) + + transportfiles = None + if configdrive.required_by(instance): + transportfiles = _create_config_drive(context, instance_path, + instance, + injected_files, + network_info, + admin_password) + return transportfiles