Merge "Include IPA Version during heartbeat"
This commit is contained in:
@@ -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
|
||||||
|
@@ -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()
|
||||||
|
@@ -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.
|
Reference in New Issue
Block a user