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 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.
 | 
			
		||||
		Reference in New Issue
	
	Block a user