Browse Source

Merge "Accept and use a TLS certificate from the agent"

changes/30/705030/35
Zuul 2 weeks ago
committed by Gerrit Code Review
parent
commit
3297e98eab
18 changed files with 245 additions and 27 deletions
  1. +5
    -0
      doc/source/contributor/webapi-version-history.rst
  2. +9
    -3
      ironic/api/controllers/v1/ramdisk.py
  3. +5
    -0
      ironic/api/controllers/v1/utils.py
  4. +3
    -1
      ironic/api/controllers/v1/versions.py
  5. +2
    -2
      ironic/common/release_mappings.py
  6. +9
    -4
      ironic/conductor/manager.py
  7. +7
    -2
      ironic/conductor/rpcapi.py
  8. +47
    -0
      ironic/conductor/utils.py
  9. +4
    -0
      ironic/conf/agent.py
  10. +3
    -1
      ironic/drivers/base.py
  11. +5
    -1
      ironic/drivers/modules/agent_base.py
  12. +2
    -1
      ironic/drivers/modules/agent_client.py
  13. +33
    -5
      ironic/tests/unit/api/controllers/v1/test_ramdisk.py
  14. +31
    -5
      ironic/tests/unit/conductor/test_manager.py
  15. +2
    -2
      ironic/tests/unit/conductor/test_rpcapi.py
  16. +52
    -0
      ironic/tests/unit/conductor/test_utils.py
  17. +22
    -0
      ironic/tests/unit/drivers/modules/test_agent_client.py
  18. +4
    -0
      releasenotes/notes/agent-verify-ca-6efa3dfc469bab02.yaml

+ 5
- 0
doc/source/contributor/webapi-version-history.rst View File

@@ -2,6 +2,11 @@
REST API Version History
========================

1.68 (Victoria, master)
-----------------------

Added the ``agent_verify_ca`` parameter to the ramdisk heartbeat API.

1.67 (Victoria, master)
-----------------------



+ 9
- 3
ironic/api/controllers/v1/ramdisk.py View File

@@ -173,9 +173,9 @@ class HeartbeatController(rest.RestController):
"""Controller handling heartbeats from deploy ramdisk."""

@expose.expose(None, types.uuid_or_name, str,
str, str, status_code=http_client.ACCEPTED)
str, str, str, status_code=http_client.ACCEPTED)
def post(self, node_ident, callback_url, agent_version=None,
agent_token=None):
agent_token=None, agent_verify_ca=None):
"""Process a heartbeat from the deploy ramdisk.

:param node_ident: the UUID or logical name of a node.
@@ -186,6 +186,7 @@ class HeartbeatController(rest.RestController):
last release before sending agent_version was introduced) will be
assumed.
:param agent_token: randomly generated validation token.
:param agent_verify_ca: TLS certificate to use to connect to the agent.
:raises: NodeNotFound if node with provided UUID or name was not found.
:raises: InvalidUuidOrName if node_ident is not valid name or UUID.
:raises: NoValidHost if RPC topic for node could not be retrieved.
@@ -202,6 +203,11 @@ class HeartbeatController(rest.RestController):
cdict = api.request.context.to_policy_values()
policy.authorize('baremetal:node:ipa_heartbeat', cdict, cdict)

if (agent_verify_ca is not None
and not api_utils.allow_verify_ca_in_heartbeat()):
raise exception.InvalidParameterValue(
_('Field "agent_verify_ca" not recognised in this version'))

rpc_node = api_utils.get_rpc_node_with_suffix(node_ident)
dii = rpc_node['driver_internal_info']
agent_url = dii.get('agent_url')
@@ -231,4 +237,4 @@ class HeartbeatController(rest.RestController):

api.request.rpcapi.heartbeat(
api.request.context, rpc_node.uuid, callback_url,
agent_version, agent_token, topic=topic)
agent_version, agent_token, agent_verify_ca, topic=topic)

+ 5
- 0
ironic/api/controllers/v1/utils.py View File

@@ -1391,3 +1391,8 @@ def allow_local_link_connection_network_type():
"""Check if network_type is allowed in ports link_local_connection"""
return (api.request.version.minor
>= versions.MINOR_64_LOCAL_LINK_CONNECTION_NETWORK_TYPE)


def allow_verify_ca_in_heartbeat():
"""Check if heartbeat accepts agent_verify_ca."""
return api.request.version.minor >= versions.MINOR_68_HEARTBEAT_VERIFY_CA

+ 3
- 1
ironic/api/controllers/v1/versions.py View File

@@ -105,6 +105,7 @@ BASE_VERSION = 1
# v1.65: Add lessee to the node object.
# v1.66: Add support for node network_data field.
# v1.67: Add support for port_uuid/portgroup_uuid in node vif_attach
# v1.68: Add agent_verify_ca to heartbeat.

MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1
@@ -174,6 +175,7 @@ MINOR_64_LOCAL_LINK_CONNECTION_NETWORK_TYPE = 64
MINOR_65_NODE_LESSEE = 65
MINOR_66_NODE_NETWORK_DATA = 66
MINOR_67_NODE_VIF_ATTACH_PORT = 67
MINOR_68_HEARTBEAT_VERIFY_CA = 68

# When adding another version, update:
# - MINOR_MAX_VERSION
@@ -181,7 +183,7 @@ MINOR_67_NODE_VIF_ATTACH_PORT = 67
# explanation of what changed in the new version
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']

MINOR_MAX_VERSION = MINOR_67_NODE_VIF_ATTACH_PORT
MINOR_MAX_VERSION = MINOR_68_HEARTBEAT_VERIFY_CA

# String representations of the minor and maximum versions
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)


+ 2
- 2
ironic/common/release_mappings.py View File

@@ -248,8 +248,8 @@ RELEASE_MAPPING = {
}
},
'master': {
'api': '1.67',
'rpc': '1.50',
'api': '1.68',
'rpc': '1.51',
'objects': {
'Allocation': ['1.1'],
'Node': ['1.35'],


+ 9
- 4
ironic/conductor/manager.py View File

@@ -91,7 +91,7 @@ class ConductorManager(base_manager.BaseConductorManager):
# NOTE(rloo): This must be in sync with rpcapi.ConductorAPI's.
# NOTE(pas-ha): This also must be in sync with
# ironic.common.release_mappings.RELEASE_MAPPING['master']
RPC_API_VERSION = '1.50'
RPC_API_VERSION = '1.51'

target = messaging.Target(version=RPC_API_VERSION)

@@ -3099,11 +3099,12 @@ class ConductorManager(base_manager.BaseConductorManager):
@messaging.expected_exceptions(exception.InvalidParameterValue)
@messaging.expected_exceptions(exception.NoFreeConductorWorker)
def heartbeat(self, context, node_id, callback_url, agent_version=None,
agent_token=None):
agent_token=None, agent_verify_ca=None):
"""Process a heartbeat from the ramdisk.

:param context: request context.
:param node_id: node id or uuid.
:param callback_url: URL to reach back to the ramdisk.
:param agent_version: The version of the agent that is heartbeating. If
not provided it either indicates that the agent that is
heartbeating is a version before sending agent_version was
@@ -3111,8 +3112,8 @@ class ConductorManager(base_manager.BaseConductorManager):
RPC version is pinned so the API isn't passing us the
agent_version, in these cases assume agent v3.0.0 (the last release
before sending agent_version was introduced).
:param callback_url: URL to reach back to the ramdisk.
:param agent_token: randomly generated validation token.
:param agent_verify_ca: TLS certificate for the agent.
:raises: NoFreeConductorWorker if there are no conductors to process
this heartbeat request.
"""
@@ -3144,9 +3145,13 @@ class ConductorManager(base_manager.BaseConductorManager):
raise exception.InvalidParameterValue(
_('TLS is required by configuration'))

if agent_verify_ca:
agent_verify_ca = utils.store_agent_certificate(
task.node, agent_verify_ca)

task.spawn_after(
self._spawn_worker, task.driver.deploy.heartbeat,
task, callback_url, agent_version)
task, callback_url, agent_version, agent_verify_ca)

@METRICS.timer('ConductorManager.vif_list')
@messaging.expected_exceptions(exception.NetworkError,


+ 7
- 2
ironic/conductor/rpcapi.py View File

@@ -103,13 +103,14 @@ class ConductorAPI(object):
heartbeat
| 1.50 - Added set_indicator_state, get_indicator_state and
| get_supported_indicators.
| 1.51 - Added agent_verify_ca to heartbeat.

"""

# NOTE(rloo): This must be in sync with manager.ConductorManager's.
# NOTE(pas-ha): This also must be in sync with
# ironic.common.release_mappings.RELEASE_MAPPING['master']
RPC_API_VERSION = '1.50'
RPC_API_VERSION = '1.51'

def __init__(self, topic=None):
super(ConductorAPI, self).__init__()
@@ -898,7 +899,7 @@ class ConductorAPI(object):
node_id=node_id, clean_steps=clean_steps)

def heartbeat(self, context, node_id, callback_url, agent_version,
agent_token=None, topic=None):
agent_token=None, agent_verify_ca=None, topic=None):
"""Process a node heartbeat.

:param context: request context.
@@ -907,6 +908,7 @@ class ConductorAPI(object):
:param topic: RPC topic. Defaults to self.topic.
:param agent_token: randomly generated validation token.
:param agent_version: the version of the agent that is heartbeating
:param agent_verify_ca: TLS certificate for the agent.
:raises: InvalidParameterValue if an invalid agent token is received.
"""
new_kws = {}
@@ -917,6 +919,9 @@ class ConductorAPI(object):
if self.client.can_send_version('1.49'):
version = '1.49'
new_kws['agent_token'] = agent_token
if self.client.can_send_version('1.51'):
version = '1.51'
new_kws['agent_verify_ca'] = agent_verify_ca
cctxt = self.client.prepare(topic=topic or self.topic, version=version)
return cctxt.call(context, 'heartbeat', node_id=node_id,
callback_url=callback_url, **new_kws)


+ 47
- 0
ironic/conductor/utils.py View File

@@ -16,6 +16,7 @@ import contextlib
import crypt
import datetime
from distutils.version import StrictVersion
import os
import secrets
import time

@@ -462,6 +463,8 @@ def wipe_internal_info_on_power_off(node):
# Wipe cached steps since they may change after reboot.
driver_internal_info.pop('agent_cached_deploy_steps', None)
driver_internal_info.pop('agent_cached_clean_steps', None)
# Remove TLS certificate since it's regenerated on each run.
driver_internal_info.pop('agent_verify_ca', None)
node.driver_internal_info = driver_internal_info


@@ -473,6 +476,8 @@ def wipe_token_and_url(task):
# Remove agent_url since it will be re-asserted
# upon the next deployment attempt.
info.pop('agent_url', None)
# Remove TLS certificate since it's regenerated on each run.
info.pop('agent_verify_ca', None)
task.node.driver_internal_info = info


@@ -1232,3 +1237,45 @@ def get_attached_vif(port):
if inspection_vif:
return (inspection_vif, 'inspecting')
return (None, None)


def store_agent_certificate(node, agent_verify_ca):
"""Store certificate received from the agent and return its path."""
existing_verify_ca = node.driver_internal_info.get(
'agent_verify_ca')
if existing_verify_ca:
if os.path.exists(existing_verify_ca):
try:
with open(existing_verify_ca, 'rt') as fp:
existing_text = fp.read()
except EnvironmentError:
with excutils.save_and_reraise_exception():
LOG.exception('Could not read the existing TLS certificate'
' for node %s', node.uuid)

if existing_text.strip() != agent_verify_ca.strip():
LOG.error('Content mismatch for agent_verify_ca for '
'node %s', node.uuid)
raise exception.InvalidParameterValue(
_('Detected change in ramdisk provided "agent_verify_ca"'))
else:
return existing_verify_ca
else:
LOG.info('Current agent_verify_ca was not found for node '
'%s, assuming take over and storing', node.uuid)

fname = os.path.join(CONF.agent.certificates_path, '%s.crt' % node.uuid)
try:
# FIXME(dtantsur): it makes more sense to create this path on conductor
# start-up, but it requires reworking a ton of unit tests.
os.makedirs(CONF.agent.certificates_path, exist_ok=True)
with open(fname, 'wt') as fp:
fp.write(agent_verify_ca)
except EnvironmentError:
with excutils.save_and_reraise_exception():
LOG.exception('Could not save the TLS certificate for node %s',
node.uuid)
else:
LOG.debug('Saved the custom certificate for node %(node)s to %(file)s',
{'node': node.uuid, 'file': fname})
return fname

+ 4
- 0
ironic/conf/agent.py View File

@@ -149,6 +149,10 @@ opts = [
mutable=True,
help=_('If set to True, callback URLs without https:// will '
'be rejected by the conductor.')),
cfg.StrOpt('certificates_path',
default='/var/lib/ironic/certificates',
help=_('Path for TLS certificates used to validate '
'connections to the ramdisk.')),
]




+ 3
- 1
ironic/drivers/base.py View File

@@ -475,12 +475,14 @@ class DeployInterface(BaseInterface):
"""
pass

def heartbeat(self, task, callback_url, agent_version):
def heartbeat(self, task, callback_url, agent_version,
agent_verify_ca=None):
"""Record a heartbeat for the node.

:param task: A TaskManager instance containing the node to act on.
:param callback_url: a URL to use to call to the ramdisk.
:param agent_version: The version of the agent that is heartbeating
:param agent_verify_ca: TLS certificate for the agent.
:return: None
"""
LOG.warning('Got heartbeat message from node %(node)s, but '


+ 5
- 1
ironic/drivers/modules/agent_base.py View File

@@ -607,12 +607,14 @@ class HeartbeatMixin(object):
manager_utils.rescuing_error_handler(task, last_error)

@METRICS.timer('HeartbeatMixin.heartbeat')
def heartbeat(self, task, callback_url, agent_version):
def heartbeat(self, task, callback_url, agent_version,
agent_verify_ca=None):
"""Process a heartbeat.

:param task: task to work with.
:param callback_url: agent HTTP API URL.
:param agent_version: The version of the agent that is heartbeating
:param agent_verify_ca: TLS certificate for the agent.
"""
# NOTE(pas-ha) immediately skip the rest if nothing to do
if (task.node.provision_state not in self.heartbeat_allowed_states
@@ -641,6 +643,8 @@ class HeartbeatMixin(object):
# datetime.datetime.strptime(var, "%Y-%m-%d %H:%M:%S.%f")
driver_internal_info['agent_last_heartbeat'] = str(
timeutils.utcnow().isoformat())
if agent_verify_ca:
driver_internal_info['agent_verify_ca'] = agent_verify_ca
node.driver_internal_info = driver_internal_info
node.save()



+ 2
- 1
ironic/drivers/modules/agent_client.py View File

@@ -77,7 +77,8 @@ class AgentClient(object):
})

def _get_verify(self, node):
return node.driver_info.get('agent_verify_ca', True)
return (node.driver_internal_info.get('agent_verify_ca')
or node.driver_info.get('agent_verify_ca', True))

def _raise_if_typeerror(self, result, node, method):
error = result.get('command_error')


+ 33
- 5
ironic/tests/unit/api/controllers/v1/test_ramdisk.py View File

@@ -225,7 +225,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
self.assertEqual(b'', response.body)
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
node.uuid, 'url', None, 'x',
topic='test-topic')
None, topic='test-topic')

@mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
def test_ok_with_json(self, mock_heartbeat):
@@ -240,7 +240,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
node.uuid, 'url', None,
'maybe some magic',
topic='test-topic')
None, topic='test-topic')

@mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
def test_ok_by_name(self, mock_heartbeat):
@@ -255,7 +255,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
node.uuid, 'url', None,
'token',
topic='test-topic')
None, topic='test-topic')

@mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
def test_ok_agent_version(self, mock_heartbeat):
@@ -271,7 +271,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
node.uuid, 'url', '1.4.1',
'meow',
topic='test-topic')
None, topic='test-topic')

@mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
def test_old_API_agent_version_error(self, mock_heartbeat):
@@ -308,5 +308,33 @@ class TestHeartbeat(test_api_base.BaseApiTest):
self.assertEqual(b'', response.body)
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
node.uuid, 'url', None,
'abcdef1',
'abcdef1', None,
topic='test-topic')

@mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
def test_ok_agent_verify_ca(self, mock_heartbeat):
node = obj_utils.create_test_node(self.context)
response = self.post_json(
'/heartbeat/%s' % node.uuid,
{'callback_url': 'url',
'agent_token': 'meow',
'agent_verify_ca': 'abcdef1'},
headers={api_base.Version.string: str(api_v1.max_version())})
self.assertEqual(http_client.ACCEPTED, response.status_int)
self.assertEqual(b'', response.body)
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
node.uuid, 'url', None,
'meow', 'abcdef1',
topic='test-topic')

@mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
def test_old_API_agent_verify_ca_error(self, mock_heartbeat):
node = obj_utils.create_test_node(self.context)
response = self.post_json(
'/heartbeat/%s' % node.uuid,
{'callback_url': 'url',
'agent_token': 'meow',
'agent_verify_ca': 'abcd'},
headers={api_base.Version.string: '1.67'},
expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, response.status_int)

+ 31
- 5
ironic/tests/unit/conductor/test_manager.py View File

@@ -7256,7 +7256,7 @@ class DoNodeAdoptionTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
self.service.heartbeat(self.context, node.uuid, 'http://callback',
agent_token='magic')
mock_heartbeat.assert_called_with(mock.ANY, mock.ANY,
'http://callback', '3.0.0')
'http://callback', '3.0.0', None)

@mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat',
autospec=True)
@@ -7279,7 +7279,7 @@ class DoNodeAdoptionTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
self.service.heartbeat(self.context, node.uuid, 'http://callback',
'1.4.1', agent_token='magic')
mock_heartbeat.assert_called_with(mock.ANY, mock.ANY,
'http://callback', '1.4.1')
'http://callback', '1.4.1', None)

@mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat',
autospec=True)
@@ -7327,7 +7327,7 @@ class DoNodeAdoptionTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
self.service.heartbeat(self.context, node.uuid, 'http://callback',
agent_token='a secret')
mock_heartbeat.assert_called_with(mock.ANY, mock.ANY,
'http://callback', '3.0.0')
'http://callback', '3.0.0', None)

@mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat',
autospec=True)
@@ -7351,7 +7351,7 @@ class DoNodeAdoptionTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
self.service.heartbeat(self.context, node.uuid, 'http://callback',
agent_token='a secret')
mock_heartbeat.assert_called_with(mock.ANY, mock.ANY,
'http://callback', '3.0.0')
'http://callback', '3.0.0', None)

@mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat',
autospec=True)
@@ -7449,7 +7449,6 @@ class DoNodeAdoptionTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
mock_spawn.reset_mock()

mock_spawn.side_effect = self._fake_spawn

exc = self.assertRaises(messaging.rpc.ExpectedException,
self.service.heartbeat, self.context,
node.uuid, 'http://callback',
@@ -7458,6 +7457,33 @@ class DoNodeAdoptionTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
self.assertIn('TLS is required', str(exc.exc_info[1]))
self.assertFalse(mock_heartbeat.called)

@mock.patch.object(conductor_utils, 'store_agent_certificate',
autospec=True)
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat',
autospec=True)
@mock.patch('ironic.conductor.manager.ConductorManager._spawn_worker',
autospec=True)
def test_heartbeat_with_agent_verify_ca(self, mock_spawn,
mock_heartbeat,
mock_store_cert):
node = obj_utils.create_test_node(
self.context, driver='fake-hardware',
provision_state=states.DEPLOYING,
target_provision_state=states.ACTIVE,
driver_internal_info={'agent_secret_token': 'a secret'})
mock_store_cert.return_value = '/path/to/crt'

self._start_service()

mock_spawn.reset_mock()

mock_spawn.side_effect = self._fake_spawn
self.service.heartbeat(self.context, node.uuid, 'http://callback',
agent_token='a secret', agent_verify_ca='abcd')
mock_heartbeat.assert_called_with(
mock.ANY, mock.ANY, 'http://callback', '3.0.0',
'/path/to/crt')


@mgr_utils.mock_record_keepalive
class DestroyVolumeConnectorTestCase(mgr_utils.ServiceSetUpMixin,


+ 2
- 2
ironic/tests/unit/conductor/test_rpcapi.py View File

@@ -516,7 +516,7 @@ class RPCAPITestCase(db_base.DbTestCase):
node_id='fake-node',
callback_url='http://ramdisk.url:port',
agent_version=None,
version='1.49')
version='1.51')

def test_heartbeat_agent_token(self):
self._test_rpcapi('heartbeat',
@@ -525,7 +525,7 @@ class RPCAPITestCase(db_base.DbTestCase):
callback_url='http://ramdisk.url:port',
agent_version=None,
agent_token='xyz1',
version='1.49')
version='1.51')

def test_destroy_volume_connector(self):
fake_volume_connector = db_utils.get_test_volume_connector()


+ 52
- 0
ironic/tests/unit/conductor/test_utils.py View File

@@ -9,6 +9,9 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import os
import tempfile
import time
from unittest import mock

@@ -2136,3 +2139,52 @@ class GetAttachedVifTestCase(db_base.DbTestCase):
vif, use = conductor_utils.get_attached_vif(self.port)
self.assertEqual('1', vif)
self.assertEqual('inspecting', use)


class StoreAgentCertificateTestCase(db_base.DbTestCase):

def setUp(self):
super(StoreAgentCertificateTestCase, self).setUp()
self.node = obj_utils.create_test_node(self.context,
driver='fake-hardware')
self.tempdir = tempfile.mkdtemp()
CONF.set_override('certificates_path', self.tempdir, group='agent')
self.fname = os.path.join(self.tempdir, '%s.crt' % self.node.uuid)

def test_store_new(self):
result = conductor_utils.store_agent_certificate(self.node,
'cert text')
self.assertEqual(self.fname, result)
with open(self.fname, 'rt') as fp:
self.assertEqual('cert text', fp.read())

def test_store_existing(self):
old_fname = os.path.join(self.tempdir, 'old.crt')
with open(old_fname, 'wt') as fp:
fp.write('cert text')

self.node.driver_internal_info['agent_verify_ca'] = old_fname
result = conductor_utils.store_agent_certificate(self.node,
'cert text')
self.assertEqual(old_fname, result)
self.assertFalse(os.path.exists(self.fname))

def test_no_change(self):
old_fname = os.path.join(self.tempdir, 'old.crt')
with open(old_fname, 'wt') as fp:
fp.write('cert text')

self.node.driver_internal_info['agent_verify_ca'] = old_fname
self.assertRaises(exception.InvalidParameterValue,
conductor_utils.store_agent_certificate,
self.node, 'new cert text')
self.assertFalse(os.path.exists(self.fname))

def test_take_over(self):
old_fname = os.path.join(self.tempdir, 'old.crt')
self.node.driver_internal_info['agent_verify_ca'] = old_fname
result = conductor_utils.store_agent_certificate(self.node,
'cert text')
self.assertEqual(self.fname, result)
with open(self.fname, 'rt') as fp:
self.assertEqual('cert text', fp.read())

+ 22
- 0
ironic/tests/unit/drivers/modules/test_agent_client.py View File

@@ -258,6 +258,28 @@ class TestAgentClient(base.TestCase):
timeout=60,
verify='/path/to/agent.crt')

def test__command_verify_internal(self):
response_data = {'status': 'ok'}
self.client.session.post.return_value = MockResponse(response_data)
method = 'standby.run_image'
image_info = {'image_id': 'test_image'}
params = {'image_info': image_info}

self.node.driver_info['agent_verify_ca'] = True
self.node.driver_internal_info['agent_verify_ca'] = '/path/to/crt'

url = self.client._get_command_url(self.node)
body = self.client._get_command_body(method, params)

response = self.client._command(self.node, method, params)
self.assertEqual(response, response_data)
self.client.session.post.assert_called_once_with(
url,
data=body,
params={'wait': 'false'},
timeout=60,
verify='/path/to/crt')

@mock.patch('time.sleep', lambda seconds: None)
def test__command_poll(self):
response_data = {'status': 'ok'}


+ 4
- 0
releasenotes/notes/agent-verify-ca-6efa3dfc469bab02.yaml View File

@@ -0,0 +1,4 @@
---
features:
- |
Adds an ability to accept a custom TLS certificate in the heartbeat API.

Loading…
Cancel
Save