Merge "Include IPA Version during heartbeat"

This commit is contained in:
Zuul 2017-12-13 17:18:25 +00:00 committed by Gerrit Code Review
commit e55d68f11e
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.