From b642f28be49f31693772231ee921ecfdbd7c80b9 Mon Sep 17 00:00:00 2001 From: Sam Betts Date: Fri, 2 Sep 2016 12:45:26 +0100 Subject: [PATCH] Receive and store agent version on heartbeat This patch enables receiving agent_version as part of heartbeat, and stores this information on driver_internal_info. This is so that Ironic can dynamically adjust which features and parameters it uses based on which version of the agent is being used. Change-Id: I400adba5d908b657751a83971811e8586f46c673 Partial-Bug: #1602265 --- api-ref/source/baremetal-api-v1-misc.inc | 4 + api-ref/source/parameters.yaml | 7 ++ .../contributor/webapi-version-history.rst | 6 ++ ironic/api/controllers/v1/ramdisk.py | 19 +++-- ironic/api/controllers/v1/utils.py | 10 +++ ironic/api/controllers/v1/versions.py | 4 +- ironic/common/release_mappings.py | 4 +- ironic/conductor/manager.py | 41 ++++++++- ironic/conductor/rpcapi.py | 16 +++- ironic/drivers/base.py | 3 +- ironic/drivers/modules/agent_base_vendor.py | 4 +- .../unit/api/controllers/v1/test_ramdisk.py | 27 +++++- ironic/tests/unit/conductor/test_manager.py | 84 ++++++++++++++++++- ironic/tests/unit/conductor/test_rpcapi.py | 3 +- .../drivers/modules/test_agent_base_vendor.py | 23 +++-- ironic/tests/unit/drivers/test_base.py | 2 +- ...rtbeat_agent_version-70f4e64b19b51d87.yaml | 8 ++ 17 files changed, 232 insertions(+), 33 deletions(-) create mode 100644 releasenotes/notes/heartbeat_agent_version-70f4e64b19b51d87.yaml diff --git a/api-ref/source/baremetal-api-v1-misc.inc b/api-ref/source/baremetal-api-v1-misc.inc index 5bbc2e9be0..078b956fbb 100644 --- a/api-ref/source/baremetal-api-v1-misc.inc +++ b/api-ref/source/baremetal-api-v1-misc.inc @@ -95,6 +95,9 @@ Normal response codes: 202 Error response codes: 400 404 +.. versionadded:: 1.36 ``agent_version`` parameter for passing the version of + the Ironic Python Agent to Ironic during heartbeat + Request ------- @@ -102,3 +105,4 @@ Request - node_ident: node_ident - callback_url: callback_url + - agent_version: agent_version diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 8c201ac770..ac403237d5 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -87,6 +87,13 @@ volume_target_id: required: true type: string +agent_version: + description: | + The version of the ironic-python-agent ramdisk, sent back to the Bare Metal + service and stored during provisioning. + in: query + required: true + type: string callback_url: description: | The URL of an active ironic-python-agent ramdisk, sent back to the Bare diff --git a/doc/source/contributor/webapi-version-history.rst b/doc/source/contributor/webapi-version-history.rst index 1de2d38ee5..fba45cd71e 100644 --- a/doc/source/contributor/webapi-version-history.rst +++ b/doc/source/contributor/webapi-version-history.rst @@ -2,6 +2,12 @@ REST API Version History ======================== +1.36 (Queens, 10.0.0) +--------------------- + +Added ``agent_version`` parameter to deploy heartbeat request for version +negotiation with Ironic Python Agent features. + 1.35 (Queens, 9.2.0) --------------------- diff --git a/ironic/api/controllers/v1/ramdisk.py b/ironic/api/controllers/v1/ramdisk.py index 8a12feca42..a2afc0e72e 100644 --- a/ironic/api/controllers/v1/ramdisk.py +++ b/ironic/api/controllers/v1/ramdisk.py @@ -152,12 +152,17 @@ class HeartbeatController(rest.RestController): """Controller handling heartbeats from deploy ramdisk.""" @expose.expose(None, types.uuid_or_name, wtypes.text, - status_code=http_client.ACCEPTED) - def post(self, node_ident, callback_url): + wtypes.text, status_code=http_client.ACCEPTED) + def post(self, node_ident, callback_url, agent_version=None): """Process a heartbeat from the deploy ramdisk. :param node_ident: the UUID or logical name of a node. :param callback_url: the URL to reach back to the ramdisk. + :param agent_version: The version of the agent that is heartbeating. + ``None`` indicates that the agent that is heartbeating is a version + before sending agent_version was introduced so agent v3.0.0 (the + last release before sending agent_version was introduced) will be + assumed. :raises: NodeNotFound if node with provided UUID or name was not found. :raises: InvalidUuidOrName if node_ident is not valid name or UUID. :raises: NoValidHost if RPC topic for node could not be retrieved. @@ -167,6 +172,10 @@ class HeartbeatController(rest.RestController): if not api_utils.allow_ramdisk_endpoints(): raise exception.NotFound() + if agent_version and not api_utils.allow_agent_version_in_heartbeat(): + raise exception.InvalidParameterValue( + _('Field "agent_version" not recognised')) + cdict = pecan.request.context.to_policy_values() policy.authorize('baremetal:node:ipa_heartbeat', cdict, cdict) @@ -178,6 +187,6 @@ class HeartbeatController(rest.RestController): e.code = http_client.BAD_REQUEST raise - pecan.request.rpcapi.heartbeat(pecan.request.context, - rpc_node.uuid, callback_url, - topic=topic) + pecan.request.rpcapi.heartbeat( + pecan.request.context, rpc_node.uuid, callback_url, + agent_version, topic=topic) diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py index 39ea32e5c2..270ce48e72 100644 --- a/ironic/api/controllers/v1/utils.py +++ b/ironic/api/controllers/v1/utils.py @@ -602,6 +602,16 @@ def allow_node_rebuild_with_configdrive(): versions.MINOR_35_REBUILD_CONFIG_DRIVE) +def allow_agent_version_in_heartbeat(): + """Check if agent version is allowed to be passed into heartbeat. + + Version 1.36 of the API added the ability for agents to pass their version + information to Ironic on heartbeat. + """ + return (pecan.request.version.minor >= + versions.MINOR_36_AGENT_VERSION_HEARTBEAT) + + def get_controller_reserved_names(cls): """Get reserved names for a given controller. diff --git a/ironic/api/controllers/v1/versions.py b/ironic/api/controllers/v1/versions.py index c99d9dd6ee..7f2d00bd56 100644 --- a/ironic/api/controllers/v1/versions.py +++ b/ironic/api/controllers/v1/versions.py @@ -72,6 +72,7 @@ BASE_VERSION = 1 # v1.33: Add node storage interface # v1.34: Add physical network field to port. # v1.35: Add ability to provide configdrive when rebuilding node. +# v1.36: Add Ironic Python Agent version support. MINOR_0_JUNO = 0 MINOR_1_INITIAL_VERSION = 1 @@ -109,6 +110,7 @@ MINOR_32_VOLUME = 32 MINOR_33_STORAGE_INTERFACE = 33 MINOR_34_PORT_PHYSICAL_NETWORK = 34 MINOR_35_REBUILD_CONFIG_DRIVE = 35 +MINOR_36_AGENT_VERSION_HEARTBEAT = 36 # When adding another version, update: # - MINOR_MAX_VERSION @@ -116,7 +118,7 @@ MINOR_35_REBUILD_CONFIG_DRIVE = 35 # explanation of what changed in the new version # - common/release_mappings.py, RELEASE_MAPPING['master']['api'] -MINOR_MAX_VERSION = MINOR_35_REBUILD_CONFIG_DRIVE +MINOR_MAX_VERSION = MINOR_36_AGENT_VERSION_HEARTBEAT # String representations of the minor and maximum versions _MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION) diff --git a/ironic/common/release_mappings.py b/ironic/common/release_mappings.py index 2f6a9ecaca..031c6a8fab 100644 --- a/ironic/common/release_mappings.py +++ b/ironic/common/release_mappings.py @@ -108,8 +108,8 @@ RELEASE_MAPPING = { } }, 'master': { - 'api': '1.35', - 'rpc': '1.41', + 'api': '1.36', + 'rpc': '1.42', 'objects': { 'Node': ['1.21'], 'Conductor': ['1.2'], diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py index fae7d56ff4..cae6322294 100644 --- a/ironic/conductor/manager.py +++ b/ironic/conductor/manager.py @@ -82,6 +82,11 @@ METRICS = metrics_utils.get_metrics_logger(__name__) SYNC_EXCLUDED_STATES = (states.DEPLOYWAIT, states.CLEANWAIT, states.ENROLL) +# NOTE(sambetts) This list is used to keep track of deprecation warnings that +# have already been issued for deploy drivers that do not accept the +# agent_version parameter and need updating. +_SEEN_AGENT_VERSION_DEPRECATIONS = [] + class ConductorManager(base_manager.BaseConductorManager): """Ironic Conductor manager main class.""" @@ -89,7 +94,7 @@ class ConductorManager(base_manager.BaseConductorManager): # NOTE(rloo): This must be in sync with rpcapi.ConductorAPI's. # NOTE(pas-ha): This also must be in sync with # ironic.common.release_mappings.RELEASE_MAPPING['master'] - RPC_API_VERSION = '1.41' + RPC_API_VERSION = '1.42' target = messaging.Target(version=RPC_API_VERSION) @@ -2549,23 +2554,51 @@ class ConductorManager(base_manager.BaseConductorManager): @METRICS.timer('ConductorManager.heartbeat') @messaging.expected_exceptions(exception.NoFreeConductorWorker) - def heartbeat(self, context, node_id, callback_url): + def heartbeat(self, context, node_id, callback_url, agent_version=None): """Process a heartbeat from the ramdisk. :param context: request context. :param node_id: node id or uuid. + :param agent_version: The version of the agent that is heartbeating. If + not provided it either indicates that the agent that is + heartbeating is a version before sending agent_version was + introduced or that we're in the middle of a rolling upgrade and the + RPC version is pinned so the API isn't passing us the + agent_version, in these cases assume agent v3.0.0 (the last release + before sending agent_version was introduced). :param callback_url: URL to reach back to the ramdisk. :raises: NoFreeConductorWorker if there are no conductors to process this heartbeat request. """ LOG.debug('RPC heartbeat called for node %s', node_id) + if agent_version is None: + agent_version = '3.0.0' + + def heartbeat_with_deprecation(task, callback_url, agent_version): + global _SEEN_AGENT_VERSION_DEPRECATIONS + # FIXME(sambetts) Remove this try/except statement in Rocky making + # taking the agent_version in the deploy driver heartbeat function + # mandatory. + try: + task.driver.deploy.heartbeat(task, callback_url, agent_version) + except TypeError: + deploy_driver_name = task.driver.deploy.__class__.__name__ + if deploy_driver_name not in _SEEN_AGENT_VERSION_DEPRECATIONS: + LOG.warning('Deploy driver %s does not support ' + 'agent_version as part of the heartbeat ' + 'request, this will be required from Rocky ' + 'onward.', deploy_driver_name) + _SEEN_AGENT_VERSION_DEPRECATIONS.append(deploy_driver_name) + task.driver.deploy.heartbeat(task, callback_url) + # NOTE(dtantsur): we acquire a shared lock to begin with, drivers are # free to promote it to an exclusive one. with task_manager.acquire(context, node_id, shared=True, purpose='heartbeat') as task: - task.spawn_after(self._spawn_worker, task.driver.deploy.heartbeat, - task, callback_url) + task.spawn_after( + self._spawn_worker, heartbeat_with_deprecation, + task, callback_url, agent_version) @METRICS.timer('ConductorManager.vif_list') @messaging.expected_exceptions(exception.NetworkError, diff --git a/ironic/conductor/rpcapi.py b/ironic/conductor/rpcapi.py index 4108641252..6563ce10f7 100644 --- a/ironic/conductor/rpcapi.py +++ b/ironic/conductor/rpcapi.py @@ -90,13 +90,14 @@ class ConductorAPI(object): | 1.39 - Added timeout optional parameter to change_node_power_state | 1.40 - Added inject_nmi | 1.41 - Added create_port + | 1.42 - Added optional agent_version to heartbeat """ # NOTE(rloo): This must be in sync with manager.ConductorManager's. # NOTE(pas-ha): This also must be in sync with # ironic.common.release_mappings.RELEASE_MAPPING['master'] - RPC_API_VERSION = '1.41' + RPC_API_VERSION = '1.42' def __init__(self, topic=None): super(ConductorAPI, self).__init__() @@ -744,17 +745,24 @@ class ConductorAPI(object): return cctxt.call(context, 'do_node_clean', node_id=node_id, clean_steps=clean_steps) - def heartbeat(self, context, node_id, callback_url, topic=None): + def heartbeat(self, context, node_id, callback_url, agent_version, + topic=None): """Process a node heartbeat. :param context: request context. :param node_id: node ID or UUID. :param callback_url: URL to reach back to the ramdisk. :param topic: RPC topic. Defaults to self.topic. + :param agent_version: the version of the agent that is heartbeating """ - cctxt = self.client.prepare(topic=topic or self.topic, version='1.34') + new_kws = {} + version = '1.34' + if self.client.can_send_version('1.42'): + version = '1.42' + new_kws['agent_version'] = agent_version + cctxt = self.client.prepare(topic=topic or self.topic, version=version) return cctxt.call(context, 'heartbeat', node_id=node_id, - callback_url=callback_url) + callback_url=callback_url, **new_kws) def object_class_action_versions(self, context, objname, objmethod, object_versions, args, kwargs): diff --git a/ironic/drivers/base.py b/ironic/drivers/base.py index 4bf0b68eec..6231831b94 100644 --- a/ironic/drivers/base.py +++ b/ironic/drivers/base.py @@ -402,11 +402,12 @@ class DeployInterface(BaseInterface): """ pass - def heartbeat(self, task, callback_url): + def heartbeat(self, task, callback_url, agent_version): """Record a heartbeat for the node. :param task: a TaskManager instance containing the node to act on. :param callback_url: a URL to use to call to the ramdisk. + :param agent_version: The version of the agent that is heartbeating :return: None """ LOG.warning('Got heartbeat message from node %(node)s, but ' diff --git a/ironic/drivers/modules/agent_base_vendor.py b/ironic/drivers/modules/agent_base_vendor.py index efb1551e83..482c7f1ade 100644 --- a/ironic/drivers/modules/agent_base_vendor.py +++ b/ironic/drivers/modules/agent_base_vendor.py @@ -272,11 +272,12 @@ class HeartbeatMixin(object): return (states.DEPLOYWAIT, states.CLEANWAIT) @METRICS.timer('HeartbeatMixin.heartbeat') - def heartbeat(self, task, callback_url): + def heartbeat(self, task, callback_url, agent_version): """Process a heartbeat. :param task: task to work with. :param callback_url: agent HTTP API URL. + :param agent_version: The version of the agent that is heartbeating """ # NOTE(pas-ha) immediately skip the rest if nothing to do if task.node.provision_state not in self.heartbeat_allowed_states: @@ -293,6 +294,7 @@ class HeartbeatMixin(object): driver_internal_info = node.driver_internal_info driver_internal_info['agent_url'] = callback_url + driver_internal_info['agent_version'] = agent_version node.driver_internal_info = driver_internal_info node.save() diff --git a/ironic/tests/unit/api/controllers/v1/test_ramdisk.py b/ironic/tests/unit/api/controllers/v1/test_ramdisk.py index d49d7dbedf..0c9e774198 100644 --- a/ironic/tests/unit/api/controllers/v1/test_ramdisk.py +++ b/ironic/tests/unit/api/controllers/v1/test_ramdisk.py @@ -185,5 +185,30 @@ class TestHeartbeat(test_api_base.BaseApiTest): self.assertEqual(http_client.ACCEPTED, response.status_int) self.assertEqual(b'', response.body) mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY, - node.uuid, 'url', + node.uuid, 'url', None, topic='test-topic') + + @mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True) + def test_ok_agent_version(self, mock_heartbeat): + node = obj_utils.create_test_node(self.context) + response = self.post_json( + '/heartbeat/%s' % node.uuid, + {'callback_url': 'url', + 'agent_version': '1.4.1'}, + headers={api_base.Version.string: str(api_v1.max_version())}) + self.assertEqual(http_client.ACCEPTED, response.status_int) + self.assertEqual(b'', response.body) + mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY, + node.uuid, 'url', '1.4.1', + topic='test-topic') + + @mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True) + def test_old_API_agent_version_error(self, mock_heartbeat): + node = obj_utils.create_test_node(self.context) + response = self.post_json( + '/heartbeat/%s' % node.uuid, + {'callback_url': 'url', + 'agent_version': '1.4.1'}, + headers={api_base.Version.string: '1.35'}, + expect_errors=True) + self.assertEqual(http_client.BAD_REQUEST, response.status_int) diff --git a/ironic/tests/unit/conductor/test_manager.py b/ironic/tests/unit/conductor/test_manager.py index 24f467f07e..a19b62bd34 100644 --- a/ironic/tests/unit/conductor/test_manager.py +++ b/ironic/tests/unit/conductor/test_manager.py @@ -6159,8 +6159,9 @@ class DoNodeAdoptionTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase): self.assertEqual(states.NOSTATE, node.target_provision_state) self.assertIsNone(node.last_error) + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat') @mock.patch('ironic.conductor.manager.ConductorManager._spawn_worker') - def test_heartbeat(self, mock_spawn): + def test_heartbeat(self, mock_spawn, mock_heartbeat): """Test heartbeating.""" node = obj_utils.create_test_node( self.context, driver='fake', @@ -6168,9 +6169,86 @@ class DoNodeAdoptionTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase): target_provision_state=states.ACTIVE) self._start_service() + + mock_spawn.reset_mock() + + def fake_spawn(func, *args, **kwargs): + func(*args, **kwargs) + return mock.MagicMock() + mock_spawn.side_effect = fake_spawn + self.service.heartbeat(self.context, node.uuid, 'http://callback') - mock_spawn.assert_called_with(self.driver.deploy.heartbeat, - mock.ANY, 'http://callback') + mock_heartbeat.assert_called_with(mock.ANY, 'http://callback', '3.0.0') + + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat') + @mock.patch('ironic.conductor.manager.ConductorManager._spawn_worker') + def test_heartbeat_agent_version(self, mock_spawn, mock_heartbeat): + """Test heartbeating.""" + node = obj_utils.create_test_node( + self.context, driver='fake', + provision_state=states.DEPLOYING, + target_provision_state=states.ACTIVE) + + self._start_service() + + mock_spawn.reset_mock() + + def fake_spawn(func, *args, **kwargs): + func(*args, **kwargs) + return mock.MagicMock() + mock_spawn.side_effect = fake_spawn + + self.service.heartbeat( + self.context, node.uuid, 'http://callback', '1.4.1') + mock_heartbeat.assert_called_with(mock.ANY, 'http://callback', '1.4.1') + + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat') + @mock.patch.object(manager, 'LOG', autospec=True) + @mock.patch('ironic.conductor.manager.ConductorManager._spawn_worker') + def test_heartbeat_agent_version_deprecated(self, mock_spawn, log_mock, + mock_heartbeat): + """Test heartbeating.""" + node = obj_utils.create_test_node( + self.context, driver='fake', + provision_state=states.DEPLOYING, + target_provision_state=states.ACTIVE) + + self._start_service() + + mock_spawn.reset_mock() + + def fake_spawn(func, *args, **kwargs): + func(*args, **kwargs) + return mock.MagicMock() + mock_spawn.side_effect = fake_spawn + + mock_heartbeat.side_effect = [TypeError("Too many parameters"), + None, TypeError("Too many parameters"), + None] + + # NOTE(sambetts) Test to make sure deploy driver that doesn't support + # version yet falls back to old behviour and logs a warning. + self.service.heartbeat( + self.context, node.uuid, 'http://callback', '1.4.1') + calls = [ + mock.call(mock.ANY, 'http://callback', '1.4.1'), + mock.call(mock.ANY, 'http://callback') + ] + mock_heartbeat.assert_has_calls(calls) + self.assertTrue(log_mock.warning.called) + + # NOTE(sambetts) Test to make sure that the deprecation warning isn't + # thrown again. + log_mock.reset_mock() + mock_heartbeat.reset_mock() + self.service.heartbeat( + self.context, node.uuid, 'http://callback', '1.4.1') + calls = [ + mock.call(mock.ANY, 'http://callback', '1.4.1'), + mock.call(mock.ANY, 'http://callback') + ] + mock_heartbeat.assert_has_calls(calls) + self.assertFalse(log_mock.warning.called) @mgr_utils.mock_record_keepalive diff --git a/ironic/tests/unit/conductor/test_rpcapi.py b/ironic/tests/unit/conductor/test_rpcapi.py index 4e2b353dba..e9a0d9850e 100644 --- a/ironic/tests/unit/conductor/test_rpcapi.py +++ b/ironic/tests/unit/conductor/test_rpcapi.py @@ -443,7 +443,8 @@ class RPCAPITestCase(db_base.DbTestCase): 'call', node_id='fake-node', callback_url='http://ramdisk.url:port', - version='1.34') + agent_version=None, + version='1.42') def test_destroy_volume_connector(self): fake_volume_connector = db_utils.get_test_volume_connector() diff --git a/ironic/tests/unit/drivers/modules/test_agent_base_vendor.py b/ironic/tests/unit/drivers/modules/test_agent_base_vendor.py index 09a7e288b9..b7787f9ebe 100644 --- a/ironic/tests/unit/drivers/modules/test_agent_base_vendor.py +++ b/ironic/tests/unit/drivers/modules/test_agent_base_vendor.py @@ -81,11 +81,14 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest): agent_url = 'url-%s' % state with task_manager.acquire(self.context, self.node.uuid, shared=True) as task: - self.deploy.heartbeat(task, agent_url) + self.deploy.heartbeat(task, agent_url, '3.2.0') self.assertFalse(task.shared) self.assertEqual( agent_url, task.node.driver_internal_info['agent_url']) + self.assertEqual( + '3.2.0', + task.node.driver_internal_info['agent_version']) self.assertEqual(0, ncrc_mock.call_count) self.assertEqual(0, rti_mock.call_count) self.assertEqual(0, cd_mock.call_count) @@ -105,7 +108,7 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest): with task_manager.acquire(self.context, self.node.uuid, shared=True) as task: self.node.provision_state = state - self.deploy.heartbeat(task, 'url') + self.deploy.heartbeat(task, 'url', '1.0.0') self.assertTrue(task.shared) self.assertEqual(0, ncrc_mock.call_count) self.assertEqual(0, rti_mock.call_count) @@ -125,7 +128,7 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest): self.context, self.node['uuid'], shared=False) as task: task.node.provision_state = states.DEPLOYWAIT task.node.target_provision_state = states.ACTIVE - self.deploy.heartbeat(task, 'http://127.0.0.1:8080') + self.deploy.heartbeat(task, 'http://127.0.0.1:8080', '1.0.0') failed_mock.assert_called_once_with( task, mock.ANY, collect_logs=True) log_mock.assert_called_once_with( @@ -155,7 +158,7 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest): task.node.provision_state = states.DEPLOYWAIT task.node.target_provision_state = states.ACTIVE done_mock.side_effect = driver_failure - self.deploy.heartbeat(task, 'http://127.0.0.1:8080') + self.deploy.heartbeat(task, 'http://127.0.0.1:8080', '1.0.0') # task.node.provision_state being set to DEPLOYFAIL # within the driver_failue, hearbeat should not call # deploy_utils.set_failed_state anymore @@ -178,7 +181,7 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest): self.node.save() with task_manager.acquire( self.context, self.node.uuid, shared=False) as task: - self.deploy.heartbeat(task, 'http://127.0.0.1:8080') + self.deploy.heartbeat(task, 'http://127.0.0.1:8080', '1.0.0') mock_touch.assert_called_once_with(mock.ANY) mock_refresh.assert_called_once_with(mock.ANY, task) @@ -206,7 +209,7 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest): failed_mock.side_effect = Exception() with task_manager.acquire( self.context, self.node.uuid, shared=False) as task: - self.deploy.heartbeat(task, 'http://127.0.0.1:8080') + self.deploy.heartbeat(task, 'http://127.0.0.1:8080', '1.0.0') mock_touch.assert_called_once_with(mock.ANY) mock_handler.assert_called_once_with(task, mock.ANY) @@ -234,7 +237,7 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest): self.node.save() with task_manager.acquire( self.context, self.node.uuid, shared=False) as task: - self.deploy.heartbeat(task, 'http://127.0.0.1:8080') + self.deploy.heartbeat(task, 'http://127.0.0.1:8080', '1.0.0') mock_touch.assert_called_once_with(mock.ANY) mock_continue.assert_called_once_with(mock.ANY, task) @@ -257,7 +260,7 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest): self.node.save() with task_manager.acquire( self.context, self.node.uuid, shared=False) as task: - self.deploy.heartbeat(task, 'http://127.0.0.1:8080') + self.deploy.heartbeat(task, 'http://127.0.0.1:8080', '1.0.0') mock_continue.assert_called_once_with(mock.ANY, task) mock_handler.assert_called_once_with(task, mock.ANY) @@ -274,9 +277,11 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest): self.node.save() with task_manager.acquire( self.context, self.node.uuid, shared=False) as task: - self.deploy.heartbeat(task, 'http://127.0.0.1:8080') + self.deploy.heartbeat(task, 'http://127.0.0.1:8080', '3.2.0') self.assertEqual('http://127.0.0.1:8080', task.node.driver_internal_info['agent_url']) + self.assertEqual('3.2.0', + task.node.driver_internal_info['agent_version']) mock_touch.assert_called_once_with(mock.ANY) diff --git a/ironic/tests/unit/drivers/test_base.py b/ironic/tests/unit/drivers/test_base.py index 95b6f48b60..18858dc30a 100644 --- a/ironic/tests/unit/drivers/test_base.py +++ b/ironic/tests/unit/drivers/test_base.py @@ -406,7 +406,7 @@ class TestDeployInterface(base.TestCase): deploy = fake.FakeDeploy() deploy.heartbeat(mock.Mock(node=mock.Mock(uuid='uuid', driver='driver')), - 'url') + 'url', '3.2.0') self.assertTrue(mock_log.called) diff --git a/releasenotes/notes/heartbeat_agent_version-70f4e64b19b51d87.yaml b/releasenotes/notes/heartbeat_agent_version-70f4e64b19b51d87.yaml new file mode 100644 index 0000000000..48ddc70cde --- /dev/null +++ b/releasenotes/notes/heartbeat_agent_version-70f4e64b19b51d87.yaml @@ -0,0 +1,8 @@ +--- +other: + - The agent heartbeat API (POST /v1/heartbeat/) can now receive a new + ``agent_version`` parameter. If received this will be stored in the node's + driver_internal_info['agent_version'] field. This information will be used + by the Bare Metal service to gracefully degrade support for agent features + that are requested by the Bare Metal service, ensuring that we don't + request a feature that an older ramdisk doesn't support.