diff --git a/nova_powervm/tests/virt/powervm/__init__.py b/nova_powervm/tests/virt/powervm/__init__.py index 5502b47f..d4c57282 100644 --- a/nova_powervm/tests/virt/powervm/__init__.py +++ b/nova_powervm/tests/virt/powervm/__init__.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from nova.compute import power_state from nova.compute import task_states from nova.objects import flavor from nova.objects import image_meta @@ -43,6 +44,7 @@ TEST_INSTANCE = { 'host': 'host1', 'flavor': TEST_FLAVOR, 'task_state': None, + 'power_state': power_state.SHUTDOWN, } TEST_INST_SPAWNING = dict(TEST_INSTANCE, task_state=task_states.SPAWNING) diff --git a/nova_powervm/tests/virt/powervm/fixtures.py b/nova_powervm/tests/virt/powervm/fixtures.py index 81633de1..805eefaa 100644 --- a/nova_powervm/tests/virt/powervm/fixtures.py +++ b/nova_powervm/tests/virt/powervm/fixtures.py @@ -91,6 +91,7 @@ class PowerVMComputeDriver(fixtures.Fixture): # Pretend it just returned one host ms_http.feed.entries = [ms_http.feed.entries[0]] self.drv.adapter.read.return_value = ms_http + self.drv.session = self.drv.adapter.session self.drv.init_host('FakeHost') def setUp(self): diff --git a/nova_powervm/tests/virt/powervm/test_driver.py b/nova_powervm/tests/virt/powervm/test_driver.py index 2911035a..ef16988e 100644 --- a/nova_powervm/tests/virt/powervm/test_driver.py +++ b/nova_powervm/tests/virt/powervm/test_driver.py @@ -45,6 +45,7 @@ from nova_powervm.tests.virt.powervm import fixtures as fx from nova_powervm.virt.powervm import driver from nova_powervm.virt.powervm import exception as p_exc from nova_powervm.virt.powervm import live_migration as lpm +from nova_powervm.virt.powervm import vm MS_HTTPRESP_FILE = "managedsystem.txt" MS_NAME = 'HV4' @@ -132,6 +133,11 @@ class TestPowerVMDriver(test.TestCase): test_drv = driver.PowerVMDriver(fake.FakeVirtAPI()) self.assertIsNotNone(test_drv) + def test_cleanup_host(self): + self.drv.cleanup_host('fake_host') + self.assertTrue( + self.drv.session.get_event_listener.return_value.shutdown.called) + def test_get_volume_connector(self): """Tests that a volume connector can be built.""" vol_connector = self.drv.get_volume_connector(mock.Mock()) @@ -1570,3 +1576,54 @@ class TestPowerVMDriver(test.TestCase): mock_bk_dev.return_value = 'info' self.assertEqual('info', self.drv._get_block_device_info('ctx', self.inst)) + + +class TestNovaEventHandler(test.TestCase): + def setUp(self): + super(TestNovaEventHandler, self).setUp() + self.mock_driver = mock.Mock() + self.handler = driver.NovaEventHandler(self.mock_driver) + + @mock.patch.object(vm, 'get_instance') + @mock.patch.object(vm, 'get_vm_qp') + def test_events(self, mock_qprops, mock_get_inst): + # Test events + event_data = [ + { + 'EventType': 'NEW_CLIENT', + 'EventData': '', + 'EventID': '1452692619554', + 'EventDetail': '', + }, + { + 'EventType': 'MODIFY_URI', + 'EventData': 'http://localhost:12080/rest/api/uom/Managed' + 'System/c889bf0d-9996-33ac-84c5-d16727083a77', + 'EventID': '1452692619555', + 'EventDetail': 'Other', + }, + { + 'EventType': 'MODIFY_URI', + 'EventData': 'http://localhost:12080/rest/api/uom/Managed' + 'System/c889bf0d-9996-33ac-84c5-d16727083a77/' + 'LogicalPartition/794654F5-B6E9-4A51-BEC2-' + 'A73E41EAA938', + 'EventID': '1452692619563', + 'EventDetail': 'ReferenceCode,Other', + }, + { + 'EventType': 'MODIFY_URI', + 'EventData': 'http://localhost:12080/rest/api/uom/Managed' + 'System/c889bf0d-9996-33ac-84c5-d16727083a77/' + 'LogicalPartition/794654F5-B6E9-4A51-BEC2-' + 'A73E41EAA938', + 'EventID': '1452692619566', + 'EventDetail': 'RMCState,PartitionState,Other', + }, + ] + + mock_qprops.return_value = pvm_bp.LPARState.RUNNING + mock_get_inst.return_value = powervm.TEST_INST1 + + self.handler.process(event_data) + self.assertTrue(self.mock_driver.emit_event.called) diff --git a/nova_powervm/tests/virt/powervm/test_vm.py b/nova_powervm/tests/virt/powervm/test_vm.py index 9805d9e6..b0ce31d1 100644 --- a/nova_powervm/tests/virt/powervm/test_vm.py +++ b/nova_powervm/tests/virt/powervm/test_vm.py @@ -16,13 +16,13 @@ # import logging - import mock from nova.compute import power_state from nova import exception from nova import objects from nova import test +from nova.virt import event from pypowervm import exceptions as pvm_exc from pypowervm.helpers import log_helper as pvm_log from pypowervm.tests import test_fixtures as pvm_fx @@ -195,6 +195,34 @@ class TestVM(test.TestCase): self.resp = lpar_http.response + def test_translate_event(self): + # (expected event, pvm state, power_state) + tests = [ + (event.EVENT_LIFECYCLE_STARTED, "running", power_state.SHUTDOWN), + (None, "running", power_state.RUNNING) + ] + for t in tests: + self.assertEqual(t[0], vm.translate_event(t[1], t[2])) + + @mock.patch.object(objects.Instance, 'get_by_uuid') + def test_get_instance(self, mock_get_uuid): + mock_get_uuid.return_value = '1111' + self.assertEqual('1111', vm.get_instance('ctx', 'ABC')) + + mock_get_uuid.side_effect = [ + exception.InstanceNotFound({'instance_id': 'fake_instance'}), + '222' + ] + self.assertEqual('222', vm.get_instance('ctx', 'ABC')) + + def test_uuid_set_high_bit(self): + self.assertEqual( + vm._uuid_set_high_bit('65e7a5f0-ceb2-427d-a6d1-e47f0eb38708'), + 'e5e7a5f0-ceb2-427d-a6d1-e47f0eb38708') + self.assertEqual( + vm._uuid_set_high_bit('f6f79d3f-eef1-4009-bfd4-172ab7e6fff4'), + 'f6f79d3f-eef1-4009-bfd4-172ab7e6fff4') + def test_instance_info(self): # Test at least one state translation diff --git a/nova_powervm/virt/powervm/driver.py b/nova_powervm/virt/powervm/driver.py index 537aa48d..207949e9 100644 --- a/nova_powervm/virt/powervm/driver.py +++ b/nova_powervm/virt/powervm/driver.py @@ -26,6 +26,7 @@ from nova.objects import flavor as flavor_obj from nova import utils as n_utils from nova.virt import configdrive from nova.virt import driver +from nova.virt import event import re from oslo_log import log as logging @@ -41,6 +42,7 @@ from pypowervm.helpers import vios_busy as vio_hlp from pypowervm.tasks import memory as pvm_mem from pypowervm.tasks import power as pvm_pwr from pypowervm.tasks import vterm as pvm_vterm +from pypowervm import util as pvm_util from pypowervm.wrappers import base_partition as pvm_bp from pypowervm.wrappers import managed_system as pvm_ms from pypowervm.wrappers import virtual_io_server as pvm_vios @@ -118,11 +120,26 @@ class PowerVMDriver(driver.ComputeDriver): LOG.info(_LI("The compute driver has been initialized.")) + def cleanup_host(self, host): + """Clean up anything that is necessary for the driver gracefully stop, + including ending remote sessions. This is optional. + """ + # Stop listening for events + try: + self.session.get_event_listener().shutdown() + except Exception: + pass + + LOG.info(_LI("The compute driver has been shutdown.")) + def _get_adapter(self): self.session = pvm_apt.Session() self.adapter = pvm_apt.Adapter( self.session, helpers=[log_hlp.log_helper, vio_hlp.vios_busy_retry_helper]) + # Register the event handler + eh = NovaEventHandler(self) + self.session.get_event_listener().subscribe(eh) def _get_disk_adapter(self): conn_info = {'adapter': self.adapter, 'host_uuid': self.host_uuid, @@ -1687,3 +1704,124 @@ class PowerVMDriver(driver.ComputeDriver): return boot_conn_type else: return boot_conn_type + + +class NovaEventHandler(pvm_apt.RawEventHandler): + """Used to receive and handle events from PowerVM.""" + inst_actions_handled = {'PartitionState'} + + def __init__(self, driver): + self._driver = driver + + def _handle_event(self, uri, etype, details, eid): + """Handle an individual event. + + :param uri: PowerVM event uri + :param etype: PowerVM event type + :param details: PowerVM event details + :param eid: PowerVM event id + """ + + # See if this uri ends with a PowerVM UUID. + if not pvm_util.is_instance_path(uri): + return + + pvm_uuid = pvm_util.get_req_path_uuid( + uri, preserve_case=True) + # If a vm event and one we handle, call the inst handler. + if (uri.endswith('LogicalPartition/' + pvm_uuid) and + (self.inst_actions_handled & set(details))): + inst = vm.get_instance(ctx.get_admin_context(), + pvm_uuid) + if inst: + LOG.debug('Handle action "%(action)s" event for instance: ' + '%(inst)s' % + dict(action=details, inst=inst.name)) + self._handle_inst_event( + inst, pvm_uuid, uri, etype, details, eid) + + def _handle_inst_event(self, inst, pvm_uuid, uri, etype, details, eid): + """Handle an instance event. + + This method will check if an instance event signals a change in the + state of the instance as known to OpenStack and if so, trigger an + event upward. + + :param inst: the instance object. + :param pvm_uuid: the PowerVM uuid of the vm + :param uri: PowerVM event uri + :param etype: PowerVM event type + :param details: PowerVM event details + :param eid: PowerVM event id + """ + # If the state of the vm changed see if it should be handled + if 'PartitionState' in details: + # Get the current state + pvm_state = vm.get_vm_qp(self._driver.adapter, pvm_uuid, + 'PartitionState') + # See if it's really a change of state from what OpenStack knows + transition = vm.translate_event(pvm_state, inst.power_state) + if transition is not None: + LOG.debug('New state for instance: %s', pvm_state, + instance=inst) + # Now create an event and sent it. + lce = event.LifecycleEvent(inst.uuid, transition) + LOG.info(_LI('Sending life cycle event for instance state ' + 'change to: %s'), pvm_state, instance=inst) + self._driver.emit_event(lce) + + def process(self, events): + """Process the event that comes back from PowerVM. + + Example of event data: + NEW_CLIENT + 1452692619554 + + + + MODIFY_URI + 1452692619557 + http://localhost:12080/rest/api/ + uom/ManagedSystem/c889bf0d-9996-33ac-84c5-d16727083a77 + + Other + + MODIFY_URI + 1452692619566 + http://localhost:12080/rest/api/ + uom/ManagedSystem/c889bf0d-9996-33ac-84c5-d16727083a77/ + LogicalPartition/794654F5-B6E9-4A51-BEC2-A73E41EAA938 + + RMCState,PartitionState,Other + + + :param events: A sequence of event dicts that has come back from the + system. + + Format: + [ + { + 'EventType': , + 'EventID': , + 'EventData': , + 'EventDetail': + }, + ] + """ + for pvm_event in events: + try: + # Pull all the pieces of the event. + uri = pvm_event['EventData'] + etype = pvm_event['EventType'] + details = pvm_event['EventDetail'] + details = details.split(',') if details else [] + eid = pvm_event['EventID'] + + if etype not in ['NEW_CLIENT']: + LOG.debug('PowerVM Event-Action: %s URI: %s Details %s' % + (etype, uri, details)) + self._handle_event(uri, etype, details, eid) + except Exception as e: + LOG.exception(e) + LOG.warning(_LW('Unable to parse event URI: %s from PowerVM.'), + uri) diff --git a/nova_powervm/virt/powervm/vm.py b/nova_powervm/virt/powervm/vm.py index 8d6b7e91..1c15cd71 100644 --- a/nova_powervm/virt/powervm/vm.py +++ b/nova_powervm/virt/powervm/vm.py @@ -21,6 +21,8 @@ import six from nova.compute import power_state from nova import exception +from nova import objects +from nova.virt import event from nova.virt import hardware from pypowervm import exceptions as pvm_exc from pypowervm.helpers import log_helper as pvm_log @@ -47,27 +49,47 @@ LOG = logging.getLogger(__name__) CONF = cfg.CONF POWERVM_TO_NOVA_STATE = { - "migrating running": power_state.RUNNING, - "running": power_state.RUNNING, - "starting": power_state.RUNNING, + pvm_bp.LPARState.MIGRATING_RUNNING: power_state.RUNNING, + pvm_bp.LPARState.RUNNING: power_state.RUNNING, + pvm_bp.LPARState.STARTING: power_state.RUNNING, - "migrating not active": power_state.SHUTDOWN, - "not activated": power_state.SHUTDOWN, + pvm_bp.LPARState.MIGRATING_NOT_ACTIVE: power_state.SHUTDOWN, + pvm_bp.LPARState.NOT_ACTIVATED: power_state.SHUTDOWN, - "hardware discovery": power_state.NOSTATE, - "not available": power_state.NOSTATE, + pvm_bp.LPARState.HARDWARE_DISCOVERY: power_state.NOSTATE, + pvm_bp.LPARState.NOT_AVAILBLE: power_state.NOSTATE, # map open firmware state to active since it can be shut down - "open firmware": power_state.RUNNING, - "resuming": power_state.NOSTATE, - "shutting down": power_state.NOSTATE, - "suspending": power_state.NOSTATE, - "unknown": power_state.NOSTATE, + pvm_bp.LPARState.OPEN_FIRMWARE: power_state.RUNNING, + pvm_bp.LPARState.RESUMING: power_state.NOSTATE, + pvm_bp.LPARState.SHUTTING_DOWN: power_state.NOSTATE, + pvm_bp.LPARState.SUSPENDING: power_state.NOSTATE, + pvm_bp.LPARState.UNKNOWN: power_state.NOSTATE, - "suspended": power_state.SUSPENDED, + pvm_bp.LPARState.SUSPENDED: power_state.SUSPENDED, - "error": power_state.CRASHED + pvm_bp.LPARState.ERROR: power_state.CRASHED } +# Groupings of PowerVM events used when considering if a state transition +# has taken place. +RUNNING_EVENTS = [ + pvm_bp.LPARState.MIGRATING_RUNNING, + pvm_bp.LPARState.RUNNING, + pvm_bp.LPARState.STARTING, + pvm_bp.LPARState.OPEN_FIRMWARE, +] +STOPPED_EVENTS = [ + pvm_bp.LPARState.NOT_ACTIVATED, + pvm_bp.LPARState.ERROR, + pvm_bp.LPARState.UNKNOWN, +] +SUSPENDED_EVENTS = [ + pvm_bp.LPARState.SUSPENDING, +] +RESUMING_EVENTS = [ + pvm_bp.LPARState.RESUMING, +] + POWERVM_STARTABLE_STATE = (pvm_bp.LPARState.NOT_ACTIVATED) POWERVM_STOPABLE_STATE = (pvm_bp.LPARState.RUNNING, pvm_bp.LPARState.STARTING, pvm_bp.LPARState.OPEN_FIRMWARE, @@ -79,6 +101,31 @@ SECURE_RMC_VSWITCH = 'MGMTSWITCH' SECURE_RMC_VLAN = 4094 +def translate_event(pvm_state, pwr_state): + """Translate the PowerVM state and see if it has changed. + + Compare the state from PowerVM to the state from OpenStack and see if + a life cycle event should be sent to up to OpenStack. + + :param pvm_state: VM state from PowerVM + :param pwr_state: Instance power state from OpenStack + :returns: life cycle event to send. + """ + trans = None + if pvm_state in RUNNING_EVENTS and pwr_state != power_state.RUNNING: + trans = event.EVENT_LIFECYCLE_STARTED + elif pvm_state in STOPPED_EVENTS and pwr_state != power_state.SHUTDOWN: + trans = event.EVENT_LIFECYCLE_STOPPED + elif (pvm_state in SUSPENDED_EVENTS and + pwr_state != power_state.SUSPENDED): + trans = event.EVENT_LIFECYCLE_SUSPENDED + elif pvm_state in RESUMING_EVENTS and pwr_state != power_state.RUNNING: + trans = event.EVENT_LIFECYCLE_RESUMED + + LOG.debug('Transistion to %s' % trans) + return trans + + def _translate_vm_state(pvm_state): """Find the current state of the lpar and convert it to the appropriate nova.compute.power_state @@ -607,6 +654,45 @@ def get_pvm_uuid(instance): return pvm_uuid.convert_uuid_to_pvm(instance.uuid).upper() +def _uuid_set_high_bit(pvm_uuid): + """Turns on the high bit of a uuid + + PowerVM uuids always set the byte 0, bit 0 to 0. + So to convert it to an OpenStack uuid we may have to set the high bit. + + :param uuid: A PowerVM compliant uuid + :returns: A standard format uuid string + """ + return "%x%s" % (int(pvm_uuid[0], 16) | 8, pvm_uuid[1:]) + + +def get_instance(context, pvm_uuid): + """Get an instance, if there is one, that corresponds to the PVM UUID + + Not finding the instance can be a pretty normal case when handling events. + Don't log exceptions for those cases. + + :param pvm_uuid: PowerVM UUID + :return: OpenStack instance or None + """ + uuid = pvm_uuid.lower() + + def get_inst(): + try: + return objects.Instance.get_by_uuid(context, uuid) + except exception.InstanceNotFound: + return objects.Instance.get_by_uuid(context, + _uuid_set_high_bit(uuid)) + + try: + return get_inst() + except exception.InstanceNotFound: + pass + except Exception as e: + LOG.debug('PowerVM UUID not found. %s', e) + return None + + def get_cnas(adapter, instance, host_uuid): """Returns the current CNAs on the instance.