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
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 errors
from ironic_python_agent import netutils from ironic_python_agent import netutils
from ironic_python_agent import utils from ironic_python_agent import utils
from ironic_python_agent import version
CONF = cfg.CONF CONF = cfg.CONF
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
MIN_IRONIC_VERSION = (1, 22)
AGENT_VERSION_IRONIC_VERSION = (1, 36)
class APIClient(object): class APIClient(object):
api_version = 'v1' api_version = 'v1'
lookup_api = '/%s/lookup' % api_version lookup_api = '/%s/lookup' % api_version
heartbeat_api = '/%s/heartbeat/{uuid}' % 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): def __init__(self, api_url):
self.api_url = api_url.rstrip('/') self.api_url = api_url.rstrip('/')
@@ -69,12 +73,39 @@ class APIClient(object):
cert=cert, cert=cert,
**kwargs) **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): def heartbeat(self, uuid, advertise_address):
path = self.heartbeat_api.format(uuid=uuid) path = self.heartbeat_api.format(uuid=uuid)
data = {'callback_url': self._get_agent_url(advertise_address)} 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: try:
response = self._request('POST', path, data=data, response = self._request('POST', path, data=data, headers=headers)
headers=self.ramdisk_api_headers)
except Exception as e: except Exception as e:
raise errors.HeartbeatError(str(e)) raise errors.HeartbeatError(str(e))
@@ -113,9 +144,10 @@ class APIClient(object):
params['node_uuid'] = node_uuid params['node_uuid'] = node_uuid
try: try:
response = self._request('GET', self.lookup_api, response = self._request(
headers=self.ramdisk_api_headers, 'GET', self.lookup_api,
params=params) headers=self._get_ironic_api_version_header(),
params=params)
except Exception: except Exception:
LOG.exception('Lookup failed') LOG.exception('Lookup failed')
return False 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 hardware
from ironic_python_agent import ironic_api_client from ironic_python_agent import ironic_api_client
from ironic_python_agent.tests.unit import base from ironic_python_agent.tests.unit import base
from ironic_python_agent import version
API_URL = 'http://agent-api.ironic.example.org/' 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): def __init__(self, content=None, status_code=200, headers=None):
content = content or {} content = content or {}
self.content = jsonutils.dumps(content) self.content = jsonutils.dumps(content)
self._json = content
self.status_code = status_code self.status_code = status_code
self.headers = headers or {} self.headers = headers or {}
def json(self):
return self._json
class TestBaseIronicPythonAgent(base.IronicAgentTest): class TestBaseIronicPythonAgent(base.IronicAgentTest):
def setUp(self): def setUp(self):
super(TestBaseIronicPythonAgent, self).setUp() super(TestBaseIronicPythonAgent, self).setUp()
self.api_client = ironic_api_client.APIClient(API_URL) self.api_client = ironic_api_client.APIClient(API_URL)
self.api_client._ironic_api_version = (
ironic_api_client.MIN_IRONIC_VERSION)
self.hardware_info = { self.hardware_info = {
'interfaces': [ 'interfaces': [
hardware.NetworkInterface( hardware.NetworkInterface(
@@ -57,11 +64,54 @@ class TestBaseIronicPythonAgent(base.IronicAgentTest):
physical_mb='8675'), 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): def test_successful_heartbeat(self):
response = FakeResponse(status_code=202) response = FakeResponse(status_code=202)
self.api_client.session.request = mock.Mock() self.api_client.session.request = mock.Mock()
self.api_client.session.request.return_value = response 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( self.api_client.heartbeat(
uuid='deadbeef-dabb-ad00-b105-f00d00bab10c', uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
@@ -73,13 +123,18 @@ class TestBaseIronicPythonAgent(base.IronicAgentTest):
data = self.api_client.session.request.call_args[1]['data'] data = self.api_client.session.request.call_args[1]['data']
self.assertEqual('POST', request_args[0]) self.assertEqual('POST', request_args[0])
self.assertEqual(API_URL + heartbeat_path, request_args[1]) 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): def test_successful_heartbeat_ip6(self):
response = FakeResponse(status_code=202) response = FakeResponse(status_code=202)
self.api_client.session.request = mock.Mock() self.api_client.session.request = mock.Mock()
self.api_client.session.request.return_value = response 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( self.api_client.heartbeat(
uuid='deadbeef-dabb-ad00-b105-f00d00bab10c', uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
@@ -91,8 +146,31 @@ class TestBaseIronicPythonAgent(base.IronicAgentTest):
data = self.api_client.session.request.call_args[1]['data'] data = self.api_client.session.request.call_args[1]['data']
self.assertEqual('POST', request_args[0]) self.assertEqual('POST', request_args[0])
self.assertEqual(API_URL + heartbeat_path, request_args[1]) self.assertEqual(API_URL + heartbeat_path, request_args[1])
self.assertEqual('{"callback_url": "http://[fc00:1111::4]:9999"}', expected_data = {
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): def test_heartbeat_requests_exception(self):
self.api_client.session.request = mock.Mock() 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.