From 903ec3ff125663ad749748dd438860ef3489e711 Mon Sep 17 00:00:00 2001 From: Sam Betts Date: Fri, 2 Sep 2016 11:37:08 +0100 Subject: [PATCH] Include IPA Version during heartbeat In order for Ironic to know what parameters can be sent to IPA commands, Ironic needs to know which version of IPA it is talking to. This patch adds a new node heartbeat parameter agent_version which will carry the IPA version information to Ironic. Change-Id: I27e3311accf3a113a48a73df372ed46ff50c7e22 Partial-Bug: #1602265 Depends-On: I400adba5d908b657751a83971811e8586f46c673 --- ironic_python_agent/ironic_api_client.py | 44 ++++++++-- .../tests/unit/test_ironic_api_client.py | 84 ++++++++++++++++++- ...nt_version_to_ironic-6fa8670ae0e7eb38.yaml | 10 +++ 3 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/start_passing_agent_version_to_ironic-6fa8670ae0e7eb38.yaml diff --git a/ironic_python_agent/ironic_api_client.py b/ironic_python_agent/ironic_api_client.py index 35d2deebf..207798712 100644 --- a/ironic_python_agent/ironic_api_client.py +++ b/ironic_python_agent/ironic_api_client.py @@ -23,17 +23,21 @@ from ironic_python_agent import encoding from ironic_python_agent import errors from ironic_python_agent import netutils from ironic_python_agent import utils +from ironic_python_agent import version CONF = cfg.CONF LOG = log.getLogger(__name__) +MIN_IRONIC_VERSION = (1, 22) +AGENT_VERSION_IRONIC_VERSION = (1, 36) + class APIClient(object): api_version = 'v1' lookup_api = '/%s/lookup' % api_version heartbeat_api = '/%s/heartbeat/{uuid}' % api_version - ramdisk_api_headers = {'X-OpenStack-Ironic-API-Version': '1.22'} + _ironic_api_version = None def __init__(self, api_url): self.api_url = api_url.rstrip('/') @@ -69,12 +73,39 @@ class APIClient(object): cert=cert, **kwargs) + def _get_ironic_api_version_header(self, version=MIN_IRONIC_VERSION): + version_str = "%d.%d" % version + return {'X-OpenStack-Ironic-API-Version': version_str} + + def _get_ironic_api_version(self): + if not self._ironic_api_version: + try: + response = self._request('GET', '/') + data = jsonutils.loads(response.content) + version = data['default_version']['version'].split('.') + self._ironic_api_version = (int(version[0]), int(version[1])) + except Exception: + LOG.exception("An error occurred while attempting to discover " + "the available Ironic API versions, falling " + "back to using version %s", + ".".join(map(str, MIN_IRONIC_VERSION))) + return MIN_IRONIC_VERSION + return self._ironic_api_version + def heartbeat(self, uuid, advertise_address): path = self.heartbeat_api.format(uuid=uuid) + data = {'callback_url': self._get_agent_url(advertise_address)} + + if self._get_ironic_api_version() >= AGENT_VERSION_IRONIC_VERSION: + data['agent_version'] = version.version_info.release_string() + headers = self._get_ironic_api_version_header( + AGENT_VERSION_IRONIC_VERSION) + else: + headers = self._get_ironic_api_version_header() + try: - response = self._request('POST', path, data=data, - headers=self.ramdisk_api_headers) + response = self._request('POST', path, data=data, headers=headers) except Exception as e: raise errors.HeartbeatError(str(e)) @@ -113,9 +144,10 @@ class APIClient(object): params['node_uuid'] = node_uuid try: - response = self._request('GET', self.lookup_api, - headers=self.ramdisk_api_headers, - params=params) + response = self._request( + 'GET', self.lookup_api, + headers=self._get_ironic_api_version_header(), + params=params) except Exception: LOG.exception('Lookup failed') return False diff --git a/ironic_python_agent/tests/unit/test_ironic_api_client.py b/ironic_python_agent/tests/unit/test_ironic_api_client.py index 6d7c2a47d..bcf36b337 100644 --- a/ironic_python_agent/tests/unit/test_ironic_api_client.py +++ b/ironic_python_agent/tests/unit/test_ironic_api_client.py @@ -20,6 +20,7 @@ from ironic_python_agent import errors from ironic_python_agent import hardware from ironic_python_agent import ironic_api_client from ironic_python_agent.tests.unit import base +from ironic_python_agent import version API_URL = 'http://agent-api.ironic.example.org/' @@ -28,14 +29,20 @@ class FakeResponse(object): def __init__(self, content=None, status_code=200, headers=None): content = content or {} self.content = jsonutils.dumps(content) + self._json = content self.status_code = status_code self.headers = headers or {} + def json(self): + return self._json + class TestBaseIronicPythonAgent(base.IronicAgentTest): def setUp(self): super(TestBaseIronicPythonAgent, self).setUp() self.api_client = ironic_api_client.APIClient(API_URL) + self.api_client._ironic_api_version = ( + ironic_api_client.MIN_IRONIC_VERSION) self.hardware_info = { 'interfaces': [ hardware.NetworkInterface( @@ -57,11 +64,54 @@ class TestBaseIronicPythonAgent(base.IronicAgentTest): physical_mb='8675'), } + def test__get_ironic_api_version_already_set(self): + self.api_client.session.request = mock.create_autospec( + self.api_client.session.request, + return_value=None) + + self.assertFalse(self.api_client.session.request.called) + self.assertEqual(ironic_api_client.MIN_IRONIC_VERSION, + self.api_client._get_ironic_api_version()) + + def test__get_ironic_api_version_error(self): + self.api_client._ironic_api_version = None + self.api_client.session.request = mock.create_autospec( + self.api_client.session.request, + return_value=None) + self.api_client.session.request.side_effect = Exception("Boom") + + self.assertEqual(ironic_api_client.MIN_IRONIC_VERSION, + self.api_client._get_ironic_api_version()) + + def test__get_ironic_api_version_fresh(self): + self.api_client._ironic_api_version = None + response = FakeResponse(status_code=200, content={ + "default_version": { + "id": "v1", + "links": [ + { + "href": "http://127.0.0.1:6385/v1/", + "rel": "self" + } + ], + "min_version": "1.1", + "status": "CURRENT", + "version": "1.31" + } + }) + self.api_client.session.request = mock.Mock() + self.api_client.session.request.return_value = response + + self.assertEqual((1, 31), self.api_client._get_ironic_api_version()) + self.assertEqual((1, 31), self.api_client._ironic_api_version) + def test_successful_heartbeat(self): response = FakeResponse(status_code=202) self.api_client.session.request = mock.Mock() self.api_client.session.request.return_value = response + self.api_client._ironic_api_version = ( + ironic_api_client.AGENT_VERSION_IRONIC_VERSION) self.api_client.heartbeat( uuid='deadbeef-dabb-ad00-b105-f00d00bab10c', @@ -73,13 +123,18 @@ class TestBaseIronicPythonAgent(base.IronicAgentTest): data = self.api_client.session.request.call_args[1]['data'] self.assertEqual('POST', request_args[0]) self.assertEqual(API_URL + heartbeat_path, request_args[1]) - self.assertEqual('{"callback_url": "http://192.0.2.1:9999"}', data) + expected_data = { + 'callback_url': 'http://192.0.2.1:9999', + 'agent_version': version.version_info.release_string()} + self.assertEqual(jsonutils.dumps(expected_data), data) def test_successful_heartbeat_ip6(self): response = FakeResponse(status_code=202) self.api_client.session.request = mock.Mock() self.api_client.session.request.return_value = response + self.api_client._ironic_api_version = ( + ironic_api_client.AGENT_VERSION_IRONIC_VERSION) self.api_client.heartbeat( uuid='deadbeef-dabb-ad00-b105-f00d00bab10c', @@ -91,8 +146,31 @@ class TestBaseIronicPythonAgent(base.IronicAgentTest): data = self.api_client.session.request.call_args[1]['data'] self.assertEqual('POST', request_args[0]) self.assertEqual(API_URL + heartbeat_path, request_args[1]) - self.assertEqual('{"callback_url": "http://[fc00:1111::4]:9999"}', - data) + expected_data = { + 'callback_url': 'http://[fc00:1111::4]:9999', + 'agent_version': version.version_info.release_string()} + self.assertEqual(jsonutils.dumps(expected_data), data) + + def test_heartbeat_agent_version_unsupported(self): + response = FakeResponse(status_code=202) + + self.api_client.session.request = mock.Mock() + self.api_client.session.request.return_value = response + self.api_client._ironic_api_version = (1, 31) + + self.api_client.heartbeat( + uuid='deadbeef-dabb-ad00-b105-f00d00bab10c', + advertise_address=('fc00:1111::4', '9999') + ) + + heartbeat_path = 'v1/heartbeat/deadbeef-dabb-ad00-b105-f00d00bab10c' + request_args = self.api_client.session.request.call_args[0] + data = self.api_client.session.request.call_args[1]['data'] + self.assertEqual('POST', request_args[0]) + self.assertEqual(API_URL + heartbeat_path, request_args[1]) + expected_data = { + 'callback_url': 'http://[fc00:1111::4]:9999'} + self.assertEqual(jsonutils.dumps(expected_data), data) def test_heartbeat_requests_exception(self): self.api_client.session.request = mock.Mock() diff --git a/releasenotes/notes/start_passing_agent_version_to_ironic-6fa8670ae0e7eb38.yaml b/releasenotes/notes/start_passing_agent_version_to_ironic-6fa8670ae0e7eb38.yaml new file mode 100644 index 000000000..1965127a0 --- /dev/null +++ b/releasenotes/notes/start_passing_agent_version_to_ironic-6fa8670ae0e7eb38.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Now passes an ``agent_version`` field to the Bare Metal service as part of + the heartbeat request if the Bare Metal API version is 1.36 or higher. + This provides the Bare Metal service with the information required to + determine what agent features are available, so that the Bare Metal service + can be upgraded to a new version before the agent is upgraded, whilst + ensuring the Bare Metal service only requests the agent features that are + available to it.