summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Betts <sam@code-smash.net>2016-09-02 12:45:26 +0100
committerSam Betts <sam@code-smash.net>2017-12-11 16:25:47 +0000
commitb642f28be49f31693772231ee921ecfdbd7c80b9 (patch)
tree3660d2932785cc4f90abddc922abc08850cc5061
parent5f563d924c0214500a043068ac03c5fa90c88454 (diff)
downloadironic-b642f28be49f31693772231ee921ecfdbd7c80b9.tar.gz
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
-rw-r--r--api-ref/source/baremetal-api-v1-misc.inc4
-rw-r--r--api-ref/source/parameters.yaml7
-rw-r--r--doc/source/contributor/webapi-version-history.rst6
-rw-r--r--ironic/api/controllers/v1/ramdisk.py19
-rw-r--r--ironic/api/controllers/v1/utils.py10
-rw-r--r--ironic/api/controllers/v1/versions.py4
-rw-r--r--ironic/common/release_mappings.py4
-rw-r--r--ironic/conductor/manager.py41
-rw-r--r--ironic/conductor/rpcapi.py16
-rw-r--r--ironic/drivers/base.py3
-rw-r--r--ironic/drivers/modules/agent_base_vendor.py4
-rw-r--r--ironic/tests/unit/api/controllers/v1/test_ramdisk.py27
-rw-r--r--ironic/tests/unit/conductor/test_manager.py84
-rw-r--r--ironic/tests/unit/conductor/test_rpcapi.py3
-rw-r--r--ironic/tests/unit/drivers/modules/test_agent_base_vendor.py23
-rw-r--r--ironic/tests/unit/drivers/test_base.py2
-rw-r--r--releasenotes/notes/heartbeat_agent_version-70f4e64b19b51d87.yaml8
17 files changed, 232 insertions, 33 deletions
diff --git a/api-ref/source/baremetal-api-v1-misc.inc b/api-ref/source/baremetal-api-v1-misc.inc
index 5bbc2e9be..078b956fb 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 8c201ac77..ac403237d 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 1de2d38ee..fba45cd71 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 8a12feca4..a2afc0e72 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 39ea32e5c..270ce48e7 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 c99d9dd6e..7f2d00bd5 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 2f6a9ecac..031c6a8fa 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 fae7d56ff..cae632229 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 410864125..6563ce10f 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 4bf0b68ee..6231831b9 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 efb1551e8..482c7f1ad 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 d49d7dbed..0c9e77419 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 24f467f07..a19b62bd3 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 4e2b353db..e9a0d9850 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 09a7e288b..b7787f9eb 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 95b6f48b6..18858dc30 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 000000000..48ddc70cd
--- /dev/null
+++ b/releasenotes/notes/heartbeat_agent_version-70f4e64b19b51d87.yaml
@@ -0,0 +1,8 @@
+---
+other:
+ - The agent heartbeat API (POST /v1/heartbeat/<node>) 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.