diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py index 85924db553..74b31cd049 100644 --- a/ironic/conductor/manager.py +++ b/ironic/conductor/manager.py @@ -82,7 +82,7 @@ class ConductorManager(base_manager.BaseConductorManager): """Ironic Conductor manager main class.""" # NOTE(rloo): This must be in sync with rpcapi.ConductorAPI's. - RPC_API_VERSION = '1.34' + RPC_API_VERSION = '1.35' target = messaging.Target(version=RPC_API_VERSION) @@ -1537,6 +1537,33 @@ class ConductorManager(base_manager.BaseConductorManager): '%(node)s'), {'portgroup': portgroup.uuid, 'node': task.node.uuid}) + @METRICS.timer('ConductorManager.destroy_volume_connector') + @messaging.expected_exceptions(exception.NodeLocked, + exception.NodeNotFound, + exception.VolumeConnectorNotFound) + def destroy_volume_connector(self, context, connector): + """Delete a volume connector. + + :param context: request context + :param connector: volume connector object + :raises: NodeLocked if node is locked by another conductor + :raises: NodeNotFound if the node associated with the connector does + not exist + :raises: VolumeConnectorNotFound if the volume connector cannot be + found + """ + LOG.debug('RPC destroy_volume_connector called for volume connector ' + '%(connector)s', + {'connector': connector.uuid}) + with task_manager.acquire(context, connector.node_id, + purpose='volume connector deletion') as task: + connector.destroy() + LOG.info(_LI('Successfully deleted volume connector ' + '%(connector)s. ' + 'The node associated with the connector was ' + '%(node)s'), + {'connector': connector.uuid, 'node': task.node.uuid}) + @METRICS.timer('ConductorManager.get_console_information') @messaging.expected_exceptions(exception.NodeLocked, exception.UnsupportedDriverExtension, @@ -1833,6 +1860,42 @@ class ConductorManager(base_manager.BaseConductorManager): return portgroup_obj + @METRICS.timer('ConductorManager.update_volume_connector') + @messaging.expected_exceptions( + exception.InvalidParameterValue, + exception.NodeLocked, + exception.NodeNotFound, + exception.VolumeConnectorNotFound, + exception.VolumeConnectorTypeAndIdAlreadyExists) + def update_volume_connector(self, context, connector): + """Update a volume connector. + + :param context: request context + :param connector: a changed (but not saved) volume connector object + :returns: an updated volume connector object + :raises: InvalidParameterValue if the volume connector's UUID is being + changed + :raises: NodeLocked if the node is already locked + :raises: NodeNotFound if the node associated with the conductor does + not exist + :raises: VolumeConnectorNotFound if the volume connector cannot be + found + :raises: VolumeConnectorTypeAndIdAlreadyExists if another connector + already exists with the same values for type and connector_id + fields + """ + LOG.debug("RPC update_volume_connector called for connector " + "%(connector)s.", + {'connector': connector.uuid}) + + with task_manager.acquire(context, connector.node_id, + purpose='volume connector update'): + connector.save() + LOG.info(_LI("Successfully updated volume connector " + "%(connector)s."), + {'connector': connector.uuid}) + return connector + @METRICS.timer('ConductorManager.get_driver_properties') @messaging.expected_exceptions(exception.DriverNotFound) def get_driver_properties(self, context, driver_name): diff --git a/ironic/conductor/rpcapi.py b/ironic/conductor/rpcapi.py index 9b4b5c765d..a61e41fa2d 100644 --- a/ironic/conductor/rpcapi.py +++ b/ironic/conductor/rpcapi.py @@ -81,11 +81,12 @@ class ConductorAPI(object): | 1.32 - Add do_node_clean | 1.33 - Added update and destroy portgroup. | 1.34 - Added heartbeat + | 1.35 - Added destroy_volume_connector and update_volume_connector """ # NOTE(rloo): This must be in sync with manager.ConductorManager's. - RPC_API_VERSION = '1.34' + RPC_API_VERSION = '1.35' def __init__(self, topic=None): super(ConductorAPI, self).__init__() @@ -735,3 +736,49 @@ class ConductorAPI(object): cctxt = self.client.prepare(topic=self.topic, version='1.31') return cctxt.call(context, 'object_backport_versions', objinst=objinst, object_versions=object_versions) + + def destroy_volume_connector(self, context, connector, topic=None): + """Delete a volume connector. + + Delete the volume connector. The conductor will lock the related node + during this operation. + + :param context: request context + :param connector: volume connector object + :param topic: RPC topic. Defaults to self.topic. + :raises: NodeLocked if node is locked by another conductor + :raises: NodeNotFound if the node associated with the connector does + not exist + :raises: VolumeConnectorNotFound if the volume connector cannot be + found + """ + cctxt = self.client.prepare(topic=topic or self.topic, version='1.35') + return cctxt.call(context, 'destroy_volume_connector', + connector=connector) + + def update_volume_connector(self, context, connector, topic=None): + """Update the volume connector's information. + + Update the volume connector's information in the database and return + a volume connector object. The conductor will lock the related node + during this operation. + + :param context: request context + :param connector: a changed (but not saved) volume connector object + :param topic: RPC topic. Defaults to self.topic. + :raises: InvalidParameterValue if the volume connector's UUID is being + changed + :raises: NodeLocked if node is locked by another conductor + :raises: NodeNotFound if the node associated with the connector does + not exist + :raises: VolumeConnectorNotFound if the volume connector cannot be + found + :raises: VolumeConnectorTypeAndIdAlreadyExists if another connector + already exists with the same values for type and connector_id + fields + :returns: updated volume connector object, including all fields. + + """ + cctxt = self.client.prepare(topic=topic or self.topic, version='1.35') + return cctxt.call(context, 'update_volume_connector', + connector=connector) diff --git a/ironic/tests/unit/conductor/test_manager.py b/ironic/tests/unit/conductor/test_manager.py index 2a966e14dc..9cf1b2944d 100644 --- a/ironic/tests/unit/conductor/test_manager.py +++ b/ironic/tests/unit/conductor/test_manager.py @@ -5471,3 +5471,95 @@ class DoNodeAdoptionTestCase( self.service.heartbeat(self.context, node.uuid, 'http://callback') mock_spawn.assert_called_with(self.driver.deploy.heartbeat, mock.ANY, 'http://callback') + + +@mgr_utils.mock_record_keepalive +class DestroyVolumeConnectorTestCase(mgr_utils.ServiceSetUpMixin, + tests_db_base.DbTestCase): + def test_destroy_volume_connector(self): + node = obj_utils.create_test_node(self.context, driver='fake') + + volume_connector = obj_utils.create_test_volume_connector( + self.context, node_id=node.id) + self.service.destroy_volume_connector(self.context, volume_connector) + self.assertRaises(exception.VolumeConnectorNotFound, + volume_connector.refresh) + self.assertRaises(exception.VolumeConnectorNotFound, + self.dbapi.get_volume_connector_by_uuid, + volume_connector.uuid) + + def test_destroy_volume_connector_node_locked(self): + node = obj_utils.create_test_node(self.context, driver='fake', + reservation='fake-reserv') + + volume_connector = obj_utils.create_test_volume_connector( + self.context, node_id=node.id) + exc = self.assertRaises(messaging.rpc.ExpectedException, + self.service.destroy_volume_connector, + self.context, volume_connector) + # Compare true exception hidden by @messaging.expected_exceptions + self.assertEqual(exception.NodeLocked, exc.exc_info[0]) + + +@mgr_utils.mock_record_keepalive +class UpdateVolumeConnectorTestCase(mgr_utils.ServiceSetUpMixin, + tests_db_base.DbTestCase): + def test_update_volume_connector(self): + node = obj_utils.create_test_node(self.context, driver='fake') + + volume_connector = obj_utils.create_test_volume_connector( + self.context, node_id=node.id, extra={'foo': 'bar'}) + new_extra = {'foo': 'baz'} + volume_connector.extra = new_extra + res = self.service.update_volume_connector(self.context, + volume_connector) + self.assertEqual(new_extra, res.extra) + + def test_update_volume_connector_node_locked(self): + node = obj_utils.create_test_node(self.context, driver='fake', + reservation='fake-reserv') + + volume_connector = obj_utils.create_test_volume_connector( + self.context, node_id=node.id) + volume_connector.extra = {'foo': 'baz'} + exc = self.assertRaises(messaging.rpc.ExpectedException, + self.service.update_volume_connector, + self.context, volume_connector) + # Compare true exception hidden by @messaging.expected_exceptions + self.assertEqual(exception.NodeLocked, exc.exc_info[0]) + + def test_update_volume_connector_type(self): + node = obj_utils.create_test_node(self.context, driver='fake') + volume_connector = obj_utils.create_test_volume_connector( + self.context, node_id=node.id, extra={'vol_id': 'fake-id'}) + new_type = 'wwnn' + volume_connector.type = new_type + res = self.service.update_volume_connector(self.context, + volume_connector) + self.assertEqual(new_type, res.type) + + def test_update_volume_connector_uuid(self): + node = obj_utils.create_test_node(self.context, driver='fake') + volume_connector = obj_utils.create_test_volume_connector( + self.context, node_id=node.id) + volume_connector.uuid = uuidutils.generate_uuid() + exc = self.assertRaises(messaging.rpc.ExpectedException, + self.service.update_volume_connector, + self.context, volume_connector) + # Compare true exception hidden by @messaging.expected_exceptions + self.assertEqual(exception.InvalidParameterValue, exc.exc_info[0]) + + def test_update_volume_connector_duplicate(self): + node = obj_utils.create_test_node(self.context, driver='fake') + volume_connector1 = obj_utils.create_test_volume_connector( + self.context, node_id=node.id) + volume_connector2 = obj_utils.create_test_volume_connector( + self.context, node_id=node.id, uuid=uuidutils.generate_uuid(), + type='diff_type') + volume_connector2.type = volume_connector1.type + exc = self.assertRaises(messaging.rpc.ExpectedException, + self.service.update_volume_connector, + self.context, volume_connector2) + # Compare true exception hidden by @messaging.expected_exceptions + self.assertEqual(exception.VolumeConnectorTypeAndIdAlreadyExists, + exc.exc_info[0]) diff --git a/ironic/tests/unit/conductor/test_rpcapi.py b/ironic/tests/unit/conductor/test_rpcapi.py index 99f57f3643..8ba915d373 100644 --- a/ironic/tests/unit/conductor/test_rpcapi.py +++ b/ironic/tests/unit/conductor/test_rpcapi.py @@ -400,3 +400,17 @@ class RPCAPITestCase(base.DbTestCase): node_id='fake-node', callback_url='http://ramdisk.url:port', version='1.34') + + def test_destroy_volume_connector(self): + fake_volume_connector = dbutils.get_test_volume_connector() + self._test_rpcapi('destroy_volume_connector', + 'call', + version='1.35', + connector=fake_volume_connector) + + def test_update_volume_connector(self): + fake_volume_connector = dbutils.get_test_volume_connector() + self._test_rpcapi('update_volume_connector', + 'call', + version='1.35', + connector=fake_volume_connector) diff --git a/ironic/tests/unit/objects/utils.py b/ironic/tests/unit/objects/utils.py index 11bccbb97d..50911ca16b 100644 --- a/ironic/tests/unit/objects/utils.py +++ b/ironic/tests/unit/objects/utils.py @@ -161,3 +161,30 @@ def create_test_portgroup(ctxt, **kw): portgroup = get_test_portgroup(ctxt, **kw) portgroup.create() return portgroup + + +def get_test_volume_connector(ctxt, **kw): + """Return a VolumeConnector object with appropriate attributes. + + NOTE: The object leaves the attributes marked as changed, such + that a create() could be used to commit it to the DB. + """ + db_volume_connector = db_utils.get_test_volume_connector(**kw) + # Let DB generate ID if it isn't specified explicitly + if 'id' not in kw: + del db_volume_connector['id'] + volume_connector = objects.VolumeConnector(ctxt) + for key in db_volume_connector: + setattr(volume_connector, key, db_volume_connector[key]) + return volume_connector + + +def create_test_volume_connector(ctxt, **kw): + """Create and return a test volume connector object. + + Create a volume connector in the DB and return a VolumeConnector object + with appropriate attributes. + """ + volume_connector = get_test_volume_connector(ctxt, **kw) + volume_connector.create() + return volume_connector