Files
ironic-python-agent/ironic_python_agent/tests/unit/test_ironic_api_client.py
Jay Faulkner 80575566b1 Allow manual setting of Ironic API Version
Typically, the Ironic API client in IPA will autodetect the API version
based on the output of a GET of the root of the API. If for some reason
this API endpoint is restricted, or the operator wishes to limit the
Ironic API version IPA uses, they can now set CONF.ironic_api_version to
avoid autodetection and force a version.

Change-Id: Ib96a1057792f45f2e4554671e32c436140463ee8
2020-10-23 15:38:42 +00:00

503 lines
20 KiB
Python

# Copyright 2013 Rackspace, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from unittest import mock
from oslo_config import cfg
from oslo_serialization import jsonutils
import requests
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/'
CONF = cfg.CONF
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(
'eth0', '00:0c:29:8c:11:b1', vendor='0x15b3',
product='0x1014'),
hardware.NetworkInterface(
'eth1', '00:0c:29:8c:11:b2',
lldp=[(1, '04885a92ec5459'),
(2, '0545746865726e6574312f3138')],
vendor='0x15b3', product='0x1014'),
],
'cpu': hardware.CPU('Awesome Jay CPU x10 9001', '9001', '10',
'ARMv9'),
'disks': [
hardware.BlockDevice('/dev/sdj', 'small', '9001', False),
hardware.BlockDevice('/dev/hdj', 'big', '9002', False),
],
'memory': hardware.Memory(total='8675309',
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_set_via_conf(self):
self.api_client._ironic_api_version = None
CONF.set_override('ironic_api_version', "1.47")
self.api_client.session.request = mock.create_autospec(
self.api_client.session.request,
return_value=None)
self.assertEqual((1, 47), self.api_client._get_ironic_api_version())
self.assertFalse(self.api_client.session.request.called)
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)
req_id = "req-14c99bd0-1bb5-4d74-972b-e282a50ce441"
self.config(global_request_id=req_id)
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',
advertise_address=('192.0.2.1', '9999')
)
heartbeat_path = 'v1/heartbeat/deadbeef-dabb-ad00-b105-f00d00bab10c'
request_args = self.api_client.session.request.call_args[0]
request_kwargs = self.api_client.session.request.call_args[1]
data = request_kwargs["data"]
self.assertEqual('POST', request_args[0])
request_headers = request_kwargs["headers"]
self.assertEqual(
req_id, request_headers["X-OpenStack-Request-ID"])
self.assertEqual(API_URL + heartbeat_path, request_args[1])
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',
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',
'agent_version': version.version_info.release_string()}
self.assertEqual(jsonutils.dumps(expected_data), data)
def test_successful_heartbeat_with_token(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_TOKEN_IRONIC_VERSION)
self.api_client.agent_token = 'magical'
self.api_client.heartbeat(
uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
advertise_address=('192.0.2.1', '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://192.0.2.1:9999',
'agent_token': 'magical',
'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_successful_heartbeat_with_verify_ca(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_VERIFY_CA_IRONIC_VERSION)
self.api_client.agent_token = 'magical'
self.api_client.heartbeat(
uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
advertise_address=('192.0.2.1', '9999'),
advertise_protocol='https',
generated_cert='I am a cert',
)
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': 'https://192.0.2.1:9999',
'agent_token': 'magical',
'agent_version': version.version_info.release_string(),
'agent_verify_ca': 'I am a cert'}
self.assertEqual(jsonutils.dumps(expected_data), data)
headers = self.api_client.session.request.call_args[1]['headers']
self.assertEqual(
'%d.%d' % ironic_api_client.AGENT_VERIFY_CA_IRONIC_VERSION,
headers['X-OpenStack-Ironic-API-Version'])
def test_heartbeat_requests_exception(self):
self.api_client.session.request = mock.Mock()
self.api_client.session.request.side_effect = Exception('api is down!')
self.assertRaises(errors.HeartbeatError,
self.api_client.heartbeat,
uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
advertise_address=('192.0.2.1', '9999'))
def test_heartbeat_invalid_status_code(self):
response = FakeResponse(status_code=404)
self.api_client.session.request = mock.Mock()
self.api_client.session.request.return_value = response
self.assertRaises(errors.HeartbeatError,
self.api_client.heartbeat,
uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
advertise_address=('192.0.2.1', '9999'))
def test_heartbeat_409_status_code(self):
response = FakeResponse(status_code=409)
self.api_client.session.request = mock.Mock()
self.api_client.session.request.return_value = response
self.assertRaises(errors.HeartbeatConflictError,
self.api_client.heartbeat,
uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
advertise_address=('192.0.2.1', '9999'))
def test_heartbeat_requests_connection_error(self):
self.api_client.session.request = mock.Mock()
self.api_client.session.request.side_effect = \
requests.exceptions.ConnectionError
self.assertRaisesRegex(errors.HeartbeatConnectionError,
'transitory network failure or blocking port',
self.api_client.heartbeat,
uuid='meow',
advertise_address=('192.0.2.1', '9999'))
@mock.patch('time.sleep', autospec=True)
@mock.patch('ironic_python_agent.ironic_api_client.APIClient._do_lookup',
autospec=True)
def test_lookup_node(self, lookup_mock, sleep_mock):
content = {
'node': {
'uuid': 'deadbeef-dabb-ad00-b105-f00d00bab10c'
},
'config': {
'heartbeat_timeout': 300
}
}
lookup_mock.return_value = content
returned_content = self.api_client.lookup_node(
hardware_info=self.hardware_info,
timeout=300,
starting_interval=1)
self.assertEqual(content, returned_content)
@mock.patch('time.sleep', autospec=True)
@mock.patch('ironic_python_agent.ironic_api_client.APIClient._do_lookup',
autospec=True)
def test_lookup_node_retries(self, lookup_mock, sleep_mock):
content = {
'node': {
'uuid': 'deadbeef-dabb-ad00-b105-f00d00bab10c'
},
'config': {
'heartbeat_timeout': 300
}
}
lookup_mock.side_effect = [False, content]
returned_content = self.api_client.lookup_node(
hardware_info=self.hardware_info,
timeout=300,
starting_interval=0.001)
self.assertEqual(content, returned_content)
@mock.patch('time.sleep', autospec=True)
@mock.patch('ironic_python_agent.ironic_api_client.APIClient._do_lookup',
autospec=True)
def test_lookup_timeout(self, lookup_mock, sleep_mock):
lookup_mock.return_value = False
self.assertRaises(errors.LookupNodeError,
self.api_client.lookup_node,
hardware_info=self.hardware_info,
timeout=0.1,
starting_interval=0.001)
def test_do_lookup(self):
content = {
'node': {
'uuid': 'deadbeef-dabb-ad00-b105-f00d00bab10c'
},
'config': {
'heartbeat_timeout': 300
}
}
response = FakeResponse(status_code=200, content=content)
self.api_client.session.request = mock.Mock()
self.api_client.session.request.return_value = response
self.assertEqual(content, self.api_client._do_lookup(
hardware_info=self.hardware_info,
node_uuid=None))
url = '{api_url}v1/lookup'.format(api_url=API_URL)
request_args = self.api_client.session.request.call_args[0]
self.assertEqual('GET', request_args[0])
self.assertEqual(url, request_args[1])
params = self.api_client.session.request.call_args[1]['params']
self.assertEqual({'addresses': '00:0c:29:8c:11:b1,00:0c:29:8c:11:b2'},
params)
def test_do_lookup_with_uuid(self):
content = {
'node': {
'uuid': 'deadbeef-dabb-ad00-b105-f00d00bab10c'
},
'config': {
'heartbeat_timeout': 300
}
}
response = FakeResponse(status_code=200, content=content)
self.api_client.session.request = mock.Mock()
self.api_client.session.request.return_value = response
self.assertEqual(content, self.api_client._do_lookup(
hardware_info=self.hardware_info,
node_uuid='someuuid'))
url = '{api_url}v1/lookup'.format(api_url=API_URL)
request_args = self.api_client.session.request.call_args[0]
self.assertEqual('GET', request_args[0])
self.assertEqual(url, request_args[1])
params = self.api_client.session.request.call_args[1]['params']
self.assertEqual({'addresses': '00:0c:29:8c:11:b1,00:0c:29:8c:11:b2',
'node_uuid': 'someuuid'},
params)
@mock.patch.object(ironic_api_client, 'LOG', autospec=True)
def test_do_lookup_transient_exceptions(self, mock_log):
exc_list = [requests.exceptions.ConnectionError,
requests.exceptions.ReadTimeout,
requests.exceptions.HTTPError,
requests.exceptions.Timeout,
requests.exceptions.ConnectTimeout]
self.api_client.session.request = mock.Mock()
for exc in exc_list:
self.api_client.session.request.reset_mock()
mock_log.reset_mock()
self.api_client.session.request.side_effect = exc
error = self.api_client._do_lookup(self.hardware_info,
node_uuid=None)
self.assertFalse(error)
mock_log.error.assert_has_calls([])
self.assertEqual(1, mock_log.warning.call_count)
@mock.patch.object(ironic_api_client, 'LOG', autospec=True)
def test_do_lookup_unknown_exception(self, mock_log):
self.api_client.session.request = mock.Mock()
self.api_client.session.request.side_effect = \
requests.exceptions.RequestException('meow')
self.assertFalse(
self.api_client._do_lookup(self.hardware_info,
node_uuid=None))
self.assertEqual(1, mock_log.exception.call_count)
@mock.patch.object(ironic_api_client, 'LOG', autospec=True)
def test_do_lookup_unknown_exception_fallback(self, mock_log):
mock_log.exception.side_effect = TypeError
self.api_client.session.request = mock.Mock()
self.api_client.session.request.side_effect = \
requests.exceptions.RequestException('meow')
self.assertRaises(errors.LookupNodeError,
self.api_client._do_lookup,
self.hardware_info,
node_uuid=None)
self.assertEqual(1, mock_log.exception.call_count)
self.assertEqual(2, mock_log.error.call_count)
def test_do_lookup_bad_response_code(self):
response = FakeResponse(status_code=400, content={
'node': {
'uuid': 'deadbeef-dabb-ad00-b105-f00d00bab10c'
}
})
self.api_client.session.request = mock.Mock()
self.api_client.session.request.return_value = response
error = self.api_client._do_lookup(self.hardware_info,
node_uuid=None)
self.assertFalse(error)
def test_do_lookup_bad_response_data(self):
response = FakeResponse(status_code=200, content={
'heartbeat_timeout': 300
})
self.api_client.session.request = mock.Mock()
self.api_client.session.request.return_value = response
error = self.api_client._do_lookup(self.hardware_info,
node_uuid=None)
self.assertFalse(error)
def test_do_lookup_no_heartbeat_timeout(self):
response = FakeResponse(status_code=200, content={
'node': {
'uuid': 'deadbeef-dabb-ad00-b105-f00d00bab10c'
}
})
self.api_client.session.request = mock.Mock()
self.api_client.session.request.return_value = response
error = self.api_client._do_lookup(self.hardware_info,
node_uuid=None)
self.assertFalse(error)
def test_do_lookup_bad_response_body(self):
response = FakeResponse(status_code=200, content={
'node_node': 'also_not_node'
})
self.api_client.session.request = mock.Mock()
self.api_client.session.request.return_value = response
error = self.api_client._do_lookup(self.hardware_info,
node_uuid=None)
self.assertFalse(error)
def test_get_agent_url_ipv4(self):
url = self.api_client._get_agent_url(('1.2.3.4', '9999'))
self.assertEqual('http://1.2.3.4:9999', url)
def test_get_agent_url_ipv6(self):
url = self.api_client._get_agent_url(('1:2::3:4', '9999'))
self.assertEqual('http://[1:2::3:4]:9999', url)
def test_get_agent_url_protocol(self):
url = self.api_client._get_agent_url(('1:2::3:4', '9999'), 'https')
self.assertEqual('https://[1:2::3:4]:9999', url)