diff --git a/nova/tests/unit/virt/powervm/__init__.py b/nova/tests/unit/virt/powervm/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/nova/tests/unit/virt/powervm/test_driver.py b/nova/tests/unit/virt/powervm/test_driver.py new file mode 100644 index 000000000000..32b722c0ce9a --- /dev/null +++ b/nova/tests/unit/virt/powervm/test_driver.py @@ -0,0 +1,85 @@ +# Copyright 2017 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 pypowervm.helpers import log_helper as pvm_hlp_log +from pypowervm.helpers import vios_busy as pvm_hlp_vbusy +from pypowervm.wrappers import managed_system as pvm_ms + +from nova.compute import power_state +from nova import test +from nova.virt import hardware +from nova.virt.powervm import driver + + +class TestPowerVMDriver(test.NoDBTestCase): + + def setUp(self): + super(TestPowerVMDriver, self).setUp() + self.drv = driver.PowerVMDriver('virtapi') + + @mock.patch('pypowervm.adapter.Adapter', autospec=True) + @mock.patch('pypowervm.adapter.Session', autospec=True) + @mock.patch('pypowervm.tasks.partition.validate_vios_ready', autospec=True) + @mock.patch('pypowervm.wrappers.managed_system.System', autospec=True) + def test_init_host(self, mock_sys, mock_vvr, mock_sess, mock_adp): + mock_sys.get.return_value = ['host_wrapper'] + self.drv.init_host('host') + mock_sess.assert_called_once_with(conn_tries=60) + mock_adp.assert_called_once_with( + mock_sess.return_value, helpers=[ + pvm_hlp_log.log_helper, pvm_hlp_vbusy.vios_busy_retry_helper]) + mock_vvr.assert_called_once_with(mock_adp.return_value) + self.assertEqual('host_wrapper', self.drv.host_wrapper) + + def test_get_info(self): + info = self.drv.get_info('inst') + self.assertIsInstance(info, hardware.InstanceInfo) + self.assertEqual(power_state.NOSTATE, info.state) + + def test_list_instances(self): + self.assertEqual([], self.drv.list_instances()) + + def test_get_available_nodes(self): + self.drv.host_wrapper = mock.create_autospec(pvm_ms.System, + instance=True) + self.assertEqual([self.drv.host_wrapper.mtms.mtms_str], + self.drv.get_available_nodes('node')) + + @mock.patch('pypowervm.wrappers.managed_system.System', autospec=True) + @mock.patch('nova.virt.powervm.host.build_host_resource_from_ms') + def test_get_available_resource(self, mock_bhrfm, mock_sys): + mock_sys.get.return_value = ['sys'] + self.drv.adapter = 'adap' + mock_bhrfm.return_value = {'foo': 'bar'} + self.assertEqual( + {'foo': 'bar', 'local_gb': 100000, 'local_gb_used': 10}, + self.drv.get_available_resource('node')) + mock_sys.get.assert_called_once_with('adap') + mock_bhrfm.assert_called_once_with('sys') + self.assertEqual('sys', self.drv.host_wrapper) + + def test_spawn(self): + # TODO(efried): Real UT once spawn is implemented. + inst = mock.Mock() + self.drv.spawn('ctx', inst, 'img_meta', 'inj_files', 'admin_pass') + self.drv.spawn('ctx', inst, 'img_meta', 'inj_files', 'admin_pass', + network_info='net_info', block_device_info='bdm') + + def test_destroy(self): + # TODO(efried): Real UT once destroy is implemented. + inst = mock.Mock() + self.drv.destroy('ctx', inst, 'net_info') + self.drv.destroy('ctx', inst, 'net_info', block_device_info='bdm', + destroy_disks=False, migrate_data='mig_data') diff --git a/nova/tests/unit/virt/powervm/test_host.py b/nova/tests/unit/virt/powervm/test_host.py new file mode 100644 index 000000000000..634c7b5369b9 --- /dev/null +++ b/nova/tests/unit/virt/powervm/test_host.py @@ -0,0 +1,65 @@ +# Copyright 2016 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 pypowervm.wrappers import managed_system as pvm_ms +from pypowervm.wrappers import mtms as pvm_mtms + +from nova import test +from nova.virt.powervm import host as pvm_host + + +class TestPowerVMHost(test.NoDBTestCase): + def test_host_resources(self): + # Create objects to test with + ms_wrapper = mock.create_autospec(pvm_ms.System, spec_set=True) + mtms = mock.create_autospec(pvm_mtms.MTMS, spec_set=True) + mtms.configure_mock(mtms_str='8484923A123456') + asio = mock.create_autospec(pvm_ms.ASIOConfig, spec_set=True) + ms_wrapper.configure_mock( + proc_units_configurable=500, + proc_units_avail=500, + memory_configurable=5242880, + memory_free=5242752, + mtms=mtms, + memory_region_size='big', + asio_config=asio) + + # Run the actual test + stats = pvm_host.build_host_resource_from_ms(ms_wrapper) + self.assertIsNotNone(stats) + + # Check for the presence of fields + fields = (('vcpus', 500), ('vcpus_used', 0), + ('memory_mb', 5242880), ('memory_mb_used', 128), + 'hypervisor_type', 'hypervisor_version', + 'hypervisor_hostname', 'cpu_info', + 'supported_instances', 'stats') + for fld in fields: + if isinstance(fld, tuple): + value = stats.get(fld[0], None) + self.assertEqual(value, fld[1]) + else: + value = stats.get(fld, None) + self.assertIsNotNone(value) + # Check for individual stats + hstats = (('proc_units', '500.00'), ('proc_units_used', '0.00')) + for stat in hstats: + if isinstance(stat, tuple): + value = stats['stats'].get(stat[0], None) + self.assertEqual(value, stat[1]) + else: + value = stats['stats'].get(stat, None) + self.assertIsNotNone(value) diff --git a/nova/virt/powervm/__init__.py b/nova/virt/powervm/__init__.py new file mode 100644 index 000000000000..9780cb485666 --- /dev/null +++ b/nova/virt/powervm/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2017 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 nova.virt.powervm import driver + +PowerVMDriver = driver.PowerVMDriver diff --git a/nova/virt/powervm/driver.py b/nova/virt/powervm/driver.py new file mode 100644 index 000000000000..c6db46f79a83 --- /dev/null +++ b/nova/virt/powervm/driver.py @@ -0,0 +1,182 @@ +# Copyright 2014, 2017 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. +"""Connection to PowerVM hypervisor through NovaLink.""" + +from oslo_log import log as logging +from pypowervm import adapter as pvm_apt +from pypowervm.helpers import log_helper as log_hlp +from pypowervm.helpers import vios_busy as vio_hlp +from pypowervm.tasks import partition as pvm_par +from pypowervm.wrappers import managed_system as pvm_ms + +from nova.compute import power_state +from nova.virt import driver +from nova.virt import hardware +from nova.virt.powervm import host + +LOG = logging.getLogger(__name__) + + +class PowerVMDriver(driver.ComputeDriver): + """PowerVM NovaLink Implementation of Compute Driver. + + https://wiki.openstack.org/wiki/PowerVM + """ + + def __init__(self, virtapi): + super(PowerVMDriver, self).__init__(virtapi) + + def init_host(self, host): + """Initialize anything that is necessary for the driver to function. + + Includes catching up with currently running VMs on the given host. + """ + # Build the adapter. May need to attempt the connection multiple times + # in case the PowerVM management API service is starting. + # TODO(efried): Implement async compute service enable/disable like + # I73a34eb6e0ca32d03e54d12a5e066b2ed4f19a61 + self.adapter = pvm_apt.Adapter( + pvm_apt.Session(conn_tries=60), + helpers=[log_hlp.log_helper, vio_hlp.vios_busy_retry_helper]) + # Make sure the Virtual I/O Server(s) are available. + pvm_par.validate_vios_ready(self.adapter) + self.host_wrapper = pvm_ms.System.get(self.adapter)[0] + LOG.info("The PowerVM compute driver has been initialized.") + + @staticmethod + def _log_operation(op, instance): + """Log entry point of driver operations.""" + LOG.info('Operation: %(op)s. Virtual machine display name: ' + '%(display_name)s, name: %(name)s', + {'op': op, 'display_name': instance.display_name, + 'name': instance.name}, instance=instance) + + def get_info(self, instance): + """Get the current status of an instance, by name (not ID!) + + :param instance: nova.objects.instance.Instance object + + Returns a InstanceInfo object containing: + + :state: the running state, one of the power_state codes + :max_mem_kb: (int) the maximum memory in KBytes allowed + :mem_kb: (int) the memory in KBytes used by the domain + :num_cpu: (int) the number of virtual CPUs for the domain + :cpu_time_ns: (int) the CPU time used in nanoseconds + :id: a unique ID for the instance + """ + # TODO(efried): Implement + return hardware.InstanceInfo(state=power_state.NOSTATE) + + def list_instances(self): + """Return the names of all the instances known to the virt host. + + :return: VM Names as a list. + """ + # TODO(efried): Get the LPAR names + return [] + + def get_available_nodes(self, refresh=False): + """Returns nodenames of all nodes managed by the compute service. + + This method is for multi compute-nodes support. If a driver supports + multi compute-nodes, this method returns a list of nodenames managed + by the service. Otherwise, this method should return + [hypervisor_hostname]. + """ + + return [self.host_wrapper.mtms.mtms_str] + + def get_available_resource(self, nodename): + """Retrieve resource information. + + This method is called when nova-compute launches, and as part of a + periodic task. + + :param nodename: Node from which the caller wants to get resources. + A driver that manages only one node can safely ignore + this. + :return: Dictionary describing resources. + """ + # TODO(efried): Switch to get_inventory, per blueprint + # custom-resource-classes-pike + # Do this here so it refreshes each time this method is called. + self.host_wrapper = pvm_ms.System.get(self.adapter)[0] + # Get host information + data = host.build_host_resource_from_ms(self.host_wrapper) + # Add the disk information + # TODO(efried): Get real stats when disk support is added. + data["local_gb"] = 100000 + data["local_gb_used"] = 10 + return data + + def spawn(self, context, instance, image_meta, injected_files, + admin_password, network_info=None, block_device_info=None): + """Create a new instance/VM/domain on the virtualization platform. + + Once this successfully completes, the instance should be + running (power_state.RUNNING). + + If this fails, any partial instance should be completely + cleaned up, and the virtualization platform should be in the state + that it was before this call began. + + :param context: security context + :param instance: nova.objects.instance.Instance + This function should use the data there to guide + the creation of the new instance. + :param nova.objects.ImageMeta image_meta: + The metadata of the image of the instance. + :param injected_files: User files to inject into instance. + :param admin_password: Administrator password to set in instance. + :param network_info: instance network information + :param block_device_info: Information about block devices to be + attached to the instance. + """ + self._log_operation('spawn', instance) + # TODO(efried): Take flavor extra specs into account + # TODO(efried): Use TaskFlow + # TODO(efried): Create the LPAR + # TODO(thorst, efried) Plug the VIFs + # TODO(thorst, efried) Create/Connect the disk + # TODO(thorst, efried) Add the config drive + # Last step is to power on the system. + # TODO(efried): Power on the LPAR + + def destroy(self, context, instance, network_info, block_device_info=None, + destroy_disks=True, migrate_data=None): + """Destroy the specified instance from the Hypervisor. + + If the instance is not found (for example if networking failed), this + function should still succeed. It's probably a good idea to log a + warning in that case. + + :param context: security context + :param instance: Instance object as returned by DB layer. + :param network_info: instance network information + :param block_device_info: Information about block devices that should + be detached from the instance. + :param destroy_disks: Indicates if disks should be destroyed + :param migrate_data: implementation specific params + """ + # TODO(thorst, efried) Add resize checks for destroy + self._log_operation('destroy', instance) + # TODO(efried): Use TaskFlow + # TODO(efried): Power off the LPAR + # TODO(thorst, efried) Add unplug vifs task + # TODO(thorst, efried) Add config drive tasks + # TODO(thorst, efried) Add volume disconnect tasks + # TODO(thorst, efried) Add disk disconnect/destroy tasks + # TODO(thorst, efried) Add LPAR id based scsi map clean up task + # TODO(efried): Delete the LPAR diff --git a/nova/virt/powervm/host.py b/nova/virt/powervm/host.py new file mode 100644 index 000000000000..3a6db47deffd --- /dev/null +++ b/nova/virt/powervm/host.py @@ -0,0 +1,65 @@ +# Copyright 2014, 2017 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 math + +from oslo_log import log as logging +from oslo_serialization import jsonutils + +from nova.objects import fields + +LOG = logging.getLogger(__name__) + +# Power VM hypervisor info +# Normally, the hypervisor version is a string in the form of '8.0.0' and +# converted to an int with nova.virt.utils.convert_version_to_int() however +# there isn't currently a mechanism to retrieve the exact version. +# Complicating this is the fact that nova conductor only allows live migration +# from the source host to the destination if the source is equal to or less +# than the destination version. PowerVM live migration limitations are +# checked by the PowerVM capabilities flags and not specific version levels. +# For that reason, we'll just publish the major level. +IBM_POWERVM_HYPERVISOR_VERSION = 8 + +# The types of LPARS that are supported. +POWERVM_SUPPORTED_INSTANCES = [ + (fields.Architecture.PPC64, fields.HVType.PHYP, fields.VMMode.HVM), + (fields.Architecture.PPC64LE, fields.HVType.PHYP, fields.VMMode.HVM)] + + +def build_host_resource_from_ms(ms_w): + """Build the host resource dict from a ManagedSystem PowerVM wrapper. + + :param ms_w: The pypowervm System wrapper describing the managed system. + """ + data = {} + # Calculate the vcpus + proc_units = ms_w.proc_units_configurable + pu_used = float(proc_units) - float(ms_w.proc_units_avail) + data['vcpus'] = int(math.ceil(float(proc_units))) + data['vcpus_used'] = int(math.ceil(pu_used)) + data['memory_mb'] = ms_w.memory_configurable + data['memory_mb_used'] = (ms_w.memory_configurable - + ms_w.memory_free) + data["hypervisor_type"] = fields.HVType.PHYP + data["hypervisor_version"] = IBM_POWERVM_HYPERVISOR_VERSION + data["hypervisor_hostname"] = ms_w.mtms.mtms_str + data["cpu_info"] = jsonutils.dumps({'vendor': 'ibm', 'arch': 'ppc64'}) + data["numa_topology"] = None + data["supported_instances"] = POWERVM_SUPPORTED_INSTANCES + stats = {'proc_units': '%.2f' % float(proc_units), + 'proc_units_used': '%.2f' % pu_used, + 'memory_region_size': ms_w.memory_region_size} + data["stats"] = stats + return data diff --git a/requirements.txt b/requirements.txt index 9158028f7ebd..88acacb6fa83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -61,3 +61,4 @@ microversion-parse>=0.1.2 # Apache-2.0 os-xenapi>=0.1.1 # Apache-2.0 tooz>=1.47.0 # Apache-2.0 cursive>=0.1.2 # Apache-2.0 +pypowervm>=1.1.1 # Apache-2.0