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
This commit is contained in:
parent
5f563d924c
commit
b642f28be4
@ -95,6 +95,9 @@ Normal response codes: 202
|
|||||||
|
|
||||||
Error response codes: 400 404
|
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
|
Request
|
||||||
-------
|
-------
|
||||||
|
|
||||||
@ -102,3 +105,4 @@ Request
|
|||||||
|
|
||||||
- node_ident: node_ident
|
- node_ident: node_ident
|
||||||
- callback_url: callback_url
|
- callback_url: callback_url
|
||||||
|
- agent_version: agent_version
|
||||||
|
@ -87,6 +87,13 @@ volume_target_id:
|
|||||||
required: true
|
required: true
|
||||||
type: string
|
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:
|
callback_url:
|
||||||
description: |
|
description: |
|
||||||
The URL of an active ironic-python-agent ramdisk, sent back to the Bare
|
The URL of an active ironic-python-agent ramdisk, sent back to the Bare
|
||||||
|
@ -2,6 +2,12 @@
|
|||||||
REST API Version History
|
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)
|
1.35 (Queens, 9.2.0)
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|
@ -152,12 +152,17 @@ class HeartbeatController(rest.RestController):
|
|||||||
"""Controller handling heartbeats from deploy ramdisk."""
|
"""Controller handling heartbeats from deploy ramdisk."""
|
||||||
|
|
||||||
@expose.expose(None, types.uuid_or_name, wtypes.text,
|
@expose.expose(None, types.uuid_or_name, wtypes.text,
|
||||||
status_code=http_client.ACCEPTED)
|
wtypes.text, status_code=http_client.ACCEPTED)
|
||||||
def post(self, node_ident, callback_url):
|
def post(self, node_ident, callback_url, agent_version=None):
|
||||||
"""Process a heartbeat from the deploy ramdisk.
|
"""Process a heartbeat from the deploy ramdisk.
|
||||||
|
|
||||||
:param node_ident: the UUID or logical name of a node.
|
:param node_ident: the UUID or logical name of a node.
|
||||||
:param callback_url: the URL to reach back to the ramdisk.
|
: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: NodeNotFound if node with provided UUID or name was not found.
|
||||||
:raises: InvalidUuidOrName if node_ident is not valid name or UUID.
|
:raises: InvalidUuidOrName if node_ident is not valid name or UUID.
|
||||||
:raises: NoValidHost if RPC topic for node could not be retrieved.
|
: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():
|
if not api_utils.allow_ramdisk_endpoints():
|
||||||
raise exception.NotFound()
|
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()
|
cdict = pecan.request.context.to_policy_values()
|
||||||
policy.authorize('baremetal:node:ipa_heartbeat', cdict, cdict)
|
policy.authorize('baremetal:node:ipa_heartbeat', cdict, cdict)
|
||||||
|
|
||||||
@ -178,6 +187,6 @@ class HeartbeatController(rest.RestController):
|
|||||||
e.code = http_client.BAD_REQUEST
|
e.code = http_client.BAD_REQUEST
|
||||||
raise
|
raise
|
||||||
|
|
||||||
pecan.request.rpcapi.heartbeat(pecan.request.context,
|
pecan.request.rpcapi.heartbeat(
|
||||||
rpc_node.uuid, callback_url,
|
pecan.request.context, rpc_node.uuid, callback_url,
|
||||||
topic=topic)
|
agent_version, topic=topic)
|
||||||
|
@ -602,6 +602,16 @@ def allow_node_rebuild_with_configdrive():
|
|||||||
versions.MINOR_35_REBUILD_CONFIG_DRIVE)
|
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):
|
def get_controller_reserved_names(cls):
|
||||||
"""Get reserved names for a given controller.
|
"""Get reserved names for a given controller.
|
||||||
|
|
||||||
|
@ -72,6 +72,7 @@ BASE_VERSION = 1
|
|||||||
# v1.33: Add node storage interface
|
# v1.33: Add node storage interface
|
||||||
# v1.34: Add physical network field to port.
|
# v1.34: Add physical network field to port.
|
||||||
# v1.35: Add ability to provide configdrive when rebuilding node.
|
# v1.35: Add ability to provide configdrive when rebuilding node.
|
||||||
|
# v1.36: Add Ironic Python Agent version support.
|
||||||
|
|
||||||
MINOR_0_JUNO = 0
|
MINOR_0_JUNO = 0
|
||||||
MINOR_1_INITIAL_VERSION = 1
|
MINOR_1_INITIAL_VERSION = 1
|
||||||
@ -109,6 +110,7 @@ MINOR_32_VOLUME = 32
|
|||||||
MINOR_33_STORAGE_INTERFACE = 33
|
MINOR_33_STORAGE_INTERFACE = 33
|
||||||
MINOR_34_PORT_PHYSICAL_NETWORK = 34
|
MINOR_34_PORT_PHYSICAL_NETWORK = 34
|
||||||
MINOR_35_REBUILD_CONFIG_DRIVE = 35
|
MINOR_35_REBUILD_CONFIG_DRIVE = 35
|
||||||
|
MINOR_36_AGENT_VERSION_HEARTBEAT = 36
|
||||||
|
|
||||||
# When adding another version, update:
|
# When adding another version, update:
|
||||||
# - MINOR_MAX_VERSION
|
# - MINOR_MAX_VERSION
|
||||||
@ -116,7 +118,7 @@ MINOR_35_REBUILD_CONFIG_DRIVE = 35
|
|||||||
# explanation of what changed in the new version
|
# explanation of what changed in the new version
|
||||||
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
|
# - 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
|
# String representations of the minor and maximum versions
|
||||||
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
||||||
|
@ -108,8 +108,8 @@ RELEASE_MAPPING = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'master': {
|
'master': {
|
||||||
'api': '1.35',
|
'api': '1.36',
|
||||||
'rpc': '1.41',
|
'rpc': '1.42',
|
||||||
'objects': {
|
'objects': {
|
||||||
'Node': ['1.21'],
|
'Node': ['1.21'],
|
||||||
'Conductor': ['1.2'],
|
'Conductor': ['1.2'],
|
||||||
|
@ -82,6 +82,11 @@ METRICS = metrics_utils.get_metrics_logger(__name__)
|
|||||||
|
|
||||||
SYNC_EXCLUDED_STATES = (states.DEPLOYWAIT, states.CLEANWAIT, states.ENROLL)
|
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):
|
class ConductorManager(base_manager.BaseConductorManager):
|
||||||
"""Ironic Conductor manager main class."""
|
"""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(rloo): This must be in sync with rpcapi.ConductorAPI's.
|
||||||
# NOTE(pas-ha): This also must be in sync with
|
# NOTE(pas-ha): This also must be in sync with
|
||||||
# ironic.common.release_mappings.RELEASE_MAPPING['master']
|
# ironic.common.release_mappings.RELEASE_MAPPING['master']
|
||||||
RPC_API_VERSION = '1.41'
|
RPC_API_VERSION = '1.42'
|
||||||
|
|
||||||
target = messaging.Target(version=RPC_API_VERSION)
|
target = messaging.Target(version=RPC_API_VERSION)
|
||||||
|
|
||||||
@ -2549,23 +2554,51 @@ class ConductorManager(base_manager.BaseConductorManager):
|
|||||||
|
|
||||||
@METRICS.timer('ConductorManager.heartbeat')
|
@METRICS.timer('ConductorManager.heartbeat')
|
||||||
@messaging.expected_exceptions(exception.NoFreeConductorWorker)
|
@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.
|
"""Process a heartbeat from the ramdisk.
|
||||||
|
|
||||||
:param context: request context.
|
:param context: request context.
|
||||||
:param node_id: node id or uuid.
|
: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.
|
:param callback_url: URL to reach back to the ramdisk.
|
||||||
:raises: NoFreeConductorWorker if there are no conductors to process
|
:raises: NoFreeConductorWorker if there are no conductors to process
|
||||||
this heartbeat request.
|
this heartbeat request.
|
||||||
"""
|
"""
|
||||||
LOG.debug('RPC heartbeat called for node %s', node_id)
|
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
|
# NOTE(dtantsur): we acquire a shared lock to begin with, drivers are
|
||||||
# free to promote it to an exclusive one.
|
# free to promote it to an exclusive one.
|
||||||
with task_manager.acquire(context, node_id, shared=True,
|
with task_manager.acquire(context, node_id, shared=True,
|
||||||
purpose='heartbeat') as task:
|
purpose='heartbeat') as task:
|
||||||
task.spawn_after(self._spawn_worker, task.driver.deploy.heartbeat,
|
task.spawn_after(
|
||||||
task, callback_url)
|
self._spawn_worker, heartbeat_with_deprecation,
|
||||||
|
task, callback_url, agent_version)
|
||||||
|
|
||||||
@METRICS.timer('ConductorManager.vif_list')
|
@METRICS.timer('ConductorManager.vif_list')
|
||||||
@messaging.expected_exceptions(exception.NetworkError,
|
@messaging.expected_exceptions(exception.NetworkError,
|
||||||
|
@ -90,13 +90,14 @@ class ConductorAPI(object):
|
|||||||
| 1.39 - Added timeout optional parameter to change_node_power_state
|
| 1.39 - Added timeout optional parameter to change_node_power_state
|
||||||
| 1.40 - Added inject_nmi
|
| 1.40 - Added inject_nmi
|
||||||
| 1.41 - Added create_port
|
| 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(rloo): This must be in sync with manager.ConductorManager's.
|
||||||
# NOTE(pas-ha): This also must be in sync with
|
# NOTE(pas-ha): This also must be in sync with
|
||||||
# ironic.common.release_mappings.RELEASE_MAPPING['master']
|
# ironic.common.release_mappings.RELEASE_MAPPING['master']
|
||||||
RPC_API_VERSION = '1.41'
|
RPC_API_VERSION = '1.42'
|
||||||
|
|
||||||
def __init__(self, topic=None):
|
def __init__(self, topic=None):
|
||||||
super(ConductorAPI, self).__init__()
|
super(ConductorAPI, self).__init__()
|
||||||
@ -744,17 +745,24 @@ class ConductorAPI(object):
|
|||||||
return cctxt.call(context, 'do_node_clean',
|
return cctxt.call(context, 'do_node_clean',
|
||||||
node_id=node_id, clean_steps=clean_steps)
|
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.
|
"""Process a node heartbeat.
|
||||||
|
|
||||||
:param context: request context.
|
:param context: request context.
|
||||||
:param node_id: node ID or UUID.
|
:param node_id: node ID or UUID.
|
||||||
:param callback_url: URL to reach back to the ramdisk.
|
:param callback_url: URL to reach back to the ramdisk.
|
||||||
:param topic: RPC topic. Defaults to self.topic.
|
: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,
|
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,
|
def object_class_action_versions(self, context, objname, objmethod,
|
||||||
object_versions, args, kwargs):
|
object_versions, args, kwargs):
|
||||||
|
@ -402,11 +402,12 @@ class DeployInterface(BaseInterface):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def heartbeat(self, task, callback_url):
|
def heartbeat(self, task, callback_url, agent_version):
|
||||||
"""Record a heartbeat for the node.
|
"""Record a heartbeat for the node.
|
||||||
|
|
||||||
:param task: a TaskManager instance containing the node to act on.
|
:param task: a TaskManager instance containing the node to act on.
|
||||||
:param callback_url: a URL to use to call to the ramdisk.
|
: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
|
:return: None
|
||||||
"""
|
"""
|
||||||
LOG.warning('Got heartbeat message from node %(node)s, but '
|
LOG.warning('Got heartbeat message from node %(node)s, but '
|
||||||
|
@ -272,11 +272,12 @@ class HeartbeatMixin(object):
|
|||||||
return (states.DEPLOYWAIT, states.CLEANWAIT)
|
return (states.DEPLOYWAIT, states.CLEANWAIT)
|
||||||
|
|
||||||
@METRICS.timer('HeartbeatMixin.heartbeat')
|
@METRICS.timer('HeartbeatMixin.heartbeat')
|
||||||
def heartbeat(self, task, callback_url):
|
def heartbeat(self, task, callback_url, agent_version):
|
||||||
"""Process a heartbeat.
|
"""Process a heartbeat.
|
||||||
|
|
||||||
:param task: task to work with.
|
:param task: task to work with.
|
||||||
:param callback_url: agent HTTP API URL.
|
: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
|
# NOTE(pas-ha) immediately skip the rest if nothing to do
|
||||||
if task.node.provision_state not in self.heartbeat_allowed_states:
|
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 = node.driver_internal_info
|
||||||
driver_internal_info['agent_url'] = callback_url
|
driver_internal_info['agent_url'] = callback_url
|
||||||
|
driver_internal_info['agent_version'] = agent_version
|
||||||
node.driver_internal_info = driver_internal_info
|
node.driver_internal_info = driver_internal_info
|
||||||
node.save()
|
node.save()
|
||||||
|
|
||||||
|
@ -185,5 +185,30 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual(http_client.ACCEPTED, response.status_int)
|
self.assertEqual(http_client.ACCEPTED, response.status_int)
|
||||||
self.assertEqual(b'', response.body)
|
self.assertEqual(b'', response.body)
|
||||||
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
|
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
|
||||||
node.uuid, 'url',
|
node.uuid, 'url', None,
|
||||||
topic='test-topic')
|
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)
|
||||||
|
@ -6159,8 +6159,9 @@ class DoNodeAdoptionTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
|
|||||||
self.assertEqual(states.NOSTATE, node.target_provision_state)
|
self.assertEqual(states.NOSTATE, node.target_provision_state)
|
||||||
self.assertIsNone(node.last_error)
|
self.assertIsNone(node.last_error)
|
||||||
|
|
||||||
|
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat')
|
||||||
@mock.patch('ironic.conductor.manager.ConductorManager._spawn_worker')
|
@mock.patch('ironic.conductor.manager.ConductorManager._spawn_worker')
|
||||||
def test_heartbeat(self, mock_spawn):
|
def test_heartbeat(self, mock_spawn, mock_heartbeat):
|
||||||
"""Test heartbeating."""
|
"""Test heartbeating."""
|
||||||
node = obj_utils.create_test_node(
|
node = obj_utils.create_test_node(
|
||||||
self.context, driver='fake',
|
self.context, driver='fake',
|
||||||
@ -6168,9 +6169,86 @@ class DoNodeAdoptionTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
|
|||||||
target_provision_state=states.ACTIVE)
|
target_provision_state=states.ACTIVE)
|
||||||
|
|
||||||
self._start_service()
|
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')
|
self.service.heartbeat(self.context, node.uuid, 'http://callback')
|
||||||
mock_spawn.assert_called_with(self.driver.deploy.heartbeat,
|
mock_heartbeat.assert_called_with(mock.ANY, 'http://callback', '3.0.0')
|
||||||
mock.ANY, 'http://callback')
|
|
||||||
|
@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
|
@mgr_utils.mock_record_keepalive
|
||||||
|
@ -443,7 +443,8 @@ class RPCAPITestCase(db_base.DbTestCase):
|
|||||||
'call',
|
'call',
|
||||||
node_id='fake-node',
|
node_id='fake-node',
|
||||||
callback_url='http://ramdisk.url:port',
|
callback_url='http://ramdisk.url:port',
|
||||||
version='1.34')
|
agent_version=None,
|
||||||
|
version='1.42')
|
||||||
|
|
||||||
def test_destroy_volume_connector(self):
|
def test_destroy_volume_connector(self):
|
||||||
fake_volume_connector = db_utils.get_test_volume_connector()
|
fake_volume_connector = db_utils.get_test_volume_connector()
|
||||||
|
@ -81,11 +81,14 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
|
|||||||
agent_url = 'url-%s' % state
|
agent_url = 'url-%s' % state
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
shared=True) as task:
|
shared=True) as task:
|
||||||
self.deploy.heartbeat(task, agent_url)
|
self.deploy.heartbeat(task, agent_url, '3.2.0')
|
||||||
self.assertFalse(task.shared)
|
self.assertFalse(task.shared)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
agent_url,
|
agent_url,
|
||||||
task.node.driver_internal_info['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, ncrc_mock.call_count)
|
||||||
self.assertEqual(0, rti_mock.call_count)
|
self.assertEqual(0, rti_mock.call_count)
|
||||||
self.assertEqual(0, cd_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,
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
shared=True) as task:
|
shared=True) as task:
|
||||||
self.node.provision_state = state
|
self.node.provision_state = state
|
||||||
self.deploy.heartbeat(task, 'url')
|
self.deploy.heartbeat(task, 'url', '1.0.0')
|
||||||
self.assertTrue(task.shared)
|
self.assertTrue(task.shared)
|
||||||
self.assertEqual(0, ncrc_mock.call_count)
|
self.assertEqual(0, ncrc_mock.call_count)
|
||||||
self.assertEqual(0, rti_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:
|
self.context, self.node['uuid'], shared=False) as task:
|
||||||
task.node.provision_state = states.DEPLOYWAIT
|
task.node.provision_state = states.DEPLOYWAIT
|
||||||
task.node.target_provision_state = states.ACTIVE
|
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(
|
failed_mock.assert_called_once_with(
|
||||||
task, mock.ANY, collect_logs=True)
|
task, mock.ANY, collect_logs=True)
|
||||||
log_mock.assert_called_once_with(
|
log_mock.assert_called_once_with(
|
||||||
@ -155,7 +158,7 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
|
|||||||
task.node.provision_state = states.DEPLOYWAIT
|
task.node.provision_state = states.DEPLOYWAIT
|
||||||
task.node.target_provision_state = states.ACTIVE
|
task.node.target_provision_state = states.ACTIVE
|
||||||
done_mock.side_effect = driver_failure
|
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
|
# task.node.provision_state being set to DEPLOYFAIL
|
||||||
# within the driver_failue, hearbeat should not call
|
# within the driver_failue, hearbeat should not call
|
||||||
# deploy_utils.set_failed_state anymore
|
# deploy_utils.set_failed_state anymore
|
||||||
@ -178,7 +181,7 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
|
|||||||
self.node.save()
|
self.node.save()
|
||||||
with task_manager.acquire(
|
with task_manager.acquire(
|
||||||
self.context, self.node.uuid, shared=False) as task:
|
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_touch.assert_called_once_with(mock.ANY)
|
||||||
mock_refresh.assert_called_once_with(mock.ANY, task)
|
mock_refresh.assert_called_once_with(mock.ANY, task)
|
||||||
@ -206,7 +209,7 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
|
|||||||
failed_mock.side_effect = Exception()
|
failed_mock.side_effect = Exception()
|
||||||
with task_manager.acquire(
|
with task_manager.acquire(
|
||||||
self.context, self.node.uuid, shared=False) as task:
|
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_touch.assert_called_once_with(mock.ANY)
|
||||||
mock_handler.assert_called_once_with(task, mock.ANY)
|
mock_handler.assert_called_once_with(task, mock.ANY)
|
||||||
@ -234,7 +237,7 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
|
|||||||
self.node.save()
|
self.node.save()
|
||||||
with task_manager.acquire(
|
with task_manager.acquire(
|
||||||
self.context, self.node.uuid, shared=False) as task:
|
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_touch.assert_called_once_with(mock.ANY)
|
||||||
mock_continue.assert_called_once_with(mock.ANY, task)
|
mock_continue.assert_called_once_with(mock.ANY, task)
|
||||||
@ -257,7 +260,7 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
|
|||||||
self.node.save()
|
self.node.save()
|
||||||
with task_manager.acquire(
|
with task_manager.acquire(
|
||||||
self.context, self.node.uuid, shared=False) as task:
|
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_continue.assert_called_once_with(mock.ANY, task)
|
||||||
mock_handler.assert_called_once_with(task, mock.ANY)
|
mock_handler.assert_called_once_with(task, mock.ANY)
|
||||||
@ -274,9 +277,11 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
|
|||||||
self.node.save()
|
self.node.save()
|
||||||
with task_manager.acquire(
|
with task_manager.acquire(
|
||||||
self.context, self.node.uuid, shared=False) as task:
|
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',
|
self.assertEqual('http://127.0.0.1:8080',
|
||||||
task.node.driver_internal_info['agent_url'])
|
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)
|
mock_touch.assert_called_once_with(mock.ANY)
|
||||||
|
|
||||||
|
|
||||||
|
@ -406,7 +406,7 @@ class TestDeployInterface(base.TestCase):
|
|||||||
deploy = fake.FakeDeploy()
|
deploy = fake.FakeDeploy()
|
||||||
deploy.heartbeat(mock.Mock(node=mock.Mock(uuid='uuid',
|
deploy.heartbeat(mock.Mock(node=mock.Mock(uuid='uuid',
|
||||||
driver='driver')),
|
driver='driver')),
|
||||||
'url')
|
'url', '3.2.0')
|
||||||
self.assertTrue(mock_log.called)
|
self.assertTrue(mock_log.called)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user