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
This commit is contained in:
Sam Betts 2016-09-02 11:37:08 +01:00
parent 831576c906
commit 903ec3ff12
3 changed files with 129 additions and 9 deletions

View File

@ -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

View File

@ -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()

View File

@ -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.