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:
parent
831576c906
commit
903ec3ff12
ironic_python_agent
releasenotes/notes
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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.
|
Loading…
x
Reference in New Issue
Block a user